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:
2026-02-25 09:34:43 +01:00
parent d8790af6bb
commit 1e354cbd41
15 changed files with 542 additions and 109 deletions
+59 -31
View File
@@ -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
}