02e4ef7544
Smoothes the rough edges that came up exercising a live deployment.
First-run bootstrap UI: /bootstrap renders a username + password form
that uses the in-memory token directly (operator no longer copies it
out of the log); /login redirects there while bootstrap is available.
Agent reliability: failJob synthetic envelopes so command.run early
returns no longer hang the server-side job; runtime probe of restic
restore --help drives --no-ownership instead of version sniffing
(0.18.x had it removed). Server unit re-shaped: ProtectSystem=full
plus ReadWritePaths=/etc/restic-manager, no ProtectHome — restore
can now write anywhere a user might want.
Restore wizard: default target is /root/rm-restore/<job-id>/ with
clearer help text. Re-init confirm input uses .field (was .input,
which doesn't exist — text was invisible).
NS-01 host delete: store DeleteHost, admin-band /hosts/{id}/delete
with hostname-confirm danger zone, audit, FK cascade, live WS close.
NS-02 enrollment-token recovery: outstanding-tokens panel on
/hosts/new, regenerate (preserves attachments) and revoke handlers
+ audit, store-level ListOutstandingEnrollmentTokens and
DeleteEnrollmentToken.
NS-03 repo init / probe surface: migration 0020 adds
hosts.repo_status + repo_status_error; WS handler projects every
init job's outcome onto the host row (idempotent already-initialised
collapses to ready); creds-save resets status and dispatches a fresh
probe; /hosts/{id}/repo/probe retry endpoint with banner.
NS-04 dashboard live + sort + filter: query-string filter
(q/status/repo_status/tag/sort/dir), 5s htmx live poll mirroring the
alerts pattern with a localStorage live toggle, sortable column
headers, filter row + clear.
Alerts page: ack'd-by line resolves user_id ULID to username.
Compose.yaml ignored — host-specific.
387 lines
22 KiB
HTML
387 lines
22 KiB
HTML
{{define "title"}}{{.Title}}{{end}}
|
|
|
|
{{define "content"}}
|
|
{{template "host_chrome" .}}
|
|
{{$page := .Page}}
|
|
{{$host := $page.Host}}
|
|
<div class="max-w-[1280px] mx-auto px-8 pb-14 pt-6 grid grid-cols-12 gap-6 items-start">
|
|
|
|
<div class="col-span-8">
|
|
|
|
{{/* ---------- Repo status (NS-03) ---------- */}}
|
|
{{if eq $host.RepoStatus "init_failed"}}
|
|
<div class="rounded-[7px] px-4 py-3.5 mb-5"
|
|
style="border: 1px solid color-mix(in oklch, var(--bad), transparent 55%); background: color-mix(in oklch, var(--bad), transparent 90%);">
|
|
<div class="flex items-center justify-between gap-3 mb-1.5">
|
|
<div class="text-[12.5px] font-semibold text-bad uppercase tracking-[0.08em]">Repo unreachable</div>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/probe">
|
|
<button type="submit" class="btn btn-sm"
|
|
{{if $page.Online}}{{else}}disabled title="host is offline"{{end}}>Retry probe</button>
|
|
</form>
|
|
</div>
|
|
<div class="text-[12.5px] text-ink-mid leading-[1.55]">
|
|
The last init / probe against this host's repo failed. Fix the
|
|
credentials below and save (the save kicks a fresh probe), or
|
|
click <span class="mono">Retry probe</span> if you've changed
|
|
something out-of-band.
|
|
</div>
|
|
{{if $host.RepoStatusError}}
|
|
<pre class="mono text-[11.5px] text-ink-mid mt-2.5 whitespace-pre-wrap leading-[1.5]">{{$host.RepoStatusError}}</pre>
|
|
{{end}}
|
|
</div>
|
|
{{else if eq $host.RepoStatus "ready"}}
|
|
<div class="text-[12px] text-ok mono mb-5">✓ repo reachable with current credentials</div>
|
|
{{end}}
|
|
|
|
{{/* ---------- Connection ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mb-3.5">Connection</h2>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/credentials" class="panel rounded-[7px] p-5">
|
|
{{if $page.CredentialsError}}
|
|
<div class="rounded-[6px] px-3.5 py-3 text-[13px] mb-4"
|
|
style="border: 1px solid color-mix(in oklch, var(--bad), transparent 60%); background: color-mix(in oklch, var(--bad), transparent 92%);">
|
|
{{$page.CredentialsError}}
|
|
</div>
|
|
{{end}}
|
|
{{if eq $page.SavedSection "credentials"}}
|
|
<div class="text-[12px] text-ok mb-3 mono">✓ saved</div>
|
|
{{end}}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="field-label" for="repo_url">Repo URL</label>
|
|
<input id="repo_url" name="repo_url" type="text" class="field mono" value="{{$page.RepoURL}}" required />
|
|
<div class="field-help">e.g. <span class="mono text-ink-mid">rest:http://192.168.0.99:8000/{{$host.Name}}/</span></div>
|
|
</div>
|
|
<div>
|
|
<label class="field-label" for="repo_username">Username</label>
|
|
<input id="repo_username" name="repo_username" type="text" class="field mono" value="{{$page.RepoUsername}}" />
|
|
<div class="field-help">Sent as the rest-server <span class="mono text-ink-mid">--htpasswd</span> user.</div>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<label class="field-label" for="repo_password">Password</label>
|
|
<input id="repo_password" name="repo_password" type="password" class="field mono" placeholder="{{if $page.HasPassword}}•••••••••••••••• · stored, leave blank to keep{{else}}— not yet set —{{end}}" autocomplete="new-password" />
|
|
<div class="field-help">Stored AEAD-encrypted; pushed to the agent over WS. Leave blank to keep the existing password.</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-line-soft flex gap-2">
|
|
<button type="submit" class="btn btn-primary">Save credentials</button>
|
|
</div>
|
|
</form>
|
|
|
|
{{/* ---------- Admin credentials (optional) ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">
|
|
Admin credentials <span class="text-ink-fade normal-case">· prune-only · optional</span>
|
|
</h2>
|
|
<form method="post" action="/hosts/{{$host.ID}}/admin-credentials" class="panel rounded-[7px] p-5">
|
|
{{if $page.AdminCredsError}}
|
|
<div class="rounded-[6px] px-3.5 py-3 text-[13px] mb-4"
|
|
style="border: 1px solid color-mix(in oklch, var(--bad), transparent 60%); background: color-mix(in oklch, var(--bad), transparent 92%);">
|
|
{{$page.AdminCredsError}}
|
|
</div>
|
|
{{end}}
|
|
{{if eq $page.SavedSection "admin_credentials"}}
|
|
<div class="text-[12px] text-ok mb-3 mono">✓ saved</div>
|
|
{{end}}
|
|
<p class="text-[12.5px] text-ink-mid leading-[1.6] mb-4 max-w-[640px]">
|
|
Only needed for rest-server repos that distinguish an append-only
|
|
user (everyday backups) from a delete-capable user (prune /
|
|
forget). For S3 / B2 / SFTP / local, leave this blank — the
|
|
everyday repo credentials handle prune too.
|
|
</p>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="field-label" for="admin_repo_url">Repo URL <span class="text-ink-fade">· usually same as above</span></label>
|
|
<input id="admin_repo_url" name="repo_url" type="text" class="field mono" value="{{$page.AdminURL}}" />
|
|
</div>
|
|
<div>
|
|
<label class="field-label" for="admin_repo_username">Username</label>
|
|
<input id="admin_repo_username" name="repo_username" type="text" class="field mono" value="{{$page.AdminUsername}}" />
|
|
</div>
|
|
<div class="col-span-2">
|
|
<label class="field-label" for="admin_repo_password">Password</label>
|
|
<input id="admin_repo_password" name="repo_password" type="password" class="field mono"
|
|
placeholder="{{if $page.HasAdminPassword}}•••••••••••••••• · stored, leave blank to keep{{else}}— not yet set —{{end}}"
|
|
autocomplete="new-password" />
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-line-soft flex gap-2 items-center">
|
|
<button type="submit" class="btn btn-primary">Save admin credentials</button>
|
|
{{if $page.HasAdminPassword}}
|
|
<button type="submit" form="admin-creds-clear" class="btn btn-secondary"
|
|
onclick="return confirm('Clear admin credentials? Prune jobs will be refused until you re-set them.');">Clear</button>
|
|
{{end}}
|
|
</div>
|
|
</form>
|
|
{{if $page.HasAdminPassword}}
|
|
<form id="admin-creds-clear" method="post" action="/hosts/{{$host.ID}}/admin-credentials/delete"></form>
|
|
{{end}}
|
|
|
|
{{/* ---------- Bandwidth ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">Bandwidth · host-wide</h2>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/bandwidth" class="panel rounded-[7px] p-5">
|
|
{{if $page.BandwidthError}}
|
|
<div class="rounded-[6px] px-3.5 py-3 text-[13px] mb-4"
|
|
style="border: 1px solid color-mix(in oklch, var(--bad), transparent 60%); background: color-mix(in oklch, var(--bad), transparent 92%);">
|
|
{{$page.BandwidthError}}
|
|
</div>
|
|
{{end}}
|
|
{{if eq $page.SavedSection "bandwidth"}}
|
|
<div class="text-[12px] text-ok mb-3 mono">✓ saved</div>
|
|
{{end}}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="field-label" for="bandwidth_up">Upload limit <span class="text-ink-fade">· KB/s · blank = no cap</span></label>
|
|
<input id="bandwidth_up" name="bandwidth_up" type="number" min="0" class="field mono" value="{{$page.BandwidthUp}}" placeholder="—" />
|
|
</div>
|
|
<div>
|
|
<label class="field-label" for="bandwidth_down">Download limit <span class="text-ink-fade">· KB/s · blank = no cap</span></label>
|
|
<input id="bandwidth_down" name="bandwidth_down" type="number" min="0" class="field mono" value="{{$page.BandwidthDown}}" placeholder="—" />
|
|
</div>
|
|
</div>
|
|
<div class="field-help mt-3">
|
|
Applies to every backup, restore, and prune job for this host. Maps to <span class="mono text-ink-mid">restic --limit-upload</span> / <span class="mono text-ink-mid">--limit-download</span>.
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-line-soft flex gap-2">
|
|
<button type="submit" class="btn btn-primary">Save bandwidth caps</button>
|
|
</div>
|
|
</form>
|
|
|
|
{{/* ---------- Maintenance ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">Maintenance · server-side cadences</h2>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/maintenance" class="panel rounded-[7px] p-5">
|
|
{{if $page.MaintenanceError}}
|
|
<div class="rounded-[6px] px-3.5 py-3 text-[13px] mb-4"
|
|
style="border: 1px solid color-mix(in oklch, var(--bad), transparent 60%); background: color-mix(in oklch, var(--bad), transparent 92%);">
|
|
{{$page.MaintenanceError}}
|
|
</div>
|
|
{{end}}
|
|
{{if eq $page.SavedSection "maintenance"}}
|
|
<div class="text-[12px] text-ok mb-3 mono">✓ saved</div>
|
|
{{end}}
|
|
|
|
{{$m := $page.Maintenance}}
|
|
<div class="grid grid-cols-12 gap-3 items-center text-[13px] mb-3 text-[11px] uppercase tracking-[0.08em] text-ink-fade">
|
|
<div class="col-span-2">Verb</div>
|
|
<div class="col-span-5">Cron cadence</div>
|
|
<div class="col-span-3">Notes</div>
|
|
<div class="col-span-2 text-right">Enabled</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-3 items-center py-3 border-t border-line-soft">
|
|
<div class="col-span-2 mono text-ink font-medium">forget</div>
|
|
<div class="col-span-5"><input type="text" name="forget_cron" class="field mono" value="{{$m.ForgetCron}}" required /></div>
|
|
<div class="col-span-3 text-[12px] text-ink-fade leading-[1.5]">Per source group, using each group's retention policy.</div>
|
|
<div class="col-span-2 text-right">
|
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" name="forget_enabled" value="1" {{if $m.ForgetEnabled}}checked{{end}} class="w-3.5 h-3.5" />
|
|
<span class="mono text-[11px]">on</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-3 items-center py-3 border-t border-line-soft">
|
|
<div class="col-span-2 mono text-ink font-medium">prune</div>
|
|
<div class="col-span-5"><input type="text" name="prune_cron" class="field mono" value="{{$m.PruneCron}}" required /></div>
|
|
<div class="col-span-3 text-[12px] text-ink-fade leading-[1.5]">Reclaims storage made dead by forget. Heavy — weekly only.</div>
|
|
<div class="col-span-2 text-right">
|
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" name="prune_enabled" value="1" {{if $m.PruneEnabled}}checked{{end}} class="w-3.5 h-3.5" />
|
|
<span class="mono text-[11px]">on</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-3 items-center py-3 border-t border-line-soft">
|
|
<div class="col-span-2 mono text-ink font-medium">check</div>
|
|
<div class="col-span-5"><input type="text" name="check_cron" class="field mono" value="{{$m.CheckCron}}" required /></div>
|
|
<div class="col-span-3 text-[12px] text-ink-fade leading-[1.5]">
|
|
<span class="mono text-ink-mid">--read-data-subset</span>
|
|
<input type="number" name="check_subset_pct" min="0" max="100" value="{{$m.CheckSubsetPct}}" class="field mono inline-block w-16 px-2 py-1" style="font-size: 11px;" />%
|
|
</div>
|
|
<div class="col-span-2 text-right">
|
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" name="check_enabled" value="1" {{if $m.CheckEnabled}}checked{{end}} class="w-3.5 h-3.5" />
|
|
<span class="mono text-[11px]">on</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-4 border-t border-line-soft flex gap-2 items-center">
|
|
<button type="submit" class="btn btn-primary">Save cadences</button>
|
|
<span class="text-[12px] text-ink-fade ml-2">Server-side ticker drives execution — independent of the agent's cron.</span>
|
|
</div>
|
|
</form>
|
|
|
|
{{/* ---------- Run now · one-time ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">Run now · one-time</h2>
|
|
<div class="panel rounded-[7px] p-5">
|
|
<p class="text-[12.5px] text-ink-mid leading-[1.6] mb-4 max-w-[640px]">
|
|
Operator-triggered. Output streams live to the job log. Cadence-driven runs land independently from the server-side ticker.
|
|
</p>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<button type="button"
|
|
hx-post="/hosts/{{$host.ID}}/repo/check"
|
|
hx-swap="none"
|
|
hx-confirm="Run check now ({{$m.CheckSubsetPct}}% data subset)?"
|
|
class="btn btn-secondary"
|
|
{{if not $page.Online}}disabled title="agent is offline"{{end}}>
|
|
check
|
|
</button>
|
|
<button type="button"
|
|
hx-post="/hosts/{{$host.ID}}/repo/prune"
|
|
hx-swap="none"
|
|
hx-confirm="Run prune now? Removes data not referenced by any snapshot — heavy operation."
|
|
class="btn btn-secondary"
|
|
{{if not $page.HasAdminPassword}}disabled title="set admin credentials first"{{else if not $page.Online}}disabled title="agent is offline"{{end}}>
|
|
prune
|
|
</button>
|
|
<button type="button"
|
|
hx-post="/hosts/{{$host.ID}}/repo/unlock"
|
|
hx-swap="none"
|
|
hx-confirm="Clear stale repo locks?"
|
|
class="btn btn-secondary"
|
|
{{if not $page.Online}}disabled title="agent is offline"{{end}}>
|
|
unlock
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* ---------- Host-default hooks ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-9 mb-3.5">Host-default hooks</h2>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/hooks" class="panel rounded-[7px] p-5">
|
|
<p class="text-[12px] text-ink-mute leading-[1.55] mb-3">
|
|
Defaults applied to every backup that doesn't set its own. Per-source-group hooks (on the
|
|
<a href="/hosts/{{$host.ID}}/sources" class="text-accent">Sources</a> tab) override these.
|
|
</p>
|
|
<div class="text-[12px] text-warn leading-[1.55] mb-3"
|
|
style="background: color-mix(in oklch, var(--warn), transparent 92%); border: 1px solid color-mix(in oklch, var(--warn), transparent 75%); padding: 8px 10px; border-radius: 5px;">
|
|
Hooks run as the agent service user — root on Linux, LocalSystem on Windows.
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="field-label" for="host_pre_hook">Pre-backup hook (default)</label>
|
|
<textarea id="host_pre_hook" name="pre_hook" class="field mono" rows="3" style="resize: vertical;"
|
|
placeholder="# default; per-group overrides win">{{$page.HostPreHook}}</textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="field-label" for="host_post_hook">Post-backup hook (default)</label>
|
|
<textarea id="host_post_hook" name="post_hook" class="field mono" rows="3" style="resize: vertical;"
|
|
placeholder="# RM_JOB_STATUS in env">{{$page.HostPostHook}}</textarea>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button type="submit" class="btn btn-primary">Save host-default hooks</button>
|
|
</div>
|
|
</form>
|
|
|
|
{{/* ---------- Danger zone ---------- */}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-bad mt-9 mb-3.5">Danger zone</h2>
|
|
<div class="panel rounded-[7px] p-5"
|
|
style="border-color: color-mix(in oklch, var(--bad), transparent 70%);">
|
|
<div class="flex items-start justify-between gap-6">
|
|
<div class="flex-1">
|
|
<div class="text-[14px] font-semibold text-ink">Re-initialise repo</div>
|
|
<p class="text-pretty text-[12.5px] text-ink-mid leading-[1.6] mt-2 max-w-[580px]">
|
|
Tries to <span class="mono text-ink-mid">DELETE</span> the rest-server's copy of this repo, then runs
|
|
<span class="mono text-ink-mid">restic init</span> against the empty path. Most rest-server setups run with
|
|
<span class="mono text-ink-mid">--append-only</span> and refuse the DELETE — the future P2R-09 flow surfaces
|
|
guided cleanup steps in that case.
|
|
</p>
|
|
<p class="text-[12px] text-ink-fade leading-[1.55] mt-2">
|
|
All snapshots are lost; this host's schedule version stays the same and the agent's
|
|
<span class="mono text-ink-mid">secrets.enc</span> is reused.
|
|
</p>
|
|
</div>
|
|
<form method="post" action="/hosts/{{$host.ID}}/repo/reinit"
|
|
class="flex-none flex flex-col items-end" style="gap: 8px;"
|
|
onsubmit="return confirm('Re-initialise the repo on host "{{$host.Name}}"? Existing snapshots are lost if the rest-server allows the wipe; restic refuses if it sees a config file already there.');">
|
|
<input type="text" name="confirm_hostname" required autocomplete="off"
|
|
placeholder="type hostname to confirm"
|
|
class="field mono"
|
|
style="width: 240px; height: 30px; padding: 0 8px; font-size: 12px;">
|
|
<button type="submit" class="btn btn-danger btn-lg whitespace-nowrap"
|
|
{{if eq $host.Status "online"}}{{else}}disabled title="host is offline"{{end}}>Re-init repo…</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* ---------- right rail ---------- */}}
|
|
<aside class="col-span-4">
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mb-3.5">Storage</h2>
|
|
<div class="panel rounded-[7px] p-5">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Repo size</div>
|
|
<div class="mono text-[20px] text-ink mt-1">{{bytes $host.RepoSizeBytes}}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-[11px] text-ink-fade uppercase tracking-[0.08em]">Snapshots</div>
|
|
<div class="mono text-[20px] text-ink mt-1">{{comma $host.SnapshotCount}}</div>
|
|
<div class="text-[11.5px] text-ink-mute mt-0.5">across {{len $page.GroupNames}} source group{{if ne (len $page.GroupNames) 1}}s{{end}}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* ---------- Repo health ---------- */}}
|
|
{{if $page.StatsView}}
|
|
{{$s := $page.StatsView}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">Repo health</h2>
|
|
<div class="panel rounded-[7px] p-5 text-[13px]">
|
|
{{if $s.LockPresent}}
|
|
<div class="rounded-[6px] px-3.5 py-3 text-[12.5px] mb-4"
|
|
style="border: 1px solid color-mix(in oklch, var(--warn), transparent 60%); background: color-mix(in oklch, var(--warn), transparent 92%);">
|
|
Stale lock detected on the most recent check. Run <span class="mono">unlock</span> above to clear it before the next backup.
|
|
</div>
|
|
{{end}}
|
|
<dl class="grid grid-cols-2 gap-y-2 gap-x-4">
|
|
{{if $s.HasTotalSize}}
|
|
<dt class="text-ink-fade">Total size</dt>
|
|
<dd class="mono text-right">{{bytes $s.TotalSizeBytes}}</dd>
|
|
{{end}}
|
|
{{if $s.HasRawSize}}
|
|
<dt class="text-ink-fade">Raw size <span class="text-ink-fade text-[11px]">· pre-dedup</span></dt>
|
|
<dd class="mono text-right">{{bytes $s.RawSizeBytes}}</dd>
|
|
{{end}}
|
|
{{if $s.HasLastCheck}}
|
|
<dt class="text-ink-fade">Last check</dt>
|
|
<dd class="mono text-right text-[12px]">
|
|
{{$s.LastCheckAgo}}
|
|
{{if $s.LastCheckStatus}} · <span class="{{if eq $s.LastCheckStatus "ok"}}text-ok{{else if eq $s.LastCheckStatus "errors_found"}}text-bad{{else}}text-ink-mid{{end}}">{{$s.LastCheckStatus}}</span>{{end}}
|
|
</dd>
|
|
{{end}}
|
|
{{if $s.HasLastPrune}}
|
|
<dt class="text-ink-fade">Last prune</dt>
|
|
<dd class="mono text-right text-[12px]">{{$s.LastPruneAgo}}</dd>
|
|
{{end}}
|
|
</dl>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if gt (len $page.GroupNames) 0}}
|
|
<h2 class="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-ink-mute mt-7 mb-3.5">Snapshots by source</h2>
|
|
<div class="panel rounded-[7px] p-4">
|
|
<div class="grid items-baseline text-[13px]" style="grid-template-columns: 1fr auto auto; gap: 8px 14px;">
|
|
{{range $page.GroupNames}}
|
|
<span class="mono text-ink">{{.}}</span>
|
|
<span class="mono text-ink-mute text-right">{{index $page.SnapshotsByTag .}}</span>
|
|
<span class="mono text-ink-fade text-[11px]">snapshots</span>
|
|
{{end}}
|
|
{{if gt $page.UntaggedSnapshots 0}}
|
|
<span class="mono text-ink-fade italic">untagged</span>
|
|
<span class="mono text-ink-mute text-right">{{$page.UntaggedSnapshots}}</span>
|
|
<span class="mono text-ink-fade text-[11px]">snapshots</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="panel rounded-[6px] px-4 py-3.5 mt-5" style="background: var(--bg);">
|
|
<div class="text-[11px] uppercase tracking-[0.08em] text-ink-fade">Untagged snapshots</div>
|
|
<p class="text-[12px] text-ink-mid mt-1.5 leading-[1.55]">
|
|
Any snapshot not tagged with one of this host's source groups is left alone — forget never touches it. Useful if someone runs
|
|
<span class="mono text-ink-mid">restic backup</span> outside restic-manager; nothing here will silently delete those.
|
|
</p>
|
|
</div>
|
|
</aside>
|
|
|
|
</div>
|
|
{{end}}
|