Initial commit: Nerd Monitor - Cross-platform system monitoring application
Features: - Multi-platform agents (Linux, macOS, Windows - AMD64 & ARM64) - Real-time CPU, RAM, and disk usage monitoring - Responsive web dashboard with live status indicators - Session-based authentication with secure credentials - Stale agent detection and removal (6+ months inactive) - Auto-refresh dashboard (5 second intervals) - 15-second agent reporting intervals - Auto-generated agent IDs from hostnames - In-memory storage (zero database setup) - Minimal dependencies (Chi router + Templ templating) Project Structure: - cmd/: Agent and Server executables - internal/: API, Auth, Stats, Storage, and UI packages - views/: Templ templates for dashboard UI - Makefile: Build automation for all platforms Ready for deployment with comprehensive documentation: - README.md: Full project documentation - QUICKSTART.md: Getting started guide - AGENTS.md: Development guidelines
This commit is contained in:
149
internal/ui/handlers.go
Normal file
149
internal/ui/handlers.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user