feat(hub): Configuration page, asset seedOrUpdate, English UI
- Add Configuration page with "Refresh Assets" button - Replace seedIfEmpty with seedOrUpdate (SHA-256 compare on startup) - Translate all Hungarian text on Apps pages to English - Add Configuration tab to all template navigation - Expand isAssetFile to match favicon patterns - Add felhom-logo.svg to website assets for the pipeline Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ type Manager struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// New creates a Manager, seeds assets from the image if the PVC dir is empty,
|
||||
// New creates a Manager, syncs assets from the image seed directory,
|
||||
// and builds the initial manifest.
|
||||
func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
m := &Manager{
|
||||
@@ -52,7 +52,7 @@ func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
logger.Printf("[ERROR] Failed to create assets dir %s: %v", assetsDir, err)
|
||||
}
|
||||
|
||||
m.seedIfEmpty()
|
||||
m.seedOrUpdate()
|
||||
|
||||
if err := m.RebuildManifest(); err != nil {
|
||||
logger.Printf("[ERROR] Failed to build asset manifest: %v", err)
|
||||
@@ -61,42 +61,47 @@ func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
return m
|
||||
}
|
||||
|
||||
// seedIfEmpty copies assets from the seed directory to the PVC directory
|
||||
// if the PVC directory has no asset files.
|
||||
func (m *Manager) seedIfEmpty() {
|
||||
entries, err := os.ReadDir(m.assetsDir)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot read assets dir: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are any asset files (not just directories or manifest.json)
|
||||
hasAssets := false
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && isAssetFile(e.Name()) {
|
||||
hasAssets = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasAssets {
|
||||
m.logger.Printf("[INFO] Assets directory has files, skipping seed")
|
||||
return
|
||||
}
|
||||
|
||||
// Seed from image
|
||||
// seedOrUpdate copies new or changed assets from the seed directory to the PVC directory.
|
||||
// On first run (empty PVC) this seeds all files. On subsequent runs it updates only files
|
||||
// whose SHA-256 checksums differ, ensuring redeployed images propagate asset changes.
|
||||
func (m *Manager) seedOrUpdate() {
|
||||
seedEntries, err := os.ReadDir(m.seedDir)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot read seed dir %s: %v (no assets will be available until uploaded)", m.seedDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
copied := 0
|
||||
// Build hash map of existing PVC assets for comparison
|
||||
existingHashes := make(map[string]string)
|
||||
pvcEntries, _ := os.ReadDir(m.assetsDir)
|
||||
for _, e := range pvcEntries {
|
||||
if e.IsDir() || !isAssetFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
h, err := fileSHA256(filepath.Join(m.assetsDir, e.Name()))
|
||||
if err == nil {
|
||||
existingHashes[e.Name()] = h
|
||||
}
|
||||
}
|
||||
|
||||
copied, skipped := 0, 0
|
||||
for _, e := range seedEntries {
|
||||
if e.IsDir() || !isAssetFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(m.seedDir, e.Name())
|
||||
|
||||
// Compare checksums — skip if unchanged
|
||||
seedHash, err := fileSHA256(src)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot hash seed asset %s: %v", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
if existingHashes[e.Name()] == seedHash {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
dst := filepath.Join(m.assetsDir, e.Name())
|
||||
if err := copyFile(src, dst); err != nil {
|
||||
m.logger.Printf("[WARN] Failed to seed asset %s: %v", e.Name(), err)
|
||||
@@ -105,11 +110,32 @@ func (m *Manager) seedIfEmpty() {
|
||||
copied++
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Seeded %d assets from image to PVC", copied)
|
||||
if copied > 0 {
|
||||
m.logger.Printf("[INFO] Asset seed: %d updated, %d unchanged", copied, skipped)
|
||||
} else {
|
||||
m.logger.Printf("[INFO] Asset seed: all %d files up-to-date", skipped)
|
||||
}
|
||||
}
|
||||
|
||||
// ReSeed re-runs the seed-or-update process and rebuilds the manifest.
|
||||
// Used by the web UI "Refresh Assets" button.
|
||||
func (m *Manager) ReSeed() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.seedOrUpdate()
|
||||
return m.rebuildManifestLocked()
|
||||
}
|
||||
|
||||
// RebuildManifest rescans the assets directory and recomputes SHA-256 hashes.
|
||||
func (m *Manager) RebuildManifest() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.rebuildManifestLocked()
|
||||
}
|
||||
|
||||
// rebuildManifestLocked does the actual manifest rebuild. Caller must hold m.mu.
|
||||
func (m *Manager) rebuildManifestLocked() error {
|
||||
entries, err := os.ReadDir(m.assetsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading assets dir: %w", err)
|
||||
@@ -147,9 +173,7 @@ func (m *Manager) RebuildManifest() error {
|
||||
Files: files,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.manifest = manifest
|
||||
m.mu.Unlock()
|
||||
|
||||
m.logger.Printf("[INFO] Asset manifest built: %d files", len(files))
|
||||
return nil
|
||||
@@ -201,7 +225,8 @@ func (m *Manager) FileCount() int {
|
||||
return len(m.manifest.Files)
|
||||
}
|
||||
|
||||
// isAssetFile returns true if the filename matches the app asset naming convention.
|
||||
// isAssetFile returns true if the filename matches the asset naming convention.
|
||||
// Matches: *-logo.svg, *-logo.png, *-screenshot-*.webp, felhom-logo.svg, felhom-favicon.svg
|
||||
func isAssetFile(name string) bool {
|
||||
if strings.Contains(name, "-logo.") {
|
||||
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".png")
|
||||
@@ -209,6 +234,9 @@ func isAssetFile(name string) bool {
|
||||
if strings.Contains(name, "-screenshot-") {
|
||||
return strings.HasSuffix(name, ".webp")
|
||||
}
|
||||
if strings.Contains(name, "-favicon.") {
|
||||
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".ico")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user