3 Commits

Author SHA1 Message Date
Ducky SSH User
7d5713ed36 chore: remove build artifacts from version control
All checks were successful
Build and Release / build (push) Successful in 15s
- Remove agent and server binaries from git tracking
- These files are now properly ignored via .gitignore
2025-12-20 08:09:26 +00:00
Ducky SSH User
cc26726ddf chore: update .gitignore to exclude temporary build files
- Add agent and server binaries to .gitignore
- Remove temporary build artifacts from repository
2025-12-20 08:09:17 +00:00
Ducky SSH User
8cb33dbc90 feat: implement real-time updates and enhance monitoring system
- Add structured logging with slog throughout application
- Implement real-time updates using Server-Sent Events and HTMX
- Add broadcaster system for instant UI updates when agents report stats
- Replace meta refresh with HTMX-powered seamless updates
- Add new API endpoints for HTMX fragments and SSE events
- Update templates to use HTMX for instant data refresh
- Enhance README with real-time features and updated documentation
- Remove obsolete template generation file
2025-12-20 08:09:02 +00:00
11 changed files with 441 additions and 105 deletions

4
.gitignore vendored
View File

@@ -31,3 +31,7 @@ Thumbs.db
# Local configuration # Local configuration
config.local.yaml config.local.yaml
.env.*.local .env.*.local
# Temporary build files
agent
server

108
README.md
View File

