Move the Alert Digest into a shared partial template, and add it to the today, tomorrow, and daily reports

This commit is contained in:
2026-06-16 20:53:27 -05:00
parent b57110e5c8
commit d321492995
9 changed files with 58 additions and 30 deletions

View File

@@ -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

View File

@@ -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`.

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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 }}
{{ . }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}
{{ . }}

View File

@@ -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 }}
{{ . }}