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:
@@ -70,6 +70,13 @@ Data model — see ``.agents/PLAN.md`` §Data Model:
|
||||
``lite`` summary the drill-down
|
||||
``ls`` shows next to each folder (phase 94;
|
||||
``folder_path = ""`` = the source root).
|
||||
* ``app_settings`` — one row per admin setting override (phase
|
||||
168, TODO.md L3): the Settings panel persists
|
||||
what used to be ``.env``-only ``BOR_*`` knobs
|
||||
here, keyed by the BARE ``Settings`` field
|
||||
name, the value in its env-native string form;
|
||||
the row WINS over the environment (resolved
|
||||
inside ``app.config.get_settings``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -649,3 +656,44 @@ class FolderSummary(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class AppSetting(Base):
|
||||
"""One admin setting override (phase 168, task 01).
|
||||
|
||||
``TODO.md`` L3: the admin Settings panel persists what used to be
|
||||
``.env``-only knobs here, and the row WINS over the environment
|
||||
(resolved by ``app.core.settings_overlay``, consumed by
|
||||
``app.config.get_settings``). The narrow key/value shape is the
|
||||
whole design (phase 168 D1): one row per setting, the BARE field
|
||||
name as the key, the value in the SAME textual form the env var
|
||||
would carry — so "anything in .env" is editable for all 75
|
||||
settings with ZERO per-field DDL (no typed columns, no wide row),
|
||||
and coercion/validation stays in exactly one place (the pydantic
|
||||
validators, phase 168 A5). A DELETE of the row is the reset
|
||||
("use the env value again") — never a NULL/empty value, so the
|
||||
resolver never has to distinguish "no override" from "override to
|
||||
empty".
|
||||
|
||||
No index beyond the PK: the whole table is read by primary key
|
||||
order in ONE ``SELECT`` per settings resolution (the
|
||||
``ui_settings`` "display fact, never a query surface" note, 0027).
|
||||
"""
|
||||
|
||||
__tablename__ = "app_settings"
|
||||
|
||||
#: The BARE ``Settings`` field name (``relevance_threshold``) — never
|
||||
#: the ``BOR_…`` form: the prefix is presentation
|
||||
#: (``"BOR_" + key.upper()``), so a stored prefix could double up.
|
||||
key: Mapped[str] = mapped_column(String(100), primary_key=True)
|
||||
#: The value in the SAME textual form the env var would carry
|
||||
#: (``"0.55"``, ``"true"``, ``"md,txt"``, a JSON array for the one
|
||||
#: ``list[str]`` field) — coercion and validation belong to
|
||||
#: pydantic, in exactly one place (phase 168 D1/A5). NOT NULL:
|
||||
#: "clear the override" is a DELETE, never an empty value.
|
||||
value: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
#: When the admin last saved it (the ``folder_summaries`` /
|
||||
#: ``kb_overview.updated_at`` house style: server-side now()).
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user