Commit graph

12 commits

Author SHA1 Message Date
1d5548c24c
v0.10.0: ITD 511 official API adapter (events + advisories + cameras) (#85)
First official-state-DOT-API pattern landing. Two adapters in one PR:

- itd_511 (event-class): polls Events (60s) + Advisories (300s) from
  https://511.idaho.gov/api/v2/get/{event,alerts}. Decodes EncodedPolyline
  to LineString via the polyline lib (bookend LineString or Point fallback);
  ITD Severity string mapped None->1 / Minor->2 / Major->3 with
  IsFullClosure=true forcing 3 regardless; RecurrenceSchedules /
  Restrictions / DetourPolyline pass through unmodified. Advisories ship
  as structural pass-through under data.advisory since the upstream
  /alerts endpoint currently returns []; per-record try/except keeps a
  surprise shape from sinking the cycle when ITD posts its first one.

- itd_511_cameras (telemetry-class): polls Cameras (600s). One event per
  camera per UTC day; image URL passes straight through to <img src>.
  Region uniform US-ID with data.source_jurisdiction preserving the raw
  upstream Source field for the ~1.2% cross-DOT border-region mirrors
  (UDOT / ODOT / WYDOT / WSDOT / NDot / MTD / DriveBC / Lemhi County).

Subject convention (v0.9.20 forward): central.traffic.<event_type>.us.id
and central.traffic_cameras.us.id.<camera_id>. Castle Rock state_511_atis
keeps its bare-state subject; consumers stay on central.traffic.>
wildcards during the A/B comparison window.

Retry predicate tightened from the Castle Rock / TomTom precedent: 5xx +
connection / timeout retry; 4xx other than 429 skip-with-warn (don't
burn quota on permanent errors); 429 honors Retry-After once then
retries. API key (alias 'idaho_511') travels in the ?key= query string,
so every error log path runs through self._redact() to scrub the URL.

Both adapters ship disabled; operator enables via GUI after registering
the API key with 'python -m set_api_key idaho_511'. Reuses existing
CENTRAL_TRAFFIC and CENTRAL_TRAFFIC_CAMERAS streams -- no archive
restart needed.

Scope-cap exception: this PR is ~1.5k lines vs. the standard 500-line
cap, authorized as a one-time exception for the first
official-state-DOT-API pattern landing. Two adapters + their tests +
real-API fixtures naturally exceed the v0.9.x adapter-cap budget.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:36:26 -06:00
Matt Johnson
b8033444ec feat(tomtom_flow): TomTom Orbis vector flow-tile telemetry adapter + CENTRAL_TRAFFIC_FLOW (v0.9.3)
Third CENTRAL_TRAFFIC-family member, first telemetry traffic source. Polls a
configured tile coverage set (Idaho metros, z=10), fetches Orbis vector flow
tiles, decodes per-segment relative_speed + road geometry, emits one telemetry
Event per road segment per poll to the new CENTRAL_TRAFFIC_FLOW stream. Renders
as colored polylines (green free-flow -> red jam) on the /telemetry map.

Production code; supervisor + gui + ARCHIVE restart (NEW event-bearing stream
central.traffic_flow.> -> archive must resubscribe). Ships disabled; needs a
"tomtom" api key in config.api_keys before enable.

- Subject central.traffic_flow.{z}.{x}.{y} (token traffic_flow, non-overlapping
  with central.traffic.>). category="flow.tomtom_flow" -> GUI event_type "flow".
- Severity from relative_speed: >=0.75=1, 0.5-0.75=2, 0.25-0.5=3, <0.25=4.
- Cadence 300s; 7-day retention (high-volume telemetry). Dedup minute-bucketed,
  inherited from the v0.9.1 SourceAdapter mixin.
- Shared tomtom_flow_parse module (decode + slippy-tile georeference) reused by
  the v0.9.4 on-demand passthrough endpoint.
- Generic framework change (Option A, ~3 lines, inert for the other 14
  adapters): Geo.geometry optional field + archive _build_geom_sql prefers it,
  so segments persist their real LineString to the PostGIS geom column.
- Idaho-only (Orbis tier confirmed live). Cameras + Navi passthrough are follow-ups.
- deps: mapbox-vector-tile (vector PBF decode); itsdangerous promoted to an
  explicit dependency (gui/csrf.py + gui/wizard.py imported it as an undeclared
  transitive that uv re-lock would otherwise prune).

Full suite: 780 passed, 1 skipped (central and unprivileged zvx, 3x each).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:25:44 +00:00
38b23f2a25
release: bump version to 0.3.0 (#30)
Pyproject was stuck at 0.1.0 since Phase 0; bumping to match
the v0.3.0 release tag landing alongside this.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
2026-05-18 14:29:28 -06:00
494ad1c799
feat(gui): implement first-run setup wizard (1b-8) (#24)
* feat(gui): implement first-run setup wizard (1b-8)

Add a 5-step setup wizard that replaces the single-step /setup:
1. Create Operator - create initial operator account
2. System Settings - configure map tile URL and attribution
3. API Keys - optionally add API keys for adapters
4. Configure Adapters - enable/disable adapters with region picker
5. Finish Setup - review and complete setup

Key changes:
- Update middleware to handle wizard URL structure and step routing
- Add wizard routes for each step with proper auth checks
- Create new templates using base_wizard.html for consistent styling
- Add audit events for system.update and setup.complete
- Update tests for new middleware behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(gui): handle CSRF errors on wizard paths

Update csrf_exception_handler to re-render wizard forms with error
message instead of redirecting to /login when CSRF validation fails.

- /setup/operator: re-render with error
- /setup/system: re-render with current system values + error
- /setup/keys: re-render with current keys list + error
- /setup/adapters: re-render with current adapter config + error
- /setup/finish: re-render with summary data + error
- /setup: redirect to /setup (middleware routes to appropriate step)

Add error display to setup_keys.html and setup_finish.html templates.
Add 7 new CSRF handler tests for wizard paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(gui): region picker render + click-to-draw

Bug A: Maps render blank on /setup/adapters for FIRMS and USGS
because Leaflet computed zero dimensions before container layout
settled. Fix: add setTimeout invalidateSize() after map creation.

Bug B: No click-to-draw functionality - only drag corners. Fix:
add L.Control.Draw for rectangle drawing with CREATED event handler
to replace existing rectangle.

Both fixes applied to:
- setup_adapters.html (wizard inline JS)
- _region_picker.html (standalone edit page)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(gui): handle revisiting /setup/operator after operator created

When an operator already exists, /setup/operator now shows a
confirmation page instead of the create form. This prevents:
- Unique constraint violations on duplicate username
- Silent creation of duplicate operators

GET /setup/operator: queries config.operators; if any exist,
renders confirmation state with existing_operator context.

POST /setup/operator: checks operator count before INSERT; if
non-zero, renders confirmation state without inserting.

Template updated with conditional to show "Operator Already
Configured" message when existing_operator is set.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(csrf): replace fastapi-csrf-protect with session-bound CSRF

Fixes CSRF race condition where every GET rotated the CSRF token,
causing POST failures when users had multiple tabs or slow connections.

Changes:
- Remove fastapi-csrf-protect dependency
- Add session-bound CSRF tokens stored in config.sessions table
- Add pre-auth CSRF for unauthenticated routes (/login, /setup/operator)
- Add csrf.py module for pre-auth token generation/validation
- Update routes to use new CSRF token handling
- Add migration 013 to add csrf_token column to sessions

The session-bound approach ensures CSRF tokens remain stable for the
duration of a session, eliminating the race condition.

Note: Route tests (test_wizard.py, test_adapters.py, etc.) need
refactoring to mock get_settings() instead of CsrfProtect dependency.
Core auth/CSRF handler tests pass (74 tests).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(csrf): update test suite for session-bound CSRF tokens

- Add CSRF fixtures to conftest.py for pre-auth and session CSRF
- Update test_wizard.py: use bypass_pre_auth_csrf and patch_route_settings
- Update test_adapters.py: set request.state.csrf_token and form mock data
- Update test_api_keys.py: add CSRF token to form data for POST routes
- Update test_streams.py: change return_value to side_effect for CSRF support
- Update test_region_picker.py: add CSRF token handling
- Update test_config_store.py: set CENTRAL_CSRF_SECRET env var in fixture

All 285 tests now pass with session-bound CSRF validation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Matt Johnson <mj@k7zvx.com>
2026-05-17 22:06:22 -06:00
Matt Johnson
f059f982bc feat(gui): add auth core, setup gate, and first-run operator creation
- Add migrations 007-010 for system config, operators, sessions, audit_log
- Implement argon2id password hashing via argon2-cffi
- Implement session-based authentication with database-stored tokens
- Add SetupGateMiddleware to redirect to /setup until first operator created
- Add SessionMiddleware to load session from cookie and attach operator
- Create /setup, /login, /logout, /change-password routes with CSRF protection
- Add periodic session cleanup task (hourly)
- Add audit logging for auth events
- Update systemd unit with EnvironmentFile for /etc/central/central.env
- Add comprehensive tests for auth, middleware, and audit modules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-17 05:30:49 +00:00
Matt Johnson
614312db36 feat(gui): add FastAPI + Jinja2 + HTMX scaffold
- FastAPI app with Jinja2 templates and Pico CSS + HTMX from CDN
- Routes: GET / (placeholder page), GET /health (JSON healthcheck)
- systemd unit (no Install section - manual start only)
- TestClient tests for both endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-17 04:32:39 +00:00
Matt Johnson
a157f39fe0 fix(nws): replace centroid filter with polygon intersection
- Add shapely dependency for geometry intersection
- Replace _point_in_region with _geometry_intersects_region
- Uses Shapely shape() and box() for proper GeoJSON handling
- Avoids false negatives on large alert polygons

Also adds antimeridian-crossing bbox rejection to RegionConfig validator.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-16 19:05:05 +00:00
Matt Johnson
c368f175a1 build: remove aiolimiter dependency
No longer needed after removing internal rate limiting from NWSAdapter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-16 17:19:15 +00:00
Ubuntu
3e392cad81 feat(config): add CLI smoke command and dependencies
Add central-cli with config-store-check command that:
- Connects via bootstrap config
- Lists adapters from config store
- Verifies crypto round-trip

Updates pyproject.toml with new dependencies:
- pydantic-settings>=2.7.0
- cryptography>=44.0.0

New entry points:
- central-migrate
- central-cli

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-15 23:08:03 +00:00
Matt Johnson
31be17430d runtime: NWS adapter, supervisor, archive consumer, systemd units
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-15 21:29:08 +00:00
Matt Johnson
714971fe99 foundation: models, adapter ABC, config, CE wire, schema
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-15 21:08:56 +00:00
Matt Johnson
36ebbcb250 scaffold: initial repository structure 2026-05-15 19:16:24 +00:00