design: v4 wireframes for P2 redesign (sources / schedules / repo)

Hi-fi mock of the four pages affected by the redesign:
* /hosts/{id}/sources — list of source groups with per-row meta
  line (includes/excludes count, retention summary, usage,
  snapshot count) and Run-now / Edit / Delete actions. Tweaks
  toggle flips between fresh-host (default empty group, Run-now
  + Delete disabled) and multi-group states.
* /hosts/{id}/sources/{gid}/edit — name (snapshot tag), includes/
  excludes textareas, retention as a 3×2 grid of keep-* cells,
  retry-on-offline, inline conflict banner above retention when
  granularity↔cadence mismatch detected.
* /hosts/{id}/schedules — slim list (status / cron / source-tags
  / actions) plus new-schedule form (cron with quick-pick chips,
  source-group multi-select via clickable check pickers, enabled
  toggle).
* /hosts/{id}/repo — connection (URL/user/password/cert pin),
  bandwidth caps, maintenance rows (forget daily / prune weekly /
  check monthly with 5% subset), danger zone re-init.

Footer carries the retention-conflict detection spec (granularity
vs cadence mismatch). Visual language matches v1: --accent cyan,
JetBrains Mono for IDs/cron, btn tokens, sub-tab nav, hairline
panels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 20:54:14 +01:00
parent 49ecb7c771
commit e717b6998c
+933
View File
@@ -0,0 +1,933 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v4 · Sources / Schedules / Repo redesign</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: oklch(0.17 0.006 250); --panel: oklch(0.20 0.007 250); --panel-hi: oklch(0.23 0.008 250);
--line: oklch(0.27 0.010 250); --line-soft: oklch(0.23 0.008 250);
--ink: oklch(0.96 0.005 250); --ink-mid: oklch(0.78 0.005 250);
--ink-mute: oklch(0.58 0.006 250); --ink-fade: oklch(0.42 0.006 250);
--ok: oklch(0.78 0.14 155); --warn: oklch(0.82 0.13 80); --bad: oklch(0.70 0.20 25);
--off: oklch(0.50 0.005 250); --accent: oklch(0.82 0.12 195);
}
html, body { background: var(--bg); color: var(--ink); }
body { font-family: 'Inter', system-ui, sans-serif; }
.mono { font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; }
.text-pretty { text-wrap: pretty; }
::selection { background: color-mix(in oklch, var(--accent), transparent 70%); }
.panel { background: var(--panel); border: 1px solid var(--line-soft); }
.hairline { box-shadow: inset 0 -1px 0 var(--line-soft); }
.dot { width: 7px; height: 7px; border-radius: 9999px; display: inline-block; }
.dot-online { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ok), transparent 80%); }
.dot-offline { background: var(--off); }
.btn {
font-size: 12px; font-weight: 500; padding: 6px 11px; border-radius: 5px;
background: transparent; border: 1px solid var(--line); color: var(--ink-mid);
transition: all 120ms ease; cursor: pointer; white-space: nowrap;
}
.btn:hover { background: var(--panel-hi); color: var(--ink); }
.btn-primary { color: oklch(0.18 0.01 195); background: var(--accent); border-color: var(--accent); }
.btn-primary:hover { filter: brightness(1.08); }
.btn-ghost { border-color: transparent; }
.btn-ghost:hover { background: var(--panel-hi); border-color: transparent; }
.btn-danger { color: var(--bad); border-color: color-mix(in oklch, var(--bad), transparent 70%); }
.btn-danger:hover { background: color-mix(in oklch, var(--bad), transparent 88%); border-color: color-mix(in oklch, var(--bad), transparent 50%); color: oklch(0.85 0.10 25); }
.btn-lg { padding: 9px 16px; font-size: 13px; }
.nav-tab { font-size: 13px; padding: 18px 0; color: var(--ink-mute); border-bottom: 2px solid transparent; margin-right: 28px; cursor: pointer; }
.nav-tab.active { color: var(--ink); border-color: var(--accent); }
.nav-tab:hover { color: var(--ink); }
.sub-tab {
font-size: 13px; padding: 12px 0; color: var(--ink-mute);
border-bottom: 1.5px solid transparent; margin-right: 24px; cursor: pointer;
letter-spacing: 0.005em;
}
.sub-tab.active { color: var(--ink); border-color: var(--ink); }
.sub-tab:hover { color: var(--ink); }
.tag {
font-size: 11px; line-height: 1; padding: 4px 7px;
border: 1px solid var(--line); color: var(--ink-mid);
border-radius: 3px; letter-spacing: 0.01em;
}
.doc { max-width: 1280px; margin: 0 auto; padding: 0 32px; }
.philosophy { padding: 56px 0 32px; border-bottom: 1px solid var(--line-soft); }
.philosophy h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.philosophy p { color: var(--ink-mid); max-width: 720px; margin-top: 14px; line-height: 1.65; text-wrap: pretty; }
.philosophy .meta { color: var(--ink-fade); font-size: 12px; margin-top: 14px; }
.stage-frame { margin: 48px -32px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.stage-label {
position: sticky; top: 0; z-index: 5;
background: color-mix(in oklch, var(--bg), transparent 8%);
backdrop-filter: blur(6px);
padding: 12px 32px; border-bottom: 1px solid var(--line-soft);
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--ink-fade);
}
.stage-label .url { color: var(--ink-mid); margin-left: 14px; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.04em; text-transform: none; }
.crumbs { font-size: 12px; color: var(--ink-mute); }
.crumbs a { color: var(--ink-mute); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line); }
.crumbs .sep { color: var(--ink-fade); margin: 0 8px; }
.field-label { display: block; font-size: 11.5px; color: var(--ink-mute); margin-bottom: 6px; letter-spacing: 0.02em; }
.field {
width: 100%; padding: 8px 10px; font-size: 13px;
background: var(--bg); border: 1px solid var(--line); color: var(--ink);
border-radius: 5px; outline: none;
}
.field:focus { border-color: var(--accent); }
.field-help { font-size: 11.5px; color: var(--ink-fade); margin-top: 5px; line-height: 1.5; }
.src-row {
padding: 16px 20px; display: grid;
grid-template-columns: 1fr 240px;
column-gap: 24px; align-items: center;
}
.schd-row {
padding: 14px 20px; display: grid;
grid-template-columns: 60px 1.4fr 2fr 240px;
column-gap: 24px; align-items: center; font-size: 13px;
}
.schd-row.head { font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; padding-top: 10px; padding-bottom: 10px; }
.keep-cell { padding: 12px; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 5px; }
.keep-cell label { display: block; font-size: 10.5px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; }
.keep-cell input {
width: 100%; margin-top: 5px; padding: 4px 6px; background: transparent;
border: none; border-bottom: 1px solid var(--line);
color: var(--ink); font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 500;
outline: none;
}
.keep-cell input:focus { border-bottom-color: var(--accent); }
.preset-chip {
font-size: 11px; padding: 4px 8px; border-radius: 3px;
border: 1px solid var(--line); background: var(--bg);
color: var(--ink-mid); cursor: pointer; font-family: 'JetBrains Mono', monospace;
}
.preset-chip:hover { background: var(--panel-hi); color: var(--ink); }
.picker {
padding: 8px 10px; background: var(--bg); border: 1px solid var(--line);
border-radius: 5px; display: flex; align-items: center; gap: 8px;
cursor: pointer; font-size: 13px;
}
.picker:hover { border-color: var(--accent); }
.picker .check { width: 14px; height: 14px; border-radius: 3px; border: 1.5px solid var(--line); display: inline-flex; align-items: center; justify-content: center; flex: none; }
.picker.checked .check { background: var(--accent); border-color: var(--accent); color: oklch(0.18 0.01 195); }
.picker.checked .check::after { content: "✓"; font-size: 10px; font-weight: 700; }
.danger-panel {
border: 1px solid color-mix(in oklch, var(--bad), transparent 65%);
border-radius: 7px; padding: 18px 22px;
background: color-mix(in oklch, var(--bad), transparent 92%);
}
/* tweaks toggle (Sources only) */
.tweaks {
position: sticky; top: 49px; z-index: 4;
background: var(--panel); border: 1px solid var(--line-soft);
border-radius: 5px; padding: 8px 12px;
margin: 12px 32px; display: inline-flex; gap: 6px; align-items: center;
font-size: 11px; color: var(--ink-mute);
}
.tweak-pill {
font-size: 11px; padding: 4px 9px; border-radius: 3px;
border: 1px solid var(--line); cursor: pointer; color: var(--ink-mid);
font-family: 'JetBrains Mono', monospace;
}
.tweak-pill.active { background: var(--accent); color: oklch(0.18 0.01 195); border-color: var(--accent); }
</style>
</head>
<body>
<div class="doc">
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v4 · Sources / Schedules / Repo redesign</div>
<h1>Schedules say <em>when</em>. Sources say <em>what</em>. Repo says <em>itself</em>.</h1>
<p>
Phase 2's first cut surfaced every restic verb as a schedule kind, and made the operator pick paths twice — once on the host, once on the schedule. That's how restic CLI thinks; it's not how an operator thinks. This redesign collapses the model around three nouns the operator actually has in their head:
</p>
<p>
<strong>Source groups</strong> own the "what" — a named bundle of include/exclude paths plus a retention policy. Default group <span class="mono" style="color:var(--ink-mid);">default</span> is created at host enrolment, ready to fill in. <strong>Schedules</strong> own the "when" — a cron expression pointing at one or more groups. Cron fires → one <span class="mono" style="color:var(--ink-mid);">restic backup</span> per group, each tagged with the group name so retention works cleanly. <strong>Repo maintenance</strong> (forget / prune / check) lives on the host detail's Repo tab with sensible default cadences — operators don't compose those by hand.
</p>
<p class="meta">
Pages mocked: <span class="mono">/hosts/dev/sources</span> · <span class="mono">/hosts/dev/sources/&lt;id&gt;/edit</span> · <span class="mono">/hosts/dev/schedules</span> · <span class="mono">/hosts/dev/repo</span>. Run-now lives on individual source-group rows; manual-schedule kind is gone. The Sources stage has a small Tweaks toggle to flip between fresh-host and multi-group states.
</p>
</header>
<!-- ====================================================================
STAGE 1 · SOURCES LIST
==================================================================== -->
<div class="stage-frame">
<div class="stage-label">Stage 1<span class="url">/hosts/dev/sources</span> · default group present (fresh-ish host)</div>
<div class="tweaks">
<span style="text-transform: uppercase; letter-spacing: 0.12em;">Tweaks</span>
<span style="color: var(--ink-fade); margin-right: 6px;">·</span>
<span class="tweak-pill" onclick="setSourcesState('default-only', this)">default only</span>
<span class="tweak-pill active" onclick="setSourcesState('multi-group', this)">multi-group</span>
</div>
<div style="background: var(--bg);">
<!-- chrome (compact) -->
<div class="hairline"><div class="doc flex items-center justify-between" style="padding: 14px 0;">
<div class="flex items-center gap-3">
<div class="mono" style="font-size:13px; color: var(--ink); font-weight:500;">restic-manager</div>
<div class="mono" style="font-size:11px; color: var(--ink-fade);">v0.2.0-alpha</div>
</div>
<div class="flex items-center gap-5">
<div class="mono" style="font-size:12px; color: var(--ink-mute);">steve@dcglab</div>
<button class="btn btn-ghost">Sign out</button>
</div>
</div></div>
<div class="hairline"><div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div></div>
<!-- host header -->
<div class="doc" style="padding: 24px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><span style="color: var(--ink-mid);">dev</span><span class="sep">/</span><span style="color: var(--ink-mid);">sources</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span class="dot dot-online"></span>
<h1 class="mono" style="font-size: 22px; font-weight: 500; color: var(--ink);">dev</h1>
<div class="flex gap-1.5"><span class="tag">homelab</span></div>
</div>
<div class="flex items-center gap-3" style="margin-top: 10px; font-size: 12.5px; color: var(--ink-mute);">
<span class="mono" style="color: var(--ink-mid);">linux/amd64</span>
<span style="color: var(--ink-fade);">·</span>
<span>online · last heartbeat <span class="mono" style="color: var(--ink-mid);">3s ago</span></span>
</div>
</div>
<button class="btn btn-primary btn-lg">+ New source group</button>
</div>
<!-- sub-tabs -->
<div class="flex items-end" style="margin-top: 22px;">
<div class="sub-tab">Snapshots <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">14</span></div>
<div class="sub-tab active">Sources <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;" id="src-count">3</span></div>
<div class="sub-tab">Schedules <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">2</span></div>
<div class="sub-tab">Repo</div>
<div class="sub-tab">Jobs</div>
<div class="sub-tab">Settings</div>
</div>
</div>
<!-- sources list -->
<div class="doc" style="padding: 22px 32px 56px;">
<p style="font-size: 12.5px; color: var(--ink-mute); margin-bottom: 14px; max-width: 720px; line-height: 1.6;">
Each source group is a named bundle of paths plus the rule for how long its snapshots stick around. Schedules point at one or more groups — one <span class="mono" style="color: var(--ink-mid);">restic backup</span> runs per group, tagged by name so <span class="mono" style="color: var(--ink-mid);">forget</span> can apply retention cleanly.
</p>
<!-- multi-group state (default visible) -->
<div class="panel" style="border-radius: 7px; overflow: hidden;" id="src-list-multi">
<!-- default -->
<div class="src-row hairline">
<div>
<div class="flex items-center" style="gap: 10px;">
<span class="tag mono" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">default</span>
</div>
<div class="mono" style="font-size: 12px; color: var(--ink-mid); margin-top: 8px;">
2 includes · 1 exclude · keep last 7, daily 14, weekly 4
</div>
<div style="font-size: 11.5px; color: var(--ink-fade); margin-top: 4px;">
used by 1 schedule · last run succeeded · 12m ago · <span class="mono">52</span> snapshots
</div>
</div>
<div class="flex justify-end" style="gap: 6px;">
<button class="btn btn-primary">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
<!-- databases -->
<div class="src-row hairline">
<div>
<div class="flex items-center" style="gap: 10px;">
<span class="tag mono" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">databases</span>
</div>
<div class="mono" style="font-size: 12px; color: var(--ink-mid); margin-top: 8px;">
2 includes · 1 exclude · keep last 7, hourly 24, daily 14, weekly 6, monthly 6
</div>
<div style="font-size: 11.5px; color: var(--ink-fade); margin-top: 4px;">
used by 2 schedules · last run succeeded · 2h ago · <span class="mono">68</span> snapshots
</div>
</div>
<div class="flex justify-end" style="gap: 6px;">
<button class="btn btn-primary">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
<!-- photos · with a real granularity↔cadence conflict -->
<div class="src-row">
<div>
<div class="flex items-center" style="gap: 10px;">
<span class="tag mono" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">photos</span>
<span class="tag" style="border-color: color-mix(in oklch, var(--warn), transparent 60%); color: var(--warn); cursor: help;"
title="keep-hourly 24 is set, but no schedule pointing at this group fires more often than once a day — hourly retention has nothing to retain. Either drop keep-hourly or add a sub-daily schedule.">keep-hourly · no sub-daily schedule</span>
</div>
<div class="mono" style="font-size: 12px; color: var(--ink-mid); margin-top: 8px;">
1 include · 0 excludes · keep hourly 24, monthly 24
</div>
<div style="font-size: 11.5px; color: var(--ink-fade); margin-top: 4px;">
used by 1 schedule · last run succeeded · 6h ago · <span class="mono">7</span> snapshots
</div>
</div>
<div class="flex justify-end" style="gap: 6px;">
<button class="btn btn-primary">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<!-- default-only state (hidden until Tweaks switches) -->
<div class="panel" style="border-radius: 7px; overflow: hidden; display: none;" id="src-list-default">
<div class="src-row">
<div>
<div class="flex items-center" style="gap: 10px;">
<span class="tag mono" style="border-color: color-mix(in oklch, var(--ink-mute), transparent 60%); color: var(--ink-mute);">default</span>
<span class="tag" style="border-color: color-mix(in oklch, var(--ink-fade), transparent 60%); color: var(--ink-fade);">empty</span>
</div>
<div class="mono" style="font-size: 12px; color: var(--ink-mid); margin-top: 8px;">
0 includes · 0 excludes · no retention set — keep everything
</div>
<div style="font-size: 11.5px; color: var(--ink-fade); margin-top: 4px;">
used by 0 schedules · never run
</div>
</div>
<div class="flex justify-end" style="gap: 6px;">
<button class="btn" disabled style="opacity: 0.5; cursor: not-allowed;" title="add at least one path before running">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-ghost" disabled style="opacity: 0.4; cursor: not-allowed;" title="default group can't be deleted while it's the only one on the host">Delete</button>
</div>
</div>
</div>
<!-- legend -->
<div style="margin-top: 16px; font-size: 11.5px; color: var(--ink-fade); display: flex; gap: 18px;">
<span><span class="mono" style="color: var(--ink-mid);">default</span> can't be deleted while it's the only group on the host.</span>
<span style="color: var(--ink-mid);">·</span>
<span>Run-now on a row dispatches one immediate backup using that group's paths and tag.</span>
</div>
</div>
</div>
</div>
<!-- ====================================================================
STAGE 2 · SOURCE GROUP EDIT
==================================================================== -->
<div class="stage-frame">
<div class="stage-label">Stage 2<span class="url">/hosts/dev/sources/01KQ.../edit</span> · editing <em>databases</em></div>
<div style="background: var(--bg);">
<div class="hairline"><div class="doc flex items-center justify-between" style="padding: 14px 0;">
<div class="flex items-center gap-3"><div class="mono" style="font-size:13px;">restic-manager</div></div>
<div class="mono" style="font-size:12px; color: var(--ink-mute);">steve@dcglab</div>
</div></div>
<div class="doc" style="padding: 24px 32px 56px;">
<div class="crumbs">
<a>Dashboard</a><span class="sep">/</span><a>dev</a><span class="sep">/</span><a>sources</a><span class="sep">/</span><span style="color: var(--ink-mid);">databases</span>
</div>
<h1 style="font-size: 22px; font-weight: 500; margin-top: 14px;">Edit source group <span class="mono" style="color: var(--ink-mid);">·</span> <span class="mono">databases</span></h1>
<p class="text-pretty" style="font-size: 13px; color: var(--ink-mute); max-width: 720px; margin-top: 8px; line-height: 1.6;">
What this group covers and how long its snapshots are worth keeping. Snapshots produced for this group carry the tag <span class="mono" style="color: var(--ink-mid);">databases</span> — change the name with care: existing snapshots keep the old tag and won't get retained by a renamed group's policy.
</p>
<div class="grid grid-cols-12" style="gap: 32px; margin-top: 28px;">
<div class="col-span-7 panel" style="border-radius: 7px; padding: 24px 28px;">
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 14px;">Identity</h3>
<div style="margin-bottom: 22px;">
<label class="field-label">Name</label>
<input type="text" class="field mono" value="databases" />
<div class="field-help">Used as the snapshot tag. Lowercase, no spaces; matches what <span class="mono" style="color: var(--ink-mid);">restic forget --tag</span> sees.</div>
</div>
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 22px 0 14px; padding-top: 18px; border-top: 1px solid var(--line-soft);">Paths</h3>
<div style="margin-bottom: 18px;">
<label class="field-label">Includes <span style="color: var(--ink-fade);">· one path per line</span></label>
<textarea class="field mono" rows="4" style="resize: vertical;">/var/lib/postgresql
/etc/postgresql</textarea>
<div class="field-help">What <span class="mono" style="color: var(--ink-mid);">restic backup</span> walks. Agent runs as root with <span class="mono" style="color: var(--ink-mid);">CAP_DAC_READ_SEARCH</span>, so any readable path is fair game.</div>
</div>
<div style="margin-bottom: 22px;">
<label class="field-label">Excludes <span style="color: var(--ink-fade);">· optional, one pattern per line</span></label>
<textarea class="field mono" rows="3" style="resize: vertical;">**/pg_wal/**
*.tmp</textarea>
<div class="field-help">Passed straight through as <span class="mono" style="color: var(--ink-mid);">--exclude</span> args.</div>
</div>
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 22px 0 14px; padding-top: 18px; border-top: 1px solid var(--line-soft);">Retention <span style="color: var(--ink-fade); font-weight: 500; text-transform: none; letter-spacing: 0.01em; margin-left: 8px;">applied nightly · all blank = keep everything</span></h3>
<!-- inline conflict banner; shown only when granularity↔cadence mismatch detected for THIS group -->
<div style="border: 1px solid color-mix(in oklch, var(--warn), transparent 60%); background: color-mix(in oklch, var(--warn), transparent 92%); border-radius: 6px; padding: 12px 14px; margin-bottom: 14px; display: flex; gap: 12px; align-items: flex-start;">
<div style="font-size: 16px; line-height: 1; color: var(--warn); padding-top: 1px;"></div>
<div style="font-size: 12.5px; color: var(--ink-mid); line-height: 1.55;">
<strong style="color: var(--ink);">keep-hourly is set, but no schedule pointing at this group fires more often than once a day.</strong>
The hourly bucket will never have snapshots to retain — restic forget treats the value as a no-op.
Either drop <span class="mono" style="color: var(--ink);">keep-hourly</span> or add a sub-daily schedule.
<span style="color: var(--ink-fade); display: block; margin-top: 4px;">Finest schedule interval: <span class="mono">24h</span> · keep-hourly requires <span class="mono">&lt; 1h</span>.</span>
</div>
</div>
<div class="grid grid-cols-3" style="gap: 12px;">
<div class="keep-cell">
<label>Keep last</label>
<input type="number" min="0" value="7" />
</div>
<div class="keep-cell">
<label>Hourly</label>
<input type="number" min="0" value="24" />
</div>
<div class="keep-cell">
<label>Daily</label>
<input type="number" min="0" value="14" />
</div>
<div class="keep-cell">
<label>Weekly</label>
<input type="number" min="0" value="6" />
</div>
<div class="keep-cell">
<label>Monthly</label>
<input type="number" min="0" value="6" />
</div>
<div class="keep-cell">
<label>Yearly</label>
<input type="number" min="0" value="" placeholder="—" />
</div>
</div>
<div style="font-size: 11.5px; color: var(--ink-fade); margin-top: 12px; line-height: 1.55;">
Translates to <span class="mono" style="color: var(--ink-mid);">restic forget --tag databases --keep-last 7 --keep-hourly 24 --keep-daily 14 --keep-weekly 6 --keep-monthly 6</span>. Forget runs nightly per host (cadence on the <a style="color: var(--accent); text-decoration: underline; text-underline-offset: 2px;">Repo tab</a>); pruning the freed data is admin-only and weekly.
</div>
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 28px 0 14px; padding-top: 18px; border-top: 1px solid var(--line-soft);">Retry on offline <span style="color: var(--ink-fade); font-weight: 500; text-transform: none; letter-spacing: 0.01em; margin-left: 8px;">cron-fired runs only</span></h3>
<div class="grid grid-cols-2" style="gap: 14px;">
<div>
<label class="field-label">Max attempts</label>
<input type="number" min="0" value="3" class="field mono" />
</div>
<div>
<label class="field-label">Initial backoff (sec)</label>
<input type="number" min="0" value="60" class="field mono" />
</div>
</div>
<div class="field-help" style="margin-top: 8px;">
Each retry doubles the wait. <strong>Manual run-now ignores this</strong> — it just fails immediately if the agent is offline, since the operator's eyes are on the page.
</div>
<div style="margin-top: 32px; padding-top: 18px; border-top: 1px solid var(--line-soft); display: flex; gap: 8px;">
<button class="btn btn-primary btn-lg">Save changes</button>
<button class="btn btn-lg">Cancel</button>
<button class="btn btn-danger btn-lg" style="margin-left: auto;">Delete group</button>
</div>
</div>
<aside class="col-span-5">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 14px;">How this fits</div>
<ol style="list-style: none; padding: 0; margin: 0;">
<li style="position: relative; padding-left: 36px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 9999px; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono', monospace;">1</span>
<div style="font-size: 13px; font-weight: 500;">Save here</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.55;">Bumps the host's schedule version; the agent picks up the new paths/retention on its next push (within seconds when online).</div>
</li>
<li style="position: relative; padding-left: 36px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 9999px; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono', monospace;">2</span>
<div style="font-size: 13px; font-weight: 500;">Schedules pointing here change behaviour</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.55;">Any schedule that includes <span class="mono" style="color: var(--ink-mid);">databases</span> in its source picker now backs up the new paths next time it fires. Currently 1 schedule references this group.</div>
</li>
<li style="position: relative; padding-left: 36px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 9999px; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono', monospace;">3</span>
<div style="font-size: 13px; font-weight: 500;">Retention applies on the next nightly forget</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.55;">Existing tagged snapshots get re-evaluated against the new keep-* rules. Untagged or differently-tagged snapshots are untouched.</div>
</li>
<li style="position: relative; padding-left: 36px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 9999px; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono', monospace;">4</span>
<div style="font-size: 13px; font-weight: 500;">Run-now from the Sources list</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.55;">Want to test? Save, go back to <a style="color: var(--accent);">Sources</a>, hit Run-now on this row — runs one immediate backup with the new config, no waiting for cron.</div>
</li>
</ol>
<div class="panel" style="border-radius: 6px; padding: 14px 16px; margin-top: 28px;">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--warn); font-weight: 600;">Heads up</div>
<p style="font-size: 12px; color: var(--ink-mid); margin-top: 6px; line-height: 1.55;">
Renaming a group <em>doesn't</em> retag its existing snapshots. If you rename <span class="mono" style="color: var(--ink-mid);">databases</span> to <span class="mono" style="color: var(--ink-mid);">postgres</span>, old snapshots with the <span class="mono" style="color: var(--ink-mid);">databases</span> tag will stop being managed by this group's retention rules — they'll fall through to "untagged forever-keep" until you manually re-tag or delete them.
</p>
</div>
</aside>
</div>
</div>
</div>
</div>
<!-- ====================================================================
STAGE 3 · SCHEDULES (LIST + EDIT)
==================================================================== -->
<div class="stage-frame">
<div class="stage-label">Stage 3<span class="url">/hosts/dev/schedules</span> · simplified — when only</div>
<div style="background: var(--bg);">
<div class="hairline"><div class="doc flex items-center justify-between" style="padding: 14px 0;">
<div class="flex items-center gap-3"><div class="mono" style="font-size:13px;">restic-manager</div></div>
<div class="mono" style="font-size:12px; color: var(--ink-mute);">steve@dcglab</div>
</div></div>
<div class="doc" style="padding: 24px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><a>dev</a><span class="sep">/</span><span style="color: var(--ink-mid);">schedules</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span class="dot dot-online"></span>
<h1 class="mono" style="font-size: 22px; font-weight: 500;">dev</h1>
<span class="mono" style="font-size: 11px; color: var(--ink-mute);">schedule version 3 <span style="color: var(--ok); margin-left: 6px;">· agent in sync</span></span>
</div>
</div>
<button class="btn btn-primary btn-lg">+ New schedule</button>
</div>
<div class="flex items-end" style="margin-top: 22px;">
<div class="sub-tab">Snapshots</div>
<div class="sub-tab">Sources</div>
<div class="sub-tab active">Schedules <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">3</span></div>
<div class="sub-tab">Repo</div>
<div class="sub-tab">Jobs</div>
<div class="sub-tab">Settings</div>
</div>
</div>
<div class="doc" style="padding: 22px 32px 32px;">
<p style="font-size: 12.5px; color: var(--ink-mute); margin-bottom: 14px; max-width: 720px; line-height: 1.6;">
A schedule is just a cron expression pointing at one or more source groups. When it fires, the agent runs a separate <span class="mono" style="color: var(--ink-mid);">restic backup</span> per chosen group — independent jobs, independent snapshots, independent retention. Failure of one group doesn't fail the others.
</p>
<div class="panel" style="border-radius: 7px; overflow: hidden;">
<div class="schd-row hairline head">
<div>Status</div>
<div>Cron</div>
<div>Sources</div>
<div></div>
</div>
<!-- row 1 -->
<div class="schd-row hairline">
<div><span class="mono" style="font-size: 11px; color: var(--ok);">enabled</span></div>
<div class="mono" style="color: var(--ink);">@hourly</div>
<div class="flex gap-1.5 flex-wrap">
<span class="tag" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">databases</span>
</div>
<div class="flex gap-1.5 justify-end">
<button class="btn btn-primary">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
<!-- row 2 -->
<div class="schd-row hairline">
<div><span class="mono" style="font-size: 11px; color: var(--ok);">enabled</span></div>
<div class="mono" style="color: var(--ink);">0 3 * * *</div>
<div class="flex gap-1.5 flex-wrap">
<span class="tag" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">default</span>
<span class="tag" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">photos</span>
<span class="tag" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">databases</span>
</div>
<div class="flex gap-1.5 justify-end">
<button class="btn btn-primary">Run now</button>
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
<!-- row 3 paused -->
<div class="schd-row">
<div><span class="mono" style="font-size: 11px; color: var(--ink-fade);">paused</span></div>
<div class="mono" style="color: var(--ink-mute);">0 3 * * 0</div>
<div class="flex gap-1.5 flex-wrap">
<span class="tag" style="opacity: 0.6;">photos</span>
</div>
<div class="flex gap-1.5 justify-end">
<button class="btn">Edit</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
<!-- inline edit-form mock -->
<div class="doc" style="padding: 16px 32px 56px;">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.18em; color: var(--ink-fade); margin-bottom: 12px;">— New schedule form —</div>
<div class="panel" style="border-radius: 7px; padding: 24px 28px;">
<div class="grid grid-cols-12" style="gap: 24px;">
<div class="col-span-7">
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 14px;">When</h3>
<label class="field-label">Cron expression</label>
<input type="text" class="field mono" value="0 */6 * * *" />
<div class="flex flex-wrap" style="gap: 6px; margin-top: 10px;">
<span class="preset-chip">0 3 * * *</span>
<span class="preset-chip">@hourly</span>
<span class="preset-chip">0 */6 * * *</span>
<span class="preset-chip">0 3 * * 0</span>
<span class="preset-chip">0 3 1 * *</span>
</div>
<div class="field-help" style="margin-top: 10px;">
Standard 5-field cron with descriptors. Server validates with the same parser the agent uses to fire — what saves here is what runs.
</div>
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 24px 0 14px; padding-top: 18px; border-top: 1px solid var(--line-soft);">What — pick one or more source groups</h3>
<div class="grid grid-cols-1" style="gap: 6px;">
<div class="picker checked"><span class="check"></span><span class="mono" style="color: var(--ink); flex: 1;">default</span><span style="font-size: 11.5px; color: var(--ink-fade);">2 paths · keep last 7, daily 14</span></div>
<div class="picker"><span class="check"></span><span class="mono" style="color: var(--ink); flex: 1;">databases</span><span style="font-size: 11.5px; color: var(--ink-fade);">2 paths · keep last 7, daily 14, weekly 6</span></div>
<div class="picker checked"><span class="check"></span><span class="mono" style="color: var(--ink); flex: 1;">photos</span><span style="font-size: 11.5px; color: var(--ink-fade);">1 path · keep monthly 24</span></div>
</div>
<div class="field-help" style="margin-top: 10px;">
Each picked group runs as a separate <span class="mono" style="color: var(--ink-mid);">restic backup</span> with its own tag — its own snapshot, its own retention. Pick multiple to fire them all on the same cron tick.
</div>
<h3 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 24px 0 14px; padding-top: 18px; border-top: 1px solid var(--line-soft);">Status</h3>
<label class="flex items-center" style="gap: 10px; font-size: 13px; cursor: pointer;">
<input type="checkbox" checked style="width: 14px; height: 14px;" />
<span>Enabled</span>
<span style="color: var(--ink-fade);">— uncheck to keep the row but stop firing.</span>
</label>
<div style="margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--line-soft); display: flex; gap: 8px;">
<button class="btn btn-primary btn-lg">Create schedule</button>
<button class="btn btn-lg">Cancel</button>
</div>
</div>
<aside class="col-span-5" style="border-left: 1px solid var(--line-soft); padding-left: 24px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 12px;">No paths, no retention, no kind</div>
<p class="text-pretty" style="font-size: 12.5px; color: var(--ink-mid); line-height: 1.6;">
That stuff lives on source groups now. A schedule's only job is to be the cron expression and to point at the groups it should fire. This keeps the form short and means schedules and sources can change independently — change a group's retention, every schedule that points at it inherits the change without further edits.
</p>
<p class="text-pretty" style="font-size: 12.5px; color: var(--ink-mid); line-height: 1.6; margin-top: 12px;">
<strong>Forget / prune / check are not schedule kinds anymore.</strong> They run on host-level cadences from the <a style="color: var(--accent); text-decoration: underline; text-underline-offset: 2px;">Repo tab</a> with sensible defaults.
</p>
<div class="panel" style="border-radius: 6px; padding: 14px 16px; margin-top: 18px; background: var(--bg);">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-fade);">If the agent is offline at fire time</div>
<p style="font-size: 12px; color: var(--ink-mid); margin-top: 6px; line-height: 1.55;">
Server retries per the group's retry policy (max attempts + exponential backoff). If the <em>command</em> reaches the agent and the backup fails for non-network reasons (path missing, permission denied), no retry — just a failed job in the log.
</p>
</div>
</aside>
</div>
</div>
</div>
</div>
</div>
<!-- ====================================================================
STAGE 4 · REPO TAB
==================================================================== -->
<div class="stage-frame">
<div class="stage-label">Stage 4<span class="url">/hosts/dev/repo</span> · connection · maintenance · danger zone</div>
<div style="background: var(--bg);">
<div class="hairline"><div class="doc flex items-center justify-between" style="padding: 14px 0;">
<div class="flex items-center gap-3"><div class="mono" style="font-size:13px;">restic-manager</div></div>
<div class="mono" style="font-size:12px; color: var(--ink-mute);">steve@dcglab</div>
</div></div>
<div class="doc" style="padding: 24px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><a>dev</a><span class="sep">/</span><span style="color: var(--ink-mid);">repo</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span class="dot dot-online"></span>
<h1 class="mono" style="font-size: 22px; font-weight: 500;">dev</h1>
</div>
</div>
</div>
<div class="flex items-end" style="margin-top: 22px;">
<div class="sub-tab">Snapshots</div>
<div class="sub-tab">Sources</div>
<div class="sub-tab">Schedules</div>
<div class="sub-tab active">Repo</div>
<div class="sub-tab">Jobs</div>
<div class="sub-tab">Settings</div>
</div>
</div>
<div class="doc grid grid-cols-12" style="padding: 24px 32px 56px; gap: 24px; align-items: start;">
<div class="col-span-8">
<!-- connection -->
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 14px;">Connection</h2>
<div class="panel" style="border-radius: 7px; padding: 18px 22px;">
<div class="grid grid-cols-2" style="gap: 18px;">
<div>
<div class="field-label">Repo URL</div>
<div class="mono" style="font-size: 13px; color: var(--ink);">rest:http://192.168.0.99:8000/dev/</div>
</div>
<div>
<div class="field-label">Username</div>
<div class="mono" style="font-size: 13px; color: var(--ink);">dev</div>
</div>
<div>
<div class="field-label">Password</div>
<div class="mono" style="font-size: 13px; color: var(--ink-mute);">•••••••••••••••• <span style="color: var(--ink-fade); font-size: 11px; margin-left: 6px;">stored, never displayed</span></div>
</div>
<div>
<div class="field-label">Cert pin</div>
<div class="mono" style="font-size: 12px; color: var(--ink-mute);"><span style="color: var(--ink-fade); font-size: 11px; margin-left: 6px;">HTTP-only behind reverse proxy</span></div>
</div>
</div>
<div style="margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line-soft); display: flex; gap: 8px;">
<button class="btn">Edit credentials</button>
<button class="btn">Regenerate password</button>
</div>
</div>
<!-- bandwidth -->
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 28px 0 14px;">Bandwidth · host-wide</h2>
<div class="panel" style="border-radius: 7px; padding: 18px 22px;">
<div class="grid grid-cols-2" style="gap: 18px;">
<div>
<label class="field-label">Upload limit <span style="color: var(--ink-fade);">· KB/s · blank = no cap</span></label>
<input type="number" class="field mono" placeholder="—" />
</div>
<div>
<label class="field-label">Download limit <span style="color: var(--ink-fade);">· KB/s · blank = no cap</span></label>
<input type="number" class="field mono" placeholder="—" />
</div>
</div>
<div class="field-help" style="margin-top: 10px;">
Applies to every backup, restore, and prune job for this host. Configure per-host because the network constraint usually <em>is</em> the host (its uplink). Per-source is rare enough we don't surface it.
</div>
</div>
<!-- maintenance -->
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 28px 0 14px;">Maintenance · auto-managed</h2>
<div class="panel" style="border-radius: 7px; overflow: hidden;">
<!-- forget -->
<div class="hairline" style="padding: 14px 22px; display: grid; grid-template-columns: 100px 1fr 1fr 220px 100px; gap: 18px; align-items: center; font-size: 13px;">
<div class="mono" style="color: var(--ink); font-weight: 500;">forget</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Cadence</div>
<div class="mono" style="color: var(--ink); margin-top: 2px;">daily · 03:00</div>
</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Last run</div>
<div class="mono" style="color: var(--ok); margin-top: 2px;">succeeded · 12h ago</div>
</div>
<div style="font-size: 12px; color: var(--ink-fade); line-height: 1.55;">
Per source group, using each group's retention policy.
</div>
<div class="flex gap-1.5 justify-end">
<span class="mono" style="font-size: 11px; color: var(--ok);">enabled</span>
<button class="btn">Edit</button>
</div>
</div>
<!-- prune -->
<div class="hairline" style="padding: 14px 22px; display: grid; grid-template-columns: 100px 1fr 1fr 220px 100px; gap: 18px; align-items: center; font-size: 13px;">
<div class="mono" style="color: var(--ink); font-weight: 500;">prune</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Cadence</div>
<div class="mono" style="color: var(--ink); margin-top: 2px;">weekly · Sun 04:00</div>
</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Last run</div>
<div class="mono" style="color: var(--ok); margin-top: 2px;">succeeded · 4d ago</div>
</div>
<div style="font-size: 12px; color: var(--ink-fade); line-height: 1.55;">
Reclaims storage made dead by forget. Heavy — runs weekly only.
</div>
<div class="flex gap-1.5 justify-end">
<span class="mono" style="font-size: 11px; color: var(--ok);">enabled</span>
<button class="btn">Edit</button>
</div>
</div>
<!-- check -->
<div style="padding: 14px 22px; display: grid; grid-template-columns: 100px 1fr 1fr 220px 100px; gap: 18px; align-items: center; font-size: 13px;">
<div class="mono" style="color: var(--ink); font-weight: 500;">check</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Cadence</div>
<div class="mono" style="color: var(--ink); margin-top: 2px;">monthly · 1st 05:00</div>
</div>
<div>
<div style="font-size: 12px; color: var(--ink-mute);">Last run</div>
<div class="mono" style="color: var(--ok); margin-top: 2px;">succeeded · 22d ago</div>
</div>
<div style="font-size: 12px; color: var(--ink-fade); line-height: 1.55;">
<span class="mono" style="color: var(--ink-mid);">--read-data-subset 5%</span> · spreads full coverage over ~20 months.
</div>
<div class="flex gap-1.5 justify-end">
<span class="mono" style="font-size: 11px; color: var(--ok);">enabled</span>
<button class="btn">Edit</button>
</div>
</div>
</div>
<!-- danger zone -->
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--bad); margin: 36px 0 14px;">Danger zone</h2>
<div class="danger-panel">
<div class="flex items-start justify-between" style="gap: 24px;">
<div style="flex: 1;">
<div style="font-size: 14px; font-weight: 600; color: var(--ink);">Re-initialise repo</div>
<p class="text-pretty" style="font-size: 12.5px; color: var(--ink-mid); line-height: 1.6; margin-top: 8px; max-width: 580px;">
Tries to <span class="mono" style="color: var(--ink-mid);">DELETE</span> the rest-server's copy of this repo, then runs <span class="mono" style="color: var(--ink-mid);">restic init</span> against the empty path. Most rest-server setups run with <span class="mono" style="color: var(--ink-mid);">--append-only</span> and refuse the DELETE — in that case the page shows guided cleanup steps instead of attempting anything destructive.
</p>
<p style="font-size: 12px; color: var(--ink-fade); line-height: 1.55; margin-top: 8px;">
All snapshots are lost; this host's schedule version stays the same and the agent's <span class="mono" style="color: var(--ink-mid);">secrets.enc</span> is reused.
</p>
</div>
<button class="btn btn-danger btn-lg" style="flex: none;">Re-init repo…</button>
</div>
</div>
</div>
<!-- right rail: storage projection -->
<aside class="col-span-4">
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 14px;">Storage</h2>
<div class="panel" style="border-radius: 7px; padding: 18px 20px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 18px;">
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Repo size</div>
<div class="mono" style="font-size: 22px; color: var(--ink); margin-top: 4px;">412 <span style="font-size: 12px; color: var(--ink-mute);">GB</span></div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">dedup ratio 6.4×</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Snapshots</div>
<div class="mono" style="font-size: 22px; color: var(--ink); margin-top: 4px;">127</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">across 3 source groups</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Last check</div>
<div class="mono" style="font-size: 14px; color: var(--ok); margin-top: 4px;">succeeded</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">22 days ago</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Lock state</div>
<div class="mono" style="font-size: 14px; color: var(--ok); margin-top: 4px;">unlocked</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">no held locks</div>
</div>
</div>
</div>
<h2 style="font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin: 28px 0 14px;">Snapshots by source</h2>
<div class="panel" style="border-radius: 7px; padding: 14px 18px;">
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 10px 14px; align-items: baseline; font-size: 13px;">
<span class="mono" style="color: var(--ink);">default</span>
<span class="mono" style="color: var(--ink-mute); text-align: right;">52</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">snapshots</span>
<span class="mono" style="color: var(--ink);">databases</span>
<span class="mono" style="color: var(--ink-mute); text-align: right;">68</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">snapshots</span>
<span class="mono" style="color: var(--ink);">photos</span>
<span class="mono" style="color: var(--ink-mute); text-align: right;">7</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">snapshots</span>
</div>
</div>
<div class="panel" style="border-radius: 6px; padding: 14px 16px; margin-top: 18px; background: var(--bg);">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-fade);">Untagged snapshots</div>
<p style="font-size: 12px; color: var(--ink-mid); margin-top: 6px; line-height: 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" style="color: var(--ink-mid);">restic backup</span> outside restic-manager; nothing here will silently delete those.
</p>
</div>
</aside>
</div>
</div>
</div>
<footer style="padding: 80px 0 60px; color: var(--ink-fade); font-size: 12px; text-wrap: pretty; max-width: 800px;">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.18em; margin-bottom: 12px; color: var(--ink-mid);">Retention-conflict detection · spec</div>
<p style="line-height: 1.7; margin-bottom: 14px;">
One server-side check: <strong>granularity ↔ cadence mismatch</strong>. A retention dimension is "orphaned" when the finest schedule interval pointing at the group can't produce snapshots in that bucket.
</p>
<p style="line-height: 1.7; margin-bottom: 8px; color: var(--ink-mid);">Detection (per source group):</p>
<ol style="line-height: 1.7; padding-left: 22px; color: var(--ink-mid);">
<li>Collect every <em>enabled</em> schedule on this host whose source-group set includes this group.</li>
<li>Parse each schedule's cron via <span class="mono" style="color: var(--ink);">robfig/cron/v3</span>. Compute the smallest interval between consecutive fires.</li>
<li>Take the minimum across all schedules — call it <span class="mono" style="color: var(--ink);">finestInterval</span>. If there are zero enabled schedules, <span class="mono" style="color: var(--ink);">finestInterval = ∞</span> (the group has no fires; conflict surfaces as "0 schedules" in the meta line, not as a pill).</li>
<li>For each non-nil keep-* dimension on the group's retention, compare:
<ul style="margin-top: 6px; padding-left: 18px; line-height: 1.65;">
<li><span class="mono" style="color: var(--ink);">keep-hourly</span> requires <span class="mono" style="color: var(--ink);">finestInterval &lt; 1h</span></li>
<li><span class="mono" style="color: var(--ink);">keep-daily</span> requires <span class="mono" style="color: var(--ink);">finestInterval &lt; 24h</span></li>
<li><span class="mono" style="color: var(--ink);">keep-weekly</span> requires <span class="mono" style="color: var(--ink);">finestInterval &lt; 7d</span></li>
<li><span class="mono" style="color: var(--ink);">keep-monthly</span> requires <span class="mono" style="color: var(--ink);">finestInterval &lt; 31d</span></li>
<li><span class="mono" style="color: var(--ink);">keep-yearly</span> requires <span class="mono" style="color: var(--ink);">finestInterval &lt; 365d</span></li>
<li><span class="mono" style="color: var(--ink);">keep-last</span> always satisfied — it counts snapshots, no time semantics.</li>
</ul>
</li>
<li>Any failed comparison → group has a conflict; the worst (finest-grained) failed dimension drives the pill text.</li>
</ol>
<p style="line-height: 1.7; margin-top: 14px;">
Re-evaluate on every schedule CRUD and every source-group CRUD; cache the result on the group row (<span class="mono" style="color: var(--ink);">conflict_dimension</span> nullable string) so the list view doesn't recompute on each render.
</p>
<div style="height: 32px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.18em; margin-bottom: 12px; color: var(--ink-mid);">v4 · open review questions</div>
<ul style="line-height: 1.75; padding-left: 18px;">
<li>Source-edit "How this fits" right rail — keep it as inline education, or trim once the model is familiar?</li>
<li>Schedules form picker — checkbox list of groups (current) vs a chips-based multi-select. Checkboxes scale fine to ≤10 groups.</li>
<li>Repo tab maintenance row — 5-column grid, fine on desktop, will need stacking ≤ ~960 px.</li>
<li>Re-init copy: trailing ellipsis on the button to telegraph the confirmation modal.</li>
</ul>
</footer>
</div>
<script>
// --- tweak: sources list state ---
// Both states are pre-rendered as HTML; we just toggle visibility +
// update the sub-tab counter. This way the rows show even if the
// host environment doesn't run the script (e.g. IDE preview pane).
function setSourcesState(key, el) {
document.querySelectorAll('.tweak-pill').forEach(p => p.classList.remove('active'));
el.classList.add('active');
const multi = document.getElementById('src-list-multi');
const def = document.getElementById('src-list-default');
if (key === 'multi-group') {
multi.style.display = ''; def.style.display = 'none';
document.getElementById('src-count').textContent = '3';
} else {
multi.style.display = 'none'; def.style.display = '';
document.getElementById('src-count').textContent = '1';
}
}
// schedule edit picker — clickable
document.querySelectorAll('.picker').forEach(el => {
el.addEventListener('click', () => el.classList.toggle('checked'));
});
</script>
</body>
</html>