Refresh convective outlook roadmap docs
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,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.
|
||||
|
||||
@@ -1,133 +1,14 @@
|
||||
# SPC Convective Outlook API Roadmap
|
||||
|
||||
Status: implemented. This file is retained as planning history; the current
|
||||
public HTTP contract is documented in [`docs/api.md`](../api.md).
|
||||
There are no active roadmap items for the SPC convective outlook API.
|
||||
|
||||
## Summary
|
||||
Current behavior is documented in:
|
||||
|
||||
Add `weatherapi` support for SPC convective outlook data stored by `weatherfeeder`.
|
||||
- [`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 was roadmap-only content while the endpoint work was pending. Current
|
||||
behavior now belongs in `README.md`, `docs/api.md`, and the relevant internal
|
||||
and integration docs.
|
||||
|
||||
Target endpoints:
|
||||
|
||||
- `GET /outlooks/convective`
|
||||
- `GET /outlooks/convective/active`
|
||||
- `GET /outlooks/convective/location`
|
||||
|
||||
Each endpoint returns the standard weatherapi response envelope. Missing latest data returns `{"data": null}`. When a latest run exists but filters match no outlooks, return the run metadata with `outlooks: []`.
|
||||
|
||||
## Architecture And Boundary Decisions
|
||||
|
||||
- `weatherapi` remains read-only. It reads `weatherfeeder` Postgres tables and does not ingest SPC data, call SPC provider APIs, create tables, or run migrations.
|
||||
- The application service owns latest-run filtering policy. The Postgres adapter reconstructs the latest stored canonical run; it should not encode HTTP query semantics.
|
||||
- HTTP adapters own route registration, query binding, request validation, and filter construction.
|
||||
- Presenters own timezone conversion and copy semantics. Repository methods normalize database timestamps to UTC.
|
||||
- Geometry is returned as stored GeoJSON. `weatherapi` must not simplify, transform, or recompute polygons.
|
||||
|
||||
## Public API Contract
|
||||
|
||||
### Routes
|
||||
|
||||
- `/outlooks/convective`: latest convective outlook run with all stored outlook polygons.
|
||||
- `/outlooks/convective/active`: latest run filtered to outlooks where `validFrom <= now < validTo`.
|
||||
- `/outlooks/convective/location`: latest run filtered to currently active outlooks where `containsLocation=true`.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
All routes support:
|
||||
|
||||
- `format=json|xml|text`
|
||||
- `units=metric|us`, accepted for consistency and with no payload effect
|
||||
- `tz` / `TZ`, affecting response timestamp rendering only
|
||||
- `day=1|2|3`
|
||||
- `outlookType=categorical|tornado|hail|wind`
|
||||
|
||||
`/outlooks/convective` and `/outlooks/convective/active` also support:
|
||||
|
||||
- `containsLocation=true|false`
|
||||
|
||||
Reject with `400 Bad Request`:
|
||||
|
||||
- `precision`
|
||||
- unknown query parameters
|
||||
- invalid `day`, `outlookType`, `containsLocation`, or timezone values
|
||||
- conflicting `tz` and `TZ`
|
||||
- `containsLocation` on `/outlooks/convective/location`
|
||||
|
||||
### Response Shape
|
||||
|
||||
Return a `model.WeatherOutlookRun`-compatible payload:
|
||||
|
||||
- run fields: `locationId`, `locationName`, `latitude`, `longitude`, `asOf`, `issuedAt`, `outlooks`
|
||||
- outlook fields: `id`, `provider`, `product`, `day`, `outlookType`, `label`, `labelText`, `severityRank`, `validFrom`, `validTo`, `issuedAt`, `expiresAt`, `forecaster`, `headline`, `summary`, `discussion`, `sourceUrl`, `imageUrl`, `containsLocation`, `geometry`
|
||||
|
||||
Timezone conversion applies to run `asOf`, run `issuedAt`, and outlook `validFrom`, `validTo`, `issuedAt`, and `expiresAt`. Active filtering compares instants and is not timezone-dependent.
|
||||
|
||||
## Implementation Stages
|
||||
|
||||
### Stage 1: Application Use Case
|
||||
|
||||
- Extend `internal/app.Repository` with `LatestConvectiveOutlookRun(ctx)`.
|
||||
- Add `app.OutlookFilter` with optional `Day`, `OutlookType`, `ContainsLocation`, and `ActiveAt` fields.
|
||||
- Add `LatestConvectiveOutlook(ctx, filter)` to `app.Service`.
|
||||
- Implement filtering by cloning the latest repository run and filtering the copied outlook slice while preserving order.
|
||||
- Use an injectable request-time value from the HTTP adapter for active/location filters.
|
||||
|
||||
### Stage 2: Postgres Read Adapter
|
||||
|
||||
- Add outlook SQL, row DTOs, mapper, and read methods under `internal/adapters/outbound/postgres`, following the weather stories read pattern.
|
||||
- Query the latest parent row from `outlook_runs` by `as_of DESC, event_emitted_at DESC`.
|
||||
- Load child rows from `outlooks` by `run_event_id`, ordered by `outlook_index ASC`.
|
||||
- Map all canonical outlook columns, including `outlook_id`, `provider`, `contains_location`, and `geometry_json`.
|
||||
- Normalize timestamps to UTC and preserve `geometry_json` as `json.RawMessage`.
|
||||
- Return `nil, nil` when no latest parent row exists.
|
||||
|
||||
### Stage 3: HTTP Adapter And Presenter
|
||||
|
||||
- Extend `internal/adapters/inbound/httpapi.Service` with `LatestConvectiveOutlook(ctx, app.OutlookFilter)`.
|
||||
- Register the three outlook routes with JSON, XML, and text support.
|
||||
- Add an outlook query binder for common query params plus `day`, `outlookType`, and `containsLocation` validation.
|
||||
- Add `outlookNow`, defaulting to `time.Now`, for deterministic active/location endpoint tests.
|
||||
- Add presenter helpers that copy the model, convert timestamps to the requested timezone, preserve geometry bytes, and never mutate repository-returned values.
|
||||
- Add `templates/outlooks_convective.txt.tmpl` for all three outlook routes.
|
||||
|
||||
### Stage 4: Documentation After Implementation
|
||||
|
||||
Update current-behavior docs only in the same change that implements the routes:
|
||||
|
||||
- `README.md`: endpoint list and short query-parameter summary.
|
||||
- `docs/api.md`: route family, query params, validation behavior, response fields, examples, `containsLocation` semantics, active filtering semantics, and GeoJSON longitude/latitude coordinate order.
|
||||
- Internal or integration docs only if implementation changes repository assumptions or adapter boundaries beyond the planned latest-run reads.
|
||||
|
||||
## Test Plan
|
||||
|
||||
- App tests: delegation, no-data behavior, active/day/type/location filters, combined filters, and non-mutating clone behavior.
|
||||
- Postgres tests: parent and child row mapping, nullable fields, UTC normalization, geometry preservation, child ordering, and no-row behavior.
|
||||
- HTTP tests: route registration, JSON/XML/text responses, null data, empty filtered outlooks, timezone conversion, valid filters, rejected query params, invalid filter values, invalid timezone, and conflicting `tz`/`TZ`.
|
||||
- Presenter tests: nil input, timezone conversion, copy semantics, and exact geometry preservation.
|
||||
|
||||
Verification commands:
|
||||
|
||||
```sh
|
||||
go test ./internal/app
|
||||
go test ./internal/adapters/outbound/postgres
|
||||
go test ./internal/adapters/inbound/httpapi
|
||||
go test ./internal/adapters/inbound/httpapi/presenter
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Assumptions And Defaults
|
||||
|
||||
- `weatherfeeder v0.11.0` or the active workspace module provides `model.WeatherOutlookRun` and `model.WeatherOutlook`.
|
||||
- The first implementation serves only latest-run views; historical browsing remains future work.
|
||||
- `/outlooks/convective` returns all stored latest-run polygons by default, including polygons that do not contain the configured location.
|
||||
- `/outlooks/convective/location` means active and `containsLocation=true`.
|
||||
- `units` is accepted but does not alter outlook payload values or field names.
|
||||
- No `weatherapi` database migration is required.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None. Route names, filtering semantics, adapter ownership, documentation timing, and verification expectations are decision-complete for implementation.
|
||||
Future changes to convective outlook behavior should be proposed in a new
|
||||
roadmap entry before implementation.
|
||||
|
||||
Reference in New Issue
Block a user