From d3214929955b592f6242c97392f933baf2571024 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 16 Jun 2026 20:53:27 -0500 Subject: [PATCH] Move the Alert Digest into a shared partial template, and add it to the today, tomorrow, and daily reports --- docs/internal/reporttemplate.md | 7 ++-- docs/templates.md | 10 +++-- internal/reporttemplate/reporttemplate.go | 1 + .../reporttemplate/reporttemplate_test.go | 40 ++++++++++++++++--- .../reporttemplate/templates/daily.md.tmpl | 4 +- .../reporttemplate/templates/hourly.md.tmpl | 8 +--- .../templates/partials/alert_digest.md.tmpl | 6 +++ .../reporttemplate/templates/today.md.tmpl | 8 ++-- .../reporttemplate/templates/tomorrow.md.tmpl | 4 +- 9 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 internal/reporttemplate/templates/partials/alert_digest.md.tmpl diff --git a/docs/internal/reporttemplate.md b/docs/internal/reporttemplate.md index 437bb27..e3f4e1d 100644 --- a/docs/internal/reporttemplate.md +++ b/docs/internal/reporttemplate.md @@ -76,9 +76,10 @@ fail rendering instead of producing incomplete Markdown. Daily and Tomorrow call the shared `daypart_forecast` partial. Today calls `today_daypart_forecast` so it can omit elapsed or missing dayparts. Daily, -Today, Tomorrow, and Hourly call the shared `precipitation_timing` partial. -Partial files are parsed with each top-level template at render time and -receive the same typed render context as the caller. +Today, Tomorrow, and Hourly call the shared `alert_digest` and +`precipitation_timing` partials. Partial files are parsed with each top-level +template at render time and receive the same typed render context as the +caller. ## Schema Contract diff --git a/docs/templates.md b/docs/templates.md index 7cdb1c8..1c2c7eb 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -16,6 +16,7 @@ are: Shared named partials live under `internal/reporttemplate/templates/partials/`: +- `alert_digest.md.tmpl`, used by Daily, Today, Tomorrow, and Hourly - `daypart_forecast.md.tmpl`, used by Daily and Tomorrow - `today_daypart_forecast.md.tmpl`, used by Today - `precipitation_timing.md.tmpl`, used by Daily, Today, Tomorrow, and Hourly @@ -113,8 +114,8 @@ Tomorrow generated text uses the same `.GeneratedText.Summary`, Hourly. `.GeneratedText.ForecastDiscussion` is a slice of paragraphs and should be rendered with `range`. -Tomorrow uses the shared `daypart_forecast` and `precipitation_timing` -partials. +Tomorrow uses the shared `alert_digest`, `daypart_forecast`, and +`precipitation_timing` partials. Tomorrow modules include the Hourly module fields plus: @@ -144,7 +145,8 @@ Daily generated text uses `.GeneratedText.Summary`, `.GeneratedText.Confidence`. Forecast discussion is a slice of paragraphs and should be rendered with `range`. -Daily uses the shared `daypart_forecast` and `precipitation_timing` partials. +Daily uses the shared `alert_digest`, `daypart_forecast`, and +`precipitation_timing` partials. Daily uses template ID `daily`, generated-text schema ID `daily`, and prompt source `internal/reporttemplate/prompts/daily.generated_text.md`. @@ -179,7 +181,7 @@ should be rendered with `range`. Today uses the `today_daypart_forecast` partial so elapsed or missing dayparts can be omitted while Daily and Tomorrow keep their fallback row. It also uses -the shared `precipitation_timing` partial. +the shared `alert_digest` and `precipitation_timing` partials. Today uses template ID `today`, generated-text schema ID `today`, and prompt source `internal/reporttemplate/prompts/today.generated_text.md`. diff --git a/internal/reporttemplate/reporttemplate.go b/internal/reporttemplate/reporttemplate.go index d5c8d63..5f31732 100644 --- a/internal/reporttemplate/reporttemplate.go +++ b/internal/reporttemplate/reporttemplate.go @@ -19,6 +19,7 @@ var templates = map[string]string{ } var templatePartials = []string{ + "templates/partials/alert_digest.md.tmpl", "templates/partials/daypart_forecast.md.tmpl", "templates/partials/precipitation_timing.md.tmpl", "templates/partials/today_daypart_forecast.md.tmpl", diff --git a/internal/reporttemplate/reporttemplate_test.go b/internal/reporttemplate/reporttemplate_test.go index 121da19..40b3af1 100644 --- a/internal/reporttemplate/reporttemplate_test.go +++ b/internal/reporttemplate/reporttemplate_test.go @@ -12,7 +12,7 @@ func TestTemplateLookup(t *testing.T) { if err != nil { t.Fatalf("Template() error = %v", err) } - for _, want := range []string{"# {{ .Report.Title }}", "**Updated:**", "## Current Conditions", "## Hourly Forecast", "## Forecast Discussion"} { + for _, want := range []string{"# {{ .Report.Title }}", "**Updated:**", `{{ template "alert_digest" . }}`, "## Current Conditions", "## Hourly Forecast", "## Forecast Discussion"} { if !strings.Contains(source, want) { t.Fatalf("template missing %q:\n%s", want, source) } @@ -24,7 +24,7 @@ func TestTomorrowTemplateLookup(t *testing.T) { if err != nil { t.Fatalf("Template() error = %v", err) } - for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", `{{ template "daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { + for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", `{{ template "alert_digest" . }}`, `{{ template "daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { if !strings.Contains(source, want) { t.Fatalf("template missing %q:\n%s", want, source) } @@ -36,7 +36,7 @@ func TestDailyTemplateLookup(t *testing.T) { if err != nil { t.Fatalf("Template() error = %v", err) } - for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", `{{ template "daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { + for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", `{{ template "alert_digest" . }}`, `{{ template "daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { if !strings.Contains(source, want) { t.Fatalf("template missing %q:\n%s", want, source) } @@ -48,7 +48,7 @@ func TestTodayTemplateLookup(t *testing.T) { if err != nil { t.Fatalf("Template() error = %v", err) } - for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", "## Current Conditions", `{{ template "today_daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { + for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", `{{ template "alert_digest" . }}`, "## Current Conditions", `{{ template "today_daypart_forecast" . }}`, `{{ template "precipitation_timing" . }}`, "## Forecast Discussion"} { if !strings.Contains(source, want) { t.Fatalf("template missing %q:\n%s", want, source) } @@ -285,8 +285,8 @@ func TestRenderHourly(t *testing.T) { } assertOrderedText(t, text, []string{ "# Hourly Report", - "## Current Conditions", "## Active Alerts", + "## Current Conditions", "## Hourly Forecast", "## Precipitation Timing", "## Forecast Discussion", @@ -351,6 +351,9 @@ func TestRenderTomorrow(t *testing.T) { {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, + AlertDigest: &testAlertDigest{ + Relevant: []testAlert{{Event: "Wind Advisory", Headline: "Wind Advisory until 8:00 PM", Severity: "Moderate"}}, + }, }, }) if err != nil { @@ -362,6 +365,8 @@ func TestRenderTomorrow(t *testing.T) { "**Forecast date:** Monday, June 15, 2026", "**Updated:** Sunday, June 14, 2026 at 9:14 AM", "Tomorrow starts dry before showers return later in the day.", + "## Active Alerts", + "- **Wind Advisory**: Wind Advisory until 8:00 PM", "- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s.", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%.", @@ -384,6 +389,8 @@ func TestRenderTomorrow(t *testing.T) { } assertOrderedText(t, text, []string{ "# Monday's Weather", + "Tomorrow starts dry before showers return later in the day.", + "## Active Alerts", "## Daypart Forecast", "- **Overnight:**", "- **Morning:**", @@ -442,6 +449,9 @@ func TestRenderDaily(t *testing.T) { {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, + AlertDigest: &testAlertDigest{ + Relevant: []testAlert{{Event: "Flood Watch", Headline: "Flood Watch until 6:00 PM", Severity: "Moderate"}}, + }, }, }) if err != nil { @@ -453,6 +463,8 @@ func TestRenderDaily(t *testing.T) { "**Forecast date:** Monday, June 15, 2026", "**Updated:** Sunday, June 14, 2026 at 9:14 AM", "The selected day starts dry before showers return later in the day.", + "## Active Alerts", + "- **Flood Watch**: Flood Watch until 6:00 PM", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.", "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", @@ -466,6 +478,8 @@ func TestRenderDaily(t *testing.T) { } assertOrderedText(t, text, []string{ "# Monday's Weather", + "The selected day starts dry before showers return later in the day.", + "## Active Alerts", "## Daypart Forecast", "- **Morning:**", "- **Afternoon:**", @@ -535,6 +549,9 @@ func TestRenderToday(t *testing.T) { {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, + AlertDigest: &testAlertDigest{ + Relevant: []testAlert{{Event: "Wind Advisory", Headline: "Wind Advisory until 8:00 PM", Severity: "Moderate"}}, + }, TodayPlanning: &testTodayPlanning{ MorningReadiness: []string{"Morning weather looks routine."}, LateDayChangeWatch: []string{"Watch late-day shower timing."}, @@ -550,6 +567,8 @@ func TestRenderToday(t *testing.T) { "**Forecast date:** Monday, June 15, 2026", "**Updated:** Monday, June 15, 2026 at 7:14 AM", "Today starts dry before showers return later in the day.", + "## Active Alerts", + "- **Wind Advisory**: Wind Advisory until 8:00 PM", "Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.", @@ -564,6 +583,8 @@ func TestRenderToday(t *testing.T) { } assertOrderedText(t, text, []string{ "# Today's Weather", + "Today starts dry before showers return later in the day.", + "## Active Alerts", "## Current Conditions", "## Daypart Forecast", "- **Morning:**", @@ -652,6 +673,12 @@ func TestRenderDaypartTemplatesUseRichHelperFields(t *testing.T) { t.Fatalf("rendered template missing %q:\n%s", want, text) } } + if strings.Contains(text, "## Active Alerts") { + t.Fatalf("rendered template includes empty Active Alerts section:\n%s", text) + } + if strings.Contains(text, "\n\n\n") { + t.Fatalf("rendered template includes excess blank lines:\n%s", text) + } }) } } @@ -903,18 +930,21 @@ type testModules struct { type testTomorrowModules struct { Dayparts []testTomorrowDaypart PrecipTiming *testPrecipTiming + AlertDigest *testAlertDigest } type testTodayModules struct { CurrentConditions *testCurrentConditions Dayparts []testTomorrowDaypart PrecipTiming *testPrecipTiming + AlertDigest *testAlertDigest TodayPlanning *testTodayPlanning } type testDailyModules struct { Dayparts []testDailyDaypart PrecipTiming *testPrecipTiming + AlertDigest *testAlertDigest } type testTodayPlanning struct { diff --git a/internal/reporttemplate/templates/daily.md.tmpl b/internal/reporttemplate/templates/daily.md.tmpl index 46a5bc4..053b555 100644 --- a/internal/reporttemplate/templates/daily.md.tmpl +++ b/internal/reporttemplate/templates/daily.md.tmpl @@ -5,9 +5,7 @@ {{ .GeneratedText.Summary }} -{{ template "daypart_forecast" . }} - -{{ template "precipitation_timing" . }} +{{ template "alert_digest" . }}{{ template "daypart_forecast" . }}{{ if and .Modules.PrecipTiming .Modules.PrecipTiming.PrecipitationWindows }}{{ template "precipitation_timing" . }}{{ else }}{{ end -}} ## Forecast Discussion {{ range .GeneratedText.ForecastDiscussion }} {{ . }} diff --git a/internal/reporttemplate/templates/hourly.md.tmpl b/internal/reporttemplate/templates/hourly.md.tmpl index c5ed0ad..bed2837 100644 --- a/internal/reporttemplate/templates/hourly.md.tmpl +++ b/internal/reporttemplate/templates/hourly.md.tmpl @@ -4,16 +4,10 @@ {{ .GeneratedText.Summary }} -## Current Conditions +{{ template "alert_digest" . }}## Current Conditions {{ with .Modules.CurrentConditions }}{{ with .TemperatureF }}Currently, it is {{ . }}°F{{ with $.Modules.CurrentConditions.ConditionTextLower }} and {{ . }}{{ end }}.{{ else }}{{ with .ConditionText }}Currently, it is {{ . }}.{{ else }}Current conditions are unavailable.{{ end }}{{ end }}{{ with .ApparentTemperatureF }} It feels like {{ . }}°F{{ with $.Modules.CurrentConditions.RelativeHumidityPercent }}, with a relative humidity of {{ . }}%{{ end }}{{ with $.Modules.CurrentConditions.WindDirectionText }} and winds from the {{ . }}{{ with $.Modules.CurrentConditions.WindSpeedMph }} at {{ . }} mph{{ end }}{{ end }}.{{ else }}{{ with .RelativeHumidityPercent }} Relative humidity is {{ . }}%.{{ end }}{{ with .WindDirectionText }} Winds are from the {{ . }}{{ with $.Modules.CurrentConditions.WindSpeedMph }} at {{ . }} mph{{ end }}.{{ end }}{{ end }}{{ else }}Current conditions are unavailable.{{ end }} -{{ if and .Modules.AlertDigest .Modules.AlertDigest.Relevant }}## Active Alerts -{{ range .Modules.AlertDigest.Relevant -}} -- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**{{ with .Headline }}: {{ . }}{{ end }} -{{ end }} - -{{ end -}} ## 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 }} diff --git a/internal/reporttemplate/templates/partials/alert_digest.md.tmpl b/internal/reporttemplate/templates/partials/alert_digest.md.tmpl new file mode 100644 index 0000000..bd0423e --- /dev/null +++ b/internal/reporttemplate/templates/partials/alert_digest.md.tmpl @@ -0,0 +1,6 @@ +{{ define "alert_digest" }}{{ if and .Modules.AlertDigest .Modules.AlertDigest.Relevant }}## Active Alerts +{{ range .Modules.AlertDigest.Relevant -}} +- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**{{ with .Headline }}: {{ . }}{{ end }} +{{ end }} + +{{ end }}{{ end }} diff --git a/internal/reporttemplate/templates/today.md.tmpl b/internal/reporttemplate/templates/today.md.tmpl index 19365dd..dd8b9d6 100644 --- a/internal/reporttemplate/templates/today.md.tmpl +++ b/internal/reporttemplate/templates/today.md.tmpl @@ -5,14 +5,12 @@ {{ .GeneratedText.Summary }} -{{ with .Modules.CurrentConditions }} +{{ template "alert_digest" . }}{{ with .Modules.CurrentConditions }} ## Current Conditions {{ with .TemperatureF }}Currently, it is {{ . }}°F{{ with $.Modules.CurrentConditions.ConditionTextLower }} and {{ . }}{{ end }}.{{ else }}{{ with .ConditionText }}Currently, it is {{ . }}.{{ else }}Current conditions are unavailable.{{ end }}{{ end }}{{ with .ApparentTemperatureF }} It feels like {{ . }}°F{{ with $.Modules.CurrentConditions.RelativeHumidityPercent }}, with a relative humidity of {{ . }}%{{ end }}{{ with $.Modules.CurrentConditions.WindDirectionText }} and winds from the {{ . }}{{ with $.Modules.CurrentConditions.WindSpeedMph }} at {{ . }} mph{{ end }}{{ end }}.{{ else }}{{ with .RelativeHumidityPercent }} Relative humidity is {{ . }}%.{{ end }}{{ with .WindDirectionText }} Winds are from the {{ . }}{{ with $.Modules.CurrentConditions.WindSpeedMph }} at {{ . }} mph{{ end }}.{{ end }}{{ end }} -{{ end }} -{{ template "today_daypart_forecast" . }} - -{{ template "precipitation_timing" . }} +{{ end -}} +{{ template "today_daypart_forecast" . }}{{ if and .Modules.PrecipTiming .Modules.PrecipTiming.PrecipitationWindows }}{{ template "precipitation_timing" . }}{{ else }}{{ end -}} ## Forecast Discussion {{ range .GeneratedText.ForecastDiscussion }} {{ . }} diff --git a/internal/reporttemplate/templates/tomorrow.md.tmpl b/internal/reporttemplate/templates/tomorrow.md.tmpl index 46a5bc4..053b555 100644 --- a/internal/reporttemplate/templates/tomorrow.md.tmpl +++ b/internal/reporttemplate/templates/tomorrow.md.tmpl @@ -5,9 +5,7 @@ {{ .GeneratedText.Summary }} -{{ template "daypart_forecast" . }} - -{{ template "precipitation_timing" . }} +{{ template "alert_digest" . }}{{ template "daypart_forecast" . }}{{ if and .Modules.PrecipTiming .Modules.PrecipTiming.PrecipitationWindows }}{{ template "precipitation_timing" . }}{{ else }}{{ end -}} ## Forecast Discussion {{ range .GeneratedText.ForecastDiscussion }} {{ . }}