Files
restic-manager/web/templates/partials/host_chrome.html
T
steve 1111124573 P3-09 + P3-X3: snapshot diff + recent-restores line
P3-09 — snapshot diff dispatcher.
- POST /api/hosts/{id}/snapshots/diff (and the unprefixed HTMX-form
  variant) takes {snapshot_a, snapshot_b}, validates both belong to
  the host (long id / short id / prefix match), checks the agent is
  online, mints a JobDiff, ships command.run with DiffPayload, writes
  a host.snapshot_diff audit row, returns HX-Redirect to the live
  job page (or JSON {job_id, job_url} for REST callers).
- Two-snapshot guard: POSTing diff(a,a) returns 422.
- UI: small panel on the host_detail right rail (visible when the
  host has 2+ snapshots) with two short-id inputs and a Diff button.
  Output renders on the standard live job page where the operator
  reads the per-line diff text directly.

P3-X3 — recent-restores line.
- hostChromeData grows RestoreStatus / RestoreAt / RestoreJobID
  populated via store.LatestJobByKind(host_id, 'restore') (already
  exists, used by the init line).
- host_chrome.html renders a small line below the existing init-status
  one with status-coloured copy + a link to the job log. Hidden when
  no restore has ever run on this host.

Tests:
- diff_test covers happy path (correct DiffPayload + HX-Redirect),
  same-id rejection (422), unknown-id rejection (422). Adds a
  seedTwoSnapshots helper since ReplaceHostSnapshots is atomic-swap
  (calling seedSnapshot twice would only leave the second).

Restage block (CLAUDE.md) deferred to the end of the restore phase.
2026-05-04 15:38:28 +01:00

155 lines
8.2 KiB
HTML

