P1-24: live dashboard — fleet summary tiles + host table
Server-rendered HTML view backed by:
- new store.FleetSummary aggregating host counts + repo bytes +
snapshot total + open alerts + last-24h job rollup in two queries.
- GET /api/hosts (JSON list of hosts in the dashboard projection).
- GET /api/fleet/summary (JSON aggregate, same shape as above).
The HTML page (web/templates/pages/dashboard.html) renders the four
summary tiles + host table directly from store data — no separate
fetch. Per-row state colour comes from .host-row.{degraded,failed,
offline} which paint a 3px left edge so problem hosts are scannable
without reading. HTMX is loaded into the base layout so per-row
"Run now" buttons can hx-post to /hosts/{id}/run-backup, a thin
HTML wrapper that funnels into a new dispatchJob helper shared
with the JSON /api/hosts/{id}/jobs endpoint.
Empty state (zero hosts) collapses to the "no hosts yet" prompt
with the + Add host CTA — matches the v1 mockup.
Template helpers (internal/server/ui/funcs.go) added for byte
formatting (412 GB / 3.7 TB), relative time (3m ago / 2d ago), and
comma grouping (1,847). Pure Go, no template-magic dependency.
Browser-verified end-to-end with seeded fixture data: five hosts
across all four states render with correct dots, accents, last-
backup pills, sizes, snapshot counts, alerts, tags, and the right
action button (Run now / Retry / Run first / View → / offline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FleetSummary is the aggregated view that powers the dashboard's
|
||||
// summary tiles. All numbers are point-in-time snapshots; the
|
||||
// caller polls.
|
||||
type FleetSummary struct {
|
||||
TotalHosts int
|
||||
HostsOnline int
|
||||
HostsDegraded int
|
||||
HostsOffline int
|
||||
|
||||
RepoBytesTotal int64
|
||||
SnapshotsTotal int
|
||||
OpenAlerts int
|
||||
|
||||
// Last-24h job rollup. JobsTotal includes every status; the
|
||||
// breakdown sums to it (modulo any in-flight queued/running).
|
||||
JobsLast24h int
|
||||
JobsLast24hSucceeded int
|
||||
JobsLast24hFailed int
|
||||
JobsLast24hCancelled int
|
||||
}
|
||||
|
||||
// FleetSummary aggregates host + job stats in two queries. Cheap on
|
||||
// SQLite at Phase 1 scale (12 hosts, a few hundred jobs/day) — no
|
||||
// caching layer; the tile is regenerated on every dashboard render.
|
||||
func (s *Store) FleetSummary(ctx context.Context) (FleetSummary, error) {
|
||||
var fs FleetSummary
|
||||
|
||||
row := s.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COUNT(*),
|
||||
COALESCE(SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN status = 'degraded' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN status = 'offline' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(repo_size_bytes), 0),
|
||||
COALESCE(SUM(snapshot_count), 0),
|
||||
COALESCE(SUM(open_alert_count), 0)
|
||||
FROM hosts`)
|
||||
if err := row.Scan(
|
||||
&fs.TotalHosts, &fs.HostsOnline, &fs.HostsDegraded, &fs.HostsOffline,
|
||||
&fs.RepoBytesTotal, &fs.SnapshotsTotal, &fs.OpenAlerts,
|
||||
); err != nil {
|
||||
return FleetSummary{}, fmt.Errorf("store: fleet summary hosts: %w", err)
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339Nano)
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT status, COUNT(*) FROM jobs WHERE created_at > ? GROUP BY status`,
|
||||
cutoff)
|
||||
if err != nil {
|
||||
return FleetSummary{}, fmt.Errorf("store: fleet summary jobs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var n int
|
||||
if err := rows.Scan(&status, &n); err != nil {
|
||||
return FleetSummary{}, fmt.Errorf("store: fleet summary scan: %w", err)
|
||||
}
|
||||
fs.JobsLast24h += n
|
||||
switch status {
|
||||
case "succeeded":
|
||||
fs.JobsLast24hSucceeded = n
|
||||
case "failed":
|
||||
fs.JobsLast24hFailed = n
|
||||
case "cancelled":
|
||||
fs.JobsLast24hCancelled = n
|
||||
}
|
||||
}
|
||||
return fs, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user