feat(controller): Hub asset syncer for logos and screenshots

Add internal/assets package that downloads and caches app assets from
Hub API with SHA-256 change detection. Assets resolve from synced cache
first, falling back to baked-in directory. Daily sync schedule +
on-demand POST /api/assets/sync endpoint.

Config: assets.sync_enabled + assets.sync_schedule (default 05:00)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 15:29:23 +01:00
parent a5fec20d31
commit 538d367cc4
7 changed files with 391 additions and 2 deletions
+16 -1
View File
@@ -11,6 +11,7 @@ import (
"sync"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
@@ -61,6 +62,9 @@ type Server struct {
// Hub push status callback — set via SetHubPushStatus for monitoring page
hubPushStatusFn func() HubPushStatusData
// Asset syncer for Hub-managed assets (optional)
assetsSyncer *assets.Syncer
}
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, updater *selfupdate.Updater, logger *log.Logger, version string) *Server {
@@ -134,6 +138,11 @@ func (s *Server) SetHubPushStatus(fn func() HubPushStatusData) {
s.hubPushStatusFn = fn
}
// SetAssetsSyncer sets the Hub asset syncer for resolving app assets.
func (s *Server) SetAssetsSyncer(as *assets.Syncer) {
s.assetsSyncer = as
}
// InRestoreMode returns true if the server is in DR restore mode.
func (s *Server) InRestoreMode() bool {
s.restoreMu.RLock()
@@ -286,7 +295,13 @@ const assetsDir = "/usr/share/felhom/assets"
func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request, filename string) {
filename = filepath.Base(filename)
path := filepath.Join(assetsDir, filename)
var path string
if s.assetsSyncer != nil {
path = s.assetsSyncer.Resolve(filename)
} else {
path = filepath.Join(assetsDir, filename)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
http.NotFound(w, r)