ui: Slice E — admin creds form + run-now buttons + repo health panel
- hostRepoPage gains AdminURL/AdminUsername/HasAdminPassword, Online,
and StatsView (pre-dereferenced projection of host_repo_stats).
- loadHostRepoPage loads the admin slot (tolerating ErrNotFound),
hub.Connected, and stats (tolerating ErrNotFound).
- renderRepoPage gains an adminErr parameter; all callers updated.
- handleUIAdminCredentialsSave / handleUIAdminCredentialsDelete added
(form-POST handlers mirroring the repo-creds pattern, with audit).
- Routes /hosts/{id}/admin-credentials POST and /delete POST registered.
- Template: Admin credentials form after Connection, Run-now HTMX
buttons after Maintenance, Repo health stats panel in right rail.
- Tests: 9 new tests covering rendering, disabled states, save/delete
round-trips, audit rows, and idempotent delete.
This commit is contained in:
@@ -42,6 +42,54 @@
|
||||
</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">
|
||||
@@ -138,6 +186,37 @@
|
||||
</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-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-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-confirm="Clear stale repo locks?"
|
||||
class="btn btn-secondary"
|
||||
{{if not $page.Online}}disabled title="agent is offline"{{end}}>
|
||||
unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* ---------- 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"
|
||||
@@ -179,6 +258,41 @@
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user