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
+41
View File
@@ -13,6 +13,7 @@ import (
"strings"
"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/metrics"
@@ -41,6 +42,14 @@ type Router struct {
// OnConfigApplied is called after a successful config apply (e.g., to push infra backup).
OnConfigApplied func()
// Asset syncer for on-demand Hub asset sync
assetsSyncer *assets.Syncer
}
// SetAssetsSyncer sets the Hub asset syncer for on-demand sync triggers.
func (r *Router) SetAssetsSyncer(as *assets.Syncer) {
r.assetsSyncer = as
}
func NewRouter(cfg *config.Config, configPath string, sett *settings.Settings, stackMgr *stacks.Manager, syncer *catalogsync.Syncer, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, metricsStore *metrics.MetricsStore, updater *selfupdate.Updater, notif *notify.Notifier, logger *log.Logger) *Router {
@@ -197,6 +206,14 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case path == "/metrics/sysinfo" && req.Method == http.MethodGet:
r.metricsSysInfo(w, req)
// POST /api/assets/sync — trigger immediate asset sync from Hub
case path == "/assets/sync" && req.Method == http.MethodPost:
r.triggerAssetSync(w, req)
// GET /api/assets/status — get asset sync status
case path == "/assets/status" && req.Method == http.MethodGet:
r.assetSyncStatus(w, req)
default:
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "endpoint not found"})
}
@@ -1005,6 +1022,30 @@ func (r *Router) configContent(w http.ResponseWriter, _ *http.Request) {
w.Write(data)
}
// --- Asset sync handlers ---
func (r *Router) triggerAssetSync(w http.ResponseWriter, req *http.Request) {
if r.assetsSyncer == nil {
writeJSON(w, http.StatusOK, apiResponse{OK: false, Error: "asset sync not configured"})
return
}
r.logger.Println("[API] Manual asset sync requested")
go func() {
if err := r.assetsSyncer.Sync(context.Background()); err != nil {
r.logger.Printf("[WARN] Manual asset sync failed: %v", err)
}
}()
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Asset sync started"})
}
func (r *Router) assetSyncStatus(w http.ResponseWriter, _ *http.Request) {
if r.assetsSyncer == nil {
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]string{"status": "not_configured"}})
return
}
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: r.assetsSyncer.Status()})
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)