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:
@@ -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/<id>/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">< 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 < 1h</span></li>
|
||||
<li><span class="mono" style="color: var(--ink);">keep-daily</span> requires <span class="mono" style="color: var(--ink);">finestInterval < 24h</span></li>
|
||||
<li><span class="mono" style="color: var(--ink);">keep-weekly</span> requires <span class="mono" style="color: var(--ink);">finestInterval < 7d</span></li>
|
||||
<li><span class="mono" style="color: var(--ink);">keep-monthly</span> requires <span class="mono" style="color: var(--ink);">finestInterval < 31d</span></li>
|
||||
<li><span class="mono" style="color: var(--ink);">keep-yearly</span> requires <span class="mono" style="color: var(--ink);">finestInterval < 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>
|
||||
Reference in New Issue
Block a user