feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public

This commit is contained in:
2026-08-23 19:58:39 -04:00
parent fc0d9a2d5c
commit cbc263a4b2
46 changed files with 1555 additions and 691 deletions
+124
View File
@@ -241,6 +241,33 @@ body::after {
whole control below 640px. */
.new-chat-btn svg { width: 16px; height: 16px; display: none; }
/* Phase 16: header auth controls (Sign in link / Sign out button) — the
same ghost pill as New chat, so the bar keeps one visual language.
ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1.
Icon-only below 640px (aria-labels/labels keep the accessible names);
≥44px touch target at every width. Exactly one is ever visible. */
.auth-link {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
}
.auth-link:hover { background: var(--brand-soft); color: var(--brand-ink); }
.auth-link:disabled { opacity: 0.6; cursor: wait; }
.auth-link svg { width: 16px; height: 16px; display: none; }
/* "Tuning" toggle (phase 15): ghost pill like New chat + a mono count
badge (brand-ink on brand-soft ≈6.9:1). The label is visually-hidden
(not removed) below 640px so the accessible name keeps the word.
@@ -724,6 +751,65 @@ body::after {
.kb-banner.is-error { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.kb-banner svg { width: 18px; height: 18px; flex: 0 0 auto; display: block; }
/* ---------- Login page (phase 16) ---------- */
/* Centered card in the standard frame: one admin, one password. */
.login-shell {
display: flex;
justify-content: center;
flex: 1;
}
.login-card {
width: 100%;
max-width: 26rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2rem 2rem 2.25rem;
}
.login-card h1 { margin: 0 0 0.4rem; font-size: 1.6rem; }
.login-sub { margin: 0 0 1.5rem; color: var(--ink-soft); }
#login-form { display: flex; flex-direction: column; gap: 0.75rem; }
#login-password {
font: inherit;
font-size: 1rem;
color: var(--ink);
background: #0d1120;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.55rem 0.75rem;
min-height: 44px;
}
#login-password:focus-visible { border-color: var(--brand); }
/* Brand button: dark ink on brand 5.2:1 (never white on brand). */
.login-submit {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
cursor: pointer;
padding-inline: 1rem;
}
.login-submit:hover:not(:disabled) { background: #7d88f5; }
.login-submit:disabled { opacity: 0.6; cursor: wait; }
/* Login failure (role=alert): err pair ≈9.1:1. */
.login-error {
margin: 0.9rem 0 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.8rem;
font-size: 0.9rem;
font-weight: 600;
}
/* ---------- Sources page ---------- */
.sources-shell {
display: flex;
@@ -755,6 +841,39 @@ body::after {
.stat-value-sm { font-size: 1.15rem; font-weight: 700; }
.stat-label { color: var(--ink-soft); font-size: 0.88rem; font-weight: 600; }
/* Phase 16: anonymous sign-in gate — the designed replacement for the
catalog (stat cards + table) until the admin signs in. */
.sources-gate {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.4rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.5rem 1.75rem;
}
.sources-gate-glyph { color: var(--brand-ink); width: 44px; height: 44px; }
.sources-gate-glyph svg { width: 44px; height: 44px; display: block; }
.sources-gate h2 { margin: 0.6rem 0 0.3rem; font-size: 1.4rem; }
.sources-gate-sub { margin: 0 auto; max-width: 30rem; color: var(--ink-soft); }
.sources-gate-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
margin-top: 0.75rem;
padding: 0.5rem 1.4rem;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font-weight: 700;
text-decoration: none;
}
.sources-gate-link:hover { background: #7d88f5; }
.table-wrap {
background: var(--surface);
border: 1px solid var(--line);
@@ -1005,6 +1124,11 @@ body::after {
.new-chat-btn { padding: 0.4rem 0.55rem; }
.new-chat-label { display: none; }
.new-chat-btn svg { display: block; }
/* Phase 16: the auth pill goes icon-only like New chat — brand text
ellipsizes as the designated squeeze target, no bar overflow. */
.auth-link { padding: 0.4rem 0.55rem; }
.auth-label { display: none; }
.auth-link svg { display: block; }
.steering-toggle { padding: 0.4rem 0.55rem; }
/* Visually hidden, NOT display:none — the accessible name keeps the
word "Tuning" next to the count badge. */