86f7c17d9d
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>
79 lines
2.2 KiB
Go
79 lines
2.2 KiB
Go
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()
|
|
}
|