From 21a7748b2c19413f77a7ac64406708d44eddc09f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 16 Jun 2026 20:34:01 -0500 Subject: [PATCH] Update upstream weatherapi alert handling --- internal/forecast/derive.go | 10 ++- internal/forecast/derive_test.go | 80 +++++++++++++++++++ .../reporttemplate/templates/hourly.md.tmpl | 6 +- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/internal/forecast/derive.go b/internal/forecast/derive.go index 29110b7..11fd918 100644 --- a/internal/forecast/derive.go +++ b/internal/forecast/derive.go @@ -490,12 +490,16 @@ func parseAlert(raw json.RawMessage) (parsedAlert, bool) { Severity: stringField(fields, "severity"), Description: firstStringField(fields, "description", "instruction"), } - start, startOK := firstTimeField(fields, "effective", "onset", "startsAt", "startTime", "sent") - end, endOK := firstTimeField(fields, "expires", "ends", "endsAt", "endTime") + start, startOK := firstTimeField(fields, "onset", "startsAt", "startTime", "effective", "sent") + end, endOK := firstTimeField(fields, "ends", "endsAt", "endTime", "expires") if !startOK || !endOK { return parsedAlert{}, false } - alert.Period = timeutil.Period{Start: start, End: end} + period := timeutil.Period{Start: start, End: end} + if !period.IsValid() { + return parsedAlert{}, false + } + alert.Period = period return alert, true } diff --git a/internal/forecast/derive_test.go b/internal/forecast/derive_test.go index 5765d2e..820c537 100644 --- a/internal/forecast/derive_test.go +++ b/internal/forecast/derive_test.go @@ -210,6 +210,86 @@ func TestAlertOverlap(t *testing.T) { } } +func TestAlertOverlapUsesOnsetAndEnds(t *testing.T) { + raw := json.RawMessage(`{"event":"Wind Advisory","headline":"Wind Advisory issued June 16 at 12:39PM CDT until June 17 at 8:00PM CDT by NWS St Louis MO","severity":"Moderate","effective":"2026-06-16T17:39:00Z","onset":"2026-06-17T18:00:00Z","ends":"2026-06-18T01:00:00Z","expires":"2026-06-17T08:45:00Z"}`) + alertRun := &weatherdata.AlertRun{Alerts: []json.RawMessage{raw}} + period := timeutil.Period{ + Start: mustParse("2026-06-17T12:00:00-05:00"), + End: mustParse("2026-06-17T21:00:00-05:00"), + } + + overlaps := AlertOverlaps(alertRun, period) + if len(overlaps) != 1 { + t.Fatalf("overlaps length = %d, want 1", len(overlaps)) + } + if overlaps[0].Period.Start.Format(time.RFC3339) != "2026-06-17T18:00:00Z" { + t.Fatalf("alert period start = %s, want onset", overlaps[0].Period.Start.Format(time.RFC3339)) + } + if overlaps[0].Period.End.Format(time.RFC3339) != "2026-06-18T01:00:00Z" { + t.Fatalf("alert period end = %s, want ends", overlaps[0].Period.End.Format(time.RFC3339)) + } + if overlaps[0].Overlap.Start.Format(time.RFC3339) != "2026-06-17T18:00:00Z" { + t.Fatalf("overlap start = %s, want onset", overlaps[0].Overlap.Start.Format(time.RFC3339)) + } + if overlaps[0].Overlap.End.Format(time.RFC3339) != "2026-06-18T01:00:00Z" { + t.Fatalf("overlap end = %s, want alert ends", overlaps[0].Overlap.End.Format(time.RFC3339)) + } +} + +func TestAlertOverlapUsesOnsetAndEndsForLocalDateRelevance(t *testing.T) { + location, err := time.LoadLocation("America/Chicago") + if err != nil { + t.Fatalf("load location: %v", err) + } + raw := json.RawMessage(`{"event":"Wind Advisory","headline":"Wind Advisory issued June 16 at 12:39PM CDT until June 17 at 8:00PM CDT by NWS St Louis MO","severity":"Moderate","effective":"2026-06-16T17:39:00Z","onset":"2026-06-17T18:00:00Z","ends":"2026-06-18T01:00:00Z","expires":"2026-06-17T08:45:00Z"}`) + alertRun := &weatherdata.AlertRun{Alerts: []json.RawMessage{raw}} + june16 := timeutil.CivilDay(time.Date(2026, 6, 16, 12, 0, 0, 0, location), location) + june17Afternoon := timeutil.Period{ + Start: time.Date(2026, 6, 17, 12, 0, 0, 0, location), + End: time.Date(2026, 6, 17, 21, 0, 0, 0, location), + } + + if overlaps := AlertOverlaps(alertRun, june16); len(overlaps) != 0 { + t.Fatalf("June 16 overlaps length = %d, want none: %#v", len(overlaps), overlaps) + } + if overlaps := AlertOverlaps(alertRun, june17Afternoon); len(overlaps) != 1 { + t.Fatalf("June 17 afternoon overlaps length = %d, want 1: %#v", len(overlaps), overlaps) + } +} + +func TestAlertOverlapSupportsOlderEffectiveExpiresPayload(t *testing.T) { + raw := json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate","effective":"2026-05-29T07:00:00-05:00","expires":"2026-05-29T10:00:00-05:00"}`) + alertRun := &weatherdata.AlertRun{Alerts: []json.RawMessage{raw}} + period := timeutil.Period{ + Start: mustParse("2026-05-29T06:00:00-05:00"), + End: mustParse("2026-05-29T11:00:00-05:00"), + } + + overlaps := AlertOverlaps(alertRun, period) + if len(overlaps) != 1 { + t.Fatalf("overlaps length = %d, want 1", len(overlaps)) + } + if overlaps[0].Period.Start.Format(time.RFC3339) != "2026-05-29T07:00:00-05:00" { + t.Fatalf("alert period start = %s, want effective fallback", overlaps[0].Period.Start.Format(time.RFC3339)) + } + if overlaps[0].Period.End.Format(time.RFC3339) != "2026-05-29T10:00:00-05:00" { + t.Fatalf("alert period end = %s, want expires fallback", overlaps[0].Period.End.Format(time.RFC3339)) + } +} + +func TestAlertOverlapSkipsInvalidSelectedPeriod(t *testing.T) { + raw := json.RawMessage(`{"event":"Wind Advisory","headline":"Invalid event period","severity":"Moderate","onset":"2026-06-17T18:00:00Z","ends":"2026-06-17T08:45:00Z"}`) + alertRun := &weatherdata.AlertRun{Alerts: []json.RawMessage{raw}} + period := timeutil.Period{ + Start: mustParse("2026-06-17T00:00:00Z"), + End: mustParse("2026-06-18T00:00:00Z"), + } + + if overlaps := AlertOverlaps(alertRun, period); len(overlaps) != 0 { + t.Fatalf("overlaps length = %d, want invalid alert skipped: %#v", len(overlaps), overlaps) + } +} + func TestBuildPrecipTimingBuildsThresholdWindows(t *testing.T) { location := time.FixedZone("Test", -5*60*60) periods := []weatherdata.ForecastPeriod{ diff --git a/internal/reporttemplate/templates/hourly.md.tmpl b/internal/reporttemplate/templates/hourly.md.tmpl index 988d25a..f820937 100644 --- a/internal/reporttemplate/templates/hourly.md.tmpl +++ b/internal/reporttemplate/templates/hourly.md.tmpl @@ -20,10 +20,8 @@ Current conditions are unavailable. ## Hourly Forecast {{ with .Modules.HourlyForecast }}{{ range .Periods }} - **{{ if .HourLabel }}{{ .HourLabel }}{{ else }}{{ .Name }}{{ end }}:**{{ with .TemperatureF }} {{ . }}°F{{ end }}{{ with .TextDescriptionLower }} and {{ . }}{{ else }}{{ with .TextDescription }} and {{ . }}{{ end }}{{ end }}.{{ if .MentionPrecipitation }}{{ with .ProbabilityOfPrecipitationPercent }} Probability of precipitation is {{ . }}%.{{ end }}{{ end }} -{{ else }} -- No hourly forecast rows are available. -{{ end }}{{ else }} -- No hourly forecast rows are available. +{{ else }}- No hourly forecast rows are available. +{{ end }}{{ else }}- No hourly forecast rows are available. {{ end }} {{ template "precipitation_timing" . }}