When the mapping file was cleaned (PH1E- → PH1E) but the legacy dashed
rows were never removed from the live DB, seed_units inserted the clean
rows AND left the legacy rows as inert aliases — yielding 8 penthouse
rows instead of 4, all with floor=null.
The self-heal now handles the 'both forms exist' case by consolidating
to exactly one row per unit: the clean-code row is the canonical
survivor, any tickets referencing the legacy row are re-pointed to it,
and the legacy row is removed. The operation is idempotent.
Also fixes a missing comma in pyproject.toml (TOML parse error).
Adds regression test test_penthouse_consolidation_when_both_forms_exist
that reproduces the live condition and fails pre-fix.
The running image (~2 weeks old) happened to carry greenlet; the rebuild
for this deploy was the first fresh in weeks and resolved
SQLAlchemy without greenlet. This exposes an unpinned dependency that any
fresh build would hit.
Add the [asyncio] extra to sqlalchemy and pin greenlet>=3.0 to prevent
future breaks on fresh builds.
The CEO dashboard requested /api/tickets?page_size=500, but the API caps
page_size at 200 (le=200 in app/routers/tickets.py), so the request
returned 422 and every KPI/chart rendered zeros.
- ceo.html: fetch all tickets by looping pages of page_size=200 until
total items are collected (with a safety bound), keeping KPIs accurate
as volume grows past 200.
- tests: add test suite anchoring the pagination contract — page_size=500
returns 422, page_size=200 returns items/total/page/page_size, and a
page loop collects every ticket without duplicates.
- pyproject: enable pytest-asyncio auto mode and tests/ discovery.
- .gitignore: un-ignore committed tests/test_*.py.