Update the Alert Digest partial template to be more concise
This commit is contained in:
@@ -81,7 +81,10 @@ Today, Tomorrow, and Hourly call the shared `alert_digest` and
|
||||
template at render time and receive the same typed render context as the
|
||||
caller. The `alert_digest` partial renders the combined Alerts and Risk
|
||||
Products section from relevant NWS alerts and curated SPC outlook digest
|
||||
records.
|
||||
records. Rendered NWS alert bullets include alert identity and timing but omit
|
||||
instruction and description text. Rendered SPC outlook bullets start at
|
||||
Enhanced Risk; lower-risk SPC entries may still exist in module snapshots and
|
||||
data packages.
|
||||
|
||||
## Schema Contract
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
"# Hourly Report",
|
||||
"Storm chances increase through late morning.",
|
||||
"## Alert Digest",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 11:00 AM to May 29 at 3:00 PM. Avoid low-water crossings.",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 11:00 AM to May 29 at 3:00 PM.",
|
||||
"## Precipitation Timing",
|
||||
"A cold front is moving into the region.",
|
||||
"A front will keep the region unsettled.",
|
||||
@@ -770,6 +770,9 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
t.Fatalf("rendered hourly report missing %q:\n%s", want, reportText)
|
||||
}
|
||||
}
|
||||
if strings.Contains(reportText, "Avoid low-water crossings.") {
|
||||
t.Fatalf("rendered hourly report includes alert instruction:\n%s", reportText)
|
||||
}
|
||||
if result.OutputPath != result.ReportPath {
|
||||
t.Fatalf("OutputPath = %q, want managed report path %q", result.OutputPath, result.ReportPath)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
||||
"Storm chances increase through late morning.",
|
||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM. Avoid low-water crossings.",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
||||
"A cold front is moving into the region.",
|
||||
"A front will keep the region unsettled.",
|
||||
} {
|
||||
@@ -99,6 +99,9 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
if strings.Contains(text, "Avoid low-water crossings.") {
|
||||
t.Fatalf("rendered template includes alert instruction:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
|
||||
@@ -61,7 +61,7 @@ func Render(id string, data any) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpl, err := template.New(id).Option("missingkey=error").Parse(source)
|
||||
tmpl, err := template.New(id).Funcs(templateFuncs()).Option("missingkey=error").Parse(source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse report template %q: %w", id, err)
|
||||
}
|
||||
|
||||
@@ -270,8 +270,7 @@ func TestRenderHourly(t *testing.T) {
|
||||
"Storm chances increase through late morning.",
|
||||
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph.",
|
||||
"## Alert Digest",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM. Avoid low-water crossings.",
|
||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from May 29 at 8:00 AM to May 29 at 2:00 PM.",
|
||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
||||
"- **9:00 AM:** 74°F and cloudy.",
|
||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
||||
"- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM.",
|
||||
@@ -285,6 +284,11 @@ func TestRenderHourly(t *testing.T) {
|
||||
if strings.Contains(text, "19%") || strings.Contains(text, "wind S") || strings.Contains(text, "## Confidence") {
|
||||
t.Fatalf("rendered template included omitted details:\n%s", text)
|
||||
}
|
||||
for _, unwanted := range []string{"Avoid low-water crossings.", "Slight risk for severe thunderstorms"} {
|
||||
if strings.Contains(text, unwanted) {
|
||||
t.Fatalf("rendered template included %q:\n%s", unwanted, text)
|
||||
}
|
||||
}
|
||||
assertOrderedText(t, text, []string{
|
||||
"# Hourly Report",
|
||||
"## Alert Digest",
|
||||
@@ -371,8 +375,7 @@ func TestRenderTomorrow(t *testing.T) {
|
||||
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
||||
"Tomorrow starts dry before showers return later in the day.",
|
||||
"## Alert Digest",
|
||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects.",
|
||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 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%.",
|
||||
@@ -388,6 +391,8 @@ func TestRenderTomorrow(t *testing.T) {
|
||||
for _, unwanted := range []string{
|
||||
"upper 50s.\n\n- **Morning:**",
|
||||
"upper 60s.\n\n- **Afternoon:**",
|
||||
"Secure outdoor objects.",
|
||||
"Slight risk for severe thunderstorms",
|
||||
} {
|
||||
if strings.Contains(text, unwanted) {
|
||||
t.Fatalf("rendered template includes blank lines between daypart bullets:\n%s", text)
|
||||
@@ -473,7 +478,7 @@ func TestRenderDaily(t *testing.T) {
|
||||
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
||||
"The selected day starts dry before showers return later in the day.",
|
||||
"## Alert Digest",
|
||||
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM. Monitor creek levels.",
|
||||
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM.",
|
||||
"- **SPC Convective Outlook**: Enhanced risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||
"- **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%.",
|
||||
@@ -486,6 +491,9 @@ func TestRenderDaily(t *testing.T) {
|
||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
if strings.Contains(text, "Monitor creek levels.") {
|
||||
t.Fatalf("rendered template included alert description:\n%s", text)
|
||||
}
|
||||
assertOrderedText(t, text, []string{
|
||||
"# Monday's Weather",
|
||||
"The selected day starts dry before showers return later in the day.",
|
||||
@@ -581,8 +589,7 @@ func TestRenderToday(t *testing.T) {
|
||||
"**Updated:** Monday, June 15, 2026 at 7:14 AM",
|
||||
"Today starts dry before showers return later in the day.",
|
||||
"## Alert Digest",
|
||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects.",
|
||||
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 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%.",
|
||||
@@ -606,7 +613,7 @@ func TestRenderToday(t *testing.T) {
|
||||
"## Precipitation Timing",
|
||||
"## Forecast Discussion",
|
||||
})
|
||||
for _, unwanted := range []string{"## Planning Notes", "Morning weather looks routine.", "Watch late-day shower timing.", "- **Evening:**", "Forecast details are limited"} {
|
||||
for _, unwanted := range []string{"## Planning Notes", "Morning weather looks routine.", "Watch late-day shower timing.", "- **Evening:**", "Forecast details are limited", "Secure outdoor objects.", "Slight risk for severe thunderstorms"} {
|
||||
if strings.Contains(text, unwanted) {
|
||||
t.Fatalf("rendered template included %q:\n%s", unwanted, text)
|
||||
}
|
||||
@@ -736,6 +743,73 @@ func TestRenderTomorrowOmitsPrecipitationTimingWithoutWindows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertDigestOmitsBelowThresholdSPCRiskOnlySection(t *testing.T) {
|
||||
rendered, err := Render("hourly", testRenderContext{
|
||||
Report: testReportContext{Title: "Hourly Report"},
|
||||
GeneratedText: testGeneratedText{
|
||||
Summary: "Storm chances remain low.",
|
||||
ForecastDiscussion: "Only isolated severe storms are possible.",
|
||||
},
|
||||
Modules: testModules{
|
||||
CurrentConditions: &testCurrentConditions{},
|
||||
HourlyForecast: &testHourlyForecast{},
|
||||
SPCConvectiveOutlooks: &testSPCOutlooks{
|
||||
RiskDigest: []testSPCRiskDigest{{LabelText: "Slight Risk", RiskLabel: "Slight risk", PeriodBegins: "June 15 at 7:00 AM", PeriodEnds: "June 16 at 7:00 AM"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
}
|
||||
text := string(rendered)
|
||||
for _, unwanted := range []string{"## Alert Digest", "SPC Convective Outlook", "Slight risk for severe thunderstorms"} {
|
||||
if strings.Contains(text, unwanted) {
|
||||
t.Fatalf("rendered template included %q for below-threshold SPC-only digest:\n%s", unwanted, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertDigestRendersEnhancedOrHigherSPCRiskWithoutAlerts(t *testing.T) {
|
||||
tests := []struct {
|
||||
label string
|
||||
risk string
|
||||
}{
|
||||
{label: "Enhanced Risk", risk: "Enhanced risk"},
|
||||
{label: "Moderate Risk", risk: "Moderate risk"},
|
||||
{label: "High Risk", risk: "High risk"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.label, func(t *testing.T) {
|
||||
rendered, err := Render("hourly", testRenderContext{
|
||||
Report: testReportContext{Title: "Hourly Report"},
|
||||
GeneratedText: testGeneratedText{
|
||||
Summary: "Severe storms are possible.",
|
||||
ForecastDiscussion: "SPC outlooks highlight the risk.",
|
||||
},
|
||||
Modules: testModules{
|
||||
CurrentConditions: &testCurrentConditions{},
|
||||
HourlyForecast: &testHourlyForecast{},
|
||||
SPCConvectiveOutlooks: &testSPCOutlooks{
|
||||
RiskDigest: []testSPCRiskDigest{{LabelText: tt.label, RiskLabel: tt.risk, PeriodBegins: "June 15 at 7:00 AM", PeriodEnds: "June 16 at 7:00 AM"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
}
|
||||
text := string(rendered)
|
||||
for _, want := range []string{
|
||||
"## Alert Digest",
|
||||
"- **SPC Convective Outlook**: " + tt.risk + " for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM.",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
|
||||
rendered, err := Render("hourly", testRenderContext{
|
||||
Report: testReportContext{
|
||||
|
||||
76
internal/reporttemplate/template_funcs.go
Normal file
76
internal/reporttemplate/template_funcs.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package reporttemplate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func templateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"hasRelevantAlerts": hasRelevantAlerts,
|
||||
"hasEnhancedOrHigherSPCRisk": hasEnhancedOrHigherSPCRisk,
|
||||
"isEnhancedOrHigherSPCRisk": isEnhancedOrHigherSPCRisk,
|
||||
}
|
||||
}
|
||||
|
||||
func hasRelevantAlerts(alertDigest any) bool {
|
||||
value := dereferenceValue(reflect.ValueOf(alertDigest))
|
||||
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
relevant := value.FieldByName("Relevant")
|
||||
return relevant.IsValid() && relevant.Kind() == reflect.Slice && relevant.Len() > 0
|
||||
}
|
||||
|
||||
func hasEnhancedOrHigherSPCRisk(outlooks any) bool {
|
||||
value := dereferenceValue(reflect.ValueOf(outlooks))
|
||||
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
riskDigest := value.FieldByName("RiskDigest")
|
||||
if !riskDigest.IsValid() || riskDigest.Kind() != reflect.Slice {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < riskDigest.Len(); i++ {
|
||||
if isEnhancedOrHigherSPCRisk(riskDigest.Index(i).Interface()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEnhancedOrHigherSPCRisk(risk any) bool {
|
||||
value := dereferenceValue(reflect.ValueOf(risk))
|
||||
if !value.IsValid() || value.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
label := stringField(value, "LabelText")
|
||||
if label == "" {
|
||||
label = stringField(value, "RiskLabel")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(label)) {
|
||||
case "enhanced risk", "moderate risk", "high risk":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func dereferenceValue(value reflect.Value) reflect.Value {
|
||||
for value.IsValid() && (value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer) {
|
||||
if value.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func stringField(value reflect.Value, name string) string {
|
||||
field := value.FieldByName(name)
|
||||
if !field.IsValid() || field.Kind() != reflect.String {
|
||||
return ""
|
||||
}
|
||||
return field.String()
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{{ define "alert_digest" }}{{ if or (and .Modules.AlertDigest .Modules.AlertDigest.Relevant) (and .Modules.SPCConvectiveOutlooks .Modules.SPCConvectiveOutlooks.RiskDigest) }}## Alert Digest
|
||||
{{ define "alert_digest" }}{{ $hasAlerts := hasRelevantAlerts .Modules.AlertDigest }}{{ $hasSPC := hasEnhancedOrHigherSPCRisk .Modules.SPCConvectiveOutlooks }}{{ if or $hasAlerts $hasSPC }}## Alert Digest
|
||||
{{ with .Modules.AlertDigest }}{{ range .Relevant -}}
|
||||
- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**: {{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }} in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.{{ with .Instruction }} {{ . }}{{ else }}{{ with .Description }} {{ . }}{{ end }}{{ end }}
|
||||
{{ end }}{{ end }}{{ with .Modules.SPCConvectiveOutlooks }}{{ range .RiskDigest -}}
|
||||
- **{{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}**: {{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }} in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.
|
||||
{{ end }}{{ end }}{{ with .Modules.SPCConvectiveOutlooks }}{{ range .RiskDigest }}{{ if isEnhancedOrHigherSPCRisk . -}}
|
||||
- **SPC Convective Outlook**: {{ with .RiskLabel }}{{ . }}{{ else }}Convective risk{{ end }} for severe thunderstorms in effect{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}.
|
||||
{{ end }}{{ end }}
|
||||
{{ end }}{{ end }}{{ end }}
|
||||
|
||||
{{ end }}{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user