Skip to content

Suite 36 complete + Apex/Durham town coverage - #38

Merged
aryavenkatesan merged 9 commits into
mainfrom
feat/suite-36-apex-durham-coverage
Jul 30, 2026
Merged

Suite 36 complete + Apex/Durham town coverage#38
aryavenkatesan merged 9 commits into
mainfrom
feat/suite-36-apex-durham-coverage

Conversation

@aryavenkatesan

Copy link
Copy Markdown
Collaborator

Completes suite 36 (ingestion funnel dead-ends) and expands town coverage to Apex + Durham. Branches off fix/suite-36-beat-persistence, so it carries all of 36.1–36.7 plus the coverage change. Supersedes #37.

What's here

Suite 36 (36.1–36.7)

  • 36.1 terminal skipped_no_town status for out-of-coverage rows + monitor note
  • 36.2 backfill 14/15 town-less events
  • 36.3 crontab-derived beat freshness (+ fixed a 4–5h tz error it uncovered)
  • 36.4 CELERY_BEAT_SYNC_EVERY = 1 so beat persists last_run_at promptly
  • 36.6 monitor stops false-warning sources that are live all-time but zero-in-window (all-time live-Event count keyed by source_name; Count("pk") since Event's PK is uuid)
  • 36.7 direct-submission town-miss now lands terminal skipped_no_town instead of a silent perpetual hold
  • 36.8 investigation (no code): the 4 processed-but-no-staged Direct raws are benign — anchors swept by the pre-suite-34 publish-deletes-staged behavior; all map to live events, zero swallowed failures

Apex + Durham coverage

  • Migration 0018_seed_apex_durham_towns seeds the two Town rows so the ~27 host/scraper submissions Gemini geocodes just over the Chatham county line stop dead-ending as skipped_no_town and publish. durham is already a valid broadcast routing region, so this aligns event coverage with broadcast.
  • Migrated the 36.1/36.7 out-of-coverage test sentinels off Apex/Durham (now covered) to Greensboro/Charlotte, which remain genuinely outside the service area.

Post-merge (deploy auto-runs on push to main)

The deploy job applies migrations on prod. After it lands I'll run reopen_skipped_towns + publish_all_approved() on prod to flush the queued Apex/Durham events onto the site.

Verification (local)

  • ruff check / ruff format --check / mypy clean
  • Full DB tier: 403 tests OK · fast tier: 153 OK

🤖 Generated with Claude Code

aryavenkatesan and others added 9 commits July 30, 2026 01:21
A staged event whose town had no `Town` row stayed `approved` forever and
re-logged a warning on every pipeline run — 27 rows on prod (Apex 15,
Durham 12) burning 27 unactionable log lines per run. That is how Siler
City's dropped events went unnoticed for weeks (35.14).

Adds `skipped_no_town`, set at the skip point, so the row leaves the
`approved` queryset and the warning fires exactly once. Added to
`deduplicator.CANDIDATE_STATUSES` so a re-scrape collapses onto the
existing row instead of inserting a fresh one each poll. Coverage fixes
are no longer free: `manage.py reopen_skipped_towns` re-opens rows once a
`Town` lands. Migration backfills the 27 existing rows.

Includes the devtools funnel fix in the same commit deliberately — the
new status fell into no funnel bucket, silently breaking the documented
"buckets sum to raw" invariant and dropping 27 rows out of the monitor.
Splitting these would plant a commit with failing devtools tests. The
monitor now surfaces a `no_town` bucket with an explanatory note, which
is what 36.1 wanted: `raw > 0, published = 0` now says why. The
reconcile test sums buckets dynamically so a future unwired bucket fails
instead of passing silently.

Co-Authored-By: Claude <noreply@anthropic.com>
All 15 had an empty `source_name`, so they predate the ingestion pipeline
— hand-entered rows whose town was never resolved. 11 of 15 are one
recurring "Senior ..." series at Covenant Place.

Mapping: Fair Game Beverage Company, bmc brewing and Chatham YMCA ->
pittsboro; Covenant Place -> carrboro (Carrboro Parks & Rec programming,
per product decision, though the building's postal address is Chapel
Hill). The Jordan Lake spring-cleanup row is left NULL on purpose: the
recreation area spans Wake and Chatham with access points in different
towns, and a wrong town puts an event on the wrong hub page.

`Event.town` stays nullable — ingestion legitimately produces unmatched
towns (36.1).

Co-Authored-By: Claude <noreply@anthropic.com>
Root cause of the hourly `healthcheck.service` failure. django-celery-beat
holds `PeriodicTask.last_run_at` in memory and writes it only when
`Scheduler._do_sync()` runs. Both sync triggers were defeated:

- `CELERY_BEAT_MAX_LOOP_INTERVAL = 6h` becomes `Scheduler.max_interval`
  (celery/beat.py:253-256), overriding django-celery-beat's
  DEFAULT_MAX_INTERVAL of 5s. `Service.start()` (beat.py:645-652) calls
  `_do_sync()` only after `time.sleep(interval)`, so beat can sleep 6h
  between sync opportunities.
- `apply_async`'s finally (beat.py:416-418) is gated on `should_sync()`,
  whose task-count clause is dead because `beat_sync_every` defaults to 0.
  A task firing shortly after a prior sync flushes nothing.

Measured on prod: a write made at 18:00:00 was still absent 4h24m later
and appeared the instant celerybeat was restarted (shutdown's
`finally: self.sync()` flushed it) — 12:00:01/runs=9 -> 18:00:00.021/runs=10.
The task was sent AND executed on time; only the bookkeeping was late.

Fix is `CELERY_BEAT_SYNC_EVERY = 1`, forcing a flush after every send.
Costs one UPDATE per fire and deliberately does not touch the loop
interval, which exists to reduce Neon serverless wake-ups.

Note this means PR #36's 7h staleness window was not "too tight" — it was
comparing against a column that lagged by up to 6h. Widening it to 13h
(the original 36.5) would have turned a true positive into a pass and
buried this bug.

Co-Authored-By: Claude <noreply@anthropic.com>
A staleness window must exceed the task's period, so every task had a
blind spot at least one period wide — a missed weekly digest rode along
for ~8 days. Now `_task_freshness` asks the schedule itself:

    expected_next = now + schedule.remaining_estimate(last_run_at)

`DEFAULT_STALENESS_HOURS` is demoted to (a) the must-exist task set, which
still catches a seeded task vanishing from the schedule (2026-07-21
outage), and (b) a fallback window for interval-backed tasks only.

Grace is keyed to `CELERY_BEAT_SYNC_EVERY`: 5 minutes when it is set (the
persistence lag is gone — see 36.4), automatically re-widening to
MAX_LOOP_INTERVAL + 1h if it is ever removed. Evaluated at call time so
`@override_settings` can reach both branches.

Also fixes a bug the tightened grace exposed. `TzAwareCrontab` converts
`last_run_at` into the schedule's tz inside `is_due()` (tzcrontab.py:39)
but inherits `remaining_estimate`/`remaining_delta` unmodified from
celery, which read `.hour`/`.minute`/`.isoweekday()` off whatever tzinfo
the datetime carries (celery/schedules.py:577-605). Calling
`remaining_estimate()` with UTC datetimes therefore compared
America/New_York crontab fields against UTC clock fields — a permanent
4-5h error every day for ingest-events-daily, scrape-sources-daily and
weekly-digest-sunday. Not a DST-only bug; it reproduces mid-June. The old
7h grace had been silently absorbing it. Fixed by converting both
operands via `.astimezone(schedule.tz)`, mirroring `is_due()`, rather than
widening grace back out.

Detection latency: ~8 days -> ~5 min for a missed weekly digest.

Co-Authored-By: Claude <noreply@anthropic.com>
36.1–36.4 built and green. 36.5 recorded as won't-fix: widening the
orphan-recovery staleness window to 13h would have converted a true
positive into a pass and buried 36.4's root cause.

OUTBOX.md carries the full change blocks (gitignored, so it does not
travel with this commit). Note the board still shows every suite back to
17 as _(pending)_ — the desktop app has never synced.

Co-Authored-By: Claude <noreply@anthropic.com>
ingest_direct_submission's out-of-coverage branch did a bare return None,
leaving the StagedEvent stuck unpublished forever. Set status=skipped_no_town
before returning (mirroring publish_all_approved's sweep path from 36.1) so the
row is terminal and serves as a dedupe anchor for re-submits. reopen_skipped_towns
already reopens these (no source filter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndow (36.6)

/devtools/monitor warned healthy sources (Carrboro, The Plant NC) whose events
are live on the site but whose surviving StagedEvent anchors predate suite 34's
delete->flip change, so the windowed published bucket reads 0 forever. Add an
un-windowed all-time live-Event count per source (keyed by source_name; Event has
no source FK or ingest timestamp), downgrade the 'none published in window' warn
when a source has live events all-time, and render a '0 in window; N live all-time'
note mirroring _raw_zero_note. Uses Count(pk) since Event's PK is uuid, not id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed `apex` and `durham` Town rows (migration 0018) so the ~27 host/scraper
submissions Gemini geocodes just over the Chatham county line stop dead-ending as
`skipped_no_town` and publish to the site. `durham` is already a valid broadcast
routing region, so this aligns event coverage with broadcast. Migrate the 36.1/36.7
out-of-coverage test sentinels off Apex/Durham (now covered) to Greensboro/Charlotte,
which remain genuinely outside the service area.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aryavenkatesan
aryavenkatesan merged commit 316174b into main Jul 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant