Files
ducoterra 1200437bf8
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
**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`.
2026-10-10 01:05:14 -04:00

82 lines
3.4 KiB
Python

"""app_settings: admin DB-side overrides for any BOR_* setting (phase 168, task 01)
Revision ID: 0028
Revises: 0027
Create Date: 2026-10-09
``TODO.md`` L3: "I want an admin panel that can edit anything in .env
and save it to the database. The database value should override the
env var. The change should take effect on clicking 'save' on that
page." This revision carries the storage half of that (phase 168 D1)
— the resolver (D2) and the live-apply hook (D3) are task 02/03.
* ``app_settings`` — one row per admin setting override, the whole
table a narrow key/value pair:
* ``key`` — VARCHAR(100) NOT NULL, PRIMARY KEY. The BARE
``Settings`` field name (``relevance_threshold``) — never the
``BOR_…`` form: the prefix is a presentation concern
(``"BOR_" + key.upper()``), so a stored prefix could double up.
The PK is the uniqueness guard: two rows for one setting would
make "which one wins" undefined, and Postgres enforces it for
free (the ``ui_settings`` single-row shape is not an option here
— there are 75 settings, and a wide row would need per-field DDL
for every new one, defeating "edit ANYTHING in .env").
* ``value`` — TEXT NOT NULL. 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). Storing the env-
native string — not a typed column — keeps coercion and
validation in exactly one place (the pydantic validators the
overlay runs the full candidate set through, phase 168 D1/A5),
and makes "anything in .env" work for all 75 settings with zero
per-field DDL. NOT NULL on purpose: "clear the override" is a
DELETE of the row (the panel's per-row Reset / Clear all), never
an empty value, so the resolver never has to distinguish
"no override" from "override to empty".
* ``updated_at`` — TIMESTAMPTZ NOT NULL, ``now()`` server default
(the ``folder_summaries`` / ``kb_overview.updated_at`` house
style): when the admin last saved the override.
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).
One additive, fully reversible migration (A13).
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "0028"
down_revision = "0027"
branch_labels = None
depends_on = None
def upgrade() -> None:
# No index beyond the PK (a display fact, never a query surface —
# the whole table is one SELECT per resolution) and no server
# default on ``value`` (NOT NULL — the reset is a DELETE).
op.create_table(
"app_settings",
sa.Column("key", sa.String(length=100), primary_key=True),
sa.Column("value", sa.Text(), nullable=False),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
def downgrade() -> None:
# The table is the only 0028 artefact — dropping it leaves 0027's
# schema byte-identical (A13, fully reversible). No data is lost
# by the drop in any meaningful sense: the values are re-enterable
# from the panel (they are an override of the env, not a source of
# truth — the env file stays the base, LOCKED A1).
op.drop_table("app_settings")