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>
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
// Package audit appends rows to the audit_log table. Every mutation
|
|
// in the API funnels through this so the operator can answer
|
|
// "who did what when?" from a single SELECT.
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type Repo struct {
|
|
Pool *pgxpool.Pool
|
|
}
|
|
|
|
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
|
|
// have to guard each call site.
|
|
func (r *Repo) Log(ctx context.Context, actor, action, subject string, detail any, nodeID string) error {
|
|
if r == nil || r.Pool == nil {
|
|
return nil
|
|
}
|
|
var detailJSON []byte
|
|
if detail != nil {
|
|
var err error
|
|
detailJSON, err = json.Marshal(detail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var subjectArg any = subject
|
|
if subject == "" {
|
|
subjectArg = nil
|
|
}
|
|
var nodeArg any = nodeID
|
|
if nodeID == "" {
|
|
nodeArg = nil
|
|
}
|
|
_, err := r.Pool.Exec(ctx,
|
|
`INSERT INTO audit_log (actor, action, subject, detail, node_id)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
actor, action, subjectArg, detailJSON, nodeArg)
|
|
return err
|
|
}
|