design: extend v1 to login / add-host / host-detail / job-log + lock components

Five hi-fi screens completing the Phase 1 surface, all in v1's dark
operator-console register.

  v1-login          Sparse centred card. Sign-in + first-error variant.
                    No marketing chrome; build version sits in footer
                    so a returning operator can spot agent drift.

  v1-add-host       Focused two-column page (form left, contextual
                    "what happens next" right) — not a modal. Two
                    states: form (state A) and minted-token result
                    with install command (state B). Backed by
                    POST /api/enrollment-tokens (P1-32).

  v1-host-detail    Persistent header (status dot, mono name, tags,
                    primary CTAs, vitals strip) over four sub-tabs
                    (Snapshots / Jobs / Repo / Settings). Snapshots
                    is the default — the thing 90% of operators
                    want when they click a host name. Right rail
                    holds Recent activity, run-now stack, and a
                    danger-zone panel.

  v1-job-log        WS-streamed log view. Three states: running (live
                    progress bar + auto-scroll cursor), succeeded
                    (summary stats + final lines), failed (error
                    panel + tail). Backed by WS /api/jobs/{id}/stream
                    (P1-21 remainder).

  v1-components     The load-bearing reference. 14 sections covering
                    tokens (colour + type scale), status, buttons,
                    form fields, tags, tabs, host row, log viewer,
                    progress bar, stat tile, modal, toast, install
                    snippet, empty-state pattern. Every CSS class is
                    real and copy-able into the Go template build.

