package handler import ( "embed" "encoding/json" "html/template" "net/http" "path" "freeleaps.com/gitea-webhook-ambassador/internal/handler" "freeleaps.com/gitea-webhook-ambassador/internal/logger" ) type DashboardHandler struct { templates *template.Template fs embed.FS projectHandler *handler.ProjectHandler adminHandler *handler.AdminHandler logsHandler *handler.LogsHandler healthHandler *handler.HealthHandler } func NewDashboardHandler(fs embed.FS, projectHandler *handler.ProjectHandler, adminHandler *handler.AdminHandler, logsHandler *handler.LogsHandler, healthHandler *handler.HealthHandler) (*DashboardHandler, error) { templates, err := template.ParseFS(fs, "templates/*.html") if err != nil { return nil, err } return &DashboardHandler{ templates: templates, fs: fs, projectHandler: projectHandler, adminHandler: adminHandler, logsHandler: logsHandler, healthHandler: healthHandler, }, nil } func (h *DashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/login": h.handleLogin(w, r) case "/dashboard": h.handleDashboard(w, r) case "/api/projects": h.handleProjects(w, r) case "/api/keys": h.handleAPIKeys(w, r) case "/api/logs": h.handleLogs(w, r) case "/api/health": h.handleHealth(w, r) default: // Serve static files if path.Ext(r.URL.Path) != "" { h.serveStaticFile(w, r) return } http.NotFound(w, r) } } func (h *DashboardHandler) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } logger.Debug("Serving login page") h.templates.ExecuteTemplate(w, "login.html", nil) } func (h *DashboardHandler) handleDashboard(w http.ResponseWriter, r *http.Request) { h.templates.ExecuteTemplate(w, "dashboard.html", nil) } func (h *DashboardHandler) handleProjects(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.projectHandler.HandleGetProjectMapping(w, r) case http.MethodPost: h.projectHandler.HandleCreateProjectMapping(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *DashboardHandler) handleAPIKeys(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.adminHandler.HandleListAPIKeys(w, r) case http.MethodPost: h.adminHandler.HandleCreateAPIKey(w, r) case http.MethodDelete: h.adminHandler.HandleDeleteAPIKey(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *DashboardHandler) handleLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } h.logsHandler.HandleGetTriggerLogs(w, r) } func (h *DashboardHandler) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Capture the health handler response recorder := newResponseRecorder(w) h.healthHandler.HandleHealth(recorder, r) // If it's not JSON or there was an error, just copy the response if recorder.Header().Get("Content-Type") != "application/json" { recorder.copyToResponseWriter(w) return } // Parse the health check response and format it for the dashboard var healthData map[string]interface{} if err := json.Unmarshal(recorder.Body(), &healthData); err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Format the response for the dashboard response := map[string]string{ "status": "healthy", } if healthData["status"] != "ok" { response["status"] = "unhealthy" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *DashboardHandler) serveStaticFile(w http.ResponseWriter, r *http.Request) { // Remove leading slash and join with assets directory filePath := path.Join("assets", r.URL.Path) data, err := h.fs.ReadFile(filePath) if err != nil { http.NotFound(w, r) return } // Set MIME type based on file extension ext := path.Ext(r.URL.Path) switch ext { case ".css": w.Header().Set("Content-Type", "text/css; charset=utf-8") case ".js": w.Header().Set("Content-Type", "application/javascript; charset=utf-8") case ".png": w.Header().Set("Content-Type", "image/png") case ".jpg", ".jpeg": w.Header().Set("Content-Type", "image/jpeg") default: w.Header().Set("Content-Type", "application/octet-stream") } // Set caching headers w.Header().Set("Cache-Control", "public, max-age=31536000") w.Write(data) } // responseRecorder is a custom ResponseWriter that records its mutations type responseRecorder struct { headers http.Header body []byte statusCode int original http.ResponseWriter } func newResponseRecorder(w http.ResponseWriter) *responseRecorder { return &responseRecorder{ headers: make(http.Header), statusCode: http.StatusOK, original: w, } } func (r *responseRecorder) Header() http.Header { return r.headers } func (r *responseRecorder) Write(body []byte) (int, error) { r.body = append(r.body, body...) return len(body), nil } func (r *responseRecorder) WriteHeader(statusCode int) { r.statusCode = statusCode } func (r *responseRecorder) Body() []byte { return r.body } func (r *responseRecorder) copyToResponseWriter(w http.ResponseWriter) { for k, v := range r.headers { w.Header()[k] = v } w.WriteHeader(r.statusCode) w.Write(r.body) }