SPC outlook issue time has never parsed: issued_at is NULL on every row, and "issued" times shown are the fetch time #201

Closed
opened 2026-10-13 14:45:51 +00:00 by claude-bot · 4 comments
Collaborator

Found 2026-10-13 while checking dev's logs before resuming the v2.0.0 soak. Not a regression in the release candidates: the parser dates from the original SPC commit (e4b8297).

Symptom

Dev (2.0.0-rc4) logged, in the last 24 hours:

914  WARNING app.services.spc SPC convective outlook missing parseable issue time for day 2
564  WARNING app.services.spc SPC convective outlook missing parseable issue time for day 3
420  WARNING app.services.spc SPC convective outlook missing parseable issue time for day 1

And in dev's database, spc_outlook_pages: 194 rows, 0 with issued_at, spanning 2026-06-24 to 2026-10-13.

Cause

NOAA's MapServer returns the issue time as a compact 12-digit string. Live sample from SPC_wx_outlks/MapServer/1 today:

{"dn":3, "valid":"202610131300", "expire":"202610141200", "issue":"202610131252", "label":"MRGL"}

spc._parse_datetime (app/services/spc.py:107) only tries datetime.fromisoformat, which rejects that shape:

>>> datetime.fromisoformat("202610131252")
ValueError: Invalid isoformat string: '202610131252'

So _parse_convective_feature, the fire parser (valid) and the extended parser all get None.

The existing test fixture already uses the real format ("issue": "202604161630" in tests/test_spc_correctness.py:257) but nothing asserts on the parsed value, so it passes.

Effect

Every consumer falls back, mostly to "now":

  • app/api/alerts.py:301,320,343: outlook.issued_at or datetime.now(...). The dashboard's SPC "issued" time is the time of the request, not of the outlook.
  • app/services/spc_impacts.py:66: the impact window base is the fetch time.
  • app/services/radar.py:504: the SPC GIF's valid date is skipped.
  • app/api/public.py:362, app/api/spc_impacts.py:255: issued_utc renders empty.
  • app/services/ai_context.py:322: the AI summary context carries no issue time.

Alert delivery is not affected. Dedup uses product_id, which is built from the raw string.

Fix

  • Teach _parse_datetime the %Y%m%d%H%M form (UTC).
  • Assert on the parsed value in a test, using the live sample above.
  • Decide whether to backfill existing rows. product_id embeds the raw issue string (D1_202610131252), so it is recoverable for convective outlooks.
  • Check production (1.8.1) for the same NULLs. Expected to be identical; not yet confirmed.

Not a v2.0.0 blocker on its own, but the Explorer's day view shows SPC risk bands (#138), so wrong SPC timestamps are visible in the headline feature.

Found 2026-10-13 while checking dev's logs before resuming the v2.0.0 soak. Not a regression in the release candidates: the parser dates from the original SPC commit (`e4b8297`). ## Symptom Dev (`2.0.0-rc4`) logged, in the last 24 hours: ``` 914 WARNING app.services.spc SPC convective outlook missing parseable issue time for day 2 564 WARNING app.services.spc SPC convective outlook missing parseable issue time for day 3 420 WARNING app.services.spc SPC convective outlook missing parseable issue time for day 1 ``` And in dev's database, `spc_outlook_pages`: **194 rows, 0 with `issued_at`**, spanning 2026-06-24 to 2026-10-13. ## Cause NOAA's MapServer returns the issue time as a compact 12-digit string. Live sample from `SPC_wx_outlks/MapServer/1` today: ```json {"dn":3, "valid":"202610131300", "expire":"202610141200", "issue":"202610131252", "label":"MRGL"} ``` `spc._parse_datetime` (`app/services/spc.py:107`) only tries `datetime.fromisoformat`, which rejects that shape: ``` >>> datetime.fromisoformat("202610131252") ValueError: Invalid isoformat string: '202610131252' ``` So `_parse_convective_feature`, the fire parser (`valid`) and the extended parser all get `None`. The existing test fixture already uses the real format (`"issue": "202604161630"` in `tests/test_spc_correctness.py:257`) but nothing asserts on the parsed value, so it passes. ## Effect Every consumer falls back, mostly to "now": - `app/api/alerts.py:301,320,343`: `outlook.issued_at or datetime.now(...)`. The dashboard's SPC "issued" time is the time of the request, not of the outlook. - `app/services/spc_impacts.py:66`: the impact window base is the fetch time. - `app/services/radar.py:504`: the SPC GIF's valid date is skipped. - `app/api/public.py:362`, `app/api/spc_impacts.py:255`: `issued_utc` renders empty. - `app/services/ai_context.py:322`: the AI summary context carries no issue time. Alert delivery is not affected. Dedup uses `product_id`, which is built from the raw string. ## Fix - [ ] Teach `_parse_datetime` the `%Y%m%d%H%M` form (UTC). - [ ] Assert on the parsed value in a test, using the live sample above. - [ ] Decide whether to backfill existing rows. `product_id` embeds the raw issue string (`D1_202610131252`), so it is recoverable for convective outlooks. - [ ] Check production (1.8.1) for the same NULLs. Expected to be identical; not yet confirmed. Not a v2.0.0 blocker on its own, but the Explorer's day view shows SPC risk bands (#138), so wrong SPC timestamps are visible in the headline feature.
Author
Collaborator

The one-line parser fix is not safe on its own for fire outlooks

Started the fix and stopped before committing, because populating issued_at wakes up code that has never run with a real value.