{{/*
host_chrome — header (status dot + name + tags + meta), vitals
strip, and the six sub-tab nav for any /hosts/{id}/... page.
Expects .Page to expose:
.Host — store.Host
.SubTab — "snapshots" | "sources" | "schedules" | "repo" | "jobs" | "settings"
.SourceGroupCount — int
.ScheduleCount — int
.ScheduleVersion — int64 (host_schedule_version)
.Crumb — string ("snapshots" / "sources" / etc — appended after host name)
*/}}
{{define "host_chrome"}}
{{$page := .Page}}
{{$host := $page.Host}}
<div class="max-w-[1280px] mx-auto px-8 pt-7">
<div class="crumbs">
<a href="/">Dashboard</a><span class="sep">/</span>
{{if eq $page.SubTab "snapshots"}}
<span class="text-ink-mid">{{$host.Name}}</span>
{{else}}
<a href="/hosts/{{$host.ID}}">{{$host.Name}}</a><span class="sep">/</span>
<span class="text-ink-mid">{{$page.Crumb}}</span>
{{end}}
</div>
{{/* ---------- header ---------- */}}
<div class="flex items-start justify-between mt-3.5">
<div>
<div class="flex items-center gap-3">
{{if eq $host.Status "online"}}
<span class="dot dot-online{{if $host.CurrentJobID}} pulse{{end}}"></span>
{{else if eq $host.Status "degraded"}}
<span class="dot dot-degraded"></span>
{{else if eq $host.Status "offline"}}
<span class="dot dot-offline"></span>
{{else}}
<span class="dot dot-failed"></span>
{{end}}
<h1 class="mono text-[26px] font-medium tracking-[0.005em] text-ink">{{$host.Name}}</h1>
<div class="flex gap-1.5">{{range $host.Tags}}<span class="tag">{{.}}</span>{{end}}</div>
{{if gt $page.ScheduleVersion 0}}
<span class="mono text-[11px] text-ink-mute ml-2">
version {{$page.ScheduleVersion}}
{{if eq $page.ScheduleVersion $host.AppliedScheduleVersion}}
<span class="text-ok">· agent in sync</span>
{{else}}
<span class="text-warn">· agent at v{{$host.AppliedScheduleVersion}}</span>
{{end}}
</span>
{{end}}
</div>
<div class="flex items-center gap-3 mt-3 text-[13px] text-ink-mute">
<span class="mono text-ink-mid">{{$host.OS}}/{{$host.Arch}}</span>
<span class="text-ink-fade">·</span>
<span>agent <span class="mono text-ink-mid">{{if $host.AgentVersion}}{{$host.AgentVersion}}{{else}}—{{end}}</span></span>
<span class="text-ink-fade">·</span>
<span>restic <span class="mono text-ink-mid">{{if $host.ResticVersion}}{{$host.ResticVersion}}{{else}}—{{end}}</span></span>
<span class="text-ink-fade">·</span>
{{if eq $host.Status "offline"}}
<span>last seen <span class="mono text-ink-mid">{{relTime $host.LastSeenAt}}</span></span>
{{else}}
<span>online · last heartbeat <span class="mono text-ink-mid">{{relTime $host.LastSeenAt}}</span></span>
{{end}}
</div>
</div>
<div class="flex items-center gap-2">
<button class="btn" disabled title="per-source-group Run-now lives on the Sources tab">Run&nbsp;backup&nbsp;now</button>
<button class="btn">Edit credentials</button>
<button class="btn btn-ghost text-base px-2.5"></button>
</div>
</div>
{{/* ---------- vitals strip ---------- */}}
<div class="grid grid-cols-12 gap-6 mt-6 py-5 border-y border-line-soft">
<div class="col-span-3">
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Last backup</div>
<div class="mono text-[18px] text-ink mt-1">
{{if eq (deref $host.LastBackupStatus) "succeeded"}}
<span class="text-ok">succeeded</span>
{{else if eq (deref $host.LastBackupStatus) "failed"}}
<span class="text-bad">failed</span>
{{else if eq (deref $host.LastBackupStatus) "cancelled"}}
<span class="text-warn">cancelled</span>
{{else}}
<span class="text-ink-fade italic">never run</span>
{{end}}
{{if $host.LastBackupAt}} · {{relTime $host.LastBackupAt}}{{end}}
</div>
</div>
<div class="col-span-3">
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Repo size</div>
<div class="mono text-[18px] text-ink mt-1">{{bytes $host.RepoSizeBytes}}</div>
</div>
<div class="col-span-3">
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Snapshots</div>
<div class="mono text-[18px] text-ink mt-1">{{comma $host.SnapshotCount}}</div>
</div>
<div class="col-span-3">
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Open alerts</div>
<div class="mono text-[18px] mt-1 {{if gt $host.OpenAlertCount 0}}text-bad{{else}}text-ok{{end}}">
{{if eq $host.OpenAlertCount 0}}0 · all clear{{else}}{{$host.OpenAlertCount}} · review →{{end}}
</div>
</div>
</div>
{{/* ---------- repo init line (P2R-09) ---------- */}}
{{if $page.InitStatus}}
<div class="text-[11.5px] text-ink-mute mt-2.5 leading-[1.5]">
{{if eq $page.InitStatus "succeeded"}}
repo ready · initialised <span class="mono text-ink-mid" {{if $page.InitAt}}title="{{$page.InitAt.Format "2006-01-02 15:04:05 MST"}}"{{end}}>{{relTime $page.InitAt}}</span>
{{else if eq $page.InitStatus "failed"}}
<span class="text-bad font-medium">init failed</span> ·
<a href="/jobs/{{$page.InitJobID}}" class="link mono">job {{$page.InitJobID}}</a> · retry from the Repo tab's danger zone
{{else if eq $page.InitStatus "running"}}
<span class="text-accent">init running…</span> · <a href="/jobs/{{$page.InitJobID}}" class="link mono">live log →</a>
{{else if eq $page.InitStatus "queued"}}
<span class="text-ink-fade">init queued</span> · <a href="/jobs/{{$page.InitJobID}}" class="link mono">job {{$page.InitJobID}}</a>
{{end}}
</div>
{{end}}
{{/* ---------- latest restore line (P3-X3) ---------- */}}
{{if $page.RestoreStatus}}
<div class="text-[11.5px] text-ink-mute mt-1 leading-[1.5]">
{{if eq $page.RestoreStatus "succeeded"}}
last restore · <span class="text-ok">succeeded</span> <span class="mono text-ink-mid">{{relTime $page.RestoreAt}}</span> ·
<a href="/jobs/{{$page.RestoreJobID}}" class="link mono">job log →</a>
{{else if eq $page.RestoreStatus "failed"}}
last restore · <span class="text-bad font-medium">failed</span> <span class="mono text-ink-mid">{{relTime $page.RestoreAt}}</span> ·
<a href="/jobs/{{$page.RestoreJobID}}" class="link mono">job log →</a>
{{else if eq $page.RestoreStatus "running"}}
<span class="text-accent">restore running…</span> · <a href="/jobs/{{$page.RestoreJobID}}" class="link mono">live log →</a>
{{else if eq $page.RestoreStatus "cancelled"}}
last restore · <span class="text-warn">cancelled</span> <span class="mono text-ink-mid">{{relTime $page.RestoreAt}}</span> ·
<a href="/jobs/{{$page.RestoreJobID}}" class="link mono">job log →</a>
{{else if eq $page.RestoreStatus "queued"}}
<span class="text-ink-fade">restore queued</span> · <a href="/jobs/{{$page.RestoreJobID}}" class="link mono">job {{$page.RestoreJobID}}</a>
{{end}}
</div>
{{end}}
{{/* ---------- secondary tabs ---------- */}}
<div class="flex items-end mt-1.5">
<a class="sub-tab {{if eq $page.SubTab "snapshots"}}active{{end}}" href="/hosts/{{$host.ID}}">Snapshots <span class="mono text-ink-fade text-[11px] ml-1">{{comma $host.SnapshotCount}}</span></a>
<a class="sub-tab {{if eq $page.SubTab "sources"}}active{{end}}" href="/hosts/{{$host.ID}}/sources">Sources <span class="mono text-ink-fade text-[11px] ml-1">{{$page.SourceGroupCount}}</span></a>
<a class="sub-tab {{if eq $page.SubTab "schedules"}}active{{end}}" href="/hosts/{{$host.ID}}/schedules">Schedules <span class="mono text-ink-fade text-[11px] ml-1">{{$page.ScheduleCount}}</span></a>
<a class="sub-tab {{if eq $page.SubTab "repo"}}active{{end}}" href="/hosts/{{$host.ID}}/repo">Repo</a>
<div class="sub-tab" title="lands later">Jobs</div>
<div class="sub-tab" title="lands later">Settings</div>
</div>
</div>
{{end}}