8b7b1479a1
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>
149 lines
7.2 KiB
HTML
149 lines
7.2 KiB
HTML
<!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>
|