package ui import ( "context" "net/http" "time" "nerd-monitor/internal/auth" "nerd-monitor/internal/store" "nerd-monitor/views" ) // Handler serves UI pages. type Handler struct { store *store.Store auth *auth.Manager } // New creates a new UI handler. func New(s *store.Store, a *auth.Manager) *Handler { return &Handler{ store: s, auth: a, } } // Dashboard renders the dashboard page. func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { agents := h.store.GetAllAgents() staleAgents := h.getStaleAgents() component := views.Dashboard(agents, staleAgents) component.Render(context.Background(), w) } // AgentDetail renders the agent detail page. func (h *Handler) AgentDetail(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") agent := h.store.GetAgent(agentID) if agent == nil { http.NotFound(w, r) return } component := views.AgentDetail(agent) component.Render(context.Background(), w) } // Login renders the login page. func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { component := views.LoginPage("") component.Render(context.Background(), w) return } if r.Method == http.MethodPost { username := r.FormValue("username") password := r.FormValue("password") token, err := h.auth.Login(username, password) if err != nil { component := views.LoginPage("Invalid credentials") component.Render(context.Background(), w) return } http.SetCookie(w, &http.Cookie{ Name: "session_token", Value: token, Path: "/", MaxAge: 86400, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) http.Redirect(w, r, "/", http.StatusSeeOther) return } http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } // Logout handles logout. func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } cookie, err := r.Cookie("session_token") if err == nil { h.auth.Logout(cookie.Value) } http.SetCookie(w, &http.Cookie{ Name: "session_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, }) http.Redirect(w, r, "/login", http.StatusSeeOther) } // RemoveStaleAgents handles bulk removal of stale agents. func (h *Handler) RemoveStaleAgents(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentIDs := r.Form["agent_ids"] for _, id := range agentIDs { h.store.DeleteAgent(id) } http.Redirect(w, r, "/", http.StatusSeeOther) } // DeleteAgent handles single agent deletion. func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentID := r.PathValue("id") h.store.DeleteAgent(agentID) http.Redirect(w, r, "/", http.StatusSeeOther) } // UpdateAgentHostname handles hostname updates for agents. func (h *Handler) UpdateAgentHostname(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentID := r.PathValue("id") hostname := r.FormValue("hostname") if hostname == "" { http.Error(w, "Hostname cannot be empty", http.StatusBadRequest) return } err := h.store.UpdateHostname(agentID, hostname) if err != nil { http.NotFound(w, r) return } http.Redirect(w, r, "/agents/"+agentID, http.StatusSeeOther) } // getStaleAgents returns agents that haven't reported in 6 months. func (h *Handler) getStaleAgents() []*store.AgentStats { const staleThreshold = 6 * 30 * 24 * time.Hour allAgents := h.store.GetAllAgents() var stale []*store.AgentStats for _, agent := range allAgents { if time.Since(agent.LastSeen) > staleThreshold { stale = append(stale, agent) } } return stale }