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
+58
View File
@@ -17,6 +17,7 @@ import (
"sync"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
"golang.org/x/crypto/bcrypt"
)
@@ -38,6 +39,7 @@ type Server struct {
staleThreshold time.Duration
versionChecker *VersionChecker
templateFetcher *TemplateFetcher
assetsMgr *assets.Manager
sessions map[string]*hubSession
sessionsMu sync.RWMutex
@@ -113,6 +115,11 @@ func (s *Server) SetTemplateFetcher(tf *TemplateFetcher) {
s.templateFetcher = tf
}
// SetAssetManager sets the asset manager for the Configuration page (optional).
func (s *Server) SetAssetManager(am *assets.Manager) {
s.assetsMgr = am
}
// ServeHTTP routes web requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
@@ -138,6 +145,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Write(chartJS)
case path == "/configuration":
if r.Method == http.MethodPost {
s.handleConfigurationAction(w, r)
} else {
s.handleConfiguration(w, r)
}
case path == "/apps" || path == "/apps/":
s.handleApps(w, r)
case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
@@ -606,3 +619,48 @@ func statusColor(status string) string {
func statusIcon(status string) string {
return "●"
}
// handleConfiguration renders the Configuration page.
func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) {
csrfToken := s.getCSRFToken(r)
assetCount := 0
assetLastSync := ""
if s.assetsMgr != nil {
assetCount = s.assetsMgr.FileCount()
if m := s.assetsMgr.GetManifest(); m != nil {
assetLastSync = m.Generated
}
}
data := map[string]interface{}{
"CSRFToken": csrfToken,
"AssetCount": assetCount,
"AssetLastSync": assetLastSync,
"Flash": r.URL.Query().Get("flash"),
}
if err := s.templates.ExecuteTemplate(w, "configuration.html", data); err != nil {
s.logger.Printf("[ERROR] configuration.html template: %v", err)
}
}
// handleConfigurationAction handles POST actions on the Configuration page.
func (s *Server) handleConfigurationAction(w http.ResponseWriter, r *http.Request) {
action := r.FormValue("action")
switch action {
case "refresh_assets":
if s.assetsMgr == nil {
http.Redirect(w, r, "/configuration?flash=assets_not_configured", http.StatusSeeOther)
return
}
if err := s.assetsMgr.ReSeed(); err != nil {
s.logger.Printf("[ERROR] Asset re-seed failed: %v", err)
http.Redirect(w, r, "/configuration?flash=assets_error", http.StatusSeeOther)
return
}
s.logger.Printf("[INFO] Manual asset re-seed completed")
http.Redirect(w, r, "/configuration?flash=assets_refreshed", http.StatusSeeOther)
default:
http.Redirect(w, r, "/configuration", http.StatusSeeOther)
}
}