Files
nerd-monitor/internal/ui/handlers.go
Ducky SSH User 50dcfcdc83
All checks were successful
Build and Release / build (push) Successful in 35s
Add logging and fix /agents/ route error
2025-12-20 07:34:02 +00:00

215 lines
6.5 KiB
Go

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
}