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:
@@ -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 host’s display name. Most operators use the box’s 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 server’s 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 & 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 doesn’t 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 won’t 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 — they’re cheap.</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes pulse-wait {
|
||||||
|
0%, 100% { opacity: 0.45; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 shouldn’t — it’s
|
||||||
|
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 shouldn’t 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 > 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 > 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 & 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 & 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;">Couldn’t reach the agent</div>
|
||||||
|
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 2px;"><span class="mono">prod-cache-01</span> didn’t 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. Don’t 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>
|
||||||
@@ -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 host’s 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 — it’s 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 1–12 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>
|
||||||
@@ -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 they’ve 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>
|
||||||
@@ -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>.
|
||||||
|
There’s 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>
|
||||||
Reference in New Issue
Block a user