Implement support for NWS weather stories

This commit is contained in:
2026-05-30 07:47:49 -05:00
parent 9ff90d33fc
commit 63749a9572
14 changed files with 214 additions and 37 deletions

View File

@@ -5,6 +5,7 @@ import (
"math"
"sort"
"strings"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
@@ -57,8 +58,17 @@ type DiscussionContext struct {
}
type WeatherStoryContext struct {
Available bool `json:"available"`
Summary string `json:"summary,omitempty"`
Available bool `json:"available"`
OfficeID string `json:"officeId,omitempty"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
AltText string `json:"altText,omitempty"`
Priority bool `json:"priority"`
Order int `json:"order"`
DownloadURL string `json:"downloadUrl,omitempty"`
}
func BuildDaily(ctx BuildContext, summary *forecast.DailySummary) (Package, error) {
@@ -258,10 +268,31 @@ func buildDiscussion(discussion *forecast.Discussion) DiscussionContext {
}
func buildWeatherStory(bundle *forecast.Bundle) *WeatherStoryContext {
if bundle == nil || bundle.WeatherStory == nil || len(bundle.WeatherStory.Raw) == 0 {
if bundle == nil || bundle.WeatherStory == nil {
return nil
}
return &WeatherStoryContext{Available: true, Summary: string(bundle.WeatherStory.Raw)}
story := bundle.WeatherStory
return &WeatherStoryContext{
Available: true,
OfficeID: story.OfficeID,
StartTime: story.StartTime,
EndTime: story.EndTime,
UpdatedAt: copyTime(story.UpdatedAt),
Title: story.Title,
Description: story.Description,
AltText: story.AltText,
Priority: story.Priority,
Order: story.Order,
DownloadURL: story.DownloadURL,
}
}
func copyTime(value *time.Time) *time.Time {
if value == nil {
return nil
}
copied := *value
return &copied
}
func scoreOutdoorWindow(daypart forecast.DaypartSummary) OutdoorWindow {

View File

@@ -162,8 +162,12 @@ func stormConfidenceInputs(bundle *forecast.Bundle) []string {
items = appendUnique(items, "Short-term discussion is available for confidence context.")
}
}
if bundle.WeatherStory != nil && len(bundle.WeatherStory.Raw) > 0 {
items = appendUnique(items, "Weather story source is available.")
if bundle.WeatherStory != nil {
if bundle.WeatherStory.Title != "" {
items = appendUnique(items, "Weather story: "+bundle.WeatherStory.Title+".")
} else {
items = appendUnique(items, "Weather story source is available.")
}
}
for _, warning := range bundle.Warnings {
if warning.Code != "" {

View File

@@ -50,8 +50,16 @@ func TestStormBriefingWithActiveAlert(t *testing.T) {
ShortTerm: &forecast.DiscussionSection{Text: "Short-term storm coverage peaks this morning."},
LongTerm: &forecast.DiscussionSection{Text: "Long-term pattern stays unsettled after the event."},
},
WeatherStory: &forecast.WeatherStory{Raw: json.RawMessage(`{"headline":"Storm risk"}`)},
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
WeatherStory: &forecast.WeatherStory{
OfficeID: "LSX",
StartTime: mustParse("2026-05-29T06:00:00Z"),
EndTime: mustParse("2026-05-29T18:00:00Z"),
Title: "Storm Risk",
Description: "Strong storms are possible.",
AltText: "Weather story graphic showing storm risk.",
Order: 1,
},
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
}
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
@@ -80,6 +88,9 @@ func TestStormBriefingWithActiveAlert(t *testing.T) {
if pkg.Storm.WeatherStory == nil {
t.Fatal("WeatherStory = nil, want available story context")
}
if pkg.Storm.WeatherStory.Title != "Storm Risk" || pkg.Storm.WeatherStory.Description != "Strong storms are possible." {
t.Fatalf("WeatherStory = %#v, want structured story context", pkg.Storm.WeatherStory)
}
if pkg.Storm.Discussion.ShortTerm != "Short-term storm coverage peaks this morning." {
t.Fatalf("Discussion.ShortTerm = %q, want short-term AFD narrative", pkg.Storm.Discussion.ShortTerm)
}