Fix circular import, primary-admin NULL trap, and add gateway quota reset
DevPlace CI / test (push) Failing after 58m59s

Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.

Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.

Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.

Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.

Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.

Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.

Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.

Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.

Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.

Full suite: 2989 passed, 1 skipped.
This commit is contained in:
2026-07-27 11:17:48 +02:00
parent b185574760
commit 571a0485c5
33 changed files with 1290 additions and 92 deletions
@@ -42,7 +42,7 @@ Open `http://<host>:<PORT>`. Useful targets: `make docker-logs` (tail), `make do
The make targets always apply the `docker-compose.containers.yml` overlay, so the admin-only Container Manager works with no extra setup. Always update through them: a plain `docker compose up -d` drops the overlay and silently removes the docker socket and CLI the manager needs.
The healthcheck gives the app a 120s start window before any failure counts (`start_period: 120s` in `docker-compose.yml`). The full startup (DB init, background service registration, uvicorn workers) takes roughly 110s, so the start period must stay well above that. If you add heavyweight init work, verify the app boots within 120s or bump the start period to match.
The healthcheck gives the app a 120s start window in which a failing probe does not count against `retries` (`start_period: 120s`), and probes every 2s inside that window (`start_interval: 2s`) so the container is marked healthy as soon as it actually serves rather than at the next 30s tick. Both settings live in `docker-compose.yml` and the `Dockerfile` and must be changed together. Normal startup is a few seconds; the 120s window is headroom for a cold page cache on a multi-GB database. Startup cost is paid once per uvicorn worker, serialized under an exclusive init lock, so anything added to `init_db` is multiplied by the worker count. If you add heavyweight init work, measure the real boot time before relying on the existing window.
## Updating
@@ -50,15 +50,19 @@ Code is bind-mounted, so most updates need no rebuild:
```bash
git pull
make docker-up # restart workers on the new code
make docker-reload # restart workers on the new code
```
Use `make docker-reload`, not `make docker-up`: `docker compose up -d` sees an unchanged container and leaves it running, so the workers keep serving the code they imported at boot. `docker-reload` restarts the app and waits for it to report healthy again.
Rebuild the image only when dependencies change:
```bash
make docker-build && make docker-up
```
A rebuild after a source-only change takes about 7 seconds. Dependencies install from `pyproject.toml` in a layer that source edits cannot invalidate, so `pip install` and the Chromium download stay cached; only the source copy and the final project install re-run.
## Release branch
`make deploy` fast-forwards the release branch: