Files
restic-manager/design/v4-sources-redesign.html
T
steve e717b6998c 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>
2026-05-02 20:54:14 +01:00

934 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>