diff --git a/docs/config.md b/docs/config.md index f631735..94e7506 100644 --- a/docs/config.md +++ b/docs/config.md @@ -201,6 +201,7 @@ reports: options: sections: - short_term + - hourly_forecast ``` Unknown reports, unknown modules, duplicate modules, incompatible report/module diff --git a/docs/internal/briefing.md b/docs/internal/briefing.md index 11dbee5..d18e025 100644 --- a/docs/internal/briefing.md +++ b/docs/internal/briefing.md @@ -27,8 +27,8 @@ Outputs: - `ModuleDefinition` values with module ID, stanza name, option type, supported reports, fact requirements, missing-data behavior, and builder - `module.Output` values for source-oriented stanzas: - `metadata`, `current_conditions`, `narrative_forecast`, `alert_digest`, - `area_forecast_discussion`, and `weather_story` + `metadata`, `current_conditions`, `narrative_forecast`, `hourly_forecast`, + `alert_digest`, `area_forecast_discussion`, and `weather_story` - `module.Output` values for derived stanzas: `derived_daily_summary`, `derived_daypart_summaries`, `precip_timing`, `outdoor_windows`, and `tomorrow_planning` diff --git a/docs/internal/module.md b/docs/internal/module.md index 4335fb9..4e02769 100644 --- a/docs/internal/module.md +++ b/docs/internal/module.md @@ -32,6 +32,7 @@ The registry recognizes these IDs: - `metadata` - `current_conditions` - `narrative_forecast` +- `hourly_forecast` - `derived_daily_summary` - `derived_daypart_summaries` - `precip_timing` diff --git a/docs/internal/prompt-input.md b/docs/internal/prompt-input.md index 3fcc84b..552aa14 100644 --- a/docs/internal/prompt-input.md +++ b/docs/internal/prompt-input.md @@ -40,6 +40,7 @@ briefing: metadata: {} current_conditions: {} narrative_forecast: {} + hourly_forecast: {} recent_changes: items: [] ``` diff --git a/examples/config.yml b/examples/config.yml index f1697d1..8812581 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -86,3 +86,4 @@ reports: - long_term - weather_story - outdoor_windows + - hourly_forecast diff --git a/internal/app/app_test.go b/internal/app/app_test.go index f65a28d..456f100 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -162,6 +162,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { !strings.Contains(string(data), "recent_changes:") || !strings.Contains(string(data), "current_conditions:") || !strings.Contains(string(data), "narrative_forecast:") || + !strings.Contains(string(data), "hourly_forecast:") || !strings.Contains(string(data), "area_forecast_discussion:") { t.Fatalf("data package missing expected content:\n%s", string(data)) } @@ -171,8 +172,10 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { currentIndex := strings.Index(string(data), " current_conditions:") narrativeIndex := strings.Index(string(data), " narrative_forecast:") summaryIndex := strings.Index(string(data), " derived_daily_summary:") - if currentIndex < 0 || narrativeIndex < 0 || summaryIndex < 0 || !(currentIndex < narrativeIndex && narrativeIndex < summaryIndex) { - t.Fatalf("data package stanza order is wrong, want current_conditions then narrative_forecast then derived_daily_summary:\n%s", string(data)) + outdoorIndex := strings.Index(string(data), " outdoor_windows:") + hourlyIndex := strings.Index(string(data), " hourly_forecast:") + if currentIndex < 0 || narrativeIndex < 0 || summaryIndex < 0 || outdoorIndex < 0 || hourlyIndex < 0 || !(currentIndex < narrativeIndex && narrativeIndex < summaryIndex && outdoorIndex < hourlyIndex) { + t.Fatalf("data package stanza order is wrong, want current_conditions then narrative_forecast then derived_daily_summary and final hourly_forecast:\n%s", string(data)) } savedDataPackage, err := promptinput.LoadYAML(data) if err != nil { @@ -192,6 +195,10 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { if !ok || narrative["product"] != "narrative" || !strings.Contains(string(data), "Morning storms, then partly sunny.") { t.Fatalf("data package narrative forecast = %#v, want narrative forecast", savedDataPackage.Briefing.Values["narrative_forecast"]) } + hourly, ok := savedDataPackage.Briefing.Values["hourly_forecast"].(map[string]any) + if !ok || hourly["product"] != "hourly" || !strings.Contains(string(data), "Showers and thunderstorms") { + t.Fatalf("data package hourly forecast = %#v, want hourly forecast", savedDataPackage.Briefing.Values["hourly_forecast"]) + } story, ok := savedDataPackage.Briefing.Values["weather_story"].(map[string]any) if !ok || story["title"] != "Several Chances for Rain Through Monday" { t.Fatalf("data package weather story = %#v, want weather story title", savedDataPackage.Briefing.Values["weather_story"]) diff --git a/internal/briefing/base_modules_test.go b/internal/briefing/base_modules_test.go index 41aa93c..055db27 100644 --- a/internal/briefing/base_modules_test.go +++ b/internal/briefing/base_modules_test.go @@ -25,6 +25,7 @@ func TestBaseModulesBuildAvailableSourceOutputs(t *testing.T) { {id: module.Metadata, stanza: "metadata"}, {id: module.CurrentConditions, stanza: "current_conditions"}, {id: module.NarrativeForecast, stanza: "narrative_forecast"}, + {id: module.HourlyForecast, stanza: "hourly_forecast"}, {id: module.AlertDigest, stanza: "alert_digest"}, {id: module.AreaForecastDiscussion, stanza: "area_forecast_discussion"}, {id: module.WeatherStory, stanza: "weather_story"}, @@ -45,6 +46,51 @@ func TestBaseModulesBuildAvailableSourceOutputs(t *testing.T) { } } +func TestHourlyForecastModuleUsesValidPeriodHourlyPeriods(t *testing.T) { + registry := MustDefaultModuleRegistry() + ctx := testModuleContext() + + output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.HourlyForecast}) + if err != nil { + t.Fatalf("BuildModule() error = %v", err) + } + value := moduleValue[HourlyForecastModule](t, output) + if value.Product != "hourly" || value.SourceLocationID != "test-grid" || len(value.Periods) != 1 { + t.Fatalf("HourlyForecast = %#v, want hourly metadata and one valid-period period", value) + } + period := value.Periods[0] + if period.TextDescription != "Showers likely." || period.TemperatureF == nil || *period.TemperatureF != 76 { + t.Fatalf("HourlyForecast period = %#v, want hourly period facts", period) + } + if period.WindDirection != "S" || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 70 { + t.Fatalf("HourlyForecast period = %#v, want compass wind and precip chance", period) + } + data, err := json.Marshal(output.Value) + if err != nil { + t.Fatalf("Marshal hourly forecast: %v", err) + } + jsonText := string(data) + for _, field := range []string{"source_location_id", "text_description", "temperature_f", "wind_direction", "probability_of_precipitation_percent", "relative_humidity_percent"} { + if !strings.Contains(jsonText, field) { + t.Fatalf("hourly json = %s, want field %s", jsonText, field) + } + } + if strings.Contains(jsonText, "wind_direction_degrees") || strings.Contains(jsonText, "Tomorrow") { + t.Fatalf("hourly json = %s, want valid-period prompt fields only", jsonText) + } +} + +func TestHourlyForecastModuleRejectsUnsupportedReports(t *testing.T) { + registry := MustDefaultModuleRegistry() + ctx := testModuleContext() + ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.Weekend) + + _, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.HourlyForecast}) + if err == nil || !strings.Contains(err.Error(), `module "hourly_forecast" is not compatible with report "weekend"`) { + t.Fatalf("BuildModule() error = %v, want incompatible report", err) + } +} + func TestNarrativeForecastModuleUsesValidPeriodNarrativePeriods(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := testModuleContext() @@ -189,11 +235,13 @@ func TestBaseModulesOmitMissingOptionalOutputs(t *testing.T) { ctx := testModuleContext() ctx.Collected.Current = nil ctx.Collected.Narrative = nil + ctx.Collected.Hourly = nil ctx.Derived.ValidPeriodNarrativePeriods = nil + ctx.Derived.ValidPeriodHourlyPeriods = nil ctx.Collected.Discussion = nil ctx.Collected.WeatherStory = nil - for _, id := range []module.ID{module.CurrentConditions, module.NarrativeForecast, module.AreaForecastDiscussion, module.WeatherStory} { + for _, id := range []module.ID{module.CurrentConditions, module.NarrativeForecast, module.HourlyForecast, module.AreaForecastDiscussion, module.WeatherStory} { output, err := registry.BuildModule(ctx, module.ConfigItem{ID: id}) if err != nil { t.Fatalf("BuildModule(%s) error = %v", id, err) @@ -276,6 +324,10 @@ func testModuleContext() ModuleContext { narrativePop := 60.0 narrativeWind := 12.0 narrativeWindDirection := 45.0 + hourlyTempF := 76.0 + hourlyPop := 70.0 + hourlyHumidity := 66.0 + hourlyWindMph := 14.0 updatedAt := mustParseModuleTime("2026-05-29T07:30:00-05:00") return ModuleContext{ Resolved: resolved, @@ -309,6 +361,30 @@ func testModuleContext() ModuleContext { }, }, }, + Hourly: &weatherdata.ForecastRun{ + LocationID: "test-grid", + LocationName: "Testville", + IssuedAt: mustParseModuleTime("2026-05-29T10:30:00-05:00"), + UpdatedAt: &updatedAt, + Product: "hourly", + Periods: []weatherdata.ForecastPeriod{ + { + StartTime: mustParseModuleTime("2026-05-29T08:00:00-05:00"), + EndTime: mustParseModuleTime("2026-05-29T09:00:00-05:00"), + TextDescription: "Showers likely.", + TemperatureF: &hourlyTempF, + WindSpeedMph: &hourlyWindMph, + WindDirectionDegrees: &windDirection, + ProbabilityOfPrecipitationPercent: &hourlyPop, + RelativeHumidityPercent: &hourlyHumidity, + }, + { + StartTime: mustParseModuleTime("2026-05-30T08:00:00-05:00"), + EndTime: mustParseModuleTime("2026-05-30T09:00:00-05:00"), + TextDescription: "Tomorrow showers.", + }, + }, + }, Alerts: &weatherdata.AlertRun{Alerts: []json.RawMessage{ json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate"}`), }}, @@ -341,6 +417,18 @@ func testModuleContext() ModuleContext { }}, }, Derived: facts.DerivedFacts{ + ValidPeriodHourlyPeriods: []weatherdata.ForecastPeriod{ + { + StartTime: mustParseModuleTime("2026-05-29T08:00:00-05:00"), + EndTime: mustParseModuleTime("2026-05-29T09:00:00-05:00"), + TextDescription: "Showers likely.", + TemperatureF: &hourlyTempF, + WindSpeedMph: &hourlyWindMph, + WindDirectionDegrees: &windDirection, + ProbabilityOfPrecipitationPercent: &hourlyPop, + RelativeHumidityPercent: &hourlyHumidity, + }, + }, ValidPeriodNarrativePeriods: []weatherdata.ForecastPeriod{ { Name: "Today", diff --git a/internal/briefing/derived_daily_summary_module.go b/internal/briefing/derived_daily_summary_module.go index ee295fb..d00fe71 100644 --- a/internal/briefing/derived_daily_summary_module.go +++ b/internal/briefing/derived_daily_summary_module.go @@ -85,10 +85,7 @@ func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.Pre if maxGust != nil { value.MaxWindGustMph = roundedInt(&maxGust.Value) } - value.DominantConditions = narrativeConditions(summary.NarrativePeriods) - if len(value.DominantConditions) == 0 { - value.DominantConditions = sortedSet(conditions) - } + value.DominantConditions = sortedSet(conditions) value.Hazards = sortedSet(hazards) return value, nil } @@ -144,22 +141,6 @@ func narrativeMaxPrecipitation(periods []weatherdata.ForecastPeriod) *forecast.T return maxPop } -func narrativeConditions(periods []weatherdata.ForecastPeriod) []string { - seen := map[string]struct{}{} - var out []string - for _, period := range periods { - if period.TextDescription == "" { - continue - } - if _, ok := seen[period.TextDescription]; ok { - continue - } - seen[period.TextDescription] = struct{}{} - out = append(out, period.TextDescription) - } - return out -} - func mostLikelyPrecipitationHour(maxPop *forecast.TimedValue, timezone string) string { if maxPop == nil || maxPop.Value <= 0 { return "" diff --git a/internal/briefing/derived_modules_test.go b/internal/briefing/derived_modules_test.go index 5e76dc4..afa75d3 100644 --- a/internal/briefing/derived_modules_test.go +++ b/internal/briefing/derived_modules_test.go @@ -33,8 +33,8 @@ func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) { if value.MostLikelyPrecipitationHour != "80% at 12 PM" || !value.ThunderMentioned { t.Fatalf("precip timing = %#v, want most likely hour and thunder", value) } - if strings.Join(value.DominantConditions, "|") != "Morning storms, then partly sunny.|Clouds linger tonight." { - t.Fatalf("DominantConditions = %#v, want ordered narrative conditions", value.DominantConditions) + if !containsString(value.DominantConditions, "Thunderstorms with gusty wind") || containsString(value.DominantConditions, "Morning storms, then partly sunny.") { + t.Fatalf("DominantConditions = %#v, want daypart conditions rather than narrative conditions", value.DominantConditions) } if value.MaxWindGustMph == nil || *value.MaxWindGustMph != 42 { t.Fatalf("MaxWindGustMph = %#v, want 42", value.MaxWindGustMph) diff --git a/internal/briefing/hourly_forecast_module.go b/internal/briefing/hourly_forecast_module.go new file mode 100644 index 0000000..19f9fe1 --- /dev/null +++ b/internal/briefing/hourly_forecast_module.go @@ -0,0 +1,123 @@ +package briefing + +import ( + "time" + + "gitea.maximumdirect.net/eric/weatherreporter/internal/module" + "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" +) + +type HourlyForecastModule struct { + Product string `json:"product,omitempty"` + IssuedAt time.Time `json:"issued_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + SourceLocation string `json:"source_location,omitempty"` + SourceLocationID string `json:"source_location_id,omitempty"` + Periods []HourlyForecastPeriod `json:"periods,omitempty"` +} + +type HourlyForecastPeriod struct { + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Name string `json:"name,omitempty"` + IsDay *bool `json:"is_day,omitempty"` + ConditionCode *int `json:"condition_code,omitempty"` + TextDescription string `json:"text_description,omitempty"` + TemperatureC *float64 `json:"temperature_c,omitempty"` + TemperatureF *float64 `json:"temperature_f,omitempty"` + TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` + TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` + TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` + TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` + DewpointC *float64 `json:"dewpoint_c,omitempty"` + DewpointF *float64 `json:"dewpoint_f,omitempty"` + WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` + WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` + WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` + WindGustMph *float64 `json:"wind_gust_mph,omitempty"` + WindDirection string `json:"wind_direction,omitempty"` + BarometricPressurePa *float64 `json:"barometric_pressure_pa,omitempty"` + BarometricPressureInHg *float64 `json:"barometric_pressure_in_hg,omitempty"` + VisibilityMeters *float64 `json:"visibility_meters,omitempty"` + VisibilityMiles *float64 `json:"visibility_miles,omitempty"` + ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` + ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` + CloudCoverPercent *float64 `json:"cloud_cover_percent,omitempty"` + ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` + PrecipitationAmountMm *float64 `json:"precipitation_amount_mm,omitempty"` + PrecipitationAmountIn *float64 `json:"precipitation_amount_in,omitempty"` + SnowfallDepthMM *float64 `json:"snowfall_depth_mm,omitempty"` + SnowfallDepthIn *float64 `json:"snowfall_depth_in,omitempty"` + UVIndex *float64 `json:"uv_index,omitempty"` + RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` +} + +func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error) { + hourly := ctx.Collected.Hourly + if hourly == nil || len(ctx.Derived.ValidPeriodHourlyPeriods) == 0 { + return nil, nil + } + value := HourlyForecastModule{ + Product: hourly.Product, + IssuedAt: hourly.IssuedAt, + UpdatedAt: copyTime(hourly.UpdatedAt), + SourceLocation: hourly.LocationName, + SourceLocationID: hourly.LocationID, + Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods), + } + if value.isEmpty() { + return nil, nil + } + return &module.Output{ID: module.HourlyForecast, StanzaName: "hourly_forecast", Value: value}, nil +} + +func hourlyForecastPeriods(periods []weatherdata.ForecastPeriod) []HourlyForecastPeriod { + out := make([]HourlyForecastPeriod, 0, len(periods)) + for _, period := range periods { + out = append(out, HourlyForecastPeriod{ + StartTime: period.StartTime, + EndTime: period.EndTime, + Name: period.Name, + IsDay: copyBool(period.IsDay), + ConditionCode: copyInt(period.ConditionCode), + TextDescription: period.TextDescription, + TemperatureC: copyFloat(period.TemperatureC), + TemperatureF: copyFloat(period.TemperatureF), + TemperatureCMin: copyFloat(period.TemperatureCMin), + TemperatureFMin: copyFloat(period.TemperatureFMin), + TemperatureCMax: copyFloat(period.TemperatureCMax), + TemperatureFMax: copyFloat(period.TemperatureFMax), + DewpointC: copyFloat(period.DewpointC), + DewpointF: copyFloat(period.DewpointF), + WindSpeedKmh: copyFloat(period.WindSpeedKmh), + WindSpeedMph: copyFloat(period.WindSpeedMph), + WindGustKmh: copyFloat(period.WindGustKmh), + WindGustMph: copyFloat(period.WindGustMph), + WindDirection: windDirectionLabel(period.WindDirectionDegrees), + BarometricPressurePa: copyFloat(period.BarometricPressurePa), + BarometricPressureInHg: copyFloat(period.BarometricPressureInHg), + VisibilityMeters: copyFloat(period.VisibilityMeters), + VisibilityMiles: copyFloat(period.VisibilityMiles), + ApparentTemperatureC: copyFloat(period.ApparentTemperatureC), + ApparentTemperatureF: copyFloat(period.ApparentTemperatureF), + CloudCoverPercent: copyFloat(period.CloudCoverPercent), + ProbabilityOfPrecipitationPercent: copyFloat(period.ProbabilityOfPrecipitationPercent), + PrecipitationAmountMm: copyFloat(period.PrecipitationAmountMm), + PrecipitationAmountIn: copyFloat(period.PrecipitationAmountIn), + SnowfallDepthMM: copyFloat(period.SnowfallDepthMM), + SnowfallDepthIn: copyFloat(period.SnowfallDepthIn), + UVIndex: copyFloat(period.UVIndex), + RelativeHumidityPercent: copyFloat(period.RelativeHumidityPercent), + }) + } + return out +} + +func (v HourlyForecastModule) isEmpty() bool { + return v.Product == "" && + v.IssuedAt.IsZero() && + v.UpdatedAt == nil && + v.SourceLocation == "" && + v.SourceLocationID == "" && + len(v.Periods) == 0 +} diff --git a/internal/briefing/modules.go b/internal/briefing/modules.go index e94d5e3..11747f0 100644 --- a/internal/briefing/modules.go +++ b/internal/briefing/modules.go @@ -154,6 +154,8 @@ func collectedFactAvailable(requirement module.FactRequirement, ctx ModuleContex return ctx.Collected.Current != nil case module.CollectedNarrativeForecast: return ctx.Collected.Narrative != nil + case module.CollectedHourlyForecast: + return ctx.Collected.Hourly != nil case module.CollectedAlerts: return ctx.Collected.Alerts != nil case module.CollectedDiscussion: @@ -274,6 +276,16 @@ func defaultModuleDefinitions() []ModuleDefinition { MissingData: module.MissingDataOmit, Builder: buildNarrativeForecastModule, }, + { + ID: module.HourlyForecast, + StanzaName: "hourly_forecast", + DefaultOptions: module.HourlyForecastOptions{}, + RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast}, + RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods}, + SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow}, + MissingData: module.MissingDataOmit, + Builder: buildHourlyForecastModule, + }, { ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", diff --git a/internal/briefing/package.go b/internal/briefing/package.go index 6c4685a..bed7a53 100644 --- a/internal/briefing/package.go +++ b/internal/briefing/package.go @@ -96,6 +96,14 @@ func copyBool(value *bool) *bool { return &copied } +func copyInt(value *int) *int { + if value == nil { + return nil + } + copied := *value + return &copied +} + func copyFloat(value *float64) *float64 { if value == nil { return nil diff --git a/internal/module/module.go b/internal/module/module.go index 7173863..f394809 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -14,6 +14,7 @@ const ( Metadata ID = "metadata" CurrentConditions ID = "current_conditions" NarrativeForecast ID = "narrative_forecast" + HourlyForecast ID = "hourly_forecast" DerivedDailySummary ID = "derived_daily_summary" DerivedDaypartSummaries ID = "derived_daypart_summaries" PrecipTiming ID = "precip_timing" @@ -106,6 +107,7 @@ type FactRequirement string const ( CollectedCurrentConditions FactRequirement = "collected.current_conditions" CollectedNarrativeForecast FactRequirement = "collected.narrative_forecast" + CollectedHourlyForecast FactRequirement = "collected.hourly_forecast" CollectedAlerts FactRequirement = "collected.alerts" CollectedDiscussion FactRequirement = "collected.discussion" CollectedWeatherStory FactRequirement = "collected.weather_story" @@ -130,6 +132,7 @@ const ( type MetadataOptions struct{} type CurrentConditionsOptions struct{} type NarrativeForecastOptions struct{} +type HourlyForecastOptions struct{} type DerivedDailySummaryOptions struct{} type DerivedDaypartSummariesOptions struct{} type PrecipTimingOptions struct{} diff --git a/internal/report/daily_report.go b/internal/report/daily_report.go index 00a838e..686b0f5 100644 --- a/internal/report/daily_report.go +++ b/internal/report/daily_report.go @@ -49,13 +49,25 @@ func dailyTodayModules() []module.ConfigItem { module.AreaForecastDiscussion, module.WeatherStory, module.OutdoorWindows, + module.HourlyForecast, ) } func dailyTomorrowModules() []module.ConfigItem { - items := dailyTodayModules() - items = append(items, module.ConfigItem{ID: module.TomorrowPlanning}) - return items + return moduleItems( + module.Metadata, + module.CurrentConditions, + module.NarrativeForecast, + module.DerivedDailySummary, + module.DerivedDaypartSummaries, + module.PrecipTiming, + module.AlertDigest, + module.AreaForecastDiscussion, + module.WeatherStory, + module.OutdoorWindows, + module.TomorrowPlanning, + module.HourlyForecast, + ) } func resolveDailyToday(req ResolveRequest) (timeutil.Period, error) { diff --git a/internal/report/period_test.go b/internal/report/period_test.go index bccb920..31e5ade 100644 --- a/internal/report/period_test.go +++ b/internal/report/period_test.go @@ -270,6 +270,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) { module.AreaForecastDiscussion, module.WeatherStory, module.OutdoorWindows, + module.HourlyForecast, }, }, { @@ -286,6 +287,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) { module.WeatherStory, module.OutdoorWindows, module.TomorrowPlanning, + module.HourlyForecast, }, }, {