The library's validate_csrf expects the raw token in the form and
the signed token in the cookie. Previously we were putting the signed
token in both places, which caused signature mismatch errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The library supports form-data tokens via token_location="body" and
token_key config options, which we missed in the initial integration.
Removed hand-rolled _validate_csrf_form helper in favor of the
library's validate_csrf method.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _validate_csrf_form helper for form-based CSRF token validation
(compares form csrf_token with fastapi-csrf-token cookie)
- Fix index route to pass operator and csrf_token to template context
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- test_gui_scaffold.py: use standalone router instead of importing app
to avoid triggering settings load during test collection
- test_setup_gate.py: expect 302 (not 307) for setup gate redirect
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Revert uvicorn port from 8088 to 8000 (1b-1 pinned value)
- Change SetupGateMiddleware redirect from 307 to 302 for consistency
with all other redirects in the codebase
Port 8000 confirmed free on CT104. Earlier change to 8088 was
incorrect — 8080 is held by NATS WebSocket, not 8000.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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>
- 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>
Update subject_for_event to handle quake.* category events.
Subject format: central.quake.event.<magnitude_tier>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
USGS Earthquake Hazards Program adapter:
- Polls GeoJSON feed (all_hour default, configurable)
- Magnitude tier classification (minor/light/moderate/strong/major/great)
- Deduplication via USGS stable event ID
- Region filter via shapely point-in-bbox
- Skips events with null magnitude (quarry blasts, etc.)
Includes comprehensive unit tests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _ADAPTER_REGISTRY dict for adapter class lookup
- Unify adapter __init__ signatures (all take config, config_store, cursor_db_path)
- NWSAdapter now accepts config_store param (unused, for signature uniformity)
- Adding new adapters requires only one dict entry, no supervisor changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
NASA FIRMS adapter for VIIRS satellite fire detections:
- Polls VIIRS_SNPP_NRT and VIIRS_NOAA20_NRT satellites
- Deduplication via stable ID (satellite📅time:lat:lon)
- Hot-reload support for region, satellites, and API key
- Confidence mapping: l/n/h -> low/nominal/high
- Severity: high=3, nominal=2, low=1
Includes comprehensive unit tests for:
- CSV parsing and event generation
- Deduplication logic
- URL building and config application
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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>
- Remove states array from NWS settings
- Add region bbox covering ID/OR/WA/MT/WY/UT/NV
- Bbox: north=49.5, south=31.0, east=-102.0, west=-124.5
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- config.streams table for JetStream retention config
- Column-filtered NOTIFY: only fires on max_age_s changes
- Prevents self-loop when supervisor updates max_bytes
- Seeds CENTRAL_WX (7d/10GB) and CENTRAL_META (1d/100MB)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add RegionConfig pydantic model with validators
- NWSAdapter now uses bbox for client-side alert filtering
- Implement apply_config for hot-reload of region changes
- Remove states-based filtering logic
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SourceAdapter now requires apply_config() for hot-reload support.
Each adapter implements its own config extraction from settings.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Stream retention GUI design
- Region picker for bbox selection
- API key management requirements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents production verification of the AsyncLimiter removal fix:
- Decrease 60-30s: poll at Tlast+30s (not 60s)
- Increase 30-60s: poll at Tlast+60s
- Decrease 60-15s: immediate poll (deadline passed)
- All subsequent intervals use new cadence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These tests pass on both fixed and unfixed code, meaning they do
not actually exercise the cadence-decrease bug. The tests were
added as part of PR #4 but direct verification showed they
do not catch the issue they claim to test.
A follow-up issue should be filed for proper regression tests
that reproduce the actual bug (AsyncLimiter blocking).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The NWSAdapter no longer has a cadence_s attribute since the
internal limiter was removed. The supervisor's rate limiting
via state.config.cadence_s and last_completed_poll is sufficient.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The NWSAdapter had an internal AsyncLimiter that duplicated the
supervisor's rate-limit guarantee. When cadence changed, only
state.adapter.cadence_s was updated, not the internal limiter,
causing the cadence-decrease bug.
Since the supervisor already enforces rate limiting via
last_completed_poll + cadence_s scheduling, the adapter-level
limiter was redundant and caused the 30-second blocking observed
in diagnostic logs.
Removes:
- aiolimiter import
- self.cadence_s attribute (unused elsewhere)
- self._limiter creation
- async with self._limiter context in _fetch_alerts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
environment.md:
- Documents CT104 as the active development location
- Lists SSH access, repository paths, and service commands
- Notes that cortex clone is parked, matt-desktop has no clones
BUG-CADENCE-DECREASE.md:
- Full investigation of the cadence-decrease hot-reload bug
- Root cause analysis: cancel_event.set() inside lock context
- Proposed fix (Option A - structural)
- Test gap identification
- Production verification steps
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The cancel_event.set() call was inside the async lock context in
_on_config_change, causing delayed signal delivery to the sleeping
loop. This manifested as cadence decreases not applying without a
restart - the loop would sleep its full original timeout before
seeing the new cadence.
Fix: _reschedule_adapter now returns the AdapterState to signal,
and _on_config_change signals AFTER releasing the lock. This ensures
immediate event delivery per asyncio semantics.
The lock protects state consistency during config fetches and updates.
The cancel_event is a one-way notification that does not need lock
protection - it simply wakes the sleeping coroutine.
See docs/BUG-CADENCE-DECREASE.md for full investigation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tests that exercise the ACTUAL running loop with cancel_event
signaling, not just AdapterState math in isolation.
Test cases:
- Test 1: Cadence decrease (60->30) wakes loop immediately
- Test 2: Cadence increase (10->20) extends wait correctly
- Test 3: Enable/disable/enable with gap > cadence polls immediately
- Test 4: Enable/disable/enable with gap < cadence waits
These tests verify the cancel_event mechanism properly interrupts
the sleeping loop when config changes occur via _on_config_change.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>