P1-25: host detail page (snapshots tab default)

GET /hosts/{id} renders the v1 host detail layout:

  - persistent header: status dot (pulse if a job is in flight),
    monospace name, tags, plus a metadata strip (os/arch, agent
    version, restic version, "last seen Xs ago" or "online · last
    heartbeat …").
  - vitals strip: four tiles for last backup (status + relative
    time), repo size, snapshot count, open alerts.
  - sub-tabs: Snapshots is active; Jobs / Repo / Settings are
    visible but inert until P2.
  - snapshot table: short id, time (absolute), paths joined with
    " · ", size, file count, restore button (disabled — wires up
    in P3).
  - right rail: run-now stack (backup live, forget/prune/check/
    unlock disabled with the Phase tag), danger-zone remove panel
    (also disabled for now).

Empty state: when a host has no snapshots yet, the table replaces
itself with a "no snapshots yet" prompt that includes the run-now
button (provided the agent is online).

Pagination cap of 50 most-recent snapshots; full pagination lands
when fleet sizes demand it.

Template helpers grew: comma() now accepts int / int32 / int64 so
templates don't fight Go's type inference; joinDot() concatenates
a []string with " · "; absTime() formats time.Time as
YYYY-MM-DD HH:MM:SS; the existing relTime() already accepts T or
*T after P1-27.

Browser-verified end-to-end with seeded fixture data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:20:21 +01:00
parent 9795492f2e
commit cc9dcff816
6 changed files with 271 additions and 4 deletions
+21 -2
View File
@@ -18,6 +18,13 @@ func funcMap() template.FuncMap {
"comma": formatComma,
"deref": derefStr,
"timeNotZero": func(t *time.Time) bool { return t != nil && !t.IsZero() },
"joinDot": func(parts []string) string { return strings.Join(parts, " · ") },
"absTime": func(t time.Time) string {
if t.IsZero() {
return "—"
}
return t.Format("2006-01-02 15:04:05")
},
}
}
@@ -112,8 +119,20 @@ func formatRelTime(v any) string {
// formatComma renders 1847 as "1,847". Used for snapshot counts and
// any other count that benefits from grouping at this scale.
func formatComma(n int) string {
s := strconv.Itoa(n)
// Accepts int / int64 / int32 — anything else returns "—".
func formatComma(v any) string {
var n int64
switch x := v.(type) {
case int:
n = int64(x)
case int32:
n = int64(x)
case int64:
n = x
default:
return "—"
}
s := strconv.FormatInt(n, 10)
if n < 1000 && n > -1000 {
return s
}