@@ -1,22 +1,25 @@
# Nerd Monitor 📊 # Nerd Monitor 📊
A lightweight, cross-platform system monitoring solution written in Go. Monitor CPU, memory, and disk usage across multiple machines with a beautiful web dashboard. A lightweight, cross-platform system monitoring solution written in Go. Monitor CPU, memory, and disk usage across multiple machines with a beautiful web dashboard featuring instant real-time updates powered by Server-Sent Events and HTMX.
![Go](https://img.shields.io/badge/Go-1.23-blue?logo=go) ![Go](https://img.shields.io/badge/Go-1.23-blue?logo=go)
![Real-time](https://img.shields.io/badge/real--time-SSE%20%2B%20HTMX-orange?logo=lightning)
![License](https://img.shields.io/badge/license-MIT-green) ![License](https://img.shields.io/badge/license-MIT-green)
![Platforms](https://img.shields.io/badge/platforms-Linux%20%7C%20macOS%20%7C%20Windows-important) ![Platforms](https://img.shields.io/badge/platforms-Linux%20%7C%20macOS%20%7C%20Windows-important)
## Features ## Features
- 🖥️ **Multi-platform Support** - Deploy agents on Linux, macOS, and Windows (AMD64 & ARM64) - 🖥️ **Multi-platform Support** - Deploy agents on Linux, macOS, and Windows (AMD64 & ARM64)
- 📈 **Real-time Monitoring** - Track CPU, RAM, and Disk usage with 15-second refresh intervals - **Instant Real-time Updates** - Stats update immediately when agents report, not on fixed intervals
- 🟢 **Live Status Indicators** - See which machines are online/offline at a glance - 🟢 **Live Status Indicators** - See which machines are online/offline at a glance
- 🔒 **Secure Authentication** - Session-based admin authentication for the dashboard - 🔒 **Secure Authentication** - Session-based admin authentication for the dashboard
- 🧹 **Stale Agent Management** - Automatically detect and remove agents inactive for 6+ months - 🧹 **Stale Agent Management** - Automatically detect and remove agents inactive for 6+ months
- 📱 **Responsive Dashboard** - Beautiful, modern UI with auto-refresh - 📱 **Responsive Dashboard** - Beautiful, modern UI with seamless HTMX-powered updates
- 🚀 **Minimal Dependencies** - Only Chi router and Templ templating engine (plus Go stdlib) - 🚀 **Minimal Dependencies** - Only Chi router, Templ templating, and HTMX (plus Go stdlib)
- ⚙️ **Auto-Generation** - Agent IDs automatically generated from hostname - ⚙️ **Auto-Generation** - Agent IDs automatically generated from hostname
- 💾 **In-Memory Storage** - Fast, zero-database setup (perfect for small to medium deployments) - 💾 **In-Memory Storage** - Fast, zero-database setup (perfect for small to medium deployments)
- 📡 **Server-Sent Events** - Real-time push notifications from server to web clients
- 📝 **Structured Logging** - Comprehensive slog-based logging throughout the application
## Quick Start ## Quick Start
@@ -54,7 +57,7 @@ Binaries are created in the `bin/` directory.
-password securepassword -password securepassword
``` ```
Then access the dashboard at `http://localhost:8080` Then access the dashboard at `http://localhost:8080`. The interface provides real-time updates - stats refresh instantly when agents report new data!
### Running Agents ### Running Agents
@@ -87,7 +90,7 @@ nerd-monitor-agent.bat --server 10.0.20.80:9090 --interval 30s
### Overview Page ### Overview Page
Shows all connected agents with real-time metrics: Shows all connected agents with instant real-time metrics:
- **Status Badge** - Green (Online) or Red (Offline) - **Status Badge** - Green (Online) or Red (Offline)
- Online: Last report within 15 seconds - Online: Last report within 15 seconds
@@ -96,16 +99,18 @@ Shows all connected agents with real-time metrics:
- **Memory Usage** - Used / Total with visual progress bar - **Memory Usage** - Used / Total with visual progress bar
- **Disk Usage** - Used / Total with visual progress bar - **Disk Usage** - Used / Total with visual progress bar
- **Last Seen** - Human-readable timestamp - **Last Seen** - Human-readable timestamp
- **Live Updates** - Stats refresh immediately when agents report new data
### Agent Detail Page ### Agent Detail Page
Click any agent hostname to see detailed statistics: Click any agent hostname to see detailed statistics with live updates:
- Large CPU usage display with progress bar - Large CPU usage display with progress bar (updates in real-time)
- Memory and disk breakdowns - Memory and disk breakdowns
- Agent ID and detailed metadata - Agent ID and detailed metadata
- Last exact timestamp - Last exact timestamp
- Delete button for removing the agent - Delete button for removing the agent
- **Instant Updates** - CPU stats refresh immediately when agent reports
### Stale Agent Management ### Stale Agent Management
@@ -122,10 +127,15 @@ Agents inactive for 6+ months:
- **HTTP Router**: Chi v5 for efficient routing - **HTTP Router**: Chi v5 for efficient routing
- **Authentication**: Session-based auth with 24-hour expiry - **Authentication**: Session-based auth with 24-hour expiry
- **Storage**: In-memory concurrent-safe store (sync.RWMutex) - **Storage**: In-memory concurrent-safe store (sync.RWMutex)
- **Real-time Broadcasting**: Server-Sent Events (SSE) for instant UI updates
- **Structured Logging**: slog-based logging with debug/info levels
- **API Endpoints**: - **API Endpoints**:
- `POST /api/report` - Agent stats reporting - `POST /api/report` - Agent stats reporting (triggers real-time broadcasts)
- `GET /api/agents` - List all agents - `GET /api/agents` - List all agents
- `GET /api/agents/{id}` - Get specific agent stats - `GET /api/agents/{id}` - Get specific agent stats
- `GET /api/dashboard/table` - HTML fragment for HTMX dashboard updates
- `GET /api/agents/{id}/stats` - HTML fragment for HTMX agent detail updates
- `GET /api/events` - Server-Sent Events for real-time notifications
### Agent (`cmd/agent/`) ### Agent (`cmd/agent/`)
@@ -138,8 +148,9 @@ Agents inactive for 6+ months:
### Views (`views/`) ### Views (`views/`)
- **Templ Templates**: Type-safe HTML templating - **Templ Templates**: Type-safe HTML templating
- **HTMX Integration**: Smooth, instant updates without page refreshes
- **Server-Sent Events**: Real-time push notifications from server
- **Responsive Design**: Works on desktop, tablet, and mobile - **Responsive Design**: Works on desktop, tablet, and mobile
- **Auto-Refresh**: Dashboard refreshes every 5 seconds
- **Color-coded Status**: Visual indicators for system health - **Color-coded Status**: Visual indicators for system health
## Project Structure ## Project Structure
@@ -148,28 +159,28 @@ Agents inactive for 6+ months:
nerd-monitor/ nerd-monitor/
├── cmd/ ├── cmd/
│ ├── agent/ │ ├── agent/
│ │ └── main.go # Agent executable │ │ └── main.go # Agent executable with slog logging
│ └── server/ │ └── server/
│ └── main.go # Server executable │ └── main.go # Server executable with slog logging
├── internal/ ├── internal/
│ ├── api/ │ ├── api/
│ │ └── api.go # API handlers │ │ └── api.go # API handlers + real-time broadcaster
│ ├── auth/ │ ├── auth/
│ │ ├── middleware.go # Auth middleware │ │ ├── middleware.go # Auth middleware with detailed logging
│ │ └── errors.go # Error definitions │ │ └── errors.go # Error definitions
│ ├── stats/ │ ├── stats/
│ │ └── stats.go # System stats collection │ │ └── stats.go # System stats collection
│ ├── store/ │ ├── store/
│ │ └── store.go # In-memory agent storage │ │ └── store.go # In-memory agent storage with logging
│ └── ui/ │ └── ui/
│ └── handlers.go # Dashboard handlers │ └── handlers.go # Dashboard handlers + HTMX endpoints
├── views/ ├── views/
│ ├── layout.templ # Base layout template │ ├── layout.templ # Base layout with HTMX + SSE
│ ├── dashboard.templ # Dashboard page │ ├── dashboard.templ # Dashboard with real-time updates
│ ├── agent_detail.templ # Agent detail page │ ├── agent_detail.templ # Agent detail with live stats
│ ├── login.templ # Login page │ ├── login.templ # Login page
│ ├── components.templ # Reusable components │ ├── components.templ # Reusable components
│ └── generate.go # Templ code generation │ └── *_templ.go # Generated Templ code
├── Makefile # Build system ├── Makefile # Build system
├── go.mod / go.sum # Go dependencies ├── go.mod / go.sum # Go dependencies
├── AGENTS.md # Agent guidelines ├── AGENTS.md # Agent guidelines
@@ -231,9 +242,10 @@ Options:
- **Agent Memory**: ~8-10 MB (depending on platform) - **Agent Memory**: ~8-10 MB (depending on platform)
- **Server Memory**: Scales with connected agents (~1 MB per 1000 agents) - **Server Memory**: Scales with connected agents (~1 MB per 1000 agents)
- **Network**: Minimal bandwidth (~1 KB per report) - **Network**: Minimal bandwidth (~1 KB per report + SSE connections)
- **Dashboard Refresh**: 5 seconds (configurable) - **Real-time Updates**: Instant UI updates when agents report stats
- **Agent Reporting**: 15 seconds (configurable) - **Agent Reporting**: 15 seconds (configurable)
- **SSE Connections**: Lightweight persistent connections for real-time notifications
## Building for Specific Platforms ## Building for Specific Platforms
@@ -261,6 +273,8 @@ make build-all
- `github.com/go-chi/chi/v5` - HTTP router - `github.com/go-chi/chi/v5` - HTTP router
- `github.com/a-h/templ` - HTML templating - `github.com/a-h/templ` - HTML templating
- `github.com/shirou/gopsutil/v3` - System statistics - `github.com/shirou/gopsutil/v3` - System statistics
- **HTMX** (CDN) - Frontend interactivity and real-time updates
- **Server-Sent Events** (Go stdlib) - Real-time push notifications
### Go Standard Library ### Go Standard Library
@@ -270,6 +284,7 @@ make build-all
- `time` - Timestamps and durations - `time` - Timestamps and durations
- `os` - Hostname, signals, I/O - `os` - Hostname, signals, I/O
- `crypto/rand` - Token generation - `crypto/rand` - Token generation
- `log/slog` - Structured logging throughout application
## Troubleshooting ## Troubleshooting
@@ -279,6 +294,15 @@ make build-all
2. Check agent logs for connection errors 2. Check agent logs for connection errors
3. Ensure correct server address and port 3. Ensure correct server address and port
4. Check firewall rules allow outbound connections 4. Check firewall rules allow outbound connections
5. Check server logs for SSE broadcasting activity
### Stats not updating in real-time
1. Verify browser supports Server-Sent Events (SSE)
2. Check browser developer tools for JavaScript errors
3. Ensure `/api/events` endpoint is accessible (requires authentication)
4. Check server logs for SSE connection/disconnection messages
5. Verify HTMX library loaded correctly
### Agent shows "Offline" immediately ### Agent shows "Offline" immediately
@@ -286,6 +310,7 @@ make build-all
2. Verify server address is reachable: `ping 10.0.20.80` 2. Verify server address is reachable: `ping 10.0.20.80`
3. Check if server is listening on the correct port 3. Check if server is listening on the correct port
4. Review server logs for errors 4. Review server logs for errors
5. Note: Agents are marked offline if no report received in 15 seconds
### Can't login to dashboard ### Can't login to dashboard
@@ -293,6 +318,7 @@ make build-all
2. Try the default: `admin` / `admin` 2. Try the default: `admin` / `admin`
3. If changed, restart server with original credentials to reconfigure 3. If changed, restart server with original credentials to reconfigure
4. Check browser cookies are enabled 4. Check browser cookies are enabled
5. Clear browser cache if SSE connections seem stuck
### Windows agent window appears then closes ### Windows agent window appears then closes
@@ -312,6 +338,18 @@ make templ
go run github.com/a-h/templ/cmd/templ@latest generate go run github.com/a-h/templ/cmd/templ@latest generate
``` ```
### Viewing Logs
The application uses structured logging with `slog`. Set log level with:
```bash
# Debug logging (shows all activity)
export GOLOGLEVEL=debug
# Info logging (default, shows important events)
export GOLOGLEVEL=info
```
### Running Tests ### Running Tests
```bash ```bash
@@ -324,20 +362,42 @@ go test ./...
gofmt -w ./cmd ./internal ./views gofmt -w ./cmd ./internal ./views
``` ```
### Testing Real-time Features
To test SSE and HTMX functionality:
```bash
# Start server
./bin/nerd-monitor-server
# In another terminal, send test stats
curl -X POST "http://localhost:8080/api/report?id=test-agent" \
-H "Content-Type: application/json" \
-d '{"hostname": "test", "cpuUsage": 25.5, "ramUsage": 1000000, "ramTotal": 8000000, "diskUsage": 50000000, "diskTotal": 100000000}'
```
## Contributing ## Contributing
Contributions are welcome! Please follow the code style guidelines in `AGENTS.md`. Contributions are welcome! Please follow the code style guidelines in `AGENTS.md`.
## Recent Updates
-**Real-time Updates**: Instant UI updates via Server-Sent Events and HTMX
-**Structured Logging**: Comprehensive slog-based logging throughout
-**Enhanced Monitoring**: Better debugging and operational visibility
## Future Enhancements ## Future Enhancements
- [ ] Database persistence (SQLite/PostgreSQL) - [ ] Database persistence (SQLite/PostgreSQL)
- [ ] Alerting system (email/Slack notifications) - [ ] Alerting system (email/Slack notifications)
- [ ] Historical data / graphing - [ ] Historical data / graphing with time-series storage
- [ ] Agent grouping and tagging - [ ] Agent grouping and tagging
- [ ] Custom metric collection - [ ] Custom metric collection
- [ ] TLS/HTTPS support - [ ] TLS/HTTPS support
- [ ] Multi-tenancy - [ ] Multi-tenancy
- [ ] API documentation (Swagger) - [ ] API documentation (Swagger/OpenAPI)
- [ ] Webhook integrations
- [ ] Mobile app companion
## License ## License

BIN
agent

Binary file not shown.

View File

@@ -43,11 +43,19 @@ func main() {
r.Get("/login", uiHandler.Login) r.Get("/login", uiHandler.Login)
r.Post("/login", uiHandler.Login) r.Post("/login", uiHandler.Login)
// Public routes (no auth required)
r.Post("/api/report", apiHandler.ReportStats)
r.Get("/api/agents", apiHandler.ListAgents)
r.Get("/api/agents/{id}", apiHandler.GetAgent)
// Protected routes (auth required) // Protected routes (auth required)
r.Group(func(protectedRoutes chi.Router) { r.Group(func(protectedRoutes chi.Router) {
protectedRoutes.Use(authMgr.Middleware) protectedRoutes.Use(authMgr.Middleware)
protectedRoutes.Get("/", uiHandler.Dashboard) protectedRoutes.Get("/", uiHandler.Dashboard)
protectedRoutes.Get("/agents/{id}", uiHandler.AgentDetail) protectedRoutes.Get("/agents/{id}", uiHandler.AgentDetail)
protectedRoutes.Get("/api/dashboard/table", uiHandler.GetDashboardTable)
protectedRoutes.Get("/api/agents/{id}/stats", uiHandler.GetAgentStats)
protectedRoutes.Get("/api/events", uiHandler.Events)
protectedRoutes.Post("/agents/{id}/hostname", uiHandler.UpdateAgentHostname) protectedRoutes.Post("/agents/{id}/hostname", uiHandler.UpdateAgentHostname)
protectedRoutes.Post("/logout", uiHandler.Logout) protectedRoutes.Post("/logout", uiHandler.Logout)
protectedRoutes.Post("/api/agents/remove-stale", uiHandler.RemoveStaleAgents) protectedRoutes.Post("/api/agents/remove-stale", uiHandler.RemoveStaleAgents)

View File

@@ -4,12 +4,57 @@ import (
"encoding/json" "encoding/json"
"log/slog" "log/slog"
"net/http" "net/http"
"sync"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"nerd-monitor/internal/stats" "nerd-monitor/internal/stats"
"nerd-monitor/internal/store" "nerd-monitor/internal/store"
) )
// Broadcaster manages Server-Sent Events clients.
type Broadcaster struct {
clients map[chan string]bool
mu sync.RWMutex
}
var apiBroadcaster = &Broadcaster{
clients: make(map[chan string]bool),
}
// Broadcast sends a message to all connected SSE clients.
func (b *Broadcaster) Broadcast(message string) {
b.mu.RLock()
defer b.mu.RUnlock()
for clientChan := range b.clients {
select {
case clientChan <- message:
default:
// Client channel is full, skip
}
}
}
// AddClient adds a new SSE client.
func (b *Broadcaster) AddClient(clientChan chan string) {
b.mu.Lock()
defer b.mu.Unlock()
b.clients[clientChan] = true
}
// RemoveClient removes an SSE client.
func (b *Broadcaster) RemoveClient(clientChan chan string) {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.clients, clientChan)
close(clientChan)
}
// GetAPIBroadcaster returns the API broadcaster instance.
func GetAPIBroadcaster() *Broadcaster {
return apiBroadcaster
}
// Handler manages HTTP requests. // Handler manages HTTP requests.
type Handler struct { type Handler struct {
store *store.Store store *store.Store
@@ -40,6 +85,12 @@ func (h *Handler) ReportStats(w http.ResponseWriter, r *http.Request) {
slog.Debug("Updating agent stats", "agentID", agentID, "hostname", stat.Hostname, "cpu", stat.CPUUsage) slog.Debug("Updating agent stats", "agentID", agentID, "hostname", stat.Hostname, "cpu", stat.CPUUsage)
h.store.UpdateAgent(agentID, &stat) h.store.UpdateAgent(agentID, &stat)
// Broadcast update to all connected SSE clients
message := "event: stats-update\ndata: {\"type\": \"stats-update\", \"agentId\": \"" + agentID + "\"}\n\n"
slog.Info("Broadcasting stats update", "agentID", agentID)
apiBroadcaster.Broadcast(message)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
slog.Debug("Stats report processed successfully", "agentID", agentID) slog.Debug("Stats report processed successfully", "agentID", agentID)

View File

@@ -2,11 +2,13 @@ package ui
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"nerd-monitor/internal/api"
"nerd-monitor/internal/auth" "nerd-monitor/internal/auth"
"nerd-monitor/internal/store" "nerd-monitor/internal/store"
"nerd-monitor/views" "nerd-monitor/views"
@@ -16,6 +18,10 @@ import (
type Handler struct { type Handler struct {
store *store.Store store *store.Store
auth *auth.Manager auth *auth.Manager
broadcaster interface {
AddClient(chan string)
RemoveClient(chan string)
}
} }
// New creates a new UI handler. // New creates a new UI handler.
@@ -23,6 +29,7 @@ func New(s *store.Store, a *auth.Manager) *Handler {
return &Handler{ return &Handler{
store: s, store: s,
auth: a, auth: a,
broadcaster: api.GetAPIBroadcaster(),
} }
} }
@@ -212,3 +219,177 @@ func (h *Handler) getStaleAgents() []*store.AgentStats {
return stale return stale
} }
// GetDashboardTable returns HTML fragment for the agent table.
func (h *Handler) GetDashboardTable(w http.ResponseWriter, r *http.Request) {
slog.Debug("HTMX dashboard table request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)
agents := h.store.GetAllAgents()
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Cache-Control", "no-cache")
if len(agents) == 0 {
w.Write([]byte(`<div class="alert alert-info">
<strong> No agents connected</strong>
<p style="margin-top: 0.5rem; font-size: 0.875rem;">
Start an agent to see its statistics appear here.
</p>
</div>`))
return
}
w.Write([]byte(`<div class="card">
<div class="card-title">Active Agents</div>
<table>
<thead>
<tr>
<th>Hostname</th>
<th>CPU Usage</th>
<th>Memory</th>
<th>Disk</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody>`))
for _, agent := range agents {
// Determine online status (agent is online if reported within last 15 seconds)
isOnline := time.Since(agent.LastSeen) < 15*time.Second
statusClass := "status-red"
statusText := "Offline"
if isOnline {
statusClass = "status-green"
statusText = "Online"
}
row := fmt.Sprintf(`<tr>
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="status-badge %s" style="margin: 0;">%s</span>
<a href="/agents/%s" style="color: #3b82f6; text-decoration: none;">%s</a>
</div>
</td>
<td>%.1f%%</td>
<td>%s / %s</td>
<td>%s / %s</td>
<td class="timestamp">%s</td>
</tr>`,
statusClass,
statusText,
agent.ID,
agent.Hostname,
agent.CPUUsage,
formatBytes(agent.RAMUsage),
formatBytes(agent.RAMTotal),
formatBytes(agent.DiskUsage),
formatBytes(agent.DiskTotal),
agent.LastSeen.Format("2006-01-02 15:04:05"))
w.Write([]byte(row))
}
w.Write([]byte(`</tbody></table></div>`))
}
// GetAgentStats returns HTML fragment for agent statistics.
func (h *Handler) GetAgentStats(w http.ResponseWriter, r *http.Request) {
slog.Debug("HTMX agent stats request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)
agentID := chi.URLParam(r, "id")
if agentID == "" {
slog.Warn("Missing agent ID in stats request", "remoteAddr", r.RemoteAddr)
http.Error(w, "missing agent id", http.StatusBadRequest)
return
}
agent := h.store.GetAgent(agentID)
if agent == nil {
slog.Warn("Agent not found for stats request", "agentID", agentID)
http.Error(w, "agent not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Cache-Control", "no-cache")
// Return just the stats content that should be updated
statsHTML := fmt.Sprintf(`<div style="font-size: 2rem; margin: 1rem 0;">%.1f%%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: %.1f%%"></div>
</div>
<div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;">
Last updated: %s
</div>`,
agent.CPUUsage,
agent.CPUUsage,
agent.LastSeen.Format("2006-01-02 15:04:05"))
w.Write([]byte(statsHTML))
}
// Events provides Server-Sent Events for real-time updates.
func (h *Handler) Events(w http.ResponseWriter, r *http.Request) {
slog.Info("SSE connection established", "remoteAddr", r.RemoteAddr)
// Set headers for Server-Sent Events
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// Create a channel for this client
clientChan := make(chan string, 10)
h.broadcaster.AddClient(clientChan)
// Clean up when client disconnects
go func() {
<-r.Context().Done()
slog.Debug("SSE client disconnected", "remoteAddr", r.RemoteAddr)
h.broadcaster.RemoveClient(clientChan)
}()
// Send initial connection event
fmt.Fprintf(w, "event: connected\ndata: {}\n\n")
w.(http.Flusher).Flush()
// Send a test event after 2 seconds
go func() {
time.Sleep(2 * time.Second)
select {
case clientChan <- "event: test\ndata: {\"message\": \"SSE working\"}\n\n":
default:
}
}()
// Listen for broadcast messages
for {
select {
case <-r.Context().Done():
return
case message := <-clientChan:
slog.Debug("Sending SSE message to client", "remoteAddr", r.RemoteAddr, "message", message)
fmt.Fprintf(w, "%s\n", message)
w.(http.Flusher).Flush()
}
}
}
// formatBytes converts bytes to human-readable format.
func formatBytes(bytes uint64) string {
const (
kb = 1024
mb = kb * 1024
gb = mb * 1024
)
switch {
case bytes >= gb:
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(gb))
case bytes >= mb:
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
case bytes >= kb:
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
default:
return fmt.Sprintf("%d B", bytes)
}
}

BIN
server

Binary file not shown.

View File

@@ -9,6 +9,10 @@ templ AgentDetail(agent *store.AgentStats) {
@BaseLayout(agent.Hostname, agentDetailContent(agent)) @BaseLayout(agent.Hostname, agentDetailContent(agent))
} }
templ AgentDetailContent(agent *store.AgentStats) {
@agentDetailContent(agent)
}
templ agentDetailContent(agent *store.AgentStats) { templ agentDetailContent(agent *store.AgentStats) {
<a href="/" class="back-link">← Back to Dashboard</a> <a href="/" class="back-link">← Back to Dashboard</a>
@@ -21,17 +25,19 @@ templ agentDetailContent(agent *store.AgentStats) {
<div class="grid" style="grid-template-columns: 1fr;"> <div class="grid" style="grid-template-columns: 1fr;">
<div class="card"> <div class="card">
<div class="card-title">CPU Usage</div> <div class="card-title">CPU Usage</div>
<div id="cpu-stats" hx-get={ templ.SafeURL("/api/agents/" + agent.ID + "/stats") } hx-trigger="stats-update" hx-swap="innerHTML">
<div class="metric-value" style="font-size: 2rem; margin: 1rem 0;"> <div class="metric-value" style="font-size: 2rem; margin: 1rem 0;">
{ FormatPercent(agent.CPUUsage) } { FormatPercent(agent.CPUUsage) }
</div> </div>
<div class="progress-bar"> <div class="progress-bar">
<div class={ "progress-fill", calcProgressClass(agent.CPUUsage/100) } style={ fmt.Sprintf("width: %.1f%%", agent.CPUUsage) }></div> <div class="progress-fill" class={ "progress-fill", calcProgressClass(agent.CPUUsage/100) } style={ fmt.Sprintf("width: %.1f%%", agent.CPUUsage) }></div>
</div> </div>
<div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;"> <div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;">
Last updated: { FormatTime(agent.LastSeen) } Last updated: { FormatTime(agent.LastSeen) }
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Memory and Disk Usage Cards --> <!-- Memory and Disk Usage Cards -->
<div class="grid" style="margin-top: 1.5rem;"> <div class="grid" style="margin-top: 1.5rem;">

View File

@@ -8,11 +8,16 @@ templ Dashboard(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
@BaseLayout("Dashboard", dashboardContent(agents, staleAgents)) @BaseLayout("Dashboard", dashboardContent(agents, staleAgents))
} }
templ DashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
@dashboardContent(agents, staleAgents)
}
templ dashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentStats) { templ dashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentStats) {
<h2 style="margin-bottom: 1rem;">Agent Status Overview</h2> <h2 style="margin-bottom: 1rem;">Agent Status Overview</h2>
@StaleAgentAlert(len(staleAgents), nil) @StaleAgentAlert(len(staleAgents), nil)
<div id="agent-table-container" hx-get="/api/dashboard/table" hx-trigger="stats-update" hx-swap="innerHTML">
if len(agents) == 0 { if len(agents) == 0 {
<div class="alert alert-info"> <div class="alert alert-info">
<strong> No agents connected</strong> <strong> No agents connected</strong>
@@ -41,6 +46,7 @@ templ dashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentSta
</table> </table>
</div> </div>
} }
</div>
if len(staleAgents) > 0 { if len(staleAgents) > 0 {
<div class="card" style="margin-top: 2rem; border-color: #d97706;"> <div class="card" style="margin-top: 2rem; border-color: #d97706;">
@@ -71,11 +77,11 @@ templ dashboardContent(agents []*store.AgentStats, staleAgents []*store.AgentSta
</div> </div>
</form> </form>
</div> </div>
}
<script> <script>
function toggleAll(checkbox) { function toggleAll(checkbox) {
const checkboxes = document.querySelectorAll('.agent-checkbox'); const checkboxes = document.querySelectorAll('.agent-checkbox');
checkboxes.forEach(cb => cb.checked = checkbox.checked); checkboxes.forEach(cb => cb.checked = checkbox.checked);
} }
</script> </script>
}
} }

View File

@@ -1,3 +0,0 @@
//go:generate templ generate
package views

View File

@@ -6,8 +6,31 @@ templ BaseLayout(title string, content templ.Component) {
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="refresh" content="5" />
<title>{ title } - Nerd Monitor</title> <title>{ title } - Nerd Monitor</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Connect to Server-Sent Events for real-time updates
const eventSource = new EventSource('/api/events');
eventSource.onmessage = function(event) {
console.log('SSE message received:', event.data);
};
eventSource.addEventListener('stats-update', function(event) {
console.log('Stats update event:', event.data);
// Trigger HTMX updates using HTMX's API
const agentTable = document.getElementById('agent-table-container');
const cpuStats = document.getElementById('cpu-stats');
if (agentTable) htmx.trigger(agentTable, 'stats-update');
if (cpuStats) htmx.trigger(cpuStats, 'stats-update');
});
eventSource.addEventListener('connected', function(event) {
console.log('SSE connected');
});
eventSource.onerror = function(event) {
console.log('SSE error:', event);
};
});
</script>
<style> <style>
* { * {
margin: 0; margin: 0;