feat: system monitoring + push agent (merge plan M3)

systems + system_stats tables; agents push JSON every N seconds
to POST /api/systems/ingest with a per-system bearer token. The
server stores each sample, keeps a `latest` snapshot for cheap
list/widget reads, prunes stats after 7d, and marks systems down
when silent > max(3x interval, 30s) — firing system.up/down
webhook events on transitions.

cmd/dash-agent is a standalone Linux collector: /proc for
cpu/mem/net/load/uptime, /sys hwmon temps, statfs disk, and
container liveness via /var/run/docker.sock when present.
No imports from the server module — the sample shape is
duplicated deliberately so the agent stays a single static
binary.

Frontend: /systems card grid with usage bars, /systems/:id with
recharts cpu/mem/net/load, temps + containers tables, and the
agent command; `system` board widget; nav + palette + WidgetDialog
picker wired.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:02:58 +02:00
1 parent 2f3e8a1879
commit 74c987c18a
16 files changed
+1541 -10

No files matched your search

+9
View File
@@ -41,6 +41,7 @@ func newServer(logger *zap.Logger, db *sql.DB, iconsDir string) *Server {
s.lookup = domain.NewLookup().Run
widget.Register(&monitorWidget{db: db})
widget.Register(&domainWidget{db: db})
widget.Register(&systemWidget{db: db})
return s
}
@@ -92,6 +93,14 @@ func (s *Server) routes() *gin.Engine {
v1.GET("/domains/:id/checks", s.domainChecks)
v1.POST("/notify/test", s.notifyTest)
v1.GET("/systems", s.listSystemsH)
v1.POST("/systems", s.createSystem)
v1.GET("/systems/:id", s.getSystem)
v1.PATCH("/systems/:id", s.patchSystem)
v1.DELETE("/systems/:id", s.deleteSystem)
v1.GET("/systems/:id/stats", s.systemStats)
v1.POST("/systems/ingest", s.ingestSystem)
v1.GET("/status", s.getStatus)
v1.GET("/widgets/types", s.widgetTypes)
v1.GET("/widgets/:id/data", s.widgetData)