From 2a33fe01cf701e28777076e7bbedd6f3ad0c3554 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 16 Jun 2026 20:08:03 -0500 Subject: [PATCH] Update to support upstream weatherfeeder v0.12.1 and add ends field to the alerts schema --- docs/api.md | 11 ++-- docs/integrations/weatherfeeder-postgres.md | 4 +- docs/internal/http-adapter.md | 4 +- docs/internal/postgres-repository.md | 5 +- go.mod | 2 +- go.sum | 4 +- .../inbound/httpapi/endpoints_test.go | 23 +++++--- .../outbound/postgres/alerts_mapper.go | 1 + .../outbound/postgres/alerts_queries.go | 1 + .../adapters/outbound/postgres/alerts_read.go | 1 + .../adapters/outbound/postgres/alerts_rows.go | 1 + .../outbound/postgres/repository_test.go | 30 ++++++++++ internal/app/service.go | 7 ++- internal/app/service_test.go | 56 +++++++++++++------ templates/alerts_active.txt.tmpl | 3 + 15 files changed, 117 insertions(+), 36 deletions(-) diff --git a/docs/api.md b/docs/api.md index c84ab16..3f4666d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -212,14 +212,17 @@ Run `data` fields: | `alerts` | array | active alerts, possibly empty | Alerts are active when `messageType` is not `Cancel`, `effective` is absent or -at or before request time, and `expires` is absent or after request time. +at or before request time, and the alert end boundary is absent or after request +time. The end boundary prefers `ends`; if `ends` is absent, `expires` is used as +a fallback for older rows or providers that do not supply an alert-period end. `onset` is presented when available but is not used as the active boundary. Alert fields include `id`, `event`, `headline`, `severity`, `urgency`, `certainty`, `status`, `messageType`, `category`, `response`, `description`, -`instruction`, `sent`, `effective`, `onset`, `expires`, `areaDescription`, -`senderName`, and `references`. Most alert fields are optional except `id` when -an alert item is present. +`instruction`, `sent`, `effective`, `onset`, `ends`, `expires`, +`areaDescription`, `senderName`, and `references`. Most alert fields are +optional except `id` when an alert item is present. `ends` is the alert-period +end; `expires` is provider expiration metadata. Reference fields are `id`, `identifier`, `sender`, and `sent`. diff --git a/docs/integrations/weatherfeeder-postgres.md b/docs/integrations/weatherfeeder-postgres.md index 61ea233..5910fce 100644 --- a/docs/integrations/weatherfeeder-postgres.md +++ b/docs/integrations/weatherfeeder-postgres.md @@ -105,8 +105,8 @@ and `event_emitted_at`. `alert_index`, `alert_id`, `event`, `headline`, `severity`, `urgency`, `certainty`, `status`, `message_type`, `category`, `response`, `description`, -`instruction`, `sent`, `effective`, `onset`, `expires`, `area_description`, -`sender_name`, and `run_event_id`. +`instruction`, `sent`, `effective`, `onset`, `ends`, `expires`, +`area_description`, `sender_name`, and `run_event_id`. ### `alert_references` diff --git a/docs/internal/http-adapter.md b/docs/internal/http-adapter.md index 73b1dd3..75f72bd 100644 --- a/docs/internal/http-adapter.md +++ b/docs/internal/http-adapter.md @@ -134,7 +134,9 @@ behavior deterministic. `/alerts/active` uses the shared `format` and `units` binder. The handler calls the application service with `alertNow().UTC()` so active alert filtering uses -the request-time instant while remaining deterministic in endpoint tests. +the request-time instant while remaining deterministic in endpoint tests. The +application service prefers alert `ends` over `expires` when deciding whether an +alert has ended. ## Outlook Filters diff --git a/docs/internal/postgres-repository.md b/docs/internal/postgres-repository.md index f1d7a6a..86dc7ba 100644 --- a/docs/internal/postgres-repository.md +++ b/docs/internal/postgres-repository.md @@ -79,8 +79,9 @@ successful responses with `data: null`. - `CurrentConditions`: aggregates recent rows from `observations` using the application-provided observation window. - `LatestAlertRun`: latest row from `alert_runs`, then child `alerts` and - `alert_references`. This is the latest stored alert snapshot; active-time - filtering is performed by the application service. + `alert_references`. This is the latest stored alert snapshot. The repository + maps both `ends` and `expires`; active-time filtering is performed by the + application service. - `LatestHourlyForecast`: latest `forecasts` row where `product = 'hourly'`, then child `forecast_periods`. - `LatestNarrativeForecast`: latest `forecasts` row where diff --git a/go.mod b/go.mod index a0e3c4b..da68148 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.5 require ( gitea.maximumdirect.net/ejr/feedapi v0.1.0 - gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0 + gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1 github.com/lib/pq v1.10.9 ) diff --git a/go.sum b/go.sum index 1be5eaf..004bdc8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ gitea.maximumdirect.net/ejr/feedapi v0.1.0 h1:ZB5QWKD5DPFV3P7vyeJqXPMcSWN9qHkDUHw1LgN9hwY= gitea.maximumdirect.net/ejr/feedapi v0.1.0/go.mod h1:3fIaFFx4ywt0TWbN8DIIBAHJn7ZQUm6PNcceqRgy3bw= -gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0 h1:U3yln3o2rGqfMvWVRwOGgQeqYuqMm+/p0XIRhK8TDUQ= -gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0/go.mod h1:VVtuwrbddWdUu21ovCSSojhH5J9P6kk0/dfnFqC4/Lw= +gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1 h1:dZYDpOd0vEIk6QljsrhFyrOY0Lt4WyEoazhqDFIZKNQ= +gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1/go.mod h1:VVtuwrbddWdUu21ovCSSojhH5J9P6kk0/dfnFqC4/Lw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/adapters/inbound/httpapi/endpoints_test.go b/internal/adapters/inbound/httpapi/endpoints_test.go index 005db77..4d34559 100644 --- a/internal/adapters/inbound/httpapi/endpoints_test.go +++ b/internal/adapters/inbound/httpapi/endpoints_test.go @@ -324,12 +324,14 @@ func TestObservationUSUnitsChangesFieldNames(t *testing.T) { } func TestAlertsUSUnitsKeepSchema(t *testing.T) { + ends := time.Date(2026, 6, 11, 14, 0, 0, 0, time.UTC) h := newHandler(t, &fakeService{ alerts: &model.WeatherAlertRun{ AsOf: time.Now().UTC(), Alerts: []model.WeatherAlert{{ ID: "abc", Headline: "A headline", + Ends: &ends, }}, }, }, "/alerts/active") @@ -359,6 +361,9 @@ func TestAlertsUSUnitsKeepSchema(t *testing.T) { if first["id"] != "abc" { t.Fatalf("expected alert id abc, got %#v", first["id"]) } + if _, ok := first["ends"].(string); !ok { + t.Fatalf("expected alert ends string, got %#v", first["ends"]) + } } func TestAlertsRouteRegistered(t *testing.T) { @@ -456,8 +461,10 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) { activeAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) setAlertNowForTest(t, activeAt) effective := activeAt.Add(-1 * time.Hour) - activeExpires := activeAt.Add(1 * time.Hour) - expiredAtBoundary := activeAt + activeEnds := activeAt.Add(1 * time.Hour) + expiredEndsAtBoundary := activeAt + expiredProviderMetadata := activeAt.Add(-30 * time.Minute) + activeProviderMetadata := activeAt.Add(30 * time.Minute) repo := &alertRepository{ alerts: &model.WeatherAlertRun{ AsOf: activeAt, @@ -467,21 +474,23 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) { Headline: "Active warning", MessageType: "Alert", Effective: &effective, - Expires: &activeExpires, + Ends: &activeEnds, + Expires: &expiredProviderMetadata, }, { ID: "expired-alert", Headline: "Expired warning", MessageType: "Alert", Effective: &effective, - Expires: &expiredAtBoundary, + Ends: &expiredEndsAtBoundary, + Expires: &activeProviderMetadata, }, { ID: "canceled-alert", Headline: "Canceled warning", MessageType: " cancel ", Effective: &effective, - Expires: &activeExpires, + Ends: &activeEnds, }, }, }, @@ -496,7 +505,7 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() - for _, want := range []string{"Alerts: 1", "active-alert", "Active warning"} { + for _, want := range []string{"Alerts: 1", "active-alert", "Active warning", "Ends:"} { if !strings.Contains(body, want) { t.Fatalf("expected %q in text body, got %q", want, body) } @@ -2448,7 +2457,7 @@ func testRenderers(t *testing.T) *render.Registry { "outlooks_convective.txt.tmpl": "Convective Outlook\n{{if .Data}}Outlooks: {{len .Data.Outlooks}}\nDiscussions: {{len .Data.Discussions}}{{range .Data.Discussions}}\nDiscussion: {{.Discussion}}{{end}}{{else}}No convective outlook data available.{{end}}", "weatherstories.txt.tmpl": "Weather Stories", "weatherstories_latest.txt.tmpl": "Latest Weather Story", - "alerts_active.txt.tmpl": "{{if .Data}}Active Alerts\nAlerts: {{len .Data.Alerts}}{{range .Data.Alerts}}\n{{.ID}}{{if .Headline}}\nHeadline: {{.Headline}}{{end}}{{end}}{{else}}No active alerts data available.{{end}}", + "alerts_active.txt.tmpl": "{{if .Data}}Active Alerts\nAlerts: {{len .Data.Alerts}}{{range .Data.Alerts}}\n{{.ID}}{{if .Headline}}\nHeadline: {{.Headline}}{{end}}{{if .Ends}}\nEnds: {{.Ends}}{{end}}{{end}}{{else}}No active alerts data available.{{end}}", "conditions_current.txt.tmpl": "Conditions text", } { tmpl, err := template.New(name).Parse(body) diff --git a/internal/adapters/outbound/postgres/alerts_mapper.go b/internal/adapters/outbound/postgres/alerts_mapper.go index 83630ee..1f93265 100644 --- a/internal/adapters/outbound/postgres/alerts_mapper.go +++ b/internal/adapters/outbound/postgres/alerts_mapper.go @@ -33,6 +33,7 @@ func mapAlertRow(row alertRow) indexedAlert { Sent: timePtr(row.Sent), Effective: timePtr(row.Effective), Onset: timePtr(row.Onset), + Ends: timePtr(row.Ends), Expires: timePtr(row.Expires), AreaDescription: stringValue(row.AreaDescription), SenderName: stringValue(row.SenderName), diff --git a/internal/adapters/outbound/postgres/alerts_queries.go b/internal/adapters/outbound/postgres/alerts_queries.go index dcb5de9..5699061 100644 --- a/internal/adapters/outbound/postgres/alerts_queries.go +++ b/internal/adapters/outbound/postgres/alerts_queries.go @@ -33,6 +33,7 @@ SELECT sent, effective, onset, + ends, expires, area_description, sender_name diff --git a/internal/adapters/outbound/postgres/alerts_read.go b/internal/adapters/outbound/postgres/alerts_read.go index 3e99f30..bfa4bc8 100644 --- a/internal/adapters/outbound/postgres/alerts_read.go +++ b/internal/adapters/outbound/postgres/alerts_read.go @@ -70,6 +70,7 @@ func (r *Repository) loadAlerts(ctx context.Context, eventID string) ([]model.We &row.Sent, &row.Effective, &row.Onset, + &row.Ends, &row.Expires, &row.AreaDescription, &row.SenderName, diff --git a/internal/adapters/outbound/postgres/alerts_rows.go b/internal/adapters/outbound/postgres/alerts_rows.go index 6f5f236..99dd034 100644 --- a/internal/adapters/outbound/postgres/alerts_rows.go +++ b/internal/adapters/outbound/postgres/alerts_rows.go @@ -35,6 +35,7 @@ type alertRow struct { Sent sql.NullTime Effective sql.NullTime Onset sql.NullTime + Ends sql.NullTime Expires sql.NullTime AreaDescription sql.NullString SenderName sql.NullString diff --git a/internal/adapters/outbound/postgres/repository_test.go b/internal/adapters/outbound/postgres/repository_test.go index b370ecb..fced42d 100644 --- a/internal/adapters/outbound/postgres/repository_test.go +++ b/internal/adapters/outbound/postgres/repository_test.go @@ -246,6 +246,36 @@ func TestAttachAlertReferencesPreservesOrder(t *testing.T) { } } +func TestMapAlertRowMapsEndsAndExpires(t *testing.T) { + ends := time.Date(2026, 6, 16, 14, 0, 0, 0, time.FixedZone("CDT", -5*3600)) + expires := time.Date(2026, 6, 16, 11, 0, 0, 0, time.FixedZone("CDT", -5*3600)) + + alert := mapAlertRow(alertRow{ + AlertIndex: 1, + AlertID: "alert-1", + Ends: sql.NullTime{Time: ends, Valid: true}, + Expires: sql.NullTime{Time: expires, Valid: true}, + }).Alert + + if alert.Ends == nil || alert.Ends.Location().String() != "UTC" || !alert.Ends.Equal(ends.UTC()) { + t.Fatalf("expected ends UTC %s, got %v", ends.UTC(), alert.Ends) + } + if alert.Expires == nil || alert.Expires.Location().String() != "UTC" || !alert.Expires.Equal(expires.UTC()) { + t.Fatalf("expected expires UTC %s, got %v", expires.UTC(), alert.Expires) + } +} + +func TestMapAlertRowNullableEnds(t *testing.T) { + alert := mapAlertRow(alertRow{ + AlertIndex: 1, + AlertID: "alert-1", + }).Alert + + if alert.Ends != nil { + t.Fatalf("expected nil ends, got %v", alert.Ends) + } +} + func TestMapCurrentConditionsRowNoSamplesReturnsNil(t *testing.T) { got := mapCurrentConditionsRow(currentConditionsRow{ SampleCount: 0, diff --git a/internal/app/service.go b/internal/app/service.go index 7963d09..5b5555f 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -147,6 +147,7 @@ func cloneAlert(alert model.WeatherAlert) model.WeatherAlert { out.Sent = copyTime(alert.Sent) out.Effective = copyTime(alert.Effective) out.Onset = copyTime(alert.Onset) + out.Ends = copyTime(alert.Ends) out.Expires = copyTime(alert.Expires) if alert.References != nil { out.References = make([]model.AlertReference, len(alert.References)) @@ -170,7 +171,11 @@ func isActiveAlert(alert model.WeatherAlert, activeAt time.Time) bool { if alert.Effective != nil && activeAt.Before(*alert.Effective) { return false } - if alert.Expires != nil && !activeAt.Before(*alert.Expires) { + endBoundary := alert.Ends + if endBoundary == nil { + endBoundary = alert.Expires + } + if endBoundary != nil && !activeAt.Before(*endBoundary) { return false } return true diff --git a/internal/app/service_test.go b/internal/app/service_test.go index d591573..2dc9256 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -132,7 +132,7 @@ func TestServiceLatestActiveAlertRunDelegatesAndFilters(t *testing.T) { if repo.alertRunCalls != 1 { t.Fatalf("expected one repository call, got %d", repo.alertRunCalls) } - assertAlertIDs(t, run, []string{"current", "effective-at-boundary", "missing-effective", "missing-expires", "later-onset"}) + assertAlertIDs(t, run, []string{"current", "effective-at-boundary", "missing-effective", "missing-expires", "later-onset", "ends-preferred"}) } func TestServiceLatestActiveAlertRunNoData(t *testing.T) { @@ -165,9 +165,9 @@ func TestServiceLatestActiveAlertRunPropagatesErrors(t *testing.T) { func TestServiceLatestActiveAlertRunKeepsMetadataWithEmptyAlerts(t *testing.T) { activeAt := testTime(12) repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{ - testAlert("expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)), - testAlert("cancel", "Cancel", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), - testAlert("future", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(14)), + testAlert("expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)), + testAlert("cancel", "Cancel", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(13)), + testAlert("future", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(14), testTimePtr(14)), })} svc := NewService(repo) @@ -213,7 +213,8 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) { *run.Alerts[0].Sent = testTime(1) *run.Alerts[0].Effective = testTime(2) *run.Alerts[0].Onset = testTime(3) - *run.Alerts[0].Expires = testTime(4) + *run.Alerts[0].Ends = testTime(4) + *run.Alerts[0].Expires = testTime(5) *run.Alerts[0].References[0].Sent = testTime(5) run.Alerts[0].ID = "changed" run.Alerts[0].References[0].ID = "changed" @@ -237,7 +238,10 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) { if original.Alerts[0].Onset == nil || !original.Alerts[0].Onset.Equal(testTime(11)) { t.Fatalf("expected original onset unchanged, got %v", original.Alerts[0].Onset) } - if original.Alerts[0].Expires == nil || !original.Alerts[0].Expires.Equal(testTime(13)) { + if original.Alerts[0].Ends == nil || !original.Alerts[0].Ends.Equal(testTime(13)) { + t.Fatalf("expected original ends unchanged, got %v", original.Alerts[0].Ends) + } + if original.Alerts[0].Expires == nil || !original.Alerts[0].Expires.Equal(testTime(12)) { t.Fatalf("expected original expires unchanged, got %v", original.Alerts[0].Expires) } if original.Alerts[0].References[0].ID != "ref-current" { @@ -246,11 +250,28 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) { if original.Alerts[0].References[0].Sent == nil || !original.Alerts[0].References[0].Sent.Equal(testTime(8)) { t.Fatalf("expected original reference sent unchanged, got %v", original.Alerts[0].References[0].Sent) } - if len(original.Alerts) != 8 { + if len(original.Alerts) != 10 { t.Fatalf("expected original alert slice unchanged, got %d entries", len(original.Alerts)) } } +func TestServiceLatestActiveAlertRunUsesEndsBeforeExpires(t *testing.T) { + activeAt := testTime(12) + repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{ + testAlert("ends-at-boundary", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)), + testAlert("ends-after-active-expires-before", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(11)), + testAlert("expires-fallback", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), nil, testTimePtr(13)), + testAlert("expires-fallback-expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), nil, testTimePtr(12)), + })} + svc := NewService(repo) + + run, err := svc.LatestActiveAlertRun(context.Background(), activeAt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertAlertIDs(t, run, []string{"ends-after-active-expires-before", "expires-fallback"}) +} + func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) { repo := &fakeRepository{outlookRun: testOutlookRun()} svc := NewService(repo) @@ -509,14 +530,16 @@ func TestServicePropagatesErrors(t *testing.T) { func testAlertRun() *model.WeatherAlertRun { return testAlertRunWithAlerts([]model.WeatherAlert{ - testAlert("current", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), - testAlert("expired", "Update", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)), - testAlert("future-effective", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(15)), - testAlert("canceled", " cancel ", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), - testAlert("effective-at-boundary", "Alert", testTimePtr(9), testTimePtr(12), testTimePtr(12), testTimePtr(14)), - testAlert("missing-effective", "Alert", testTimePtr(9), nil, nil, testTimePtr(14)), - testAlert("missing-expires", "Alert", testTimePtr(9), testTimePtr(10), nil, nil), - testAlert("later-onset", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(13), testTimePtr(14)), + testAlert("current", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(12)), + testAlert("expired", "Update", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)), + testAlert("future-effective", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(15), testTimePtr(15)), + testAlert("canceled", " cancel ", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(13)), + testAlert("effective-at-boundary", "Alert", testTimePtr(9), testTimePtr(12), testTimePtr(12), testTimePtr(14), testTimePtr(14)), + testAlert("missing-effective", "Alert", testTimePtr(9), nil, nil, testTimePtr(14), testTimePtr(14)), + testAlert("missing-expires", "Alert", testTimePtr(9), testTimePtr(10), nil, nil, nil), + testAlert("later-onset", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(13), testTimePtr(14), testTimePtr(14)), + testAlert("ends-preferred", "Alert", testTimePtr(9), testTimePtr(10), nil, testTimePtr(14), testTimePtr(11)), + testAlert("ends-at-boundary", "Alert", testTimePtr(9), testTimePtr(10), nil, testTimePtr(12), testTimePtr(14)), }) } @@ -533,7 +556,7 @@ func testAlertRunWithAlerts(alerts []model.WeatherAlert) *model.WeatherAlertRun } } -func testAlert(id string, messageType string, sent *time.Time, effective *time.Time, onset *time.Time, expires *time.Time) model.WeatherAlert { +func testAlert(id string, messageType string, sent *time.Time, effective *time.Time, onset *time.Time, ends *time.Time, expires *time.Time) model.WeatherAlert { refSent := testTime(8) return model.WeatherAlert{ ID: id, @@ -551,6 +574,7 @@ func testAlert(id string, messageType string, sent *time.Time, effective *time.T Sent: sent, Effective: effective, Onset: onset, + Ends: ends, Expires: expires, AreaDescription: "St. Louis City", SenderName: "NWS St. Louis", diff --git a/templates/alerts_active.txt.tmpl b/templates/alerts_active.txt.tmpl index 60278cd..5b85ce9 100644 --- a/templates/alerts_active.txt.tmpl +++ b/templates/alerts_active.txt.tmpl @@ -11,6 +11,9 @@ Headline: {{$alert.Headline}} {{- if $alert.Severity}} Severity: {{$alert.Severity}} {{- end}} +{{- if $alert.Ends}} +Ends: {{$alert.Ends}} +{{- end}} {{- if $alert.Expires}} Expires: {{$alert.Expires}} {{- end}}