feat(dashboard): Operations-Dashboard mit Live-Health/Resources/Audit/HAProxy
Vorher: Dashboard war Counts + statische Cards. Jetzt operativer Überblick — was läuft, was klemmt, was wurde gerade geändert. Backend (4 neue Endpoints): * GET /api/v1/system/services — systemctl is-active für 8 services (edgeguard-api, scheduler, haproxy, nftables, unbound, chrony, squid, postgresql). Inklusive ActiveEnterTimestamp. * GET /api/v1/system/resources — /proc/loadavg, meminfo, statfs(/), nf_conntrack count+max, uptime. * GET /api/v1/audit/recent?limit=N — letzte audit_log entries. audit-Repo bekommt ListRecent + Entry struct. * GET /api/v1/haproxy/stats — parsed haproxy 'show stat' CSV vom /run/haproxy/admin.sock (postinst addet edgeguard zu haproxy- group für socket-read; haproxy-group exists nach apt install). Frontend Dashboard rewrite: * PageHeader + KPI-Strip (6 tiles, wie zuvor) — bleibt. * Resources-Strip: Load (1/5/15) + Mem-Progress + Disk-Progress + Conntrack-Progress + Uptime. * Service-Health-Grid: 8 Karten mit StatusDot + state. * Recent-Activity-Card (audit-log): action-Tag + actor + subject + relative time. * HAProxy-Backends-Card: backend/server + UP/DOWN-Tag + sessions + bytes_in/out + last_change_age. * WireGuard live (handshake-age, traffic) — bleibt aus früherem Stand. * Cluster + Firewall + SSL + Routing Cards — bleiben. * Polling 10s für services/resources/haproxy, 15s für audit. Plus: postinst usermod -a -G haproxy edgeguard für admin.sock read-permission. Version 1.0.43. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ package audit
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -16,6 +17,47 @@ type Repo struct {
|
||||
|
||||
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
||||
|
||||
// Entry mirrors one audit_log row — ListRecent returns these for
|
||||
// the dashboard's recent-activity card.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Subject *string `json:"subject,omitempty"`
|
||||
Detail json.RawMessage `json:"detail,omitempty"`
|
||||
NodeID *string `json:"node_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListRecent returns the most recent audit entries, newest first.
|
||||
// Pass 0 for a sensible default (10).
|
||||
func (r *Repo) ListRecent(ctx context.Context, limit int) ([]Entry, error) {
|
||||
if r == nil || r.Pool == nil {
|
||||
return []Entry{}, nil
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT id, actor, action, subject, detail, node_id, created_at
|
||||
FROM audit_log
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]Entry, 0, limit)
|
||||
for rows.Next() {
|
||||
var e Entry
|
||||
if err := rows.Scan(&e.ID, &e.Actor, &e.Action, &e.Subject, &e.Detail, &e.NodeID, &e.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Log writes one audit_log row. detail is JSON-encodable (typically a
|
||||
// map[string]any) — empty map means "no payload". If pool is nil
|
||||
// (e.g. dev env without DB), Log silently no-ops so handlers don't
|
||||
|
||||
Reference in New Issue
Block a user