package briefing import ( "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/facts" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) type MetadataModule struct { RunID string `json:"run_id"` ReportID report.ID `json:"report_id"` Variant string `json:"variant,omitempty"` PromptID string `json:"prompt_id"` GeneratedAt time.Time `json:"generated_at"` Units string `json:"units"` Timezone string `json:"timezone"` ValidPeriod timeutil.Period `json:"valid_period"` Location *LocationContext `json:"location,omitempty"` SourceWarnings []SourceWarningSummary `json:"source_warnings,omitempty"` Alerts *AlertDigestModule `json:"alerts,omitempty"` } type SourceWarningSummary struct { Source string `json:"source"` Code string `json:"code"` Severity string `json:"severity"` Message string `json:"message"` CompletenessImpact string `json:"completeness_impact,omitempty"` } type CurrentConditionsModule struct { ConditionText string `json:"condition_text,omitempty"` IsDay *bool `json:"is_day,omitempty"` TemperatureC *float64 `json:"temperature_c,omitempty"` TemperatureF *float64 `json:"temperature_f,omitempty"` ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` DewpointC *float64 `json:"dewpoint_c,omitempty"` DewpointF *float64 `json:"dewpoint_f,omitempty"` RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` WindDirectionDegrees *float64 `json:"wind_direction_degrees,omitempty"` } type AlertDigestModule struct { Checked bool `json:"checked"` ActiveCount int `json:"active_count"` RelevantCount int `json:"relevant_count"` Missing bool `json:"missing,omitempty"` Relevant []AlertSummary `json:"relevant,omitempty"` } type AlertSummary struct { Event string `json:"event,omitempty"` Headline string `json:"headline,omitempty"` Severity string `json:"severity,omitempty"` } type AreaForecastDiscussionModule struct { Product string `json:"product,omitempty"` KeyMessages []string `json:"key_messages,omitempty"` ShortTerm string `json:"short_term,omitempty"` LongTerm string `json:"long_term,omitempty"` } type WeatherStoryModule struct { Available bool `json:"available"` OfficeID string `json:"office_id,omitempty"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` UpdatedAt *time.Time `json:"updated_at,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` AltText string `json:"alt_text,omitempty"` Priority bool `json:"priority"` Order int `json:"order"` DownloadURL string `json:"download_url,omitempty"` } func buildMetadataModule(ctx ModuleContext, _ any) (*module.Output, error) { metadata := ctx.Resolved.Metadata() value := MetadataModule{ RunID: metadata.RunID, ReportID: metadata.ReportID, Variant: variantForReport(metadata.ReportID), PromptID: metadata.PromptID, GeneratedAt: metadata.GeneratedAt, Units: ctx.Units, Timezone: ctx.Timezone, ValidPeriod: metadata.ValidPeriod, Location: copyLocation(ctx.Location), SourceWarnings: sourceWarningSummaries(ctx.Collected.SourceWarnings), Alerts: alertDigest(ctx.Collected, ctx.Derived.AlertOverlaps), } return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil } func buildCurrentConditionsModule(ctx ModuleContext, _ any) (*module.Output, error) { current := ctx.Collected.Current if current == nil { return nil, nil } value := CurrentConditionsModule{ ConditionText: current.ConditionText, IsDay: copyBool(current.IsDay), TemperatureC: copyFloat(current.TemperatureC), TemperatureF: copyFloat(current.TemperatureF), ApparentTemperatureC: copyFloat(current.ApparentTemperatureC), ApparentTemperatureF: copyFloat(current.ApparentTemperatureF), DewpointC: copyFloat(current.DewpointC), DewpointF: copyFloat(current.DewpointF), RelativeHumidityPercent: copyFloat(current.RelativeHumidityPercent), WindSpeedKmh: copyFloat(current.WindSpeedKmh), WindSpeedMph: copyFloat(current.WindSpeedMph), WindDirectionDegrees: copyFloat(current.WindDirectionDegrees), } if value.isEmpty() { return nil, nil } return &module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: value}, nil } func buildAlertDigestModule(ctx ModuleContext, _ any) (*module.Output, error) { value := alertDigest(ctx.Collected, ctx.Derived.AlertOverlaps) if value == nil { value = &AlertDigestModule{} } return &module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: *value}, nil } func buildAreaForecastDiscussionModule(ctx ModuleContext, _ any) (*module.Output, error) { discussion := ctx.Collected.Discussion if discussion == nil { return nil, nil } value := AreaForecastDiscussionModule{ Product: discussion.Product, KeyMessages: append([]string(nil), discussion.KeyMessages...), } if discussion.ShortTerm != nil { value.ShortTerm = discussion.ShortTerm.Text } if discussion.LongTerm != nil { value.LongTerm = discussion.LongTerm.Text } if value.Product == "" && len(value.KeyMessages) == 0 && value.ShortTerm == "" && value.LongTerm == "" { return nil, nil } return &module.Output{ID: module.AreaForecastDiscussion, StanzaName: "area_forecast_discussion", Value: value}, nil } func buildWeatherStoryModule(ctx ModuleContext, _ any) (*module.Output, error) { story := ctx.Collected.WeatherStory if story == nil { return nil, nil } value := WeatherStoryModule{ 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, } return &module.Output{ID: module.WeatherStory, StanzaName: "weather_story", Value: value}, nil } func sourceWarningSummaries(warnings []weatherdata.SourceWarning) []SourceWarningSummary { out := make([]SourceWarningSummary, 0, len(warnings)) for _, warning := range warnings { out = append(out, SourceWarningSummary{ Source: warning.Source, Code: warning.Code, Severity: warning.Severity, Message: warning.Message, CompletenessImpact: warning.CompletenessImpact, }) } return out } func alertDigest(collected facts.CollectedFacts, overlaps []forecast.AlertOverlap) *AlertDigestModule { missing := sourceMissing(collected.SourceProvenance, "alerts") if collected.Alerts == nil && !missing { return nil } value := &AlertDigestModule{Missing: missing} if collected.Alerts != nil { value.Checked = true value.ActiveCount = len(collected.Alerts.Alerts) } value.RelevantCount = len(overlaps) for _, overlap := range overlaps { value.Relevant = append(value.Relevant, AlertSummary{ Event: overlap.Event, Headline: overlap.Headline, Severity: overlap.Severity, }) } return value } func sourceMissing(sources []weatherdata.Source, name string) bool { for _, source := range sources { if source.Name == name && source.Missing { return true } } return false } func (v CurrentConditionsModule) isEmpty() bool { return v.ConditionText == "" && v.IsDay == nil && v.TemperatureC == nil && v.TemperatureF == nil && v.ApparentTemperatureC == nil && v.ApparentTemperatureF == nil && v.DewpointC == nil && v.DewpointF == nil && v.RelativeHumidityPercent == nil && v.WindSpeedKmh == nil && v.WindSpeedMph == nil && v.WindDirectionDegrees == nil }