initial setup ready
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>restic-manager · Phase 0 wireframes</title>
|
||||
<style>
|
||||
/* Wireframe-grade only. No brand. No polish.
|
||||
Purpose: confirm information architecture & API coverage
|
||||
before locking spec.md §6.1 (REST) and §6.2 (WS) shapes. */
|
||||
|
||||
:root {
|
||||
--ink: #1a1a1a;
|
||||
--mute: #666;
|
||||
--line: #999;
|
||||
--soft: #ddd;
|
||||
--bg: #f5f5f4;
|
||||
--panel: #fff;
|
||||
--note: #b45309; /* annotations only, single accent so they read as "meta" */
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: 13px/1.5 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1200px;
|
||||
margin: 32px auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 { font-weight: 600; margin: 0; }
|
||||
|
||||
.doc-header {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.doc-header h1 { font-size: 18px; }
|
||||
.doc-header p { color: var(--mute); margin: 8px 0 0; max-width: 760px; }
|
||||
|
||||
/* ---- screen frame ---- */
|
||||
.screen {
|
||||
background: var(--panel);
|
||||
border: 1px dashed var(--line);
|
||||
margin: 48px 0;
|
||||
position: relative;
|
||||
}
|
||||
.screen-label {
|
||||
position: absolute;
|
||||
top: -10px; left: 16px;
|
||||
background: var(--bg);
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--mute);
|
||||
}
|
||||
.screen-body { padding: 32px; }
|
||||
|
||||
/* ---- block primitives ---- */
|
||||
.box {
|
||||
border: 1px dashed var(--line);
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
}
|
||||
.box.solid { border-style: solid; border-color: var(--soft); }
|
||||
.box.placeholder {
|
||||
background: repeating-linear-gradient(
|
||||
45deg, transparent 0 8px, #f0efee 8px 16px
|
||||
);
|
||||
color: var(--mute);
|
||||
text-align: center;
|
||||
padding: 24px 12px;
|
||||
}
|
||||
|
||||
.row { display: flex; gap: 12px; }
|
||||
.row > * { flex: 1; }
|
||||
.stack { display: flex; flex-direction: column; gap: 12px; }
|
||||
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
|
||||
.label { color: var(--mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.value { font-size: 14px; }
|
||||
.small { font-size: 11px; color: var(--mute); }
|
||||
.strong { font-weight: 600; }
|
||||
.pill { display: inline-block; border: 1px solid var(--line); padding: 1px 8px; font-size: 11px; }
|
||||
.btn { display: inline-block; border: 1px solid var(--ink); padding: 4px 12px; font-size: 12px; background: var(--panel); cursor: pointer; }
|
||||
.btn.ghost { border-color: var(--line); color: var(--mute); }
|
||||
.btn.danger { border-style: dashed; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px dashed var(--soft); font-weight: normal; }
|
||||
th { color: var(--mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
|
||||
/* ---- annotation callouts ----
|
||||
Every element that depends on a backend source carries a [src] tag
|
||||
so we can audit spec.md §6 coverage in one pass. */
|
||||
.src {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
color: var(--note);
|
||||
border: 1px solid var(--note);
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.src::before { content: ""; }
|
||||
|
||||
/* margin annotation lane */
|
||||
.annotated { display: grid; grid-template-columns: 1fr 280px; gap: 24px; }
|
||||
.ann-lane { font-size: 11px; color: var(--note); }
|
||||
.ann-lane h4 { color: var(--note); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
|
||||
.ann-lane ul { margin: 0; padding-left: 16px; }
|
||||
.ann-lane li { margin-bottom: 6px; line-height: 1.45; }
|
||||
|
||||
/* ---- top app chrome ---- */
|
||||
.chrome {
|
||||
border-bottom: 1px solid var(--soft);
|
||||
padding: 12px 32px;
|
||||
display: flex; align-items: center; gap: 24px;
|
||||
background: var(--panel);
|
||||
}
|
||||
.chrome .logo { font-weight: 600; }
|
||||
.chrome nav { display: flex; gap: 16px; color: var(--mute); }
|
||||
.chrome nav .active { color: var(--ink); border-bottom: 1px solid var(--ink); }
|
||||
.chrome .right { margin-left: auto; color: var(--mute); font-size: 12px; }
|
||||
|
||||
/* ---- tabs ---- */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--soft);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.tabs a {
|
||||
padding: 8px 16px;
|
||||
border: 1px dashed var(--line);
|
||||
border-bottom: none;
|
||||
margin-right: -1px;
|
||||
color: var(--mute);
|
||||
text-decoration: none;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tabs a.active {
|
||||
color: var(--ink);
|
||||
background: var(--panel);
|
||||
border-style: solid;
|
||||
border-color: var(--soft);
|
||||
}
|
||||
|
||||
/* status dots — unstyled, just outline */
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border: 1px solid var(--ink); border-radius: 50%; vertical-align: middle; margin-right: 4px; }
|
||||
.dot.off { background: var(--panel); }
|
||||
.dot.ok { background: var(--ink); }
|
||||
.dot.degraded { background: repeating-linear-gradient(45deg, var(--ink) 0 2px, transparent 2px 4px); }
|
||||
|
||||
/* log stream */
|
||||
.log {
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 12px 16px;
|
||||
height: 320px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--soft);
|
||||
}
|
||||
.log .ts { color: #888; }
|
||||
.log .err { color: #f88; }
|
||||
|
||||
/* progress bar */
|
||||
.progress {
|
||||
background: var(--soft);
|
||||
height: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress > span {
|
||||
display: block;
|
||||
background: var(--ink);
|
||||
height: 100%;
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
/* annotations bullet style */
|
||||
details summary { cursor: pointer; color: var(--note); font-size: 11px; }
|
||||
details[open] { margin-bottom: 8px; }
|
||||
|
||||
/* TOC */
|
||||
.toc { background: var(--panel); border: 1px solid var(--soft); padding: 16px 20px; margin-bottom: 32px; }
|
||||
.toc ol { margin: 8px 0 0; padding-left: 20px; }
|
||||
.toc a { color: var(--ink); }
|
||||
|
||||
/* findings */
|
||||
.findings { border: 1px solid var(--note); padding: 16px 20px; margin-top: 48px; background: #fffbeb; }
|
||||
.findings h3 { color: var(--note); margin-bottom: 12px; }
|
||||
.findings ol { padding-left: 20px; margin: 0; }
|
||||
.findings li { margin-bottom: 8px; }
|
||||
.findings code { background: rgba(180,83,9,.08); padding: 1px 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<header class="doc-header">
|
||||
<h1>restic-manager · Phase 0 wireframes</h1>
|
||||
<p>
|
||||
Low-fidelity wireframes for Phase 1/2 screens. Purpose: confirm the data each
|
||||
screen needs before the API in spec.md §6.1 and the WS messages in §6.2 are
|
||||
locked in. Grayscale on purpose — visual design is deferred to Phase 5
|
||||
(and a focused hi-fi pass on the restore wizard in Phase 3).
|
||||
</p>
|
||||
<p>
|
||||
<span class="src">[GET /api/...]</span> tags mark REST data sources.
|
||||
<span class="src">[WS: ...]</span> tags mark WebSocket message dependencies.
|
||||
Open the “Findings” section at the bottom for spec gaps.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav class="toc">
|
||||
<strong>Screens</strong>
|
||||
<ol>
|
||||
<li><a href="#dashboard">Dashboard — fleet overview</a></li>
|
||||
<li><a href="#host-detail">Host detail — 5 tabs</a></li>
|
||||
<li><a href="#job-detail">Job detail — live log</a></li>
|
||||
<li><a href="#findings">Findings — gaps in spec.md §6</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SCREEN 1 · DASHBOARD -->
|
||||
<!-- ============================================================ -->
|
||||
<section id="dashboard" class="screen">
|
||||
<span class="screen-label">Screen 1 · Dashboard (/)</span>
|
||||
|
||||
<div class="chrome">
|
||||
<div class="logo">restic-manager</div>
|
||||
<nav>
|
||||
<span class="active">Dashboard</span>
|
||||
<span>Hosts</span>
|
||||
<span>Jobs</span>
|
||||
<span>Repos</span>
|
||||
<span>Alerts</span>
|
||||
<span>Audit</span>
|
||||
<span>Settings</span>
|
||||
</nav>
|
||||
<div class="right">user: alice (admin) · logout</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-body annotated">
|
||||
<div>
|
||||
<!-- Fleet summary strip -->
|
||||
<div class="grid-3" style="margin-bottom:24px">
|
||||
<div class="box solid">
|
||||
<div class="label">Fleet status</div>
|
||||
<div class="value strong">10 online · 1 offline · 1 degraded</div>
|
||||
<div class="small">Last sync 12s ago</div>
|
||||
</div>
|
||||
<div class="box solid">
|
||||
<div class="label">Storage (sum across repos)</div>
|
||||
<div class="value strong">2.4 TB across 12 repos</div>
|
||||
<div class="small">+18 GB last 24h</div>
|
||||
</div>
|
||||
<div class="box solid">
|
||||
<div class="label">Open alerts</div>
|
||||
<div class="value strong">3 · 1 critical</div>
|
||||
<div class="small">2 unacked</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter / search -->
|
||||
<div class="row" style="margin-bottom:16px; align-items:center">
|
||||
<div class="box" style="flex:3">[ search hosts · filter by tag · status ]</div>
|
||||
<div style="flex:0">
|
||||
<span class="btn">+ Add host</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 24px 0 12px">Hosts</h3>
|
||||
|
||||
<!-- Host card grid -->
|
||||
<div class="grid-3">
|
||||
|
||||
<!-- card: healthy -->
|
||||
<div class="box solid">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between">
|
||||
<div class="strong">prod-db-01 <span class="small">linux/amd64</span></div>
|
||||
<span class="pill"><span class="dot ok"></span>online</span>
|
||||
</div>
|
||||
<hr style="border:none; border-top:1px dashed var(--soft); margin:8px 0">
|
||||
<div class="label">Last backup</div>
|
||||
<div class="value">2h ago · success</div>
|
||||
<div class="label" style="margin-top:8px">Repo</div>
|
||||
<div class="value">412 GB · 1,284 snapshots</div>
|
||||
<div class="label" style="margin-top:8px">Alerts</div>
|
||||
<div class="value">—</div>
|
||||
<div style="margin-top:12px; display:flex; gap:8px">
|
||||
<span class="btn">View</span>
|
||||
<span class="btn ghost">Backup now</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- card: failed last -->
|
||||
<div class="box solid">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between">
|
||||
<div class="strong">staging-app <span class="small">linux/arm64</span></div>
|
||||
<span class="pill"><span class="dot degraded"></span>degraded</span>
|
||||
</div>
|
||||
<hr style="border:none; border-top:1px dashed var(--soft); margin:8px 0">
|
||||
<div class="label">Last backup</div>
|
||||
<div class="value">9h ago · <span class="strong">failed</span></div>
|
||||
<div class="label" style="margin-top:8px">Repo</div>
|
||||
<div class="value">88 GB · 412 snapshots</div>
|
||||
<div class="label" style="margin-top:8px">Alerts</div>
|
||||
<div class="value">2 · 1 critical</div>
|
||||
<div style="margin-top:12px; display:flex; gap:8px">
|
||||
<span class="btn">View</span>
|
||||
<span class="btn ghost">Retry</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- card: offline -->
|
||||
<div class="box solid">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between">
|
||||
<div class="strong">laptop-bob <span class="small">windows/amd64</span></div>
|
||||
<span class="pill"><span class="dot off"></span>offline</span>
|
||||
</div>
|
||||
<hr style="border:none; border-top:1px dashed var(--soft); margin:8px 0">
|
||||
<div class="label">Last seen</div>
|
||||
<div class="value">3d ago</div>
|
||||
<div class="label" style="margin-top:8px">Repo</div>
|
||||
<div class="value">142 GB · 88 snapshots</div>
|
||||
<div class="label" style="margin-top:8px">Alerts</div>
|
||||
<div class="value">1</div>
|
||||
<div style="margin-top:12px; display:flex; gap:8px">
|
||||
<span class="btn">View</span>
|
||||
<span class="btn ghost" style="opacity:.4">Backup now</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- placeholder rest -->
|
||||
<div class="box placeholder">… more host cards (12 total in target deployment)</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent jobs -->
|
||||
<h3 style="margin: 32px 0 12px">Recent activity (fleet-wide)</h3>
|
||||
<div class="box solid" style="padding:0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th><th>Host</th><th>Kind</th><th>Status</th><th>Duration</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>2h ago</td><td>prod-db-01</td><td>backup</td><td>succeeded</td><td>00:14:22</td><td><span class="small">view</span></td></tr>
|
||||
<tr><td>3h ago</td><td>web-02</td><td>backup</td><td>succeeded</td><td>00:08:11</td><td><span class="small">view</span></td></tr>
|
||||
<tr><td>9h ago</td><td>staging-app</td><td>backup</td><td><span class="strong">failed</span></td><td>00:01:03</td><td><span class="small">view</span></td></tr>
|
||||
<tr><td>1d ago</td><td>prod-db-01</td><td>check</td><td>succeeded</td><td>00:42:17</td><td><span class="small">view</span></td></tr>
|
||||
<tr><td>1d ago</td><td>web-01</td><td>prune</td><td>succeeded</td><td>00:04:55</td><td><span class="small">view</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- annotation lane -->
|
||||
<aside class="ann-lane">
|
||||
<h4>Data sources</h4>
|
||||
<ul>
|
||||
<li><strong>Fleet summary strip</strong> — no endpoint in §6.1. Either (a) add <code>GET /api/fleet/summary</code> or (b) compute client-side from <code>GET /api/hosts</code> + <code>GET /api/alerts</code>. <em>Recommend (a)</em> — cheaper than fanout, and Prometheus already needs the rollup (§14.4).</li>
|
||||
<li><strong>Host cards</strong> — <code>GET /api/hosts</code> must return: status, last_backup_at, last_backup_status, repo_size_bytes, snapshot_count, open_alert_count, agent_version. Domain model (§5) only has <code>status</code> + <code>last_seen_at</code>. Need to extend list response.</li>
|
||||
<li><strong>"Backup now" button</strong> — <code>POST /api/hosts/:id/jobs</code> with <code>{kind: "backup"}</code>.</li>
|
||||
<li><strong>Recent activity</strong> — <code>GET /api/jobs?limit=N&order=desc</code>. Spec doesn't document query params; need to add.</li>
|
||||
<li><strong>HTMX cadence</strong> — this page polls every ~10s with <code>hx-trigger="every 10s"</code> on the summary + cards. WS push isn't needed here.</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SCREEN 2 · HOST DETAIL -->
|
||||
<!-- ============================================================ -->
|
||||
<section id="host-detail" class="screen">
|
||||
<span class="screen-label">Screen 2 · Host detail (/hosts/:id)</span>
|
||||
|
||||
<div class="chrome">
|
||||
<div class="logo">restic-manager</div>
|
||||
<nav>
|
||||
<span>Dashboard</span>
|
||||
<span class="active">Hosts</span>
|
||||
<span>Jobs</span>
|
||||
<span>Repos</span>
|
||||
<span>Alerts</span>
|
||||
<span>Audit</span>
|
||||
<span>Settings</span>
|
||||
</nav>
|
||||
<div class="right">user: alice (admin)</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-body annotated">
|
||||
<div>
|
||||
<!-- Host header -->
|
||||
<div class="box solid" style="margin-bottom:24px">
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:16px">
|
||||
<div>
|
||||
<div class="small">« Dashboard / Hosts</div>
|
||||
<h2 style="margin:4px 0">prod-db-01</h2>
|
||||
<div class="small">linux/amd64 · agent 0.4.2 · restic 0.17.1 · last seen 12s ago</div>
|
||||
<div style="margin-top:8px">
|
||||
<span class="pill"><span class="dot ok"></span>online</span>
|
||||
<span class="pill">tag: prod</span>
|
||||
<span class="pill">tag: db</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:6px; align-items:flex-end">
|
||||
<div class="small">Currently: <span class="strong">idle</span></div>
|
||||
<div style="display:flex; gap:8px">
|
||||
<span class="btn">Backup now</span>
|
||||
<span class="btn ghost">Run check</span>
|
||||
<span class="btn ghost">…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<a href="#" class="active">Snapshots</a>
|
||||
<a href="#">Schedules</a>
|
||||
<a href="#">Jobs</a>
|
||||
<a href="#">Repo</a>
|
||||
<a href="#">Settings</a>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Snapshots (active) -->
|
||||
<div>
|
||||
<div class="row" style="margin-bottom:12px">
|
||||
<div class="box" style="flex:3">[ filter by tag · path · date range ]</div>
|
||||
<div class="box" style="flex:1">[ sort: newest first ]</div>
|
||||
</div>
|
||||
<div class="box solid" style="padding:0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Snapshot</th><th>Time</th><th>Paths</th><th>Tags</th><th>Size</th><th>Files</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>3a8f1e</code></td><td>2h ago</td><td>/var/lib/postgres</td><td>auto, daily</td><td>412 GB</td><td>1.2M</td><td><span class="small">restore · diff</span></td></tr>
|
||||
<tr><td><code>8c7b22</code></td><td>1d ago</td><td>/var/lib/postgres</td><td>auto, daily</td><td>411 GB</td><td>1.2M</td><td><span class="small">restore · diff</span></td></tr>
|
||||
<tr><td><code>4f0a99</code></td><td>2d ago</td><td>/var/lib/postgres, /etc</td><td>auto, weekly</td><td>411 GB</td><td>1.2M</td><td><span class="small">restore · diff</span></td></tr>
|
||||
<tr><td colspan="7" class="small" style="text-align:center; padding:12px">… 1,281 more · load more</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other tabs collapsed previews -->
|
||||
<hr style="margin:32px 0; border:none; border-top:1px dashed var(--soft)">
|
||||
<div class="small" style="margin-bottom:8px">Other tabs (preview, not navigated):</div>
|
||||
|
||||
<div class="grid-2">
|
||||
|
||||
<!-- TAB: Schedules -->
|
||||
<div class="box solid">
|
||||
<div class="strong" style="margin-bottom:8px">Tab · Schedules</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Kind</th><th>Cron</th><th>Paths</th><th>Retention</th><th>Enabled</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>backup</td><td>0 2 * * *</td><td>/var/lib/postgres</td><td>7d/4w/12m</td><td>[x]</td></tr>
|
||||
<tr><td>forget+prune</td><td>0 4 * * 0</td><td>—</td><td>per policy</td><td>[x]</td></tr>
|
||||
<tr><td>check</td><td>0 5 1 * *</td><td>—</td><td>—</td><td>[ ]</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px"><span class="btn">+ New schedule</span></div>
|
||||
<details style="margin-top:12px">
|
||||
<summary>schedule editor (expanded form)</summary>
|
||||
<div class="stack" style="margin-top:8px">
|
||||
<div class="box">kind: [backup ▾]</div>
|
||||
<div class="box">cron: [ 0 2 * * * ] <span class="small">human: every day at 02:00</span></div>
|
||||
<div class="box">paths: [ /var/lib/postgres ] [+ add]</div>
|
||||
<div class="box">excludes: [ *.tmp, /tmp ]</div>
|
||||
<div class="box">tags: [ auto, daily ]</div>
|
||||
<div class="box">retention: keep [7] daily, [4] weekly, [12] monthly · keep-tag [ ]</div>
|
||||
<div class="box">bandwidth: upload [ ] KB/s · download [ ] KB/s <span class="small">§14.2</span></div>
|
||||
<div class="box">pre-hook: [ pg_dump ... ] <span class="small">§14.3 admin-only</span></div>
|
||||
<div class="box">post-hook: [ ... ]</div>
|
||||
<div class="box">enabled: [x]</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Jobs -->
|
||||
<div class="box solid">
|
||||
<div class="strong" style="margin-bottom:8px">Tab · Jobs (host-scoped)</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Started</th><th>Kind</th><th>Status</th><th>Duration</th><th>By</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>2h ago</td><td>backup</td><td>succeeded</td><td>00:14:22</td><td>schedule</td></tr>
|
||||
<tr><td>1d ago</td><td>check</td><td>succeeded</td><td>00:42:17</td><td>schedule</td></tr>
|
||||
<tr><td>2d ago</td><td>backup</td><td>cancelled</td><td>00:00:42</td><td>alice</td></tr>
|
||||
<tr><td>3d ago</td><td>backup</td><td>failed</td><td>00:01:09</td><td>schedule</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Repo -->
|
||||
<div class="box solid">
|
||||
<div class="strong" style="margin-bottom:8px">Tab · Repo</div>
|
||||
<div class="grid-2">
|
||||
<div><div class="label">URL</div><div>rest:https://restic.lab…/prod-db-01</div></div>
|
||||
<div><div class="label">Kind</div><div>rest (append-only)</div></div>
|
||||
<div><div class="label">Total size</div><div>412 GB</div></div>
|
||||
<div><div class="label">Dedup ratio</div><div>4.2×</div></div>
|
||||
<div><div class="label">Snapshots</div><div>1,284</div></div>
|
||||
<div><div class="label">Last check</div><div>1d ago · clean</div></div>
|
||||
<div><div class="label">Lock state</div><div>unlocked</div></div>
|
||||
<div><div class="label">Credential</div><div>append-only · rotated 14d ago</div></div>
|
||||
</div>
|
||||
<div style="margin-top:12px; display:flex; gap:8px">
|
||||
<span class="btn">Run check</span>
|
||||
<span class="btn ghost">Unlock</span>
|
||||
<span class="btn ghost">Forget+prune (admin)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Settings -->
|
||||
<div class="box solid">
|
||||
<div class="strong" style="margin-bottom:8px">Tab · Settings</div>
|
||||
<div class="stack">
|
||||
<div class="box"><div class="label">Tags</div><div>prod, db [+ add]</div></div>
|
||||
<div class="box"><div class="label">Default pre-hook</div><div>(empty)</div></div>
|
||||
<div class="box"><div class="label">Default post-hook</div><div>(empty)</div></div>
|
||||
<div class="box"><div class="label">Hook shell</div><div>/bin/sh</div></div>
|
||||
<div class="box"><div class="label">Default bandwidth caps</div><div>none</div></div>
|
||||
<div class="box">
|
||||
<div class="label">Enrollment</div>
|
||||
<div>enrolled 42d ago · <span class="btn ghost">Regenerate token</span></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="label">Agent</div>
|
||||
<div>0.4.2 · auto-update [x] · <span class="btn ghost">Force update now</span></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="label danger" style="color:var(--note)">Danger zone</div>
|
||||
<div><span class="btn danger">Remove host</span> <span class="small">does not touch repo data</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- annotations -->
|
||||
<aside class="ann-lane">
|
||||
<h4>Data sources</h4>
|
||||
<ul>
|
||||
<li><strong>Host header</strong> — <code>GET /api/hosts/:id</code>. <em>Gap:</em> "currently running job" not in domain model. Either join a <code>current_job_id</code> on Host, or have UI poll <code>GET /api/jobs?host_id=X&status=running</code>.</li>
|
||||
<li><strong>Snapshots tab</strong> — <code>GET /api/hosts/:id/snapshots</code>. Filtering needs server support: <code>?tag=</code>, <code>?path=</code>, <code>?since=</code>. Tag autocomplete needs distinct list — either client-derived or new endpoint.</li>
|
||||
<li><strong>Schedules tab</strong> — <code>GET /api/hosts/:id/schedules</code> + <code>POST/PUT/DELETE</code>. Editor exposes §14.2 bandwidth and §14.3 hooks — both stored as JSON blobs on Schedule, but UI needs structured fields. Confirm <code>retention_policy</code> JSON shape.</li>
|
||||
<li><strong>Jobs tab</strong> — <code>GET /api/jobs?host_id=X</code>. <em>Gap:</em> "By" column wants user-or-schedule attribution. AuditLog has it; Job table doesn't expose <code>actor</code> directly. Either denormalize onto Job or join.</li>
|
||||
<li><strong>Repo tab</strong> — <code>GET /api/hosts/:id/repo</code>. <em>Gap:</em> spec lists size/last-check/lock state. Add: dedup ratio, snapshot count, credential rotation timestamp, append-only flag. (Some derive from <code>restic stats</code>.)</li>
|
||||
<li><strong>Settings tab</strong> — mostly host-row edits. New: <code>POST /api/hosts/:id/agent/update</code> for force-update (§4.2 self-update). <em>Gap:</em> spec doesn't surface this.</li>
|
||||
<li><strong>HTMX cadence</strong> — tab content swap via <code>?tab=jobs</code> hyperlinks (server renders partial). Header polls every 10s for currently-running state.</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SCREEN 3 · JOB DETAIL -->
|
||||
<!-- ============================================================ -->
|
||||
<section id="job-detail" class="screen">
|
||||
<span class="screen-label">Screen 3 · Job detail (/jobs/:id) — running state</span>
|
||||
|
||||
<div class="chrome">
|
||||
<div class="logo">restic-manager</div>
|
||||
<nav>
|
||||
<span>Dashboard</span>
|
||||
<span>Hosts</span>
|
||||
<span class="active">Jobs</span>
|
||||
<span>Repos</span>
|
||||
<span>Alerts</span>
|
||||
<span>Audit</span>
|
||||
<span>Settings</span>
|
||||
</nav>
|
||||
<div class="right">user: alice (admin)</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-body annotated">
|
||||
<div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="box solid" style="margin-bottom:16px">
|
||||
<div class="small">« prod-db-01 / Jobs</div>
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-top:4px">
|
||||
<div>
|
||||
<h2 style="margin:0">backup · prod-db-01</h2>
|
||||
<div class="small">job <code>j_01HJ8K7</code> · started 4m12s ago · triggered by alice</div>
|
||||
<div style="margin-top:8px">
|
||||
<span class="pill"><span class="dot ok"></span>running</span>
|
||||
<span class="pill">schedule: nightly-pg</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px">
|
||||
<span class="btn danger">Cancel job</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="grid-2" style="margin-bottom:16px">
|
||||
<div class="box solid">
|
||||
<div class="label">Progress</div>
|
||||
<div class="value strong" style="margin:4px 0">38% · ~6m remaining</div>
|
||||
<div class="progress"><span></span></div>
|
||||
<div class="small" style="margin-top:6px">156 GB of 412 GB · 482k of 1.2M files</div>
|
||||
</div>
|
||||
<div class="box solid">
|
||||
<div class="grid-2">
|
||||
<div><div class="label">Files new</div><div>2,103</div></div>
|
||||
<div><div class="label">Files changed</div><div>418</div></div>
|
||||
<div><div class="label">Bytes added</div><div>2.4 GB</div></div>
|
||||
<div><div class="label">Throughput</div><div>42 MB/s</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live log -->
|
||||
<div class="label" style="margin-bottom:6px">Live log <span class="small">(streaming via WS)</span></div>
|
||||
<div class="log">
|
||||
<span class="ts">14:02:11</span> [agent] starting restic backup --json
|
||||
<span class="ts">14:02:11</span> [agent] pre_hook: pg_dump | gzip > /tmp/dump.sql.gz
|
||||
<span class="ts">14:02:48</span> [pre_hook] dump complete (1.2 GB)
|
||||
<span class="ts">14:02:49</span> [restic] open repository
|
||||
<span class="ts">14:02:50</span> [restic] lock repository
|
||||
<span class="ts">14:02:50</span> [restic] load index files
|
||||
<span class="ts">14:02:53</span> [restic] start scan
|
||||
<span class="ts">14:02:55</span> [restic] start backup on /var/lib/postgres
|
||||
<span class="ts">14:03:01</span> [restic] {"message_type":"status","percent_done":0.04,"total_files":1234567,"files_done":48234,"total_bytes":442000000000,"bytes_done":17600000000}
|
||||
<span class="ts">14:04:22</span> [restic] {"message_type":"status","percent_done":0.18,"...}
|
||||
<span class="ts">14:05:55</span> [restic] {"message_type":"status","percent_done":0.31,"...}
|
||||
<span class="ts">14:06:23</span> <span class="err">[restic] warning: failed to lstat /var/lib/postgres/pg_wal/.lock</span>
|
||||
<span class="ts">14:06:24</span> [restic] {"message_type":"status","percent_done":0.38,"...}
|
||||
<span style="color:#888">▌</span>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<div><span class="small">[ ] auto-scroll [ ] show stderr only download full log</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<aside class="ann-lane">
|
||||
<h4>Data sources</h4>
|
||||
<ul>
|
||||
<li><strong>Header</strong> — <code>GET /api/jobs/:id</code>. Need: kind, host, started_at, actor (user / schedule / system), status, schedule_id, schedule_name. <em>Gap:</em> Job table has <code>scheduled_id</code> but no actor/user_id; need to join AuditLog or denormalize.</li>
|
||||
<li><strong>Progress block</strong> — live updates from <code>WS /api/jobs/:id/stream</code>. The WS message <code>job.progress</code> (§6.2) needs a documented JSON shape: <code>{percent_done, files_done, total_files, bytes_done, total_bytes, eta_seconds, throughput_bps}</code>. Spec leaves this vague.</li>
|
||||
<li><strong>Stats panel</strong> — on completion mirrors <code>restic backup --json</code> summary fields: <code>files_new</code>, <code>files_changed</code>, <code>files_unmodified</code>, <code>data_added</code>, <code>total_bytes_processed</code>, <code>duration</code>, <code>snapshot_id</code>. Lives in <code>Job.stats</code> JSON.</li>
|
||||
<li><strong>Live log</strong> — <code>WS</code> messages of type <code>log.stream</code> (agent → server) fan out to browsers subscribed to <code>/api/jobs/:id/stream</code>. UI distinguishes <code>stdout</code> / <code>stderr</code> / <code>event</code> — the schema's <code>JobLog.stream</code> enum already covers this.</li>
|
||||
<li><strong>Cancel</strong> — <code>POST /api/jobs/:id/cancel</code> → server emits <code>command.cancel</code> WS to agent (§6.2). UI should optimistically show "cancelling…" until WS confirms <code>job.finished</code>.</li>
|
||||
<li><strong>HTMX caveat</strong> — this is the one screen where progressive enhancement isn't enough; live log requires WS. Plan: <code>hx-ext="ws"</code> with <code>ws-connect</code>, server sends innerHTML-fragment patches for the progress + log areas. Falls back to 2s polling without WS.</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- FINDINGS -->
|
||||
<!-- ============================================================ -->
|
||||
<section id="findings" class="findings">
|
||||
<h3>Findings — gaps in spec.md §6 surfaced by Phase 0 wireframing</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Aggregate fleet endpoint missing.</strong> Dashboard summary strip and Prometheus metrics (§14.4) both need fleet rollups. Add <code>GET /api/fleet/summary</code> returning host counts by status, total repo bytes, open alert counts. Cheaper than client fanout and reused by /metrics.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Host list response is too thin.</strong> Domain model Host (§5) has status + last_seen_at; cards need <code>last_backup_at</code>, <code>last_backup_status</code>, <code>repo_size_bytes</code>, <code>snapshot_count</code>, <code>open_alert_count</code>, <code>current_job_id</code>. Either add columns or compute server-side and include in <code>GET /api/hosts</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Job actor not modelled.</strong> Job table tracks <code>scheduled_id</code> but not <em>who</em> (user vs schedule vs system) triggered a run-now. Dashboard "Recent activity" and Jobs tab both want this. Add <code>Job.actor_kind</code> + <code>Job.actor_id</code> — cheaper than joining AuditLog every time.
|
||||
</li>
|
||||
<li>
|
||||
<strong>WS <code>job.progress</code> JSON shape is undefined.</strong> §6.2 lists the message name only. Lock the shape now: <code>{percent_done: float, files_done: int, total_files: int, bytes_done: int, total_bytes: int, eta_seconds: int, throughput_bps: int}</code>. Keeps client + agent in lockstep before Phase 1 codes against it.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Repo response needs more fields.</strong> §6.1 says size/last-check/lock state. Wireframe also wants: dedup ratio, snapshot count, credential rotation timestamp, append-only flag. Most derive from <code>restic stats</code> + Credential row — expose them through <code>GET /api/hosts/:id/repo</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Snapshot filtering needs server support.</strong> Tag/path/date filters belong on the server (12-host fleets are small but a single host can hold thousands of snapshots). Add query params to <code>GET /api/hosts/:id/snapshots</code>: <code>?tag=</code>, <code>?path=</code>, <code>?since=</code>, <code>?limit=</code>. Distinct-tag list endpoint optional — could be derived client-side at first.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Job listing needs query params.</strong> Recent activity, host-scoped jobs, and the Jobs page all use <code>GET /api/jobs</code>. Lock down: <code>?host_id=</code>, <code>?kind=</code>, <code>?status=</code>, <code>?since=</code>, <code>?limit=</code>, <code>?order=</code>. Pagination too.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Agent self-update endpoint not in §6.1.</strong> §4.2 describes the mechanism but no REST endpoint exists. Settings tab wants a "Force update now" button — add <code>POST /api/hosts/:id/agent/update</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Schedule retention/options JSON shape.</strong> §14.2 (bandwidth) and §14.3 (hooks) both extend <code>Schedule</code>. Document the canonical shape now (<code>retention_policy</code>, <code>options.limit_upload</code>, <code>options.limit_download</code>, <code>pre_hook</code>, <code>post_hook</code>) so the schedule editor and the agent can both target it.
|
||||
</li>
|
||||
<li>
|
||||
<strong>HTMX-vs-WS responsibility split.</strong> Decision: only the Job detail screen needs WS. Dashboard, Hosts, Snapshots use HTMX polling (10s). This avoids fan-out complexity for v1; revisit if dashboard feels stale.
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user