package web import ( "fmt" "html/template" "log" "net/http" "os" "path/filepath" "strings" "sync" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) type Server struct { cfg *config.Config stackMgr *stacks.Manager logger *log.Logger version string tmpl *template.Template sessions map[string]*session sessionsMu sync.RWMutex } func NewServer(cfg *config.Config, stackMgr *stacks.Manager, logger *log.Logger, version string) *Server { s := &Server{ cfg: cfg, stackMgr: stackMgr, logger: logger, version: version, sessions: make(map[string]*session), } s.loadTemplates() go s.cleanupSessions() return s } func (s *Server) loadTemplates() { s.tmpl = template.Must( template.New("").Funcs(s.templateFuncMap()).ParseFS(templateFS, "templates/*.html"), ) } // ServeHTTP handles all non-API web requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case path == "/" || path == "/dashboard": s.dashboardHandler(w, r) case path == "/stacks": s.stacksHandler(w, r) case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/logs"): name := strings.TrimPrefix(path, "/stacks/") name = strings.TrimSuffix(name, "/logs") s.logsHandler(w, r, name) case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/deploy"): name := strings.TrimPrefix(path, "/stacks/") name = strings.TrimSuffix(name, "/deploy") s.deployHandler(w, r, name) case path == "/static/style.css": s.serveCSSHandler(w, r) case path == "/static/felhom-logo.svg": s.serveLogoHandler(w, r) case strings.HasPrefix(path, "/static/assets/"): s.serveAsset(w, r, strings.TrimPrefix(path, "/static/assets/")) case strings.HasPrefix(path, "/apps/"): slug := strings.TrimPrefix(path, "/apps/") s.appDetailHandler(w, r, slug) default: http.NotFound(w, r) } } func (s *Server) render(w http.ResponseWriter, name string, data interface{}) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil { s.logger.Printf("[ERROR] Template error (%s): %v", name, err) http.Error(w, "Internal error", http.StatusInternalServerError) } } // --- Static file / asset serving --- func (s *Server) serveCSSHandler(w http.ResponseWriter, r *http.Request) { data, err := templateFS.ReadFile("templates/style.css") if err != nil { http.Error(w, "CSS not found", 500) return } w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") w.Write(data) } func (s *Server) serveLogoHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "public, max-age=86400") fmt.Fprint(w, felhomLogoSVG) } // serveAsset serves baked-in app assets (logos, screenshots) from /usr/share/felhom/assets/ 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) if _, err := os.Stat(path); os.IsNotExist(err) { http.NotFound(w, r) return } w.Header().Set("Cache-Control", "public, max-age=86400") http.ServeFile(w, r, path) }