Add implementation plan to clean up expired alerts
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -1,287 +1,104 @@
|
||||
# Implement Weatherfeeder Outlook V2 Support
|
||||
|
||||
## Status
|
||||
|
||||
Implementation, current-behavior documentation, and automated verification are
|
||||
complete. This file now serves as the checklist and verification record for the
|
||||
outlook v2 compatibility update. Authoritative implemented behavior is
|
||||
documented in `docs/api.md`, `docs/integrations/weatherfeeder-postgres.md`, and
|
||||
`docs/internal/`.
|
||||
|
||||
## Verification Record
|
||||
|
||||
Completed on 2026-06-12:
|
||||
|
||||
- `go test ./internal/app`
|
||||
- `go test ./internal/adapters/outbound/postgres`
|
||||
- `go test ./internal/adapters/inbound/httpapi/presenter`
|
||||
- `go test ./internal/adapters/inbound/httpapi`
|
||||
- `go test ./...`
|
||||
- `go build ./cmd/weatherapi`
|
||||
|
||||
The database-backed endpoint checklist requires a reachable PostgreSQL database
|
||||
populated by weatherfeeder outlook v2 tables. It was not run in this workspace
|
||||
because no such populated database was available.
|
||||
# Active Alert Filtering Cleanup
|
||||
|
||||
## Summary
|
||||
|
||||
Implement `weatherapi` support for the `weatherfeeder` SPC outlook v2 contract described in `docs/roadmap/outlook.md`.
|
||||
Fix `/alerts/active` so it returns alerts that are active at request time, not merely every alert from the latest stored alert snapshot.
|
||||
|
||||
This is a compatibility update for the existing convective outlook route family. Do not add or remove public routes. Update dependency, repository reads, app filtering, presenters, templates, tests, and current-behavior docs so `weatherapi` reads `weather.outlook.v2` data from the new weatherfeeder Postgres table shape.
|
||||
The current implementation reads the latest `alert_runs` parent row, loads all child `alerts`, and presents the run unchanged. This can expose expired alerts when the latest persisted weatherfeeder snapshot is stale or when a snapshot contains alerts that later expire before the next successful ingestion cycle.
|
||||
|
||||
## Required End State
|
||||
This cleanup preserves the existing route, response envelope, repository contract, and weatherfeeder table ownership. The change should be implemented as app-layer filtering over the latest stored snapshot, following the same architectural pattern already used by convective outlook active filtering.
|
||||
|
||||
- `go.mod` depends on the released `weatherfeeder` version containing `weather.outlook.v2` support.
|
||||
- Existing public routes remain available:
|
||||
- `GET /outlooks/convective`
|
||||
- `GET /outlooks/convective/active`
|
||||
- `GET /outlooks/convective/location`
|
||||
- `weatherapi` remains read-only and does not create, migrate, or repair weatherfeeder tables.
|
||||
- The Postgres repository reads `outlook_runs`, `outlooks`, and `outlook_discussions`.
|
||||
- The repository no longer expects polygon-level `headline`, `summary`, or `discussion` columns on `outlooks`.
|
||||
- Returned `WeatherOutlookRun` values include run-level `Discussions` loaded from `outlook_discussions`.
|
||||
- App-level filtering trims both `Outlooks` and `Discussions` so discussions are present only for days with retained outlooks.
|
||||
- Presenters copy and timezone-convert run-level discussion `updatedAt` values.
|
||||
- JSON/XML/text responses expose run-level `discussions` and do not expose polygon-level outlook prose fields.
|
||||
- `data: null` semantics for no latest run are preserved.
|
||||
- Filtered no-match responses return a non-null run with `outlooks: []` and `discussions: []`.
|
||||
- Public query behavior remains compatible: `format`, `units`, `tz` / `TZ`, `day`, `outlookType`, and allowed `containsLocation` behavior are preserved; `precision` and unknown params remain rejected.
|
||||
## Current Behavior To Correct
|
||||
|
||||
## Guardrails
|
||||
- `GET /alerts/active` calls `LatestAlertRun(ctx)`.
|
||||
- `LatestAlertRun(ctx)` returns the latest stored alert snapshot from Postgres.
|
||||
- All child alerts for that run are returned unchanged.
|
||||
- The endpoint does not compare `effective`, `onset`, `expires`, `status`, or `messageType` to the request time.
|
||||
- Expired alerts can therefore appear under an endpoint named `/alerts/active`.
|
||||
|
||||
- Do not implement weatherfeeder table creation or migration in weatherapi.
|
||||
- Do not call SPC or any upstream weather provider from weatherapi.
|
||||
- Do not introduce historical browsing or cross-run accumulation in this change.
|
||||
- Do not remove `/outlooks/convective/location`; keep it as an API-compatible active local-outlook route.
|
||||
- Do not keep stale v1 SQL reads for `outlooks.headline`, `outlooks.summary`, or `outlooks.discussion`.
|
||||
- Do not commit a `replace` directive for `weatherfeeder`.
|
||||
- Keep SQL in `*_queries.go`, row DTOs in `*_rows.go`, row mapping in `*_mapper.go`, and request/presentation policy in HTTP/presenter packages.
|
||||
## Target Behavior
|
||||
|
||||
## Stage 1: Dependency And Compile Boundary
|
||||
- `GET /alerts/active` returns the latest stored alert run with `alerts` filtered to items active at request time.
|
||||
- Missing latest alert run still returns `{ "data": null }`.
|
||||
- A latest run with no currently active alerts returns a non-null run object with `alerts: []`.
|
||||
- The repository remains a latest-snapshot reader and does not own active-time policy.
|
||||
- The app service owns alert active filtering policy.
|
||||
- The HTTP adapter supplies the request-time instant using an injectable clock for tests.
|
||||
- Presentation remains responsible only for response shape, units no-op behavior, and rendering.
|
||||
|
||||
## Active Alert Policy
|
||||
|
||||
Use a single app-layer predicate for determining whether an alert is active at an instant `activeAt`.
|
||||
|
||||
An alert is active when all of the following are true:
|
||||
|
||||
- `messageType` is not `Cancel`, case-insensitive after trimming.
|
||||
- `effective` is absent or `effective <= activeAt`.
|
||||
- `expires` is absent or `activeAt < expires`.
|
||||
|
||||
Additional policy notes:
|
||||
|
||||
- Do not use `onset` as a required active boundary. `onset` can describe hazard onset and may be later than alert effective time; using it as a hard lower bound could hide valid watches, warnings, or advisories that are already in effect from an alerting perspective.
|
||||
- Do not filter by `status` in the first cleanup unless existing model/test data proves a specific non-active status must be excluded. NWS active feeds commonly use `Actual`; persisted historical snapshots may include other values, but time and cancellation policy are the high-confidence active criteria.
|
||||
- Treat nil `expires` as active if the other criteria pass. This preserves data when an upstream alert omits an expiration, while still allowing future tightening if real data shows nil expiration should be suppressed.
|
||||
- Preserve input alert order after filtering.
|
||||
- Preserve run metadata such as `asOf`, location fields, latitude, and longitude even when all alerts are filtered out.
|
||||
|
||||
## Public API Impact
|
||||
|
||||
- Route remains `GET /alerts/active`.
|
||||
- Supported query parameters remain `format` and `units`.
|
||||
- `precision`, `tz` / `TZ`, and unknown query parameters remain rejected unless a separate roadmap explicitly changes alert query support.
|
||||
- JSON/XML/text format support remains unchanged.
|
||||
- Response envelope remains `{ "data": ... }`.
|
||||
- No Postgres schema or weatherfeeder migration is required.
|
||||
|
||||
## Stage 1: App Service Filtering
|
||||
|
||||
### Changes
|
||||
|
||||
1. Update `go.mod` to the released `gitea.maximumdirect.net/ejr/weatherfeeder` version that includes:
|
||||
- `standards.SchemaWeatherOutlookV2`;
|
||||
- `model.WeatherOutlookRun.Discussions`;
|
||||
- `model.WeatherOutlookDiscussion`;
|
||||
- `model.WeatherOutlook` without `Headline`, `Summary`, or `Discussion`.
|
||||
2. Run `go mod tidy`.
|
||||
3. Fix compile errors from removed polygon-level outlook prose fields.
|
||||
4. Search for outlook v1 and polygon prose references:
|
||||
- Keep `app.Repository.LatestAlertRun(ctx)` unchanged.
|
||||
- Add an app-level alert read use case, either:
|
||||
- `LatestActiveAlertRun(ctx context.Context, activeAt time.Time) (*model.WeatherAlertRun, error)`, or
|
||||
- `LatestAlerts(ctx context.Context, filter AlertFilter) (*model.WeatherAlertRun, error)` with `AlertFilter.ActiveAt`.
|
||||
- Prefer the first option unless another alert route is being added at the same time; it is narrower and clearer for the existing API.
|
||||
- Implement the new service method by:
|
||||
- reading `s.repo.LatestAlertRun(ctx)`;
|
||||
- returning `nil, nil` when no run exists;
|
||||
- cloning the run before mutation;
|
||||
- filtering cloned `Alerts` with the active predicate;
|
||||
- returning the cloned run.
|
||||
- Add package-local helpers for:
|
||||
- cloning `WeatherAlertRun`;
|
||||
- cloning `WeatherAlert` values deeply enough to avoid mutating repository-owned slices;
|
||||
- copying `References` slices;
|
||||
- evaluating `isActiveAlert(alert, activeAt)`.
|
||||
|
||||
```sh
|
||||
rg "SchemaWeatherOutlookV1|weather\.outlook\.v1|\.Headline|\.Summary|\.Discussion|headline|summary|discussion" internal docs templates README.md
|
||||
```
|
||||
### Clone Requirements
|
||||
|
||||
5. Treat matches carefully:
|
||||
- Keep unrelated alert and forecast-discussion uses.
|
||||
- Remove or update outlook polygon-level prose references.
|
||||
- Replace outlook-specific schema references with v2 where applicable.
|
||||
The clone must protect repository-returned data from service mutation:
|
||||
|
||||
### Expected Files
|
||||
|
||||
- `go.mod`
|
||||
- `go.sum`
|
||||
- `internal/adapters/outbound/postgres/outlooks_rows.go`
|
||||
- `internal/adapters/outbound/postgres/outlooks_mapper.go`
|
||||
- `internal/adapters/outbound/postgres/outlooks_queries.go`
|
||||
- `internal/adapters/outbound/postgres/outlooks_read.go`
|
||||
- `internal/adapters/inbound/httpapi/presenter/outlook.go`
|
||||
- `internal/app/service.go`
|
||||
- outlook-related tests under `internal/app`, `internal/adapters/outbound/postgres`, `internal/adapters/inbound/httpapi`, and presenter tests
|
||||
|
||||
### Verification
|
||||
|
||||
At this stage, run a compile-oriented focused set. It may fail until later stages are complete, but failures should identify remaining v1/prose references.
|
||||
|
||||
```sh
|
||||
go test ./internal/app ./internal/adapters/outbound/postgres ./internal/adapters/inbound/httpapi/presenter
|
||||
```
|
||||
|
||||
## Stage 2: Postgres Query, Row, And Mapper Updates
|
||||
|
||||
### Query Changes
|
||||
|
||||
Update `internal/adapters/outbound/postgres/outlooks_queries.go`.
|
||||
|
||||
Parent query:
|
||||
|
||||
- Keep latest run ordering:
|
||||
|
||||
```sql
|
||||
ORDER BY as_of DESC, event_emitted_at DESC
|
||||
LIMIT 1
|
||||
```
|
||||
|
||||
- Continue selecting parent fields needed by `WeatherOutlookRun`.
|
||||
- Optionally select `discussion_count` for test/sanity visibility, but do not expose it in the API response model.
|
||||
|
||||
Outlook child query:
|
||||
|
||||
- Remove these v1 columns from the `SELECT` list:
|
||||
- `headline`
|
||||
- `summary`
|
||||
- `discussion`
|
||||
- Continue selecting:
|
||||
- `outlook_index`
|
||||
- `outlook_id`
|
||||
- `provider`
|
||||
- `product`
|
||||
- `day`
|
||||
- `outlook_type`
|
||||
- `label`
|
||||
- `label_text`
|
||||
- `severity_rank`
|
||||
- `valid_from`
|
||||
- `valid_to`
|
||||
- `issued_at`
|
||||
- `expires_at`
|
||||
- `forecaster`
|
||||
- `source_url`
|
||||
- `image_url`
|
||||
- `contains_location`
|
||||
- `geometry_json`
|
||||
- Keep `WHERE run_event_id = $1`.
|
||||
- Keep `ORDER BY outlook_index ASC`.
|
||||
|
||||
Discussion child query:
|
||||
|
||||
- Add `queryOutlookDiscussionsForRun`:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
discussion_index,
|
||||
day,
|
||||
headline,
|
||||
summary,
|
||||
discussion,
|
||||
updated_at
|
||||
FROM outlook_discussions
|
||||
WHERE run_event_id = $1
|
||||
ORDER BY discussion_index ASC
|
||||
```
|
||||
|
||||
### Row DTO Changes
|
||||
|
||||
Update `internal/adapters/outbound/postgres/outlooks_rows.go`:
|
||||
|
||||
- Remove `Headline`, `Summary`, and `Discussion` from `outlookRow`.
|
||||
- Add `DiscussionCount` to `outlookRunParentRow` only if selected by parent query.
|
||||
- Add `outlookDiscussionRow`:
|
||||
- `DiscussionIndex int`
|
||||
- `Day int`
|
||||
- `Headline sql.NullString`
|
||||
- `Summary sql.NullString`
|
||||
- `Discussion sql.NullString`
|
||||
- `UpdatedAt sql.NullTime`
|
||||
|
||||
### Mapper Changes
|
||||
|
||||
Update `internal/adapters/outbound/postgres/outlooks_mapper.go`:
|
||||
|
||||
- `mapOutlookRow` maps only v2 polygon fields.
|
||||
- Keep geometry validation with `json.Valid` and return contextual mapper errors for invalid JSON.
|
||||
- Continue copying geometry bytes with `append([]byte(nil), geometry...)`.
|
||||
- Continue normalizing outlook timestamps to UTC.
|
||||
- Add `mapOutlookDiscussionRow` returning `model.WeatherOutlookDiscussion`:
|
||||
- `Day` from row day;
|
||||
- string fields via existing `stringValue` helper;
|
||||
- `UpdatedAt` via existing `timePtr` helper, ensuring UTC normalization.
|
||||
|
||||
### Read Flow Changes
|
||||
|
||||
Update `internal/adapters/outbound/postgres/outlooks_read.go`:
|
||||
|
||||
- `LatestConvectiveOutlookRun` loads parent row as today.
|
||||
- After parent row:
|
||||
- call `loadOutlooks(ctx, row.EventID)`;
|
||||
- call `loadOutlookDiscussions(ctx, row.EventID)`;
|
||||
- attach both to the run.
|
||||
- Add `loadOutlookDiscussions` mirroring `loadOutlooks` style:
|
||||
- query with context;
|
||||
- scan rows;
|
||||
- map rows;
|
||||
- return iteration errors;
|
||||
- wrap query, scan, map, and iteration errors with operation context.
|
||||
- Preserve `nil, nil` on missing parent row.
|
||||
- allocate a new `Alerts` slice;
|
||||
- allocate new `References` slices for each alert;
|
||||
- copy pointer time fields only if the model uses pointer fields for alert timestamps;
|
||||
- preserve string, boolean, enum, and scalar values exactly.
|
||||
|
||||
### Tests
|
||||
|
||||
Update or add tests in `internal/adapters/outbound/postgres`:
|
||||
Add app tests covering:
|
||||
|
||||
- `mapOutlookRow` maps v2 polygon fields and no longer expects polygon-level prose.
|
||||
- `mapOutlookRow` rejects invalid `geometry_json`.
|
||||
- nullable label/forecaster/source/image/severity fields map correctly.
|
||||
- `mapOutlookDiscussionRow` maps day/headline/summary/discussion/updatedAt and normalizes time to UTC.
|
||||
- nullable discussion fields map to empty/omitted canonical values.
|
||||
- latest run read loads outlooks in `outlook_index ASC` order.
|
||||
- latest run read loads discussions in `discussion_index ASC` order.
|
||||
- missing latest parent returns `nil, nil`.
|
||||
- query/scan/iteration failures remain context-wrapped.
|
||||
|
||||
### Verification
|
||||
|
||||
```sh
|
||||
go test ./internal/adapters/outbound/postgres
|
||||
```
|
||||
|
||||
## Stage 3: Application Filtering And Copy Semantics
|
||||
|
||||
### Changes
|
||||
|
||||
Update `internal/app/service.go`.
|
||||
|
||||
Clone behavior:
|
||||
|
||||
- `cloneOutlookRun` must deep-copy `Discussions` in addition to `Outlooks`.
|
||||
- Ensure outlook severity pointers, geometry bytes, latitude/longitude pointers, and issuedAt pointers remain copied.
|
||||
- Add a helper such as `cloneOutlookDiscussion` if useful.
|
||||
|
||||
Filtering behavior:
|
||||
|
||||
- Filter `Outlooks` as today using `OutlookFilter`.
|
||||
- After filtering outlooks, filter `Discussions` to only days represented by retained outlooks.
|
||||
- Preserve discussion order from the repository result.
|
||||
- If no outlooks remain, set discussions to an empty slice, not stale unfiltered discussions.
|
||||
- Preserve nil/missing run behavior: repository `nil, nil` still returns `nil, nil`.
|
||||
|
||||
Recommended helper shape:
|
||||
|
||||
```go
|
||||
func filterOutlookDiscussions(discussions []model.WeatherOutlookDiscussion, outlooks []model.WeatherOutlook) []model.WeatherOutlookDiscussion
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Build a set of retained outlook days.
|
||||
- If the set is empty, return `[]model.WeatherOutlookDiscussion{}` when discussions was non-nil or when consistent empty-array output is desired.
|
||||
- Include each discussion only if `discussion.Day` is in the set.
|
||||
- Do not synthesize discussions.
|
||||
|
||||
### Query Compatibility
|
||||
|
||||
Do not change query binder behavior in this stage unless tests reveal compile fallout from the model update. Public query behavior remains:
|
||||
|
||||
- `day` accepted on all outlook routes.
|
||||
- `outlookType` accepted on all outlook routes.
|
||||
- `containsLocation` accepted on `/outlooks/convective` and `/outlooks/convective/active`.
|
||||
- `containsLocation` rejected on `/outlooks/convective/location`.
|
||||
- `format`, `units`, and `tz` / `TZ` accepted.
|
||||
- `precision` and unknown params rejected.
|
||||
|
||||
### Tests
|
||||
|
||||
Update `internal/app/service_test.go`:
|
||||
|
||||
- Delegation test still asserts one repository call.
|
||||
- Day filter retains matching outlooks and only matching day discussions.
|
||||
- Outlook type filter retains discussions only for days with retained outlooks.
|
||||
- Active filter retains discussions only for days with active retained outlooks.
|
||||
- `containsLocation=false` returns an empty outlook/discussion run for v2-style test data.
|
||||
- Filtered no-match result has empty `Outlooks` and empty `Discussions`.
|
||||
- Mutating returned outlooks/discussions does not mutate repository-owned data.
|
||||
- Geometry byte copy and pointer copy behavior remains covered.
|
||||
- repository delegation and error propagation;
|
||||
- `nil, nil` when the repository has no latest alert run;
|
||||
- expired alerts are removed when `activeAt >= expires`;
|
||||
- alerts expiring exactly at `activeAt` are inactive;
|
||||
- alerts effective exactly at `activeAt` are active;
|
||||
- future-effective alerts are inactive;
|
||||
- missing `effective` does not make an otherwise valid alert inactive;
|
||||
- missing `expires` does not make an otherwise valid alert inactive;
|
||||
- `messageType=Cancel` is excluded case-insensitively;
|
||||
- alert order is preserved;
|
||||
- run metadata is preserved when all alerts are filtered out;
|
||||
- filtering does not mutate the repository-owned run, alerts, references, or timestamp pointers.
|
||||
|
||||
### Verification
|
||||
|
||||
@@ -289,94 +106,33 @@ Update `internal/app/service_test.go`:
|
||||
go test ./internal/app
|
||||
```
|
||||
|
||||
## Stage 4: Presenter And Text Template Updates
|
||||
## Stage 2: HTTP Adapter Wiring
|
||||
|
||||
### Presenter Changes
|
||||
### Changes
|
||||
|
||||
Update `internal/adapters/inbound/httpapi/presenter/outlook.go`:
|
||||
|
||||
- Include `Discussions` when constructing the presented `model.WeatherOutlookRun`.
|
||||
- Convert `WeatherOutlookDiscussion.UpdatedAt` using `inLocationTimePtr`.
|
||||
- Deep-copy discussions so presenter output mutation cannot affect repository/app input.
|
||||
- Remove `Headline`, `Summary`, and `Discussion` assignments from `copyOutlook`.
|
||||
- Continue converting:
|
||||
- run `AsOf`;
|
||||
- run `IssuedAt`;
|
||||
- outlook `ValidFrom`;
|
||||
- outlook `ValidTo`;
|
||||
- outlook `IssuedAt`;
|
||||
- outlook `ExpiresAt`.
|
||||
- Continue preserving geometry copy behavior.
|
||||
- Continue returning `nil` for nil input.
|
||||
|
||||
Recommended helper:
|
||||
- Extend `internal/adapters/inbound/httpapi.Service` with the app service method selected in Stage 1.
|
||||
- Add adapter-local clock injection near `alerts_endpoint.go`:
|
||||
|
||||
```go
|
||||
func copyOutlookDiscussion(d model.WeatherOutlookDiscussion, tz *time.Location) model.WeatherOutlookDiscussion
|
||||
var alertNow = time.Now
|
||||
```
|
||||
|
||||
### Template Changes
|
||||
- Update `/alerts/active` handler to call the new active-alert service method with `alertNow().UTC()`.
|
||||
- Keep `bindQuery` as the binder so public query support remains `format` and `units` only.
|
||||
- Keep the text template name `alerts_active.txt.tmpl`.
|
||||
- Do not add timezone support in this cleanup. Alert timestamp presentation currently returns canonical model timestamps; timezone support would be a separate public API expansion.
|
||||
|
||||
Update `templates/outlooks_convective.txt.tmpl`:
|
||||
### Tests
|
||||
|
||||
- Render run-level discussions from `.Data.Discussions`.
|
||||
- Do not reference outlook-level `.Headline`, `.Summary`, or `.Discussion`.
|
||||
- Keep no-data output when `.Data` is nil.
|
||||
- Keep useful output when `.Data` exists and `.Data.Outlooks` is empty.
|
||||
- Include discussion day/headline/summary/discussion fields in a readable, conditional way.
|
||||
- Avoid printing zero timestamps or `<nil>` values for absent `updatedAt`.
|
||||
Update HTTP tests covering:
|
||||
|
||||
### Presenter Tests
|
||||
|
||||
Update `internal/adapters/inbound/httpapi/presenter/payload_test.go`:
|
||||
|
||||
- `OutlookRunPayload(nil, ...)` returns nil.
|
||||
- Output includes copied `Discussions`.
|
||||
- Discussion `UpdatedAt` is converted to requested timezone.
|
||||
- Mutating output discussions does not mutate input discussions.
|
||||
- Mutating output geometry does not mutate input geometry.
|
||||
- Outlook timestamps still convert to requested timezone.
|
||||
- Polygon-level prose fields are not expected in output structs/tests.
|
||||
|
||||
### Verification
|
||||
|
||||
```sh
|
||||
go test ./internal/adapters/inbound/httpapi/presenter
|
||||
```
|
||||
|
||||
## Stage 5: HTTP Endpoint Behavior And Tests
|
||||
|
||||
### Handler Behavior
|
||||
|
||||
Keep route registration and handler structure unchanged unless compile updates require mechanical edits.
|
||||
|
||||
Ensure endpoint responses reflect the updated presenter/app behavior:
|
||||
|
||||
- `/outlooks/convective` returns latest run with v2 shape.
|
||||
- `/outlooks/convective/active` applies active filter and trims discussions.
|
||||
- `/outlooks/convective/location` applies active local filter and trims discussions.
|
||||
- `data: null` remains unchanged when no run exists.
|
||||
- Filtered no-match response returns data object with empty outlook and discussion arrays.
|
||||
|
||||
### Endpoint Tests
|
||||
|
||||
Update `internal/adapters/inbound/httpapi/endpoints_test.go`:
|
||||
|
||||
- test fakes build `model.WeatherOutlookRun` with `Discussions`.
|
||||
- JSON success asserts run-level `discussions` are present.
|
||||
- JSON success asserts outlook polygons do not include polygon-level prose.
|
||||
- XML success renders run-level discussions without errors.
|
||||
- Text success renders run-level discussion content.
|
||||
- Timezone test checks conversion of:
|
||||
- run `asOf`;
|
||||
- run `issuedAt`;
|
||||
- outlook times;
|
||||
- discussion `updatedAt`.
|
||||
- Null data test remains unchanged.
|
||||
- Filtered no-match test expects both `outlooks` and `discussions` empty.
|
||||
- Query acceptance still covers `units`, `format`, `tz` / `TZ`, `day`, `outlookType`, and allowed `containsLocation`.
|
||||
- Query rejection still covers `precision`, unknown params, invalid day, invalid outlookType, invalid containsLocation, invalid timezone, conflicting `tz` / `TZ`, and `containsLocation` on `/location`.
|
||||
- Active/location route tests continue using injectable `outlookNow`.
|
||||
- route registration remains unchanged;
|
||||
- handler passes `alertNow().UTC()` to the service;
|
||||
- JSON success response renders filtered service output;
|
||||
- text output renders zero active alerts as `Alerts: 0` when data is non-null;
|
||||
- `data: null` still renders no-data behavior;
|
||||
- `units=us` remains accepted and has no schema effect;
|
||||
- `precision`, `tz`, `TZ`, and unknown query params still return `400`.
|
||||
|
||||
### Verification
|
||||
|
||||
@@ -384,97 +140,57 @@ Update `internal/adapters/inbound/httpapi/endpoints_test.go`:
|
||||
go test ./internal/adapters/inbound/httpapi
|
||||
```
|
||||
|
||||
## Stage 6: Documentation Updates
|
||||
## Stage 3: Presenter And Template Check
|
||||
|
||||
After code behavior is updated, update current-behavior documentation. Do not leave v2 behavior only in roadmap files.
|
||||
### Changes
|
||||
|
||||
### Public Docs
|
||||
- Keep `presenter.AlertsPayload` as a pass-through unless filtering requires defensive copy behavior at presentation time.
|
||||
- Do not move active filtering into the presenter.
|
||||
- Review `templates/alerts_active.txt.tmpl` and ensure it behaves correctly when `Data` is non-null and `Alerts` is empty.
|
||||
- If needed, add an explicit text fixture/assertion rather than changing template wording broadly.
|
||||
|
||||
Update `docs/api.md`:
|
||||
### Tests
|
||||
|
||||
- State outlook endpoints serve weatherfeeder `weather.outlook.v2` data.
|
||||
- Run fields include `discussions`.
|
||||
- Add discussion field table:
|
||||
- `day`;
|
||||
- `headline`;
|
||||
- `summary`;
|
||||
- `discussion`;
|
||||
- `updatedAt`.
|
||||
- Remove polygon-level `headline`, `summary`, and `discussion` from outlook fields.
|
||||
- State v2 outlooks are already location-filtered by weatherfeeder.
|
||||
- State `containsLocation` is expected to be true for v2 outlooks.
|
||||
- Explain `/outlooks/convective/location` remains an active local-outlook compatibility route under v2.
|
||||
- State endpoint filters also filter `discussions` to days represented by retained outlooks.
|
||||
- State current endpoints use latest-run semantics and do not accumulate historical active outlook rows.
|
||||
- Update examples to show run-level `discussions`.
|
||||
- Ensure examples use valid GeoJSON polygon or multipolygon, not a point, if demonstrating SPC geometry.
|
||||
Add or update presenter/template-sensitive tests only if current endpoint tests do not prove:
|
||||
|
||||
Update `README.md` only if its overview implies all-polygons or old response shape.
|
||||
- nil data renders no-data text;
|
||||
- non-null data with empty alerts renders an active-alert run with `Alerts: 0`;
|
||||
- expired/canceled alerts do not appear in text output after service filtering.
|
||||
|
||||
### Integration/Internal Docs
|
||||
|
||||
Update `docs/integrations/weatherfeeder-postgres.md`:
|
||||
|
||||
- Update dependency version.
|
||||
- Table family for convective outlook includes `outlook_runs`, `outlooks`, and `outlook_discussions`.
|
||||
- Latest run selection remains `as_of DESC, event_emitted_at DESC`.
|
||||
- Child ordering includes:
|
||||
- outlooks by `outlook_index ASC`;
|
||||
- outlook discussions by `discussion_index ASC`.
|
||||
- `outlook_runs` includes `discussion_count` if listed.
|
||||
- `outlooks` no longer includes `headline`, `summary`, or `discussion`.
|
||||
- Add `outlook_discussions` columns.
|
||||
- State weatherapi assumes weatherfeeder's outlook v2 table reset has already been applied.
|
||||
|
||||
Update `docs/internal/postgres-repository.md`:
|
||||
|
||||
- `LatestConvectiveOutlookRun` loads parent, outlook children, and discussion children.
|
||||
- Add outlook discussion child ordering.
|
||||
|
||||
Update `docs/internal/presenters.md`:
|
||||
|
||||
- Outlook presenter copies and timezone-converts run-level discussions.
|
||||
- Outlook presenter no longer handles polygon-level prose.
|
||||
|
||||
Update `docs/internal/http-adapter.md`:
|
||||
|
||||
- Outlook filters trim discussions to retained outlook days.
|
||||
- `/location` is retained for compatibility and active local-outlook behavior.
|
||||
|
||||
Update `docs/operations.md` and `docs/troubleshooting.md` only if they mention the old weatherfeeder table shape or should warn operators that weatherapi requires weatherfeeder v2 outlook tables.
|
||||
|
||||
### Roadmap Docs
|
||||
|
||||
After implementation is complete, replace or update `docs/roadmap/outlook.md` and `docs/roadmap/implementation.md` according to repository convention:
|
||||
|
||||
- Either mark the roadmap complete and point to current docs.
|
||||
- Or move any remaining deferred ideas to a future roadmap file.
|
||||
|
||||
Do not leave roadmap files claiming unimplemented work after the implementation has shipped.
|
||||
|
||||
### Documentation Checks
|
||||
|
||||
Run searches for stale outlook v1/current behavior:
|
||||
### Verification
|
||||
|
||||
```sh
|
||||
rg "weather\.outlook\.v1|outlooks\.headline|outlooks\.summary|outlooks\.discussion|polygon-level|all stored outlook|all polygons|headline, summary, discussion" docs README.md internal templates
|
||||
go test ./internal/adapters/inbound/httpapi ./internal/adapters/inbound/httpapi/presenter
|
||||
```
|
||||
|
||||
Allowed matches:
|
||||
## Stage 4: Documentation Updates
|
||||
|
||||
- explicit historical/legacy notes, if any;
|
||||
- unrelated alert or forecast discussion prose fields;
|
||||
- roadmap files that are intentionally historical.
|
||||
### Changes
|
||||
|
||||
## Stage 7: Full Verification
|
||||
Update current-behavior docs after implementation is complete:
|
||||
|
||||
- `docs/api.md`:
|
||||
- clarify that `/alerts/active` returns the latest alert run filtered to alerts active at request time;
|
||||
- document that no current active alerts returns a run with `alerts: []` when a latest run exists;
|
||||
- keep `data: null` limited to no stored alert run;
|
||||
- keep supported query params as `format` and `units`.
|
||||
- `README.md` if endpoint summaries mention active alerts.
|
||||
- `docs/internal/postgres-repository.md`:
|
||||
- clarify that `LatestAlertRun` reads the latest stored snapshot and active filtering is performed in the app service.
|
||||
- `docs/policy/architecture.md` only if its latest-resource wording needs to distinguish latest snapshots from request-time derived active views.
|
||||
|
||||
Do not document this unimplemented behavior outside `docs/roadmap/` before the code change lands.
|
||||
|
||||
### Tests
|
||||
|
||||
No doc-specific tests are required unless existing docs consistency tests cover endpoint summaries.
|
||||
|
||||
## Stage 5: Final Verification
|
||||
|
||||
Run focused tests:
|
||||
|
||||
```sh
|
||||
go test ./internal/app
|
||||
go test ./internal/adapters/outbound/postgres
|
||||
go test ./internal/adapters/inbound/httpapi/presenter
|
||||
go test ./internal/adapters/inbound/httpapi
|
||||
go test ./internal/app ./internal/adapters/inbound/httpapi ./internal/adapters/inbound/httpapi/presenter
|
||||
```
|
||||
|
||||
Run the full suite:
|
||||
@@ -483,42 +199,32 @@ Run the full suite:
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Optional build check:
|
||||
Manual smoke checks after deployment:
|
||||
|
||||
```sh
|
||||
go build ./cmd/weatherapi
|
||||
curl 'https://weather.api.rakestrawhome.com/alerts/active?format=json'
|
||||
curl 'https://weather.api.rakestrawhome.com/alerts/active?format=text'
|
||||
```
|
||||
|
||||
Manual verification against a database populated by weatherfeeder outlook v2 tables:
|
||||
Expected behavior when all alerts in the latest stored run are expired:
|
||||
|
||||
```http
|
||||
GET /outlooks/convective
|
||||
GET /outlooks/convective/active
|
||||
GET /outlooks/convective/location
|
||||
GET /outlooks/convective?day=1
|
||||
GET /outlooks/convective?outlookType=tornado
|
||||
GET /outlooks/convective?containsLocation=false
|
||||
GET /outlooks/convective?format=text&tz=America/Chicago
|
||||
```
|
||||
- JSON returns a non-null `data` object with `alerts: []`.
|
||||
- Text output shows the run metadata and `Alerts: 0`.
|
||||
|
||||
Expected manual results:
|
||||
## Guardrails
|
||||
|
||||
- Latest run includes run-level `discussions`.
|
||||
- Active route filters outlooks and discussions together.
|
||||
- Location route returns active local outlooks and remains valid.
|
||||
- `containsLocation=false` returns an empty run for v2 data.
|
||||
- Text format renders discussions without template errors.
|
||||
- XML format renders without errors.
|
||||
- No endpoint queries historical active rows across older runs.
|
||||
- Do not change weatherfeeder ingestion behavior in this cleanup.
|
||||
- Do not alter Postgres tables or SQL filtering unless a later performance issue justifies it.
|
||||
- Do not make `weatherapi` poll NWS or any upstream provider.
|
||||
- Do not rename `/alerts/active`.
|
||||
- Do not add alert history endpoints in this cleanup.
|
||||
- Do not introduce generic filtering frameworks; use small app-local helpers.
|
||||
- Do not move route query validation into the app layer.
|
||||
- Do not move active filtering into presenters or text templates.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- A weatherfeeder release containing outlook v2 exists before final implementation is committed.
|
||||
- Operators have reset/recreated weatherfeeder outlook tables according to weatherfeeder transition docs.
|
||||
- Existing weatherapi route names remain stable for external consumers.
|
||||
- Keeping `containsLocation` query support on non-location routes is useful backward compatibility even though v2 data should always be local.
|
||||
- Latest-run semantics are correct for current convective outlook endpoints.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None. This plan fixes the route, dependency, storage, filtering, presentation, and documentation decisions required to support weatherfeeder outlook v2 while preserving weatherapi's existing public route family.
|
||||
- `weatherfeeder` persists alert snapshots that were active at ingestion time, but `weatherapi` must still enforce request-time active semantics.
|
||||
- Cancellation messages are operational metadata, not active hazards, and should not be returned by `/alerts/active`.
|
||||
- Missing `expires` should not suppress an alert unless future real-world data shows this creates stale records.
|
||||
- Time comparisons should use UTC instants; timezone presentation is not part of this cleanup.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Weatherfeeder Outlook V2 Support
|
||||
|
||||
## Status
|
||||
|
||||
Implemented in current behavior docs and code. The authoritative implemented
|
||||
contracts now live in:
|
||||
|
||||
- [`docs/api.md`](../api.md) for public outlook routes, query parameters, and
|
||||
response fields;
|
||||
- [`docs/integrations/weatherfeeder-postgres.md`](../integrations/weatherfeeder-postgres.md)
|
||||
for weatherfeeder-owned table assumptions;
|
||||
- [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md),
|
||||
[`docs/internal/http-adapter.md`](../internal/http-adapter.md), and
|
||||
[`docs/internal/presenters.md`](../internal/presenters.md) for internal
|
||||
behavior.
|
||||
|
||||
`docs/roadmap/implementation.md` remains as the implementation checklist and
|
||||
verification record for this compatibility update.
|
||||
|
||||
## Delivered Behavior
|
||||
|
||||
- Existing public routes remain available:
|
||||
- `GET /outlooks/convective`
|
||||
- `GET /outlooks/convective/active`
|
||||
- `GET /outlooks/convective/location`
|
||||
- The Postgres repository reads weatherfeeder outlook v2 tables:
|
||||
`outlook_runs`, `outlooks`, and `outlook_discussions`.
|
||||
- Outlook polygons no longer include prose fields.
|
||||
- Run-level `discussions` are loaded, copied, presented, and timezone-converted.
|
||||
- Application filtering trims `discussions` to days represented by retained
|
||||
outlooks.
|
||||
- Missing latest data still returns `data: null`.
|
||||
- Filtered no-match responses return a run object with `outlooks: []` and
|
||||
`discussions: []`.
|
||||
- `weatherapi` remains read-only and does not create, migrate, or repair
|
||||
weatherfeeder tables.
|
||||
Reference in New Issue
Block a user