fix(gui): generic model_list editor for list-of-model adapters + TomTom bbox validation & quota (v0.9.9)

Fixes a shared form_descriptors 500 (NotImplementedError: unsupported list
type) that broke the Edit page for ALL FOUR adapters whose settings carry a
list[<BaseModel>] field: tomtom_incidents, tomtom_flow, state_511_atis,
state_511_atis_cameras.

- form_descriptors: list[BaseModel] -> generic "model_list" widget with
  recursive per-column sub_field descriptors.
- New _partials/model_list.html: vanilla-JS repeatable-row editor
  (add/remove/renumber), driven entirely by sub_fields (no adapter-name
  branching). Single-region edit pages render byte-identically.
- TomTom: BBox/Settings Pydantic validators (10,000 km^2 cap, coord ranges,
  min<max, cadence_s>=60, unique names) as the single source of truth
  (enforced at supervisor load AND GUI POST). Duck-typed quota_estimate hook
  + read-only quota panel; POST hard-blocks estimates over the 2,500/mo free
  tier (422). TOMTOM_FREE_TIER_CALLS_PER_MONTH is a tunable for paid tiers.
- routes: model_list form parse, row-aware ValidationError messages, 422 for
  model_list failures (single-region region errors still re-render at 200).
- tests: 11 new (real-Jinja render across 3 adapters + byte-identical nws
  no-regression guard, POST persist + oversized/degenerate/duplicate/cadence/
  quota 422 matrix, quota estimate). Full suite 848 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-26 05:57:34 +00:00
commit 7a5092c77f
8 changed files with 483 additions and 6 deletions

View file

@ -98,7 +98,7 @@ def test_subject_for_idaho(adapter):
def test_subject_unknown(adapter):
e = adapter._build_event(INC[0], BBox(name="x", min_lon=0, min_lat=0, max_lon=1, max_lat=1, state_code=""))
e = adapter._build_event(INC[0], BBox(name="x", min_lon=0, min_lat=0, max_lon=0.5, max_lat=0.5, state_code=""))
assert adapter.subject_for(e) == "central.traffic.incident.unknown"
@ -143,7 +143,7 @@ def test_inherits_dedup_mixin():
# --- v0.9.5.1 per-bbox cadence -----------------------------------------------
def _b(name, cadence_s=None):
return BBox(name=name, min_lon=0, min_lat=0, max_lon=1, max_lat=1,
return BBox(name=name, min_lon=0, min_lat=0, max_lon=0.5, max_lat=0.5,
state_code="ID", cadence_s=cadence_s)