phase: 168_admin_settings_panel
**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:
@@ -60,6 +60,13 @@
|
||||
view). No mobile dropdown copy is needed: the link lives
|
||||
in the SAME #app-nav element the hamburger opens. -->
|
||||
<a href="/theme.html" class="nav-link" id="nav-theme" hidden>Theme</a>
|
||||
<!-- Phase 168 (task 05): the Settings link is admin-only — hidden
|
||||
by default, header.js reveals it once whoami says admin,
|
||||
exactly like the Theme link above (the phase-34 one-bar
|
||||
contract; the shell's eighth view). No mobile dropdown copy
|
||||
is needed: the link lives in the SAME #app-nav element the
|
||||
hamburger opens. A token user never sees it. -->
|
||||
<a href="/settings.html" class="nav-link" id="nav-settings" hidden>Settings</a>
|
||||
<!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is
|
||||
outside the nav; see styles.css .sign-in-mobile rules). -->
|
||||
<a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden>
|
||||
@@ -1478,6 +1485,133 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Phase 168 (task 05): the Settings view — the shell's eighth
|
||||
view (the #view-tokens skeleton, verbatim): the 72rem
|
||||
.container, the anonymous sign-in gate (the #tokens-gate
|
||||
shape), and the ship-hidden #settings-content (settings.js
|
||||
reveals it for admin only). The catalogue's <tbody> elements
|
||||
are created by settings.js (one per group, each opening with
|
||||
a <tr class="settings-group-row">); the .table-wrap +
|
||||
sticky-thead rules in styles.css carry the bounded-scroll +
|
||||
sticky-header contract (the phase-156 catalogue language).
|
||||
No <script> tag: the router lazy-imports settings.js
|
||||
(rule 6 — everything local). -->
|
||||
<section class="view" id="view-settings" hidden inert aria-label="Settings" tabindex="-1">
|
||||
<div class="container settings-shell">
|
||||
<div class="page-head">
|
||||
<h1>Settings</h1>
|
||||
<p class="page-sub">
|
||||
Values saved here are stored in the database and win over
|
||||
the server's environment variables; clearing one hands
|
||||
control back to the environment. <code>.env</code> is never
|
||||
written — the environment stays the base, the database row
|
||||
wins.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Phase 168 (task 05): anonymous sign-in gate — the EXACT
|
||||
#tokens-gate shape (phase 79) and the same .sources-gate
|
||||
visual language: every setting is what the login locks.
|
||||
Visible for anonymous, hidden for the admin (settings.js).
|
||||
The gate's Sign in returns to the Settings view (the
|
||||
header's ?next= convention; the static href is the no-JS
|
||||
fallback). -->
|
||||
<section class="sources-gate" id="settings-gate" aria-labelledby="settings-gate-title" hidden>
|
||||
<div class="sources-gate-glyph" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><circle cx="12" cy="14.5" r="1.4" fill="currentColor" stroke="none"/><path d="M12 16v2"/></svg>
|
||||
</div>
|
||||
<h2 id="settings-gate-title">Sign in to change settings</h2>
|
||||
<p class="sources-gate-sub">
|
||||
Editing application settings is admin-only. Chat — and any
|
||||
document an answer cites — stays open to everyone.
|
||||
</p>
|
||||
<a class="sources-gate-link" href="/login.html?next=/settings.html">Sign in</a>
|
||||
</section>
|
||||
|
||||
<!-- Phase 168 (task 05): the panel — SHIPS hidden
|
||||
(anonymous-safe; the gate is what anonymous sees).
|
||||
settings.js reveals it once the cached whoami says admin
|
||||
(the #theme-content pattern) and renders the catalogue's
|
||||
groups/rows from GET /api/admin/settings. -->
|
||||
<div id="settings-content" hidden>
|
||||
<!-- Shown when the server's overlay_enabled is false
|
||||
(the BOR_SETTINGS_DB=0 kill switch — D6): the panel
|
||||
stays editable, but a save cannot take effect. -->
|
||||
<div class="kb-banner is-error" id="settings-disabled" role="alert" hidden>
|
||||
DB overrides are switched off on the server
|
||||
(<code>BOR_SETTINGS_DB=0</code>) — saving stores the
|
||||
value but it will not take effect.
|
||||
</div>
|
||||
|
||||
<!-- The ONE status region (the #tokens-status precedent):
|
||||
settings.js sets textContent here — a save's outcome,
|
||||
a reset's outcome, a clear-all's outcome, nothing else
|
||||
(the error line has its own role=alert below). -->
|
||||
<span class="settings-status" id="settings-status" role="status" aria-live="polite"></span>
|
||||
|
||||
<!-- The error line (the #theme-error precedent): the
|
||||
server's 422 detail (it names the offending field)
|
||||
and the house load-failure lines; the rows keep their
|
||||
edits on failure. -->
|
||||
<p class="settings-error" id="settings-error" role="alert" hidden></p>
|
||||
|
||||
<!-- The client-side filter (the phase-101 tokens D4
|
||||
contract): a case-insensitive substring over the label
|
||||
+ BOR_* name cells, applied on input with NO fetch,
|
||||
re-applied after every re-load (the query survives
|
||||
re-shows — the phase-77 contract). -->
|
||||
<div class="settings-search">
|
||||
<label for="settings-filter">Search settings</label>
|
||||
<input
|
||||
id="settings-filter"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- The scroll card: the .table-wrap + sticky-th rules in
|
||||
styles.css are already the bounded-scroll + sticky-
|
||||
header contract (the phase-156 catalogue language).
|
||||
The <tbody> elements are created by settings.js (one
|
||||
per group, each opening with a settings-group-row). -->
|
||||
<div class="table-wrap">
|
||||
<table class="settings-table">
|
||||
<caption class="visually-hidden">Application settings — the value in effect and where it comes from</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Setting</th>
|
||||
<th scope="col">Value</th>
|
||||
<th scope="col">Source</th>
|
||||
<th scope="col"><span class="visually-hidden">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="settings-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- The action row: Save (dirty rows only — ships disabled
|
||||
until a row is edited; settings.js runs the §7.4
|
||||
never-stale lifecycle), Clear all overrides (the house
|
||||
two-step inline confirm, settings.js), and the
|
||||
"N unsaved changes" hint (textContent only, aria-hidden
|
||||
— the rows' .is-dirty state + the status line carry
|
||||
the information). -->
|
||||
<div class="settings-actions">
|
||||
<button type="button" class="btn primary" id="settings-save" disabled>Save settings</button>
|
||||
<button type="button" class="btn ghost" id="settings-clear-all">Clear all overrides</button>
|
||||
<span class="settings-dirty" id="settings-dirty" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<!-- The shared description target for every
|
||||
restart-required row's flag (aria-describedby). -->
|
||||
<p class="visually-hidden" id="settings-restart-desc">
|
||||
Applies on the next restart — the app reads this setting
|
||||
once at boot, so the saved value takes effect when the
|
||||
app next starts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Phase 79 (task 05): the in-app token gate — a body-level
|
||||
|
||||
Reference in New Issue
Block a user