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:
Ducky SSH User
2025-12-20 04:51:12 +00:00
commit 765590a1a8
21 changed files with 2144 additions and 0 deletions

164
cmd/agent/main.go Normal file
View File

@@ -0,0 +1,164 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"nerd-monitor/internal/stats"
)
func main() {
var (
server = flag.String("server", "", "server URL (required)")
interval = flag.Duration("interval", 15*time.Second, "reporting interval")
agentID = flag.String("id", "", "agent ID (auto-generated if empty)")
)
flag.Parse()
if *server == "" {
log.Fatal("--server flag is required")
}
// Normalize server URL (add http:// if missing)
*server = normalizeURL(*server)
// Generate or use provided agent ID
id := *agentID
if id == "" {
var err error
id, err = generateAgentID()
if err != nil {
log.Fatalf("Failed to generate agent ID: %v", err)
}
}
log.Printf("Starting agent with ID: %s\n", id)
log.Printf("Reporting to: %s every %v\n", *server, *interval)
// Initialize stats collector
collector := stats.NewCollector()
// Setup graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Report loop
ticker := time.NewTicker(*interval)
defer ticker.Stop()
// Report immediately on start
reportStats(id, *server, collector)
for {
select {
case <-ticker.C:
reportStats(id, *server, collector)
case <-sigChan:
log.Println("Agent shutting down gracefully...")
os.Exit(0)
}
}
}
// reportStats collects and reports system statistics to the server.
func reportStats(agentID, serverURL string, collector *stats.Collector) {
hostname, err := os.Hostname()
if err != nil {
hostname = agentID
}
stat, err := collector.Collect(hostname)
if err != nil {
log.Printf("Error collecting stats: %v", err)
return
}
// Marshal to JSON
body, err := json.Marshal(stat)
if err != nil {
log.Printf("Error marshaling stats: %v", err)
return
}
// Send to server
reportURL := fmt.Sprintf("%s/api/report?id=%s", serverURL, agentID)
resp, err := http.Post(reportURL, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("Error reporting stats: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("Server returned status %d", resp.StatusCode)
return
}
log.Printf("Stats reported: CPU %.1f%% | RAM %s / %s | Disk %s / %s",
stat.CPUUsage,
formatBytes(stat.RAMUsage),
formatBytes(stat.RAMTotal),
formatBytes(stat.DiskUsage),
formatBytes(stat.DiskTotal),
)
}
// generateAgentID creates an ID based on hostname.
func generateAgentID() (string, error) {
hostname, err := os.Hostname()
if err != nil {
return "", fmt.Errorf("failed to get hostname: %w", err)
}
return hostname, nil
}
// normalizeURL ensures the URL has a valid scheme and port.
func normalizeURL(server string) string {
// If no scheme, add http://
if !strings.Contains(server, "://") {
server = "http://" + server
}
// Parse to check if port is specified
u, err := url.Parse(server)
if err != nil {
return server
}
// If no port specified, add default port 8080
if u.Port() == "" {
return u.Scheme + "://" + u.Hostname() + ":8080"
}
return server
}
// 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)
}
}

75
cmd/server/main.go Normal file
View File

@@ -0,0 +1,75 @@
package main
import (
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/go-chi/chi/v5"
"nerd-monitor/internal/api"
"nerd-monitor/internal/auth"
"nerd-monitor/internal/store"
"nerd-monitor/internal/ui"
)
func main() {
var (
addr = flag.String("addr", "0.0.0.0", "server address")
port = flag.String("port", "8080", "server port")
username = flag.String("username", "admin", "admin username")
password = flag.String("password", "admin", "admin password")
)
flag.Parse()
// Initialize dependencies
s := store.New()
authMgr := auth.New(*username, *password)
apiHandler := api.New(s)
uiHandler := ui.New(s, authMgr)
// Setup router
r := chi.NewRouter()
// Public routes (no auth required)
r.Post("/api/report", apiHandler.ReportStats)
r.Get("/api/agents", apiHandler.ListAgents)
r.Get("/api/agents/{id}", apiHandler.GetAgent)
r.Get("/login", uiHandler.Login)
r.Post("/login", uiHandler.Login)
// Protected routes (auth required)
r.Group(func(protectedRoutes chi.Router) {
protectedRoutes.Use(authMgr.Middleware)
protectedRoutes.Get("/", uiHandler.Dashboard)
protectedRoutes.Get("/agents/{id}", uiHandler.AgentDetail)
protectedRoutes.Post("/logout", uiHandler.Logout)
protectedRoutes.Post("/api/agents/remove-stale", uiHandler.RemoveStaleAgents)
protectedRoutes.Post("/api/agents/{id}/delete", uiHandler.DeleteAgent)
})
// Setup server
server := &http.Server{
Addr: *addr + ":" + *port,
Handler: r,
}
// Graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Shutting down server...")
server.Close()
os.Exit(0)
}()
log.Printf("Starting server on http://%s:%s\n", *addr, *port)
log.Printf("Login with %s / %s\n", *username, *password)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}