Decouple historical alert-snapshot capture from public-page gating #129
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#129
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #22.
nws_alert_snapshotsis the only table holding the full official alert body (description,instruction,area_desc,hazards,geometry,affected_zones,vtec) — the raw material the Explorer needs. Rows are created inapp/services/public_alerts.py:117(persist_alert_snapshot), reached only viaget_or_create_public_alert_page.Actual gating (corrected after reading the code)
The original text of this issue said capture required
PUBLIC_ALERT_PAGES_ENABLEDandLocation.public_enabled. That was wrong in one direction and understated in another. What the code actually does:settings.public_alert_pages_enabledgates capture (app/config.py:253, default False) — checked in_ensure_public_alert_page(alert_processor.py:116) and again inget_or_create_public_alert_page(public_alerts.py:212). This part was right.Location.public_enableddoes NOT gate capture.get_or_create_public_alert_pagenever inspects it — a page and token are created for every matched location when the flag is on.public_enabledis enforced at serve time only (app/api/public.py:611for/p/{token}, and in the feed routes). So this was wrong.Capture is coupled to channel dispatch, which is the bigger gap. In
_dispatch_to_locations,_ensure_public_alert_pageis called from exactly two places: line 281, only when a page already exists, and line 351, inside the per-channel dispatch loop._process_cancelis gated the same way (has_channel_record or has_existing_page, line 406).The consequence: a location with no channel subscriptions never gets a snapshot at all, even with
PUBLIC_ALERT_PAGES_ENABLED=true. The dashboard record is still written unconditionally (_record_and_dispatch(..., dashboard_only=True), line 292), so such a location accruessent_alertsrows — event, severity, headline, times — and nothing else. Dashboard-only locations are precisely the ones whose history is currently emptiest.Change
Capture a snapshot for every matched location on the dashboard-record path, independent of both the public-pages flag and channel dispatch. Keep
public_alert_pages_enabledandLocation.public_enabledgoverning only exposure — who may read a snapshot and whether a/p/{token}page is minted.Capture and publication become independent concerns, which is what they always should have been.
Tasks
_capture_alert_snapshothelper, savepoint-wrapped like_ensure_public_alert_page, called for every matched location in_dispatch_to_locationsbefore the channel loop._process_cancel, so CAN messages are captured for every location with records rather than only channel-dispatched ones — a cancel is part of the lifecycle the timeline needs.get_or_create_public_alert_pagevia its existingsnapshot=parameter so the page path does not redundantly re-persist.public_alert_pages/public_tokenscreation gated exactly as today.(location_id, lifecycle_id, content_hash)unique-constraint race between overlapping poll runs quietly — the row exists either way, and logging a traceback every poll would be noise.content_hashand do not create rows.Why this is first
History not captured is gone forever. Every day this is unshipped is a day the Explorer will have nothing to show for — and today the gap is total for dashboard-only locations.
Picking this up on branch
feat/history-capture-ungate.Reading the code first turned up two corrections to this issue as originally filed, now folded into the body above:
Location.public_enableddoes not gate capture —get_or_create_public_alert_pagenever checks it. It gates serving only.alert_processor.py:351) or when a page already exists (:281). So a location with no channel subscriptions gets no snapshot at all, even withPUBLIC_ALERT_PAGES_ENABLED=true. That is a larger gap than "the flag is off by default", and it hits dashboard-only locations hardest.Done in #143 (merged to
main).Shipped:
_capture_alert_snapshot— fail-soft, savepoint-wrapped — now runs for every matched location on the dashboard path, before and independent of channel dispatch, and on the Cancel path for every location holding records. The snapshot is threaded intoget_or_create_public_alert_pagevia its existingsnapshot=parameter so the page path does not re-persist.IntegrityErroron the(location_id, lifecycle_id, content_hash)constraint is caught and logged at debug, since overlapping polls can race and the row exists either way. Public page and token creation remain gated onpublic_alert_pages_enabled— capture and exposure are now separate concerns.Tests: new
tests/test_history_capture.pycovers capture with no channel subscriptions and public pages disabled (asserting zeroPublicAlertPage/PublicTokenrows), dedup across three identical polls, append on a CON content change, and capture on Cancel.Verified locally in Docker against every CI gate: ruff clean,
compileallOK, 783 SQLite-tier tests passed,alembic upgrade headclean through0030on a fresh Postgres 16, 4 Postgres-tier tests passed. CI on the PR was green in 4m30s.Two follow-ups, neither blocking:
nws_alert_snapshotsrow count and table size after a week of real polling, especially before #133 settles the 13-month horizon, since this change raises the write rate for any deployment that had dashboard-only locations.test_quiet_hours_suppress_channel_send_but_keep_dashboard_recordnow emits aRuntimeWarning(unawaited coroutine): it passes aMagicMocksession, and_capture_alert_snapshothas no early return, so it reachesbegin_nested()on the mock and the failure is swallowed fail-soft. Production behaviour is correct and covered. An identical warning already exists intest_webhook_ssrf. One-line mock fix if it becomes annoying.