diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index ddce272..cac1ec3 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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 `` 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. diff --git a/docs/roadmap/outlook.md b/docs/roadmap/outlook.md deleted file mode 100644 index 6591a40..0000000 --- a/docs/roadmap/outlook.md +++ /dev/null @@ -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.