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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user