phase: 168_admin_settings_panel
Build and Push Containers / build-and-push-app (push) Successful in 4m4s
Build and Push Containers / build-and-push-db (push) Successful in 15s

**Phase 168 — Admin Settings panel — final verification: PASS**

1. **Implemented/verified:** `/settings.html` (admin-only) with the 75-field catalogue (`app/core/settings_catalog.py`), DB-over-env overrides in Postgres (`app_settings`, migration 0028 at head, `settings_overlay.py` resolver, `app/api/admin_settings.py` — 4 admin-only routes), live-apply on save with no restart + restart-required badges, Reset/Clear all, write-only secrets, `.env`-never-written, `BOR_SETTINGS_DB=0` kill switch.
2. **Phase E2E: 12/12** — run with `E2E_APP_PORT=8200` (ports 8123/8124 are held by an unrelated "undercooked" project; no process killed, no code change — the documented env override).
3. **Regression set: all 7 suites green in isolation** (theme tab, API tokens, configurable brand, global tuning, admin auth, cache busting, wide desktop column — 38 tests).
4. **Full suite: unit+integration exit 0 (4066 passed), app/ coverage 99%** (gate >90%); **ruff clean, pyright 0 errors / 0 warnings.**
5. **Log contract (rule 10) verified live:** per-turn + per-response byte-budget lines present; settings write lines carry key + action, **never the value**.
6. **Byte-budget audit: all endpoints under the 262,144 B budget**; settings catalogue 23,513 B (gzip 3,326).
7. **Defect fixed (one):** criterion 5 had no integration pin — added `test_bad_row_inserted_directly_never_bricks_boot` to `tests/integration/test_admin_settings_api.py` (malformed row inserted directly → fresh `create_app()` boots, row skipped with logged reason, rest of table applies, `/api/health` 200) — 12/12 green.
8. **Docs pass:** `README.md` and `AGENTS.md` reviewed in full — both consistent with the as-built feature (Settings page table, Settings section, Configuration-reference note, `.env.example` block); **no changes required**.
9. **Cleanup:** audit app killed (by PID); `app_settings` left empty (0 rows); all changes left in the working tree (no commit, per protocol).
10. **Completion criteria:** all 7 checkboxes verified green (criterion 7's commit is the harness's).
11. **Next pending phase:** `170_edit_bubble_sizing`.
This commit is contained in:
2026-10-10 01:05:14 -04:00
parent abe7388dda
commit 1200437bf8
79 changed files with 7148 additions and 378 deletions
+238
View File
@@ -4646,6 +4646,232 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
padding: 0.45rem 0.8rem;
}
/* ---------- Settings view (phase 168) ----------
The admin settings panel: ONE .table-wrap card (the phase-156
bounded scroll region — max-height: min(70vh, 64rem) — and the
sticky thead via the .table-wrap rule) holding the 75-row
catalogue (grouped, type-correct controls, literal-text source
badges — B5: text + color, never color alone), the client-side
search row, and the Save / Clear-all action row. Every color is
an EXISTING AA token (B3: no new palette variable, no new :root
token — the phase-156 rule): the badges are TEXT and the color
is decoration. Nothing animates, so prefers-reduced-motion needs
no new rule. */
.settings-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* The catalogue: FULL-WIDTH (AGENTS.md rule 5 — the .git-sources-
table language: no skinny list), one cell per column, the
.table-wrap card + its bounded scroll + the sticky thead carry
the phase-156 contract. */
.settings-table {
width: 100%;
border-collapse: collapse;
min-width: 560px;
font-size: 0.93rem;
}
.settings-table th, .settings-table td {
text-align: left;
vertical-align: top;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
}
.settings-table td { white-space: nowrap; }
/* The group band: the --surface row, ink text (15.3:1, AA). */
.settings-group-row th {
background: var(--surface);
color: var(--ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.settings-table tbody tr.settings-row:hover td { background: var(--bg); }
/* The Setting cell: the label (bold) over the BOR_* name (mono,
ink-soft 8.2:1 on --surface, AA). */
.settings-name-cell label { margin: 0; font-weight: 600; min-width: 0; }
.settings-env {
font-family: var(--mono);
font-size: 0.78rem;
color: var(--ink-soft);
}
/* The restart-required flag (LOCKED A4, boot-wired): the accent
STATE family (--accent-ink on --accent-bg 9.5:1, AA) — text +
color, never color alone (B5); the title / aria-describedby
carry the when-it-applies copy. */
.settings-flag {
display: inline-block;
margin-top: 0.25rem;
padding: 0.06rem 0.5rem;
border: 1px solid var(--accent-line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
background: var(--accent-bg);
color: var(--accent-ink);
}
/* The Value cell: the type-correct control (number / select /
textarea / text / password) — the .token-label input language
(--line hairline, --surface fill, ink text 15.3:1, AA; the
values are data — mono) + the ≥44px target + the global
:focus-visible ring. */
.settings-control {
width: 100%;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.85rem;
}
.settings-control:disabled { opacity: 0.6; }
/* The Source cell: the literal-text badge — the three is-*
variants are SOLID existing tokens (B3): DB is brand-ink on
brand-soft (9.0:1, AA), env is ink-soft on surface (8.2:1, AA;
the --line border keeps it visible on the card), default is
ink-soft on line (7.1:1, AA). The badge is TEXT — the color
is decoration (B5). */
.settings-src {
display: inline-block;
padding: 0.12rem 0.55rem;
border-radius: 999px;
font-family: var(--mono);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
}
.settings-src.is-db { background: var(--brand-soft); color: var(--brand-ink); }
.settings-src.is-env { background: var(--surface); color: var(--ink-soft); border: 1px solid var(--line); }
.settings-src.is-default { background: var(--line); color: var(--ink-soft); }
/* A dirty row: the --brand left marker on the row's first cell
(an inset box-shadow — no layout shift) so "unsaved" survives
grayscale + the N-unsaved-changes hint (text, B5). */
.settings-row.is-dirty > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
/* The per-row Reset: the .token-revoke language (≥44px, --line
hairline, transparent fill, ink-soft, the err hover pair — it
hands control back to the environment) + the dimmed :disabled
(nothing to reset / read-only). */
.settings-reset {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.settings-reset:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.settings-reset:disabled { opacity: 0.5; cursor: default; }
/* The search row: the .token-create flex (wraps below 640px via
the media query below) + the #token-label input language. */
.settings-search {
display: flex;
align-items: center;
gap: 0.6rem;
}
.settings-search label { white-space: nowrap; }
#settings-filter {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
#settings-filter:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* The action row: Save (the .token-generate brand pill — the
§7.4 in-flight state is the dimmed :disabled + aria-busy) and
Clear all (the .token-revoke ghost — the destructive err hover
pair; the two-step confirm reuses the .history-confirm-* rules
unchanged). */
.settings-actions {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
#settings-save {
min-height: 44px;
padding: 0.4rem 1.1rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
#settings-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
#settings-save:disabled { opacity: 0.6; cursor: wait; }
#settings-clear-all {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
#settings-clear-all:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
/* The "N unsaved changes" hint (textContent only, aria-hidden):
the .tokens-status voice (mono, ink-soft). */
.settings-dirty {
font-family: var(--mono);
font-size: 0.8rem;
color: var(--brand-ink);
}
/* The ONE status region (the .tokens-status mirror — the
min-height holds the layout so a line never reflows the table).
The error line (the .theme-error mirror). */
.settings-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
.settings-error {
display: block;
margin: 0;
min-height: 1.2em;
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
color: var(--err-ink);
}
/* The no-match row (the .tokens-empty-row language). */
.settings-no-match td {
padding: 0.9rem 1rem;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.85rem;
white-space: normal;
}
/* ---------- Shared page (phase 51, task 03) ----------
/shared/<token>: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shared page reads exactly like the
@@ -6160,6 +6386,18 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
.theme-actions { flex-direction: column; align-items: stretch; }
#theme-save { width: 100%; }
.theme-reset { width: 100%; }
/* Phase 168 (task 05): the Settings panel squeezes — the search
row stacks (the label over the full-width input) and the action
row stacks (Save full width, Clear all under it; the ≥44px
targets are the global rules above — the stack keeps them on
phones). The actions cell wraps so the Reset control fits the
phone width (the table's own horizontal scroll covers the
columns). */
.settings-search { flex-direction: column; align-items: stretch; }
.settings-actions { flex-direction: column; align-items: stretch; }
#settings-save { width: 100%; }
#settings-clear-all { width: 100%; }
.settings-actions-cell { white-space: normal; }
/* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the
shell is full-width here (the 72rem cap never binds on a phone —