package ui import ( "context" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5" "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) { slog.Debug("Incoming dashboard request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) slog.Debug("Rendering dashboard", "agentCount", len(h.store.GetAllAgents()), "staleAgentCount", len(h.getStaleAgents())) agents := h.store.GetAllAgents() staleAgents := h.getStaleAgents() component := views.Dashboard(agents, staleAgents) component.Render(context.Background(), w) slog.Debug("Dashboard rendered successfully") } // AgentDetail renders the agent detail page. func (h *Handler) AgentDetail(w http.ResponseWriter, r *http.Request) { slog.Debug("Incoming agent detail request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) agentID := chi.URLParam(r, "id") slog.Debug("Retrieving agent detail", "agentID", agentID) agent := h.store.GetAgent(agentID) if agent == nil { slog.Warn("Agent not found for detail view", "agentID", agentID) http.NotFound(w, r) return } component := views.AgentDetail(agent) component.Render(context.Background(), w) slog.Debug("Agent detail rendered successfully", "agentID", agentID) } // Login renders the login page. func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { slog.Debug("Incoming login request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) if r.Method == http.MethodGet { slog.Debug("Rendering login page") component := views.LoginPage("") component.Render(context.Background(), w) return } if r.Method == http.MethodPost { username := r.FormValue("username") password := r.FormValue("password") slog.Debug("Attempting login", "username", username, "remoteAddr", r.RemoteAddr) token, err := h.auth.Login(username, password) if err != nil { slog.Warn("Login failed - invalid credentials", "username", username, "remoteAddr", r.RemoteAddr, "error", err) component := views.LoginPage("Invalid credentials") component.Render(context.Background(), w) return } slog.Info("Login successful", "username", username, "remoteAddr", r.RemoteAddr) 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 } slog.Warn("Invalid method for login", "method", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } // Logout handles logout. func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { slog.Debug("Incoming logout request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) if r.Method != http.MethodPost { slog.Warn("Invalid method for logout", "method", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } cookie, err := r.Cookie("session_token") if err == nil { slog.Info("Logging out user", "remoteAddr", r.RemoteAddr) h.auth.Logout(cookie.Value) } else { slog.Debug("Logout attempted without valid session", "remoteAddr", r.RemoteAddr) } 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) { slog.Debug("Incoming remove stale agents request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) if r.Method != http.MethodPost { slog.Warn("Invalid method for remove stale agents", "method", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentIDs := r.Form["agent_ids"] slog.Info("Removing stale agents", "count", len(agentIDs), "agentIDs", agentIDs) for _, id := range agentIDs { h.store.DeleteAgent(id) } slog.Info("Stale agents removed successfully", "count", len(agentIDs)) http.Redirect(w, r, "/", http.StatusSeeOther) } // DeleteAgent handles single agent deletion. func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) { slog.Debug("Incoming delete agent request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) if r.Method != http.MethodPost { slog.Warn("Invalid method for delete agent", "method", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentID := chi.URLParam(r, "id") slog.Info("Deleting agent", "agentID", agentID) h.store.DeleteAgent(agentID) slog.Info("Agent deleted successfully", "agentID", agentID) http.Redirect(w, r, "/", http.StatusSeeOther) } // UpdateAgentHostname handles hostname updates for agents. func (h *Handler) UpdateAgentHostname(w http.ResponseWriter, r *http.Request) { slog.Debug("Incoming update agent hostname request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr) if r.Method != http.MethodPost { slog.Warn("Invalid method for update agent hostname", "method", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } agentID := chi.URLParam(r, "id") hostname := r.FormValue("hostname") if hostname == "" { slog.Warn("Empty hostname provided for update", "agentID", agentID) http.Error(w, "Hostname cannot be empty", http.StatusBadRequest) return } slog.Info("Updating agent hostname", "agentID", agentID, "newHostname", hostname) err := h.store.UpdateHostname(agentID, hostname) if err != nil { slog.Warn("Failed to update hostname - agent not found", "agentID", agentID, "error", err) http.NotFound(w, r) return } slog.Info("Agent hostname updated successfully", "agentID", agentID, "hostname", hostname) 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 }