Refresh convective outlook roadmap docs
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-06-11 15:50:06 +00:00
parent 83cb9abcb9
commit b4149f00c5
2 changed files with 19 additions and 363 deletions

View File

@@ -1,239 +1,14 @@
# SPC Convective Outlook API Implementation Plan
# SPC Convective Outlook API Roadmap
Status: implemented. This file is retained as planning history; current
behavior is documented in [`docs/api.md`](../api.md) and the internal docs under
[`docs/internal/`](../internal/).
There are no active roadmap items for the SPC convective outlook API.
## Summary
Current behavior is documented in:
Implement the `weatherapi` SPC convective outlook API described in [`docs/roadmap/outlook.md`](outlook.md).
- [`docs/api.md`](../api.md)
- [`docs/internal/http-adapter.md`](../internal/http-adapter.md)
- [`docs/internal/presenters.md`](../internal/presenters.md)
- [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md)
- [`docs/integrations/weatherfeeder-postgres.md`](../integrations/weatherfeeder-postgres.md)
This plan is execution-oriented. Implement stages in order. Keep `weatherapi` read-only: it must read `weatherfeeder` Postgres tables, reconstruct canonical model payloads, apply API filters, and present results over HTTP. Do not add ingestion, upstream SPC calls, table creation, migrations, or historical browsing.
The implementation target is `gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0`, which provides the canonical outlook model and Postgres table contract this feature consumes.
Target routes:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
All routes return the existing `data` envelope, support JSON/XML/text, and serve latest-run views only.
## Stage 1: Application Use Case And Filtering
Implement the core use case before touching HTTP or SQL.
Required changes:
- Extend `internal/app.Repository` with:
- `LatestConvectiveOutlookRun(context.Context) (*model.WeatherOutlookRun, error)`
- Add `internal/app.OutlookFilter` with:
- `Day *int`
- `OutlookType string`
- `ContainsLocation *bool`
- `ActiveAt *time.Time`
- Add `LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error)` to `internal/app.Service`.
- Implement `LatestConvectiveOutlook` by calling `repo.LatestConvectiveOutlookRun(ctx)`, returning `nil, nil` for no run, cloning the run, and filtering the cloned `Outlooks` slice.
- Filtering rules:
- `Day`: keep outlooks with `outlook.Day == *Day`.
- `OutlookType`: keep outlooks with exact normalized value matching `outlook.OutlookType`.
- `ContainsLocation`: keep outlooks with `outlook.ContainsLocation == *ContainsLocation`.
- `ActiveAt`: keep outlooks where `!ActiveAt.Before(outlook.ValidFrom)` and `ActiveAt.Before(outlook.ValidTo)`.
- Preserve original outlook order.
- Do not mutate the repository-returned run, slice, geometry, or pointer fields.
Tests:
- Add fake repository fields/methods in `internal/app/service_test.go`.
- Test repository delegation and no-data behavior.
- Test each filter independently.
- Test combined filters.
- Test active boundary behavior: includes exact `validFrom`, excludes exact `validTo`.
- Test filtered result keeps run metadata and can return `outlooks: []`.
- Test filtering does not mutate the original run or outlook slice.
Run:
```sh
go test ./internal/app
```
## Stage 2: Postgres Outlook Read Adapter
Add latest-run reconstruction from `weatherfeeder` outlook tables.
Required changes:
- Add `outlooks_queries.go`, `outlooks_rows.go`, `outlooks_mapper.go`, and `outlooks_read.go` under `internal/adapters/outbound/postgres`.
- Query latest parent from `outlook_runs`:
- selected columns: `event_id`, `location_id`, `location_name`, `latitude`, `longitude`, `as_of`, `issued_at`
- order: `as_of DESC, event_emitted_at DESC`
- limit: `1`
- Query child rows from `outlooks` by `run_event_id = $1`, ordered by `outlook_index ASC`.
- Select and map all canonical child columns:
- `outlook_index`, `outlook_id`, `provider`, `product`, `day`, `outlook_type`, `label`, `label_text`, `severity_rank`, `valid_from`, `valid_to`, `issued_at`, `expires_at`, `forecaster`, `headline`, `summary`, `discussion`, `source_url`, `image_url`, `contains_location`, `geometry_json`
- Map parent row into `model.WeatherOutlookRun` with UTC-normalized `AsOf` and `IssuedAt` pointer.
- Map child row into `model.WeatherOutlook` with UTC-normalized timestamps.
- Convert `geometry_json` into `json.RawMessage` without parsing or reserializing.
- Return `nil, nil` when the latest parent query returns `sql.ErrNoRows`.
- Wrap query, scan, iteration, and geometry decode/conversion failures with operation context.
- Confirm `var _ app.Repository = (*Repository)(nil)` still compiles.
Tests:
- Extend `internal/adapters/outbound/postgres/repository_test.go` or add outlook-specific mapper tests in the same package.
- Test parent nullable mapping and UTC normalization.
- Test child mapping for every canonical field, including `outlook_id`, `provider`, `contains_location`, and `geometry_json`.
- Test nullable fields map to nil/zero omitted model values consistently with existing helpers.
- Test geometry bytes are preserved exactly.
- Add read-path tests following existing repository test style if the package already has DB-backed query tests available; otherwise keep mapper-focused tests and rely on compile-time interface satisfaction.
Run:
```sh
go test ./internal/adapters/outbound/postgres
```
## Stage 3: HTTP Route Definitions And Query Binding
Add route wiring and strict query validation.
Required changes:
- Extend `internal/adapters/inbound/httpapi.Service` with:
- `LatestConvectiveOutlook(context.Context, app.OutlookFilter) (*model.WeatherOutlookRun, error)`
- Update HTTP test fakes to implement the new method.
- Add `outlooks_endpoint.go` under `internal/adapters/inbound/httpapi`.
- Register outlook definitions from `Definitions(svc)` after weather stories or before discussion; route order should not affect behavior.
- Add an outlook query binder that accepts common `format`, `units`, `tz`, `TZ`, plus outlook-specific filters.
- Reuse existing timezone parsing and common query normalization patterns.
- Validate:
- `day` must be integer `1`, `2`, or `3`.
- `outlookType` must be one of `categorical`, `tornado`, `hail`, `wind`; normalize case/space like other common values.
- `containsLocation` must parse as boolean when allowed.
- `precision` and unknown params are rejected.
- `containsLocation` is rejected on `/outlooks/convective/location`.
- Add package-level `outlookNow = time.Now` for active/location filters.
- Route filter construction:
- `/outlooks/convective`: use user filters only.
- `/outlooks/convective/active`: add `ActiveAt=outlookNow().UTC()` plus user filters.
- `/outlooks/convective/location`: add `ActiveAt=outlookNow().UTC()` and `ContainsLocation=true`; reject explicit `containsLocation`.
- Return `response.Envelope{Data: presenter.OutlookRunPayload(run, req.Units, req.Timezone)}`.
Tests:
- Route registration for all three paths.
- JSON success for each route.
- No repository data returns `data: null`.
- Filtered no-match run returns `outlooks: []`.
- `day`, `outlookType`, and `containsLocation` query params construct expected filters.
- Active and location endpoints use deterministic `outlookNow` in tests.
- `precision`, unknown params, invalid day/type/bool, invalid timezone, conflicting `tz`/`TZ`, and `containsLocation` on `/location` return `400 Bad Request`.
Run:
```sh
go test ./internal/adapters/inbound/httpapi
```
## Stage 4: Presenter And Text Template
Add outlook response presentation while preserving canonical shape.
Required changes:
- Add `presenter/outlook.go`.
- Implement `OutlookRunPayload(run *model.WeatherOutlookRun, _ Units, tz *time.Location) any`.
- Return nil for nil input.
- Copy run fields and outlook entries before timestamp conversion.
- Convert run `AsOf`, run `IssuedAt`, outlook `ValidFrom`, `ValidTo`, `IssuedAt`, and `ExpiresAt` with existing timezone helper behavior.
- Preserve `Latitude`, `Longitude`, `SeverityRank`, and `Geometry` without mutating source pointers or slices.
- Keep `units` accepted but ignored.
- Add `templates/outlooks_convective.txt.tmpl`.
- Template should render a clear no-data message when `.Data` is nil and concise outlook summaries when data exists.
Tests:
- Presenter nil input returns nil.
- Timezone conversion affects all run/outlook timestamps and preserves instant.
- Source model is not mutated.
- Geometry bytes are preserved exactly.
- Endpoint text response uses the new template.
- XML response renders without handler error.
Run:
```sh
go test ./internal/adapters/inbound/httpapi/presenter
go test ./internal/adapters/inbound/httpapi
```
## Stage 5: Current-Behavior Documentation
Update docs only after the endpoint behavior is implemented in code.
Required changes:
- Update `README.md` endpoint list with:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
- Update `README.md` query parameter summary to mention outlook filters: `day`, `outlookType`, and `containsLocation`.
- Update `docs/api.md` with an Outlook endpoints section covering:
- route behavior
- supported query params and rejected params
- active filtering semantics
- `containsLocation` semantics
- response fields and nullability
- GeoJSON geometry coordinate order
- examples for full and location-filtered responses
- text format note
- Update `docs/internal/http-adapter.md`, `docs/internal/presenters.md`, `docs/internal/postgres-repository.md`, and `docs/integrations/weatherfeeder-postgres.md` only if the implementation adds details not already covered by existing internal/integration docs.
- Do not move implemented behavior into roadmap docs; roadmap docs should remain future/planning context until retired.
Tests:
- No documentation-specific tests are currently required unless existing tests check docs.
- Run full test suite after docs updates to catch template or route registration regressions.
Run:
```sh
go test ./...
```
## Stage 6: Final Verification And Cleanup
Before committing the implementation:
- Run focused tests:
```sh
go test ./internal/app
go test ./internal/adapters/outbound/postgres
go test ./internal/adapters/inbound/httpapi
go test ./internal/adapters/inbound/httpapi/presenter
```
- Run full tests:
```sh
go test ./...
```
- Check status and diff:
```sh
git status --short
git diff --stat
```
- Confirm no unrelated files are included.
- Confirm `go.mod` still requires `gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0`.
- Confirm no weatherfeeder ingestion behavior, schema creation, migration logic, or upstream SPC calls were added.
- Confirm current-behavior docs no longer describe outlook endpoints as future work once implementation is complete, or leave roadmap docs clearly marked as historical/planning if they remain.
## Open Questions
None. The feature roadmap defines the route names, latest-run semantics, filters, adapter ownership, documentation timing, and validation behavior needed for implementation.
Future changes to convective outlook behavior should be proposed in a new
roadmap entry before implementation.