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
+90
View File
@@ -1990,3 +1990,93 @@ class UiSettingsOut(BaseModel):
accent_bg: str
accent_ink: str
accent_line: str
class AdminSettingItem(BaseModel):
"""One row of the admin settings catalogue
(``GET``/``PUT``/``DELETE /api/admin/settings[/{key}]``, phase 168,
task 04).
One entry per ``Settings`` field (75 today — the catalogue is
completeness-pinned, ``tests/unit/test_settings_catalog.py``), in
``GROUPS`` order: the field's metadata (``group`` + ``label`` + the
three capability flags), where its EFFECTIVE value comes from
(``source``: ``db`` = an overridable row is in effect, ``env`` = the
``BOR_*`` var is in the process environment, ``default`` = the code
default), and the value itself — ``value`` in the SAME env-native
string form the env var would carry (bool → ``"true"``/``"false"``,
the one ``list[str]`` field → a JSON array, everything else
``str()``; the stringification lives in the router, D1), ``None``
while masked. LOCKED A4: a ``secret`` field is write-only —
``masked=True``, ``value=None``, ``db_value=None`` and
``has_value`` (is a non-empty value in effect at all) are all the
read side ever reports; the plaintext appears in no response.
"""
key: str
env_var: str
group: str
label: str
type: Literal["int", "float", "bool", "str", "json"]
secret: bool
restart_required: bool
overridable: bool
source: Literal["db", "env", "default"]
#: The effective value in env-native string form (``None`` masked).
value: str | None
#: Locked A4: ``True`` for a write-only field — ``value`` is ``None``
#: while masked and the plaintext is in no response.
masked: bool
#: For a masked field: is a non-empty value in effect at all.
has_value: bool
#: An (allowed) row exists in ``app_settings`` for this key.
has_override: bool
#: The stored string while it is a stored non-secret override; ``None``
#: otherwise (a masked or non-overridable row never exposes its text).
db_value: str | None
#: The code default, stringified (``""`` for a masked field).
default: str
class AdminSettingsTotals(BaseModel):
"""The catalogue's footer counts (phase 168, task 04): how many
settings there are and how many an ``app_settings`` row currently
overrides (in effect — ``source == "db"``)."""
count: int
overridden: int
class AdminSettingsOut(BaseModel):
"""``GET /api/admin/settings`` response (phase 168, task 04): the
whole catalogue — one :class:`AdminSettingItem` per ``Settings``
field, in ``GROUPS`` order (the stable order the E2E pins
against) — plus the footer counts and the ``BOR_SETTINGS_DB``
state: while ``overlay_enabled`` is ``false`` the panel shows a
banner instead of pretending a save took effect (the kill switch
disables the mechanism, not the panel — D6)."""
items: list[AdminSettingItem]
totals: AdminSettingsTotals
overlay_enabled: bool
class AdminSettingIn(BaseModel):
"""``PUT /api/admin/settings/{key}`` body (phase 168, task 04):
always a string — the value in the env-native string form (D1
stores strings; coercion and validation belong to pydantic's
``Settings`` validators, in exactly one place, A5). An empty
string for a non-secret field is a legitimate value (e.g. clearing
``git_sources``); an empty string for a SECRET field is a 422 from
the router (an empty API key / session secret is a self-lockout —
``DELETE`` is the explicit clear)."""
value: str
class AdminSettingsCleared(BaseModel):
"""``DELETE /api/admin/settings`` response (phase 168, task 04):
how many ``app_settings`` rows the "Clear all overrides" action
deleted."""
cleared: int