This locks the visual register before P1-23 onwards. Each Phase 1
template gets a {{define}} matching a section in v1-components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 19:05:39 +01:00
parent cca525a04d
commit 8b7b1479a1
5 changed files with 1995 additions and 0 deletions
+347
View File
@@ -0,0 +1,347 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Add host</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);
--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); }
.field-label { font-size: 12px; color: var(--ink-mid); margin-bottom: 6px; display: block; }
.field-help { font-size: 12px; color: var(--ink-mute); margin-top: 6px; line-height: 1.55; text-wrap: pretty; }
.field {
width: 100%; padding: 9px 12px;
background: var(--bg); border: 1px solid var(--line-soft);
color: var(--ink); border-radius: 5px;
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 120ms ease;
}
.field:focus { border-color: var(--accent); }
.field.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
.field.with-prefix { padding-left: 64px; }
.field-prefix {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
font-family: 'JetBrains Mono', monospace; font-size: 12px;
color: var(--ink-mute); pointer-events: none;
}
.btn {
font-size: 13px; font-weight: 500; padding: 8px 14px; border-radius: 5px;
background: transparent; border: 1px solid var(--line); color: var(--ink-mid);
transition: all 120ms ease; cursor: pointer;
}
.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; color: var(--ink); }
.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); }
.tag {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11px; line-height: 1; padding: 5px 7px 5px 9px;
border: 1px solid var(--line); color: var(--ink-mid);
border-radius: 3px; letter-spacing: 0.01em;
}
.tag .x { color: var(--ink-fade); cursor: pointer; }
.tag .x:hover { color: var(--ink); }
.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: 680px; 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 { padding: 16px 32px 0; font-size: 11px; color: var(--ink-fade); letter-spacing: 0.18em; text-transform: uppercase; }
.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; }
</style>
</head>
<body>
<div class="doc">
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Add host</div>
<h1>Add a host.</h1>
<p>
A focused two-column page, not a modal: the form lives where the cursor
needs it, the contextual help and security footnote live where the eye
naturally drifts. After the form is submitted the same URL renders the
result state — token + install command — so the operator never loses
their place.
</p>
<p class="meta">
Backed by <span class="mono" style="color: var(--ink-mid);">POST /api/enrollment-tokens</span>
(P1-32). Repo creds become an AEAD blob bound to the token hash;
<span class="mono" style="color: var(--ink-mid);">ConsumeEnrollmentToken</span> rebinds them
under the new host_id and the WS push lands them on the agent.
</p>
</header>
<!-- Stage 1: form state -->
<div class="stage-label">State A · form</div>
<div class="stage-frame">
<div style="background: var(--bg);">
<!-- chrome (same as dashboard) -->
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end" style="padding-top: 0;">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<!-- page header -->
<div class="doc" style="padding: 36px 32px 12px;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><span style="color: var(--ink-mid);">Add host</span></div>
<h1 style="font-size: 24px; font-weight: 500; letter-spacing: -0.012em; margin-top: 10px;">Add a host</h1>
<p style="color: var(--ink-mute); font-size: 13px; margin-top: 6px; max-width: 580px;" class="text-pretty">
Mints a one-time enrolment token (TTL 1 hour) and binds the repo credentials to it.
The token can only be used once — generate a fresh one if it expires or you typed
something wrong.
</p>
</div>
<!-- two-column body -->
<div class="doc grid grid-cols-12 gap-8" style="padding: 28px 32px 56px;">
<!-- form -->
<form class="col-span-7 panel" style="border-radius: 7px; padding: 28px 32px;">
<h3 style="font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 18px;">Host</h3>
<div style="margin-bottom: 22px;">
<label class="field-label" for="ah-name">Hostname</label>
<input id="ah-name" type="text" class="field mono" value="prod-redis-01">
<div class="field-help">Becomes the hosts display name. Most operators use the boxs actual hostname so logs line up.</div>
</div>
<div style="margin-bottom: 28px;">
<label class="field-label">Tags</label>
<div class="flex items-center gap-1.5 flex-wrap" style="padding: 8px; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 5px;">
<span class="tag">prod <span class="x">×</span></span>
<span class="tag">cache <span class="x">×</span></span>
<input type="text" placeholder="add tag…" class="mono" style="flex: 1; min-width: 80px; background: transparent; border: none; color: var(--ink); font-size: 12px; padding: 4px 6px; outline: none;">
</div>
<div class="field-help">Free-form. Used for filtering and grouping on the dashboard.</div>
</div>
<h3 style="font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); margin-bottom: 18px; padding-top: 6px; border-top: 1px solid var(--line-soft); padding-top: 24px;">Restic repository</h3>
<div style="margin-bottom: 22px;">
<label class="field-label" for="ah-url">Repo URL</label>
<div style="position: relative;">
<input id="ah-url" type="text" class="field mono with-prefix" value="rest:https://restic.unraid.lab/prod-redis-01/">
<span class="field-prefix">repo:</span>
</div>
<div class="field-help">Whatever <span class="mono" style="color: var(--ink-mid);">restic -r</span> would accept. Most fleets terminate at a <span class="mono" style="color: var(--ink-mid);">restic/rest-server</span>; <span class="mono" style="color: var(--ink-mid);">s3:</span> and <span class="mono" style="color: var(--ink-mid);">b2:</span> URLs work equally well.</div>
</div>
<div style="margin-bottom: 22px;">
<label class="field-label" for="ah-user">Repo username <span style="color: var(--ink-fade); font-weight: 400;">· optional</span></label>
<input id="ah-user" type="text" class="field mono" value="prod-redis-01">
<div class="field-help">For <span class="mono" style="color: var(--ink-mid);">rest-server</span> with htpasswd, this is the per-host user.</div>
</div>
<div style="margin-bottom: 28px;">
<label class="field-label" for="ah-pass">Repo password</label>
<input id="ah-pass" type="password" class="field" value="••••••••••••••••">
<div class="field-help">Encrypted at rest using the servers AEAD key. Pushed to the agent only over the authenticated WebSocket.</div>
</div>
<div style="display: flex; gap: 8px; padding-top: 18px; border-top: 1px solid var(--line-soft);">
<button type="submit" class="btn btn-primary">Mint token &amp; show install command</button>
<button type="button" class="btn">Cancel</button>
</div>
</form>
<!-- contextual help -->
<aside class="col-span-5">
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-fade); margin-bottom: 12px;">What happens next</div>
<ol style="counter-reset: step; padding: 0; margin: 0; list-style: none;">
<li class="step" style="position: relative; padding-left: 32px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 50%; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono';">1</span>
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">You get a one-time install command</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.6;" class="text-pretty">
A <span class="mono" style="color: var(--ink-mid);">curl … | sh</span> snippet with the server URL and a 1h token baked in.
</div>
</li>
<li style="position: relative; padding-left: 32px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 50%; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono';">2</span>
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">You run it on the box you want to back up</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.6;" class="text-pretty">
Installer creates a service user, drops the agent binary, registers a sandboxed systemd unit, and enrols.
</div>
</li>
<li style="position: relative; padding-left: 32px; padding-bottom: 18px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 50%; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono';">3</span>
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">The host appears on the dashboard within seconds</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.6;" class="text-pretty">
Server pushes the encrypted repo creds over the WS on first <span class="mono" style="color: var(--ink-mid);">hello</span>; agent decrypts and persists to <span class="mono" style="color: var(--ink-mid);">secrets.enc</span>.
</div>
</li>
<li style="position: relative; padding-left: 32px;">
<span style="position: absolute; left: 0; top: 0; width: 22px; height: 22px; border: 1px solid var(--line); border-radius: 50%; font-size: 11px; line-height: 20px; text-align: center; color: var(--ink-mute); font-family: 'JetBrains Mono';">4</span>
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">You hit “Run backup now”</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px; line-height: 1.6;" class="text-pretty">
First snapshot lands in the repo. Subsequent ones run on whatever schedule you set (Phase 2).
</div>
</li>
</ol>
<div style="margin-top: 32px; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line-soft); border-radius: 6px;">
<div style="font-size: 11px; color: var(--warn); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 6px;">Prerequisite</div>
<p style="font-size: 12px; color: var(--ink-mid); line-height: 1.6;" class="text-pretty">
<span class="mono" style="color: var(--ink);">restic</span> ≥ 0.16 must already be installed on the target host. The agent does not install it for you — different distros, different package managers, too much surface area to maintain.
</p>
</div>
</aside>
</div>
</div>
</div>
<!-- Stage 2: result state -->
<div class="stage-label">State B · token minted, install command shown</div>
<div class="stage-frame">
<div style="background: var(--bg);">
<!-- chrome -->
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<div class="doc" style="padding: 36px 32px 12px;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><span style="color: var(--ink-mid);">Add host</span><span class="sep">/</span><span style="color: var(--ink);">prod-redis-01</span></div>
<div class="flex items-center gap-3 mt-2.5">
<h1 style="font-size: 24px; font-weight: 500; letter-spacing: -0.012em;">Token minted</h1>
<span class="mono" style="font-size: 11px; padding: 4px 8px; background: color-mix(in oklch, var(--ok), transparent 88%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">expires in 59m 54s</span>
</div>
<p style="color: var(--ink-mute); font-size: 13px; margin-top: 6px; max-width: 580px;" class="text-pretty">
Run the snippet below on the target box. The host will appear on the
dashboard within a few seconds of the agent connecting.
</p>
</div>
<div class="doc" style="padding: 28px 32px 56px;">
<!-- install command -->
<div class="panel" style="border-radius: 7px; overflow: hidden;">
<div class="hairline flex items-center justify-between" style="padding: 12px 16px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em;">Install command · paste-and-run</div>
<div class="flex items-center gap-2">
<button class="btn btn-ghost mono" style="font-size: 11px;">Download install-prod-redis-01.sh</button>
<button class="btn">Copy</button>
</div>
</div>
<pre class="mono" style="margin: 0; padding: 18px 18px; font-size: 12.5px; line-height: 1.75; color: var(--ink-mid); white-space: pre-wrap; word-break: break-all;">curl -fsSL <span style="color: var(--accent);">https://restic.lab.example/install.sh</span> | sudo \
RM_SERVER=<span style="color: var(--accent);">https://restic.lab.example</span> \
RM_TOKEN=<span style="color: var(--accent);">HdqFbQh8U-I1fb52iP1M8qxvoYS5t9VZ-T-yghr_CzA</span> sh</pre>
</div>
<!-- live status of the new host -->
<div class="grid grid-cols-12 gap-6" style="margin-top: 28px;">
<div class="col-span-7 panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 14px;">Awaiting agent connection</div>
<div class="flex items-center gap-3">
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--ink-fade); animation: pulse-wait 1.6s ease-in-out infinite;"></span>
<span class="mono" style="font-size: 14px; color: var(--ink);">prod-redis-01</span>
<span style="font-size: 12px; color: var(--ink-mute);">— enrolment will mark this online</span>
</div>
<div style="margin-top: 14px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 5px; font-family: 'JetBrains Mono'; font-size: 11.5px; color: var(--ink-mute); line-height: 1.7;">
<div>11:42:18.221 <span style="color: var(--ink-mid);">server</span> token minted · 1h ttl</div>
<div style="color: var(--ink-fade);"> <span style="color: var(--ink-fade);">awaiting POST /api/agents/enroll …</span></div>
</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: 12px;">If the agent doesnt appear</div>
<ul style="list-style: none; padding: 0; margin: 0; font-size: 13px; color: var(--ink-mid); line-height: 1.65;" class="text-pretty">
<li style="padding: 8px 0; border-bottom: 1px solid var(--line-soft);">Check the box can reach <span class="mono" style="color: var(--ink);">https://restic.lab.example</span> over HTTPS.</li>
<li style="padding: 8px 0; border-bottom: 1px solid var(--line-soft);">Check <span class="mono" style="color: var(--ink);">restic --version</span> ≥ 0.16 — the installer wont bail on this, but backups will fail.</li>
<li style="padding: 8px 0; border-bottom: 1px solid var(--line-soft);">Check <span class="mono" style="color: var(--ink);">journalctl -u restic-manager-agent -n 50</span> on the target box.</li>
<li style="padding: 8px 0;">Token expired? Mint a new one — theyre cheap.</li>
</ul>
</aside>
</div>
</div>
</div>
</div>
</div>
<style>
@keyframes pulse-wait {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
</style>
</body>
</html>
+703
View File
@@ -0,0 +1,703 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Components</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>
/* ============================================================
v1 design tokens. Whatever the Go templates need, lives here.
Anything not in this file does not exist in v1. New components
get added here first, then templated.
============================================================ */
:root {
/* surface */
--bg: oklch(0.17 0.006 250);
--panel: oklch(0.20 0.007 250);
--panel-hi: oklch(0.23 0.008 250);
/* line */
--line: oklch(0.27 0.010 250);
--line-soft: oklch(0.23 0.008 250);
/* ink */
--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);
/* state */
--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);
/* one accent */
--accent: oklch(0.82 0.12 195);
--accent-dim:oklch(0.55 0.10 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; }
::selection { background: color-mix(in oklch, var(--accent), transparent 70%); }
/* ---------- LAYOUT PRIMITIVES ---------- */
.doc { max-width: 1280px; margin: 0 auto; padding: 0 32px; }
.panel { background: var(--panel); border: 1px solid var(--line-soft); }
.hairline { box-shadow: inset 0 -1px 0 var(--line-soft); }
/* ---------- STATUS DOTS ---------- */
.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-degraded { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in oklch, var(--warn), transparent 80%); }
.dot-offline { background: var(--off); }
.dot-failed { background: var(--bad); box-shadow: 0 0 0 3px color-mix(in oklch, var(--bad), transparent 80%); }
.pulse { animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent), transparent 80%); }
50% { box-shadow: 0 0 0 6px color-mix(in oklch, var(--accent), transparent 92%); }
}
/* ---------- BUTTONS ---------- */
.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;
}
.btn:hover { background: var(--panel-hi); color: var(--ink); }
.btn:disabled, .btn[disabled] { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.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 { font-size: 13px; padding: 9px 14px; }
/* ---------- TAGS / CHIPS ---------- */
.tag {
display: inline-flex; align-items: center; gap: 5px;
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;
}
.tag-removable .x { color: var(--ink-fade); cursor: pointer; padding-left: 2px; }
.tag-status {
border: 1px solid color-mix(in oklch, var(--token), transparent 70%);
color: var(--token);
background: color-mix(in oklch, var(--token), transparent 88%);
}
/* ---------- FORM FIELDS ---------- */
.field-label { font-size: 12px; color: var(--ink-mid); margin-bottom: 6px; display: block; }
.field-help { font-size: 12px; color: var(--ink-mute); margin-top: 6px; line-height: 1.55; text-wrap: pretty; }
.field-error { font-size: 12px; color: oklch(0.85 0.10 25); margin-top: 6px; }
.field {
width: 100%; padding: 9px 12px;
background: var(--bg); border: 1px solid var(--line-soft);
color: var(--ink); border-radius: 5px;
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 120ms ease;
}
.field:focus { border-color: var(--accent); }
.field.invalid { border-color: color-mix(in oklch, var(--bad), transparent 50%); }
.field.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
.field.with-prefix { padding-left: 64px; }
.field-prefix {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
font-family: 'JetBrains Mono', monospace; font-size: 12px;
color: var(--ink-mute); pointer-events: none;
}
/* ---------- TABS ---------- */
.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; }
.sub-tab.active { color: var(--ink); border-color: var(--ink); }
/* ---------- HOST ROW (the dashboard's load-bearing component) ---------- */
.host-row {
display: grid; align-items: center;
grid-template-columns: 24px 1.4fr 0.95fr 1.5fr 0.75fr 0.7fr 0.7fr 1.1fr 92px;
padding: 11px 16px; font-size: 13px;
border-left: 3px solid transparent;
}
.host-row.head { padding-top: 10px; padding-bottom: 10px; font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; border-left-width: 3px; }
.host-row.degraded { border-left-color: color-mix(in oklch, var(--warn), transparent 50%); }
.host-row.failed { border-left-color: color-mix(in oklch, var(--bad), transparent 50%); }
.host-row.offline { border-left-color: color-mix(in oklch, var(--off), transparent 70%); }
.host-row:hover { background: var(--panel-hi); }
/* ---------- LOG VIEWER ---------- */
.log {
background: var(--bg); border: 1px solid var(--line-soft);
border-radius: 7px;
font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7;
overflow: hidden;
}
.log-line { display: grid; grid-template-columns: 14ch 8ch 1fr; column-gap: 14px; padding: 1px 16px; align-items: baseline; }
.log-line:first-child { padding-top: 12px; }
.log-line:last-child { padding-bottom: 12px; }
.log-ts { color: var(--ink-fade); }
.log-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-fade); }
.log-stream-stdout { color: var(--ink-mid); }
.log-stream-stderr { color: oklch(0.78 0.13 50); }
.log-stream-event { color: var(--accent); }
/* ---------- PROGRESS BAR ---------- */
.progress-track { background: var(--bg); border: 1px solid var(--line-soft); height: 6px; border-radius: 9999px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 9999px; transition: width 250ms ease; }
.progress-fill.ok { background: var(--ok); }
.progress-fill.bad { background: var(--bad); }
/* ---------- TOAST ---------- */
.toast {
display: flex; align-items: flex-start; gap: 12px;
background: var(--panel); border: 1px solid var(--line-soft);
padding: 12px 14px; border-radius: 7px;
box-shadow: 0 8px 24px -12px rgba(0,0,0,0.4);
max-width: 420px;
}
.toast-ok { border-left: 3px solid var(--ok); }
.toast-bad { border-left: 3px solid var(--bad); }
.toast .x { color: var(--ink-fade); cursor: pointer; padding: 2px 4px; margin-left: auto; }
/* ---------- MODAL ---------- */
.modal-backdrop {
position: relative; background: rgba(0,0,0,0.45);
border: 1px dashed var(--line-soft); border-radius: 7px;
padding: 48px;
}
.modal {
background: var(--panel); border: 1px solid var(--line-soft);
border-radius: 8px; max-width: 480px; margin: 0 auto;
box-shadow: 0 24px 48px -16px rgba(0,0,0,0.6);
}
.modal-head { padding: 18px 22px; border-bottom: 1px solid var(--line-soft); }
.modal-body { padding: 18px 22px; font-size: 13px; line-height: 1.65; color: var(--ink-mid); text-wrap: pretty; }
.modal-foot { padding: 14px 22px; border-top: 1px solid var(--line-soft); display: flex; gap: 8px; justify-content: flex-end; }
/* ---------- INSTALL SNIPPET ---------- */
.snippet { border: 1px solid var(--line-soft); border-radius: 6px; overflow: hidden; }
.snippet-head { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line-soft); font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; }
.snippet pre {
margin: 0; padding: 14px; font-family: 'JetBrains Mono', monospace;
font-size: 12px; color: var(--ink-mid); line-height: 1.7;
white-space: pre-wrap; word-break: break-all;
}
.snippet pre .var { color: var(--accent); }
/* ---------- DOCUMENT SHELL FOR THIS REFERENCE PAGE ---------- */
.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: 680px; margin-top: 14px; line-height: 1.65; text-wrap: pretty; }
.philosophy .meta { color: var(--ink-fade); font-size: 12px; margin-top: 14px; }
.section { padding: 48px 0 24px; border-bottom: 1px solid var(--line-soft); }
.section h2 {
font-size: 11px; font-weight: 600; color: var(--ink-fade);
text-transform: uppercase; letter-spacing: 0.16em;
margin-bottom: 20px;
}
.section h2 .num { color: var(--ink); margin-right: 8px; }
.section h2 .name { color: var(--ink); letter-spacing: 0.04em; font-weight: 600; }
.section h2 .desc { color: var(--ink-fade); font-weight: 400; margin-left: 12px; }
.swatch-row { display: grid; grid-template-columns: repeat(8, 1fr); gap: 12px; }
.swatch { background: var(--panel); border: 1px solid var(--line-soft); border-radius: 6px; overflow: hidden; }
.swatch .chip { height: 56px; }
.swatch .meta { padding: 8px 10px; }
.swatch .name { font-size: 12px; color: var(--ink); font-weight: 500; }
.swatch .var-name { font-size: 11px; color: var(--ink-fade); font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
</style>
</head>
<body>
<div class="doc">
<!-- Philosophy preamble -->
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Component reference</div>
<h1>The system, in one place.</h1>
<p>
Every reusable piece across the v1 mockups, lifted out and shown beside its
states. If something appears in a screen but not here, it shouldnt — its
either drift or a candidate for promotion. The Go templates (P1-23
onwards) lean on this file: a partial gets a name in here before it gets
a <code class="mono" style="color: var(--ink); background: var(--panel); padding: 1px 6px; border-radius: 3px;">{{define}}</code> in the templates.
</p>
<p class="meta">
Every CSS class on this page is real and copy-able into the Tailwind
build. Anything inline is a one-off and shouldnt be.
</p>
</header>
<!-- ============================================================
1 · TOKENS
============================================================ -->
<section class="section">
<h2><span class="num">01</span><span class="name">Tokens</span><span class="desc">colours · type · spacing · the alphabet of v1</span></h2>
<!-- surface + ink + line -->
<div style="margin-bottom: 28px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px;">Surface, line, ink</div>
<div class="swatch-row">
<div class="swatch"><div class="chip" style="background: var(--bg);"></div><div class="meta"><div class="name">bg</div><div class="var-name">--bg</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--panel);"></div><div class="meta"><div class="name">panel</div><div class="var-name">--panel</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--panel-hi);"></div><div class="meta"><div class="name">panel-hi</div><div class="var-name">--panel-hi</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--line);"></div><div class="meta"><div class="name">line</div><div class="var-name">--line</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink);"></div><div class="meta"><div class="name">ink</div><div class="var-name">--ink</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-mid);"></div><div class="meta"><div class="name">ink-mid</div><div class="var-name">--ink-mid</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-mute);"></div><div class="meta"><div class="name">ink-mute</div><div class="var-name">--ink-mute</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-fade);"></div><div class="meta"><div class="name">ink-fade</div><div class="var-name">--ink-fade</div></div></div>
</div>
</div>
<!-- state + accent -->
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px;">State + accent</div>
<div class="swatch-row" style="grid-template-columns: repeat(5, 1fr);">
<div class="swatch"><div class="chip" style="background: var(--ok);"></div><div class="meta"><div class="name">ok</div><div class="var-name">--ok · success / online</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--warn);"></div><div class="meta"><div class="name">warn</div><div class="var-name">--warn · degraded / cache-warning</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--bad);"></div><div class="meta"><div class="name">bad</div><div class="var-name">--bad · failed / alert</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--off);"></div><div class="meta"><div class="name">off</div><div class="var-name">--off · offline (neutral)</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--accent);"></div><div class="meta"><div class="name">accent</div><div class="var-name">--accent · running, links</div></div></div>
</div>
</div>
<!-- type scale -->
<div style="margin-top: 32px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 16px;">Type scale</div>
<div class="panel" style="border-radius: 7px; padding: 16px 20px;">
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">28 / 500</span>
<span style="font-size: 28px; font-weight: 500; letter-spacing: -0.018em;">Hero / numeric stat</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">stat tile values</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">22 / 500</span>
<span style="font-size: 22px; font-weight: 500; letter-spacing: -0.012em;">Page title</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">h1 in body</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">18 / 500</span>
<span style="font-size: 18px; font-weight: 500;">Section header</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">h2 above panels</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">13 / 400</span>
<span style="font-size: 13px;">Body & table cells</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">default for prose</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">12 / 400</span>
<span style="font-size: 12px; color: var(--ink-mute);">Helper, captions, secondary detail</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">.field-help, meta lines</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0;">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">11 / 600 · 0.08em</span>
<span style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Section heading · column header</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">all caps tracking</span>
</div>
</div>
</div>
</section>
<!-- ============================================================
2 · STATUS
============================================================ -->
<section class="section">
<h2><span class="num">02</span><span class="name">Status</span><span class="desc">five states · only place colour is used as semantic</span></h2>
<div class="panel" style="border-radius: 7px;">
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-online"></span></span>
<span style="font-weight: 500;">online</span>
<span style="color: var(--ink-mute); font-size: 12px;">heartbeat received within last 90 seconds</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-online</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-online pulse"></span></span>
<span style="font-weight: 500;">online · running</span>
<span style="color: var(--ink-mute); font-size: 12px;">a job is in flight on this host — pulse only when active</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-online.pulse</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-degraded"></span></span>
<span style="font-weight: 500;">degraded</span>
<span style="color: var(--ink-mute); font-size: 12px;">online, but open alerts &gt; 0 — soft amber, not loud</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-degraded</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-offline"></span></span>
<span style="font-weight: 500;">offline</span>
<span style="color: var(--ink-mute); font-size: 12px;">no heartbeat for &gt; 90 seconds — neutral, not alarming</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-offline</span>
</div>
<div style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-failed"></span></span>
<span style="font-weight: 500;">last job failed</span>
<span style="color: var(--ink-mute); font-size: 12px;">distinct from offline — host is up, but its last job did not succeed</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-failed</span>
</div>
</div>
</section>
<!-- ============================================================
3 · BUTTONS
============================================================ -->
<section class="section">
<h2><span class="num">03</span><span class="name">Buttons</span><span class="desc">one primary per page · everything else is the neutral secondary</span></h2>
<div class="panel" style="border-radius: 7px; padding: 24px;">
<div class="flex flex-wrap items-center gap-3" style="margin-bottom: 24px;">
<button class="btn btn-primary btn-lg">+ Add host</button>
<button class="btn btn-primary">Run backup now</button>
<button class="btn">Run now</button>
<button class="btn btn-ghost">View →</button>
<button class="btn btn-danger">Cancel job</button>
<button class="btn" disabled>Disabled</button>
</div>
<div class="grid grid-cols-3 gap-6" style="font-size: 12px; color: var(--ink-mute); line-height: 1.55;">
<div><span style="color: var(--ink); font-weight: 500;">Primary</span> · the one verb the page is about. Use <span style="color: var(--ink);">at most once per page</span>.</div>
<div><span style="color: var(--ink); font-weight: 500;">Secondary</span> · everything else. Run-now per row, all panel actions.</div>
<div><span style="color: var(--ink); font-weight: 500;">Ghost</span> · inline / “View →” affordances inside cards and rows.</div>
<div><span style="color: var(--ink); font-weight: 500;">Danger</span> · destructive verbs only. Always pair with a confirmation modal for irreversible ones.</div>
<div><span style="color: var(--ink); font-weight: 500;">Disabled</span> · 0.4 opacity, not-allowed cursor, no hover.</div>
<div><span style="color: var(--ink); font-weight: 500;">Sizes</span> · default 12px. <span class="mono" style="color: var(--ink);">.btn-lg</span> for the page-level primary (slightly bigger affordance).</div>
</div>
</div>
</section>
<!-- ============================================================
4 · FORM FIELDS
============================================================ -->
<section class="section">
<h2><span class="num">04</span><span class="name">Form fields</span><span class="desc">labels above · helper below · 0.005em letter-spacing</span></h2>
<div class="grid grid-cols-2 gap-6">
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 18px;">
<label class="field-label">Default text</label>
<input class="field" type="text" value="prod-redis-01">
<div class="field-help">Helper text. Plain prose.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">Monospace text</label>
<input class="field mono" type="text" value="rest:https://restic.unraid.lab/host/">
<div class="field-help">Use <span class="mono" style="color: var(--ink);">.field.mono</span> for URLs, IDs, anything machine-shaped.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">With prefix</label>
<div style="position: relative;">
<input class="field mono with-prefix" type="text" value="https://restic.unraid.lab/host/">
<span class="field-prefix">repo:</span>
</div>
</div>
<div>
<label class="field-label">Password</label>
<input class="field" type="password" value="••••••••••••••••">
</div>
</div>
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 18px;">
<label class="field-label">Focus state</label>
<input class="field" type="text" value="prod-redis-01" style="border-color: var(--accent);">
<div class="field-help">Border becomes <span style="color: var(--accent);">accent</span> on focus.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">Invalid</label>
<input class="field invalid" type="text" value="not a url">
<div class="field-error">repo_url must look like <span class="mono">rest:</span> / <span class="mono">s3:</span> / <span class="mono">b2:</span></div>
</div>
<div>
<label class="field-label">Tag chip input</label>
<div class="flex items-center gap-1.5 flex-wrap" style="padding: 8px; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 5px;">
<span class="tag tag-removable">prod <span class="x">×</span></span>
<span class="tag tag-removable">cache <span class="x">×</span></span>
<input class="mono" placeholder="add tag…" style="flex: 1; min-width: 80px; background: transparent; border: none; color: var(--ink); font-size: 12px; padding: 4px 6px; outline: none;">
</div>
<div class="field-help">Free-form tags. Comma or Enter to commit.</div>
</div>
</div>
</div>
</section>
<!-- ============================================================
5 · TAGS / CHIPS
============================================================ -->
<section class="section">
<h2><span class="num">05</span><span class="name">Tags &amp; chips</span><span class="desc">labels for hosts · status pills · removable in form</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px;">
<div class="flex items-center gap-2 flex-wrap" style="margin-bottom: 16px;">
<span class="tag">prod</span>
<span class="tag">db</span>
<span class="tag">homelab</span>
<span class="tag">storage</span>
<span class="tag">edge</span>
<span class="tag">test</span>
<span class="tag tag-removable">prod <span class="x">×</span></span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--ok), transparent 88%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">succeeded</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--accent), transparent 88%); color: var(--accent); border: 1px solid color-mix(in oklch, var(--accent), transparent 70%); border-radius: 3px;">running</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--bad), transparent 88%); color: var(--bad); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); border-radius: 3px;">failed</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--warn), transparent 88%); color: var(--warn); border: 1px solid color-mix(in oklch, var(--warn), transparent 70%); border-radius: 3px;">cancelled</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--ok), transparent 90%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">expires in 59m</span>
</div>
</div>
</section>
<!-- ============================================================
6 · TABS
============================================================ -->
<section class="section">
<h2><span class="num">06</span><span class="name">Tabs</span><span class="desc">primary nav (accent underline) · sub-nav (ink underline)</span></h2>
<div class="panel" style="border-radius: 7px; padding: 0 24px;">
<nav class="flex items-end" style="border-bottom: 1px solid var(--line-soft);">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
<div class="panel" style="border-radius: 7px; padding: 0 24px; margin-top: 16px;">
<nav class="flex items-end">
<div class="sub-tab active">Snapshots <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">1,847</span></div>
<div class="sub-tab">Jobs <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">47 in 24h</span></div>
<div class="sub-tab">Repo</div>
<div class="sub-tab">Settings</div>
</nav>
</div>
</section>
<!-- ============================================================
7 · HOST ROW (3 states)
============================================================ -->
<section class="section">
<h2><span class="num">07</span><span class="name">Host row</span><span class="desc">the dashboard's load-bearing row · degraded/failed/offline get a left edge</span></h2>
<div class="panel" style="border-radius: 7px;">
<div class="host-row head hairline">
<div></div>
<div>Host</div>
<div>OS · arch</div>
<div>Last backup</div>
<div class="text-right">Repo size</div>
<div class="text-right">Snapshots</div>
<div class="text-right">Alerts</div>
<div>Tags</div>
<div></div>
</div>
<div class="host-row hairline">
<div><span class="dot dot-online"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">healthy-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--ok);">succeeded</span> · <span class="mono">5m ago</span></div>
<div class="text-right mono" style="color: var(--ink);">87 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">2,103</div>
<div class="text-right mono" style="color: var(--ink-mute);"></div>
<div class="flex gap-1.5"><span class="tag">prod</span></div>
<div class="text-right"><button class="btn">Run now</button></div>
</div>
<div class="host-row degraded hairline">
<div><span class="dot dot-degraded"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">degraded-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--ok);">succeeded</span> · <span class="mono">1h ago</span></div>
<div class="text-right mono" style="color: var(--ink);">128 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">1,402</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">3</div>
<div class="flex gap-1.5"><span class="tag">prod</span></div>
<div class="text-right"><button class="btn">Run now</button></div>
</div>
<div class="host-row failed hairline">
<div><span class="dot dot-online"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">failed-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--bad); font-weight:500;">failed</span> · <span class="mono">47m ago</span></div>
<div class="text-right mono" style="color: var(--ink);">97 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">2,847</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">1</div>
<div class="flex gap-1.5"><span class="tag">ci</span></div>
<div class="text-right"><button class="btn">Retry</button></div>
</div>
<div class="host-row offline">
<div><span class="dot dot-offline"></span></div>
<div class="mono" style="color: var(--ink-mid); font-weight: 500;">offline-host</div>
<div class="mono" style="color: var(--ink-mute); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mute);">last seen <span class="mono">2d ago</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">64 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mute);">127</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">1</div>
<div class="flex gap-1.5"><span class="tag">dev</span></div>
<div class="text-right"><span class="mono text-xs" style="color: var(--ink-fade);">offline</span></div>
</div>
</div>
</section>
<!-- ============================================================
8 · LOG VIEWER
============================================================ -->
<section class="section">
<h2><span class="num">08</span><span class="name">Log viewer</span><span class="desc">live or replayed · ts · stream · payload · color reserved for events &amp; stderr</span></h2>
<div class="log">
<div class="log-line"><span class="log-ts">11:43:21.039</span><span class="log-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.000,"total_files":21402,"files_done":0}</span></div>
<div class="log-line"><span class="log-ts">11:43:21.412</span><span class="log-tag">OUT</span><span class="log-stream-stdout">scan finished in 0.371s</span></div>
<div class="log-line"><span class="log-ts">11:43:25.812</span><span class="log-tag">ERR</span><span class="log-stream-stderr">warn: file changed during read: /var/lib/postgresql/13/main/pg_wal/000000010000007800000042</span></div>
<div class="log-line"><span class="log-ts">11:43:31.625</span><span class="log-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.380,"files_done":8124,"bytes_done":1503870976}</span></div>
<div class="log-line"><span class="log-ts" style="color: var(--accent);">11:43:32.122</span><span class="log-tag" style="color: var(--accent);">···</span><span style="color: var(--accent);"></span></div>
</div>
</section>
<!-- ============================================================
9 · PROGRESS BAR
============================================================ -->
<section class="section">
<h2><span class="num">09</span><span class="name">Progress bar</span><span class="desc">running · complete · failed</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 22px;">
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--ink); font-weight:500;">running · 38%</span><span class="mono" style="color: var(--ink-mute);">42 MB/s · ETA 2m 14s</span></div>
<div class="progress-track"><div class="progress-fill" style="width: 38%;"></div></div>
</div>
<div style="margin-bottom: 22px;">
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--ok);">complete · 100%</span><span class="mono" style="color: var(--ink-mute);">finished in 5m 21s</span></div>
<div class="progress-track"><div class="progress-fill ok" style="width: 100%;"></div></div>
</div>
<div>
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--bad);">failed · 0%</span><span class="mono" style="color: var(--ink-mute);">repo locked</span></div>
<div class="progress-track"><div class="progress-fill bad" style="width: 4%;"></div></div>
</div>
</div>
</section>
<!-- ============================================================
10 · STAT TILE
============================================================ -->
<section class="section">
<h2><span class="num">10</span><span class="name">Stat tile</span><span class="desc">used in summary strips · fleet, host header, job summary</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px;">
<div class="grid grid-cols-4 gap-6">
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Hosts</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">12 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">total</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono" style="color: var(--ok);">10</span> online · <span class="mono" style="color: var(--warn);">1</span> degraded</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Backed up</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">4.9 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">TB</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono">23,649</span> snapshots</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Last 24h</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">147 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">jobs</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono" style="color: var(--ok);">144</span> ok · <span class="mono" style="color: var(--bad);">2</span> failed</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Open alerts</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px; color: var(--bad);">5</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;">oldest <span class="mono" style="color: var(--ink-mid);">3h</span></div>
</div>
</div>
</div>
</section>
<!-- ============================================================
11 · MODAL
============================================================ -->
<section class="section">
<h2><span class="num">11</span><span class="name">Modal</span><span class="desc">used for confirms · destructive actions · short prompts</span></h2>
<div class="modal-backdrop">
<div class="modal">
<div class="modal-head">
<h3 style="font-size: 16px; font-weight: 500; letter-spacing: -0.005em;">Remove host?</h3>
</div>
<div class="modal-body">
<p>
Removes <span class="mono" style="color: var(--ink);">prod-cache-01</span> from
the dashboard and revokes its agent token. Backup data on the
rest-server is left intact — delete that yourself if you want it gone.
</p>
<p style="margin-top: 10px; color: var(--ink-mute); font-size: 12px;">This action is logged in the audit trail.</p>
</div>
<div class="modal-foot">
<button class="btn">Cancel</button>
<button class="btn btn-danger">Remove host</button>
</div>
</div>
</div>
</section>
<!-- ============================================================
12 · TOAST
============================================================ -->
<section class="section">
<h2><span class="num">12</span><span class="name">Toast</span><span class="desc">success and error variants · auto-dismiss after 4s</span></h2>
<div style="display: flex; flex-direction: column; gap: 12px; align-items: flex-end;">
<div class="toast toast-ok">
<span class="dot dot-online" style="margin-top: 4px;"></span>
<div style="flex: 1;">
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">Token minted</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 2px;">install command shown below — expires in 1h</div>
</div>
<span class="x">×</span>
</div>
<div class="toast toast-bad">
<span class="dot dot-failed" style="margin-top: 4px;"></span>
<div style="flex: 1;">
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">Couldnt reach the agent</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 2px;"><span class="mono">prod-cache-01</span> didnt respond within 5s. Try again or check the WS connection.</div>
</div>
<span class="x">×</span>
</div>
</div>
</section>
<!-- ============================================================
13 · INSTALL SNIPPET
============================================================ -->
<section class="section">
<h2><span class="num">13</span><span class="name">Install snippet</span><span class="desc">load-bearing affordance on Add host · Empty state · Settings → Agents</span></h2>
<div class="snippet">
<div class="snippet-head">
<span>install command · 59m left</span>
<div class="flex gap-2">
<button class="btn btn-ghost mono" style="font-size: 11px;">Download .sh</button>
<button class="btn">Copy</button>
</div>
</div>
<pre>curl -fsSL <span class="var">https://restic.lab.example/install.sh</span> | sudo \
RM_SERVER=<span class="var">https://restic.lab.example</span> \
RM_TOKEN=<span class="var">HdqFbQh8U-I1fb52iP1M8qxvoYS5t9VZ-T-yghr_CzA</span> sh</pre>
</div>
</section>
<!-- ============================================================
14 · EMPTY STATE PATTERN
============================================================ -->
<section class="section" style="border-bottom: none;">
<h2><span class="num">14</span><span class="name">Empty-state pattern</span><span class="desc">nothing-yet screens · centred prompt + the affordance that fixes it</span></h2>
<div class="panel" style="border-radius: 7px; padding: 60px 40px; text-align: center; background: radial-gradient(ellipse at top, color-mix(in oklch, var(--accent), transparent 95%), transparent 60%), var(--panel);">
<h3 style="font-size: 18px; font-weight: 500; letter-spacing: -0.005em;">Nothing here yet.</h3>
<p style="color: var(--ink-mid); max-width: 480px; margin: 12px auto 22px; line-height: 1.65; font-size: 13px; text-wrap: pretty;">
Empty states always pair a one-sentence explanation with a single
primary affordance. Dont fill the space with stats or graphics —
the void <em>is</em> the message.
</p>
<button class="btn btn-primary">Take the action that fixes this</button>
</div>
</section>
<!-- final note -->
<div style="padding: 48px 0 96px; text-align: center; font-size: 12px; color: var(--ink-fade);">
end of v1 component reference · 14 sections · ~all the pieces the Phase 1 templates need
</div>
</div>
</body>
</html>
+384
View File
@@ -0,0 +1,384 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Host detail</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-degraded { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in oklch, var(--warn), transparent 80%); }
.dot-offline { background: var(--off); }
.dot-failed { background: var(--bad); box-shadow: 0 0 0 3px color-mix(in oklch, var(--bad), transparent 80%); }
.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;
}
.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); }
.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); }
/* secondary tabs (within host detail) */
.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: 680px; 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); }
.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; }
/* snapshots table */
.snap-row { display: grid; align-items: center;
grid-template-columns: 0.8fr 1fr 2fr 0.7fr 0.7fr 0.7fr;
padding: 10px 16px; font-size: 13px; border-left: 3px solid transparent;
}
.snap-row:hover { background: var(--panel-hi); }
.snap-row.head { padding-top: 9px; padding-bottom: 9px; font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; }
</style>
</head>
<body>
<div class="doc">
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Host detail</div>
<h1>One host, every angle.</h1>
<p>
Reached by clicking any host name on the dashboard. Persistent header
carries the hosts identity and key vitals; below, four tabs
(<em>Snapshots / Jobs / Repo / Settings</em>) pivot the rest of the page
without losing context. <em>Snapshots</em> is the default — its the
thing 90% of operators want to see when they click a host name.
</p>
<p class="meta">
The right-rail action stack stays present across all four tabs so
“Run backup now” and “Edit credentials” are always one click away. The
snapshot rows themselves are clickable — they lead to the restore wizard
(P3-01) when that lands.
</p>
</header>
<div class="stage-frame">
<div style="background: var(--bg);">
<!-- chrome -->
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<!-- host header -->
<div class="doc" style="padding: 28px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><span style="color: var(--ink-mid);">prod-db-01</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: 26px; font-weight: 500; letter-spacing: 0.005em; color: var(--ink);">prod-db-01</h1>
<div class="flex gap-1.5"><span class="tag">prod</span><span class="tag">db</span></div>
</div>
<div class="flex items-center gap-3" style="margin-top: 12px; font-size: 13px; color: var(--ink-mute);">
<span class="mono" style="color: var(--ink-mid);">linux/amd64</span>
<span style="color: var(--ink-fade);">·</span>
<span>agent <span class="mono" style="color: var(--ink-mid);">v0.1.0</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>restic <span class="mono" style="color: var(--ink-mid);">0.17.3</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>last seen <span class="mono" style="color: var(--ink-mid);">3s ago</span></span>
</div>
</div>
<div class="flex items-center gap-2">
<button class="btn btn-primary">Run backup now</button>
<button class="btn">Edit credentials</button>
<button class="btn btn-ghost" style="font-size: 14px; padding: 6px 10px;"></button>
</div>
</div>
<!-- key vitals strip -->
<div class="grid grid-cols-12 gap-6" style="margin-top: 24px; padding: 18px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);">
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Last backup</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;"><span style="color: var(--ok);">succeeded</span> · 3m ago</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">1.4 GB transferred · 38s</div>
</div>
<div class="col-span-3">
<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: 18px; 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 class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Snapshots</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;">1,847</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">oldest 18 months ago</div>
</div>
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Repo health</div>
<div class="mono" style="font-size: 18px; color: var(--ok); margin-top: 4px;">unlocked · ok</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">last <span class="mono">restic check</span> 4d ago</div>
</div>
</div>
<!-- secondary tabs -->
<div class="flex items-end" style="margin-top: 6px;">
<div class="sub-tab active">Snapshots <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">1,847</span></div>
<div class="sub-tab">Jobs <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">47 in 24h</span></div>
<div class="sub-tab">Repo</div>
<div class="sub-tab">Settings</div>
</div>
</div>
<!-- snapshots tab body -->
<div class="doc grid grid-cols-12 gap-6" style="padding: 24px 32px 56px; align-items: start;">
<!-- main column: snapshots table -->
<div class="col-span-9">
<div class="flex items-center justify-between" style="margin-bottom: 14px;">
<div class="flex items-center gap-3">
<h2 style="font-size: 13px; font-weight: 600; letter-spacing: 0.01em;">Snapshots</h2>
<div style="font-size: 12px; color: var(--ink-fade);">showing 12 of 1,847</div>
</div>
<div class="flex items-center gap-2">
<input type="text" placeholder="filter by path, tag, hostname…"
class="mono"
style="padding: 5px 10px; font-size: 12px; background: var(--panel); border: 1px solid var(--line-soft); color: var(--ink); border-radius: 5px; width: 280px; outline: none;"
/>
<button class="btn">Date range ▾</button>
<button class="btn">Sort: newest ▾</button>
</div>
</div>
<div class="panel" style="border-radius: 7px; overflow: hidden;">
<div class="snap-row head hairline">
<div>Snapshot id</div>
<div>Time</div>
<div>Paths</div>
<div class="text-right">Size</div>
<div class="text-right">Files</div>
<div></div>
</div>
<!-- 12 rows -->
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">91bbc80d</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 11:43:21</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,418</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">7a3c1f88</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 11:13:09</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,417</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">f50e2bbc</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 10:43:02</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,415</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">2d916ae4</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 10:12:47</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,415</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">b0c4e1f2</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 09:42:18</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,414</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">a8801c3f</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 09:11:55</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,414</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">e91f4d72</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 08:42:01</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,412</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">3d44a9e8</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 08:11:33</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,412</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">4f8c0c11</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 07:42:08</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,410</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">c113c3d2</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 07:11:42</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,410</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row hairline">
<div class="mono" style="color: var(--ink); font-weight: 500;">9be1aa0d</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 06:42:11</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,408</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
<div class="snap-row">
<div class="mono" style="color: var(--ink); font-weight: 500;">2eaf9c50</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">2026-05-01 06:12:49</div>
<div class="mono" style="color: var(--ink-mid); font-size: 12px;">/var/lib/postgresql · /etc/postgresql</div>
<div class="text-right mono" style="color: var(--ink);">1.4 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">12,408</div>
<div class="text-right"><button class="btn btn-ghost">Restore →</button></div>
</div>
</div>
<!-- pagination footer -->
<div class="flex items-center justify-between" style="padding: 16px 4px; font-size: 12px; color: var(--ink-mute);">
<span>showing snapshots 112 of 1,847</span>
<div class="flex items-center gap-2">
<button class="btn btn-ghost" disabled style="opacity: 0.4;">← previous</button>
<button class="btn btn-ghost">next →</button>
</div>
</div>
</div>
<!-- right rail -->
<aside class="col-span-3" style="display: flex; flex-direction: column; gap: 16px;">
<!-- recent activity preview -->
<div class="panel" style="border-radius: 7px; padding: 14px 16px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 10px;">Recent activity</div>
<div style="font-size: 12px; line-height: 1.7;">
<div class="mono"><span style="color: var(--ok);"></span> backup ok <span style="color: var(--ink-fade);">·</span> <span style="color: var(--ink-mute);">3m ago</span></div>
<div class="mono"><span style="color: var(--ok);"></span> backup ok <span style="color: var(--ink-fade);">·</span> <span style="color: var(--ink-mute);">33m ago</span></div>
<div class="mono"><span style="color: var(--ok);"></span> backup ok <span style="color: var(--ink-fade);">·</span> <span style="color: var(--ink-mute);">1h ago</span></div>
<div class="mono"><span style="color: var(--ok);"></span> check ok <span style="color: var(--ink-fade);">·</span> <span style="color: var(--ink-mute);">4d ago</span></div>
<div class="mono"><span style="color: var(--ok);"></span> forget ok <span style="color: var(--ink-fade);">·</span> <span style="color: var(--ink-mute);">7d ago</span></div>
</div>
<a class="text-xs" style="color: var(--ink-mute); display: inline-block; margin-top: 10px; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line);">View all jobs →</a>
</div>
<!-- Run-now actions -->
<div class="panel" style="border-radius: 7px; padding: 14px 16px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 10px;">Run-now</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<button class="btn" style="text-align: left; justify-content: flex-start; width: 100%;">backup</button>
<button class="btn" style="text-align: left; justify-content: flex-start; width: 100%;">forget</button>
<button class="btn" style="text-align: left; justify-content: flex-start; width: 100%;">prune <span style="font-size: 10px; color: var(--ink-fade); margin-left: 6px;">admin only</span></button>
<button class="btn" style="text-align: left; justify-content: flex-start; width: 100%;">check</button>
<button class="btn" style="text-align: left; justify-content: flex-start; width: 100%;">unlock</button>
</div>
</div>
<!-- Danger zone -->
<div class="panel" style="border-radius: 7px; padding: 14px 16px;">
<div style="font-size: 11px; color: var(--bad); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 10px; font-weight: 600;">Danger zone</div>
<p style="font-size: 12px; color: var(--ink-mute); line-height: 1.6; margin-bottom: 12px;" class="text-pretty">
Removes the host record. The repo data on the rest-server is left intact —
you delete that yourself.
</p>
<button class="btn btn-danger" style="width: 100%;">Remove host…</button>
</div>
</aside>
</div>
</div>
</div>
</div>
</body>
</html>
+413
View File
@@ -0,0 +1,413 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Job log</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);
--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); }
.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;
}
.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%); }
.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); }
.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: 680px; 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 { padding: 16px 32px 0; font-size: 11px; color: var(--ink-fade); letter-spacing: 0.18em; text-transform: uppercase; }
.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; }
/* progress bar */
.progress-track {
background: var(--bg); border: 1px solid var(--line-soft);
height: 6px; border-radius: 9999px; overflow: hidden;
}
.progress-fill {
height: 100%; background: var(--accent);
border-radius: 9999px;
transition: width 250ms ease;
}
/* log viewer */
.log {
background: var(--bg);
border: 1px solid var(--line-soft);
border-radius: 7px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px; line-height: 1.7;
overflow: hidden;
}
.log-line {
display: grid;
grid-template-columns: 14ch 8ch 1fr;
column-gap: 14px;
padding: 1px 16px;
align-items: baseline;
}
.log-line:first-child { padding-top: 14px; }
.log-line.ts { color: var(--ink-fade); }
.log-stream-stdout { color: var(--ink-mid); }
.log-stream-stderr { color: oklch(0.78 0.13 50); }
.log-stream-event { color: var(--accent); }
.log-stream-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-fade); }
.log-payload-ok { color: var(--ink); }
.log-payload-warn { color: var(--warn); }
.log-payload-err { color: var(--bad); }
</style>
</head>
<body>
<div class="doc">
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Live job log</div>
<h1>Watching restic chug.</h1>
<p>
The screen an operator stares at when something is in flight. Header
identifies the job (kind · host · status). A live progress bar shows the
bytes-to-go signal restic emits. The log itself is the focus: monospace,
tight line-height, color reserved for the few lines that matter — events
and stderr.
</p>
<p class="meta">
Backed by <span class="mono" style="color: var(--ink-mid);">WS /api/jobs/{id}/stream</span>
(P1-21 remainder). Auto-scrolls until the operator scrolls away; a
“follow” pill appears when theyve scrolled up so they can re-attach.
</p>
</header>
<!-- Stage 1: running -->
<div class="stage-label">State A · running</div>
<div class="stage-frame">
<div style="background: var(--bg);">
<!-- chrome -->
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<!-- job header -->
<div class="doc" style="padding: 28px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><a>prod-db-01</a><span class="sep">/</span><span style="color: var(--ink-mid);">job 01KQH…E59B</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent), transparent 80%); animation: pulse 2.4s ease-in-out infinite;"></span>
<h1 style="font-size: 22px; font-weight: 500; letter-spacing: -0.01em;">backup <span style="color: var(--ink-fade);">·</span> <span class="mono" style="color: var(--ink); font-weight: 500;">prod-db-01</span></h1>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--accent), transparent 88%); color: var(--accent); border: 1px solid color-mix(in oklch, var(--accent), transparent 70%); border-radius: 3px;">running</span>
</div>
<div class="flex items-center gap-3" style="margin-top: 10px; font-size: 12.5px; color: var(--ink-mute);">
<span>job <span class="mono" style="color: var(--ink-mid);">01KQH7DZJ8M5N3DH277E59B</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>started <span class="mono" style="color: var(--ink-mid);">3m 18s ago</span> by <span class="mono" style="color: var(--ink-mid);">steve@dcglab</span></span>
</div>
</div>
<div class="flex items-center gap-2">
<button class="btn">Pin to top</button>
<button class="btn btn-danger">Cancel job</button>
</div>
</div>
<!-- progress bar -->
<div style="margin-top: 24px; padding-bottom: 24px;">
<div class="flex items-center justify-between mb-2.5">
<div class="flex items-center gap-3 text-sm">
<span class="mono" style="color: var(--ink); font-weight: 500;">38%</span>
<span style="color: var(--ink-mute);">·</span>
<span class="mono" style="color: var(--ink-mid);">1.4 GB</span> <span style="color: var(--ink-mute);">of <span class="mono" style="color: var(--ink-mid);">3.7 GB</span></span>
<span style="color: var(--ink-mute);">·</span>
<span class="mono" style="color: var(--ink-mid);">8,124 files</span>
<span style="color: var(--ink-mute);">of <span class="mono" style="color: var(--ink-mid);">21,402</span></span>
</div>
<div class="text-sm" style="color: var(--ink-mute);">
<span class="mono" style="color: var(--ink-mid);">42 MB/s</span> · ETA <span class="mono" style="color: var(--ink-mid);">2m 14s</span>
</div>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: 38%;"></div>
</div>
</div>
</div>
<!-- log viewer -->
<div class="doc" style="padding: 0 32px 56px;">
<div class="flex items-center justify-between" style="margin-bottom: 12px;">
<div class="flex items-center gap-3">
<h2 style="font-size: 13px; font-weight: 600; letter-spacing: 0.01em;">Stream</h2>
<span style="font-size: 11.5px; color: var(--ink-fade);">following · auto-scroll on · 1,247 lines</span>
</div>
<div class="flex items-center gap-2">
<button class="btn">Filter ▾</button>
<button class="btn">Pause stream</button>
<button class="btn">Download .log</button>
</div>
</div>
<div class="log">
<div class="log-line"><span class="ts">11:43:21.039</span><span class="log-stream-tag">EVENT</span><span class="log-payload-ok"><span class="log-stream-event">{"message_type":"status","percent_done":0.000,"total_files":21402,"files_done":0,"total_bytes":3958374400,"bytes_done":0}</span></span></div>
<div class="log-line"><span class="ts">11:43:21.412</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">scan finished in 0.371s</span></div>
<div class="log-line"><span class="ts">11:43:21.504</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.001,"total_files":21402,"files_done":12,"bytes_done":1048576}</span></div>
<div class="log-line"><span class="ts">11:43:22.512</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.020,"files_done":418,"bytes_done":81256000}</span></div>
<div class="log-line"><span class="ts">11:43:23.521</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.062,"files_done":1287,"bytes_done":253640000}</span></div>
<div class="log-line"><span class="ts">11:43:24.530</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.108,"files_done":2289,"bytes_done":444121600}</span></div>
<div class="log-line"><span class="ts">11:43:25.541</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.155,"files_done":3267,"bytes_done":637534720}</span></div>
<div class="log-line"><span class="ts">11:43:25.812</span><span class="log-stream-tag">ERR</span><span class="log-stream-stderr"><span class="log-payload-warn">warn: file changed during read: /var/lib/postgresql/13/main/pg_wal/000000010000007800000042</span></span></div>
<div class="log-line"><span class="ts">11:43:26.554</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.198,"files_done":4187,"bytes_done":815874048}</span></div>
<div class="log-line"><span class="ts">11:43:27.566</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.241,"files_done":5108,"bytes_done":993951744}</span></div>
<div class="log-line"><span class="ts">11:43:28.580</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.284,"files_done":6029,"bytes_done":1172029440}</span></div>
<div class="log-line"><span class="ts">11:43:29.594</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.327,"files_done":6948,"bytes_done":1349838336}</span></div>
<div class="log-line"><span class="ts">11:43:30.609</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.358,"files_done":7596,"bytes_done":1475174400}</span></div>
<div class="log-line"><span class="ts">11:43:31.625</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.380,"files_done":8124,"bytes_done":1503870976}</span></div>
<div class="log-line" style="padding-bottom: 14px;"><span class="ts" style="color: var(--accent);">11:43:32.122</span><span class="log-stream-tag" style="color: var(--accent);">···</span><span style="color: var(--accent);"></span></div>
</div>
</div>
</div>
</div>
<!-- Stage 2: completed (success) -->
<div class="stage-label">State B · completed (succeeded)</div>
<div class="stage-frame">
<div style="background: var(--bg);">
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<div class="doc" style="padding: 28px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><a>prod-db-01</a><span class="sep">/</span><span style="color: var(--ink-mid);">job 01KQH…E59B</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px color-mix(in oklch, var(--ok), transparent 80%);"></span>
<h1 style="font-size: 22px; font-weight: 500; letter-spacing: -0.01em;">backup <span style="color: var(--ink-fade);">·</span> <span class="mono" style="color: var(--ink); font-weight: 500;">prod-db-01</span></h1>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--ok), transparent 88%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">succeeded</span>
</div>
<div class="flex items-center gap-3" style="margin-top: 10px; font-size: 12.5px; color: var(--ink-mute);">
<span>job <span class="mono" style="color: var(--ink-mid);">01KQH7DZJ8M5N3DH277E59B</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>finished <span class="mono" style="color: var(--ink-mid);">11:48:42 (5m 21s)</span></span>
</div>
</div>
<div class="flex items-center gap-2">
<button class="btn">Run again</button>
<button class="btn">Download .log</button>
</div>
</div>
<!-- summary stats -->
<div class="grid grid-cols-12 gap-6" style="margin-top: 24px; padding: 18px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);">
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Bytes added</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;">142 <span style="font-size: 12px; color: var(--ink-mute);">MB</span></div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">of <span class="mono">3.7 GB</span> processed</div>
</div>
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Files</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;">21,402</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;"><span class="mono">187</span> new · <span class="mono">42</span> changed</div>
</div>
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Throughput</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;">42 <span style="font-size: 12px; color: var(--ink-mute);">MB/s</span></div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">avg over 5m 21s</div>
</div>
<div class="col-span-3">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Snapshot</div>
<div class="mono" style="font-size: 18px; color: var(--ink); margin-top: 4px;">91bbc80d</div>
<div style="font-size: 11.5px; color: var(--ink-mute); margin-top: 2px;">added to repo</div>
</div>
</div>
</div>
<!-- log tail (final lines) -->
<div class="doc" style="padding: 24px 32px 56px;">
<div class="flex items-center justify-between" style="margin-bottom: 12px;">
<div class="flex items-center gap-3">
<h2 style="font-size: 13px; font-weight: 600; letter-spacing: 0.01em;">Stream <span style="font-weight: 400; color: var(--ink-fade);">· complete</span></h2>
<span style="font-size: 11.5px; color: var(--ink-fade);">2,418 lines</span>
</div>
<div class="flex items-center gap-2">
<button class="btn">Show all lines</button>
<button class="btn">Filter ▾</button>
</div>
</div>
<div class="log">
<div class="log-line"><span class="ts">11:48:38.044</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.987,"files_done":21127,"bytes_done":3905990656}</span></div>
<div class="log-line"><span class="ts">11:48:39.058</span><span class="log-stream-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.998,"files_done":21358,"bytes_done":3950182400}</span></div>
<div class="log-line"><span class="ts">11:48:40.064</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">processed 21402 files, 3.689 GiB in 5:19</span></div>
<div class="log-line"><span class="ts">11:48:41.022</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">snapshot 91bbc80d saved</span></div>
<div class="log-line"><span class="ts">11:48:42.108</span><span class="log-stream-tag">EVENT</span><span class="log-payload-ok"><span class="log-stream-event">{"message_type":"summary","files_new":187,"files_changed":42,"files_unmodified":21173,"data_added":148908544,"total_files_processed":21402,"total_bytes_processed":3958374400,"snapshot_id":"91bbc80d4a17ed718462a26f3e6ad72d0cde7aa9fbf0629efaac1eaa943f5665","total_duration":319.234}</span></span></div>
<div class="log-line" style="padding-bottom: 14px;"><span class="ts">11:48:42.337</span><span class="log-stream-tag" style="color: var(--ok);">END</span><span style="color: var(--ok);">restic exited 0 · success</span></div>
</div>
</div>
</div>
</div>
<!-- Stage 3: failed -->
<div class="stage-label">State C · failed</div>
<div class="stage-frame">
<div style="background: var(--bg);">
<div class="hairline" style="background: var(--bg);">
<div class="doc flex items-center justify-between" style="padding: 16px 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.1.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" style="background: var(--bg);">
<div class="doc flex items-end">
<nav class="flex items-end">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
</div>
<div class="doc" style="padding: 28px 32px 0;">
<div class="crumbs"><a>Dashboard</a><span class="sep">/</span><a>build-runner</a><span class="sep">/</span><span style="color: var(--ink-mid);">job 01KQH…9F8C</span></div>
<div class="flex items-start justify-between" style="margin-top: 14px;">
<div>
<div class="flex items-center gap-3">
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--bad); box-shadow: 0 0 0 4px color-mix(in oklch, var(--bad), transparent 80%);"></span>
<h1 style="font-size: 22px; font-weight: 500; letter-spacing: -0.01em;">backup <span style="color: var(--ink-fade);">·</span> <span class="mono" style="color: var(--ink); font-weight: 500;">build-runner</span></h1>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--bad), transparent 88%); color: var(--bad); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); border-radius: 3px;">failed</span>
</div>
<div class="flex items-center gap-3" style="margin-top: 10px; font-size: 12.5px; color: var(--ink-mute);">
<span>job <span class="mono" style="color: var(--ink-mid);">01KQH3FX9F8C…</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>finished <span class="mono" style="color: var(--ink-mid);">10:53:18 (1.4s)</span></span>
<span style="color: var(--ink-fade);">·</span>
<span>exit code <span class="mono" style="color: var(--bad);">1</span></span>
</div>
</div>
<div class="flex items-center gap-2">
<button class="btn">Retry job</button>
<button class="btn">Open alert</button>
</div>
</div>
<!-- failure summary panel -->
<div class="panel" style="margin-top: 24px; padding: 16px 18px; border-radius: 7px; border-color: color-mix(in oklch, var(--bad), transparent 60%);">
<div style="font-size: 11px; color: var(--bad); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 6px;">Failure</div>
<p style="font-size: 13.5px; color: var(--ink); line-height: 1.6;" class="text-pretty">
<span class="mono" style="color: var(--bad);">unable to acquire lock</span> — the repo at <span class="mono" style="color: var(--ink-mid);">rest:https://restic.unraid.lab/build-runner/</span> is locked by another operation.
</p>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 10px; line-height: 1.6;" class="text-pretty">
Most likely a stale lock from a previous run that didn't clean up. Run <span class="mono" style="color: var(--ink);">unlock</span> on this host's repo, then retry the backup.
</div>
</div>
</div>
<div class="doc" style="padding: 24px 32px 56px;">
<div class="log">
<div class="log-line"><span class="ts">10:53:17.001</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">opening repository at rest:https://restic.unraid.lab/build-runner/</span></div>
<div class="log-line"><span class="ts">10:53:17.388</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">repository f4b81ec7 opened</span></div>
<div class="log-line"><span class="ts">10:53:17.602</span><span class="log-stream-tag">OUT</span><span class="log-stream-stdout">created new cache in /var/lib/restic-manager/cache</span></div>
<div class="log-line"><span class="ts">10:53:18.211</span><span class="log-stream-tag">ERR</span><span class="log-stream-stderr"><span class="log-payload-err">Fatal: unable to create lock in backend: repository is already locked exclusively by PID 12047 on build-runner by root (UID 0, GID 0) lock was created at 2026-05-01 10:39:14 (14m18s ago) storage ID a4f1b3d8</span></span></div>
<div class="log-line" style="padding-bottom: 14px;"><span class="ts">10:53:18.298</span><span class="log-stream-tag" style="color: var(--bad);">END</span><span style="color: var(--bad);">restic exited 1 · failed</span></div>
</div>
</div>
</div>
</div>
</div>
<style>
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent), transparent 80%); }
50% { box-shadow: 0 0 0 6px color-mix(in oklch, var(--accent), transparent 92%); }
}
</style>
</body>
</html>
+148
View File
@@ -0,0 +1,148 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Login</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); --bad: oklch(0.70 0.20 25);
--accent: oklch(0.82 0.12 195);
}
html, body { background: var(--bg); color: var(--ink); }
body { font-family: 'Inter', system-ui, sans-serif; min-height: 100vh; }
.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
::selection { background: color-mix(in oklch, var(--accent), transparent 70%); }
.text-pretty { text-wrap: pretty; }
.field-label { font-size: 12px; color: var(--ink-mid); margin-bottom: 6px; display: block; letter-spacing: 0.005em; }
.field {
width: 100%; padding: 10px 12px;
background: var(--panel); border: 1px solid var(--line-soft);
color: var(--ink); border-radius: 5px;
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 120ms ease;
}
.field:focus { border-color: var(--accent); }
.field.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; letter-spacing: 0.01em; }
.btn {
font-size: 13px; font-weight: 500;
padding: 9px 14px; border-radius: 5px;
background: transparent; border: 1px solid var(--line); color: var(--ink-mid);
transition: all 120ms ease;
}
.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-block { width: 100%; padding: 10px 14px; }
.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: 680px; 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); }
</style>
</head>
<body>
<div class="doc">
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Login</div>
<h1>Sign in.</h1>
<p>
The first chrome an operator meets. Everything irrelevant to the act of
signing in is removed: no marketing, no “sign up”, no animated background.
The form lives where the cursor needs it; the build version sits in the
footer so a returning operator can spot a mismatch with the agents.
</p>
<p class="meta">
First-run operators land on the empty <span class="mono" style="color: var(--ink-mid);">/bootstrap</span>
page instead — see the dedicated mockup. A regular sign-in always has at
least one user already minted.
</p>
</header>
<div class="stage-frame">
<div style="background: var(--bg); min-height: 720px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 32px;">
<!-- centred card -->
<div style="width: 360px;">
<!-- wordmark -->
<div class="flex items-baseline gap-2 mb-10" style="justify-content: center;">
<div class="mono" style="font-size: 16px; color: var(--ink); font-weight: 500; letter-spacing: 0.01em;">restic-manager</div>
</div>
<h2 style="font-size: 18px; font-weight: 500; letter-spacing: -0.005em; margin-bottom: 28px; text-align: center;">Sign in to continue</h2>
<form>
<div style="margin-bottom: 14px;">
<label class="field-label" for="login-username">Username</label>
<input id="login-username" type="text" class="field mono" autocomplete="username" value="steve">
</div>
<div style="margin-bottom: 22px;">
<label class="field-label" for="login-password">Password</label>
<input id="login-password" type="password" class="field" autocomplete="current-password" value="••••••••••••••••">
</div>
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
</form>
<div style="margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line-soft); text-align: center;">
<p class="text-pretty" style="font-size: 12px; color: var(--ink-mute); line-height: 1.65;">
Forgot your password? An admin can reset it from
<span class="mono" style="color: var(--ink-mid);">Settings → Users</span>.
Theres no recovery email — this is self-hosted infrastructure.
</p>
</div>
</div>
<!-- footer -->
<div style="margin-top: 80px; display: flex; gap: 14px; align-items: center; font-size: 11px; color: var(--ink-fade);">
<span class="mono">restic-manager v0.1.0-alpha</span>
<span>·</span>
<span class="mono">build f9c2351</span>
<span>·</span>
<a class="underline underline-offset-4 decoration-1" style="text-decoration-color: var(--line);">docs</a>
<span>·</span>
<a class="underline underline-offset-4 decoration-1" style="text-decoration-color: var(--line);">source</a>
</div>
</div>
</div>
<!-- error variant -->
<section style="margin: 48px 0 96px;">
<h2 style="font-size: 14px; font-weight: 600; color: var(--ink-mute); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 20px;">Error variant</h2>
<div style="background: var(--panel); border: 1px solid var(--line-soft); border-radius: 7px; padding: 32px;">
<div style="width: 360px; margin: 0 auto;">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 24px; text-align: center;">Sign in to continue</h3>
<div style="background: color-mix(in oklch, var(--bad), transparent 88%); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); padding: 10px 12px; border-radius: 5px; margin-bottom: 18px; font-size: 12px; color: oklch(0.85 0.10 25);">
Invalid username or password.
</div>
<div style="margin-bottom: 14px;">
<label class="field-label">Username</label>
<input type="text" class="field mono" value="steve">
</div>
<div style="margin-bottom: 22px;">
<label class="field-label">Password</label>
<input type="password" class="field" value="••••••••">
</div>
<button class="btn btn-primary btn-block">Sign in</button>
</div>
</div>
</section>
</div>
</body>
</html>