spc_outlook_expires (app/services/spc_impacts.py:66) computes expiry as issued_at at 0800Z plus day days. Today issued_at is always NULL, so the base is "now", which is within hours of the real issue time. That is why the result has been roughly right by accident.

  • Convective and extended read issue. Parsing it gives the same expiry as today, minus the drift around 00Z. Safe.
  • Fire has no issue field at all. The fire layers expose only valid, expire, idp_filedate, idp_ingestdate, idp_source, and _parse_fire_feature passes valid as the issue time. For a Day 2 fire outlook valid is tomorrow 1200Z, so once it parses, expiry becomes tomorrow + 2 days: one day later than now. A Day 2 Critical/Extreme fire record would stay active on the dashboard a day too long.

_spc_valid_date (app/services/radar.py:498) has the same shape (issued_at + day) and would shift the Day 2 fire map date the same way.

Options for fire

  1. Use idp_filedate (epoch ms; _parse_datetime already handles numbers) as the issue time. Sample today: file date 07:45Z for the 1200Z-valid Day 1 product, so it is a close proxy for issuance.
  2. Stop deriving expiry from issue time and use the expire field NOAA already sends on every feature, for all three product families. More correct, larger change, and it alters when existing records clear.

Leaning 1 for a contained fix, with 2 as a follow-up. Needs a decision before any code lands, since both change alert-record lifetimes.

## The one-line parser fix is not safe on its own for fire outlooks Started the fix and stopped before committing, because populating `issued_at` wakes up code that has never run with a real value. `spc_outlook_expires` (`app/services/spc_impacts.py:66`) computes expiry as `issued_at` at 0800Z **plus `day` days**. Today `issued_at` is always NULL, so the base is "now", which is within hours of the real issue time. That is why the result has been roughly right by accident. - **Convective and extended** read `issue`. Parsing it gives the same expiry as today, minus the drift around 00Z. Safe. - **Fire** has no `issue` field at all. The fire layers expose only `valid`, `expire`, `idp_filedate`, `idp_ingestdate`, `idp_source`, and `_parse_fire_feature` passes `valid` as the issue time. For a **Day 2** fire outlook `valid` is tomorrow 1200Z, so once it parses, expiry becomes tomorrow + 2 days: **one day later than now**. A Day 2 Critical/Extreme fire record would stay active on the dashboard a day too long. `_spc_valid_date` (`app/services/radar.py:498`) has the same shape (`issued_at + day`) and would shift the Day 2 fire map date the same way. ## Options for fire 1. Use `idp_filedate` (epoch ms; `_parse_datetime` already handles numbers) as the issue time. Sample today: file date 07:45Z for the 1200Z-valid Day 1 product, so it is a close proxy for issuance. 2. Stop deriving expiry from issue time and use the `expire` field NOAA already sends on every feature, for all three product families. More correct, larger change, and it alters when existing records clear. Leaning 1 for a contained fix, with 2 as a follow-up. Needs a decision before any code lands, since both change alert-record lifetimes.
Author
Collaborator

Confirmed on production (1.8.1), 2026-10-13: spc_outlook_pages has 75 rows, 0 with issued_at, and the same warning fired 1,933 times in the last 24 hours (902 / 567 / 464 for days 2 / 3 / 1). Identical to dev, so this is not specific to the release candidates.

Confirmed on production (1.8.1), 2026-10-13: `spc_outlook_pages` has **75 rows, 0 with `issued_at`**, and the same warning fired 1,933 times in the last 24 hours (902 / 567 / 464 for days 2 / 3 / 1). Identical to dev, so this is not specific to the release candidates.
Author
Collaborator

Decision (@rbrooks, 2026-10-13): option 1 now, option 2 later if needed. Implemented in #202 (af7ee6e), awaiting review and merge.

Correction to my earlier comment: the fire feed's file time on 2026-10-13 is 05:45Z, not 07:45Z (I mis-converted the epoch value). It is still within minutes of issuance, so the reasoning for option 1 stands; #202 and its tests use the correct value.

Leaving this open until #202 is merged. Existing rows are not backfilled by #202.

**Decision (@rbrooks, 2026-10-13): option 1 now, option 2 later if needed.** Implemented in #202 (`af7ee6e`), awaiting review and merge. Correction to my earlier comment: the fire feed's file time on 2026-10-13 is **05:45Z**, not 07:45Z (I mis-converted the epoch value). It is still within minutes of issuance, so the reasoning for option 1 stands; #202 and its tests use the correct value. Leaving this open until #202 is merged. Existing rows are not backfilled by #202.
Author
Collaborator

Merged via #202 (af7ee6e). CI run 243 green in 7m23s, both test tiers.

Closing as fixed on main. Not yet released: it ships in the next release candidate, and the first real confirmation is issued_at populating on new spc_outlook_pages rows on dev, with the "missing parseable issue time" warning gone from the log.

Not done, by choice: existing rows are not backfilled, and option 2 (take expiry from NOAA's expire field) is deferred. Open a new issue for either if wanted.

Merged via #202 (`af7ee6e`). CI run 243 green in 7m23s, both test tiers. Closing as fixed on `main`. Not yet released: it ships in the next release candidate, and the first real confirmation is `issued_at` populating on new `spc_outlook_pages` rows on dev, with the "missing parseable issue time" warning gone from the log. Not done, by choice: existing rows are not backfilled, and option 2 (take expiry from NOAA's `expire` field) is deferred. Open a new issue for either if wanted.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot#201
No description provided.