Document convective outlook endpoints

This commit is contained in:
2026-06-11 15:46:00 +00:00
parent e897ae52df
commit 993621e3b3
7 changed files with 228 additions and 20 deletions

View File

@@ -2,8 +2,8 @@
`weatherapi` is a read-only HTTP API for weather data already stored by `weatherapi` is a read-only HTTP API for weather data already stored by
`weatherfeeder` in PostgreSQL. It exposes the latest observations, current `weatherfeeder` in PostgreSQL. It exposes the latest observations, current
conditions, alerts, forecasts, discussions, and weather stories as JSON, XML, conditions, alerts, forecasts, discussions, weather stories, and convective
or text. outlooks as JSON, XML, or text.
## Quickstart ## Quickstart
@@ -26,6 +26,11 @@ database credentials through your normal secret-management process.
- Hourly and narrative forecasts - Hourly and narrative forecasts
- Forecast discussions - Forecast discussions
- Weather stories - Weather stories
- Convective outlooks
Common query parameters include `format`, `units`, and route-specific options
such as forecast `precision`, timezone `tz` / `TZ`, and outlook filters
`day`, `outlookType`, and `containsLocation`.
See [`docs/api.md`](docs/api.md) for the HTTP contract. See [`docs/api.md`](docs/api.md) for the HTTP contract.

View File

@@ -63,16 +63,17 @@ Unknown query parameters are rejected with `400 Bad Request`.
| `format` | `json`, `xml`, `text` | configured default | all endpoints | | `format` | `json`, `xml`, `text` | configured default | all endpoints |
| `units` | `metric`, `us` | `metric` | all endpoints | | `units` | `metric`, `us` | `metric` | all endpoints |
| `precision` | integer `0` through `2` | `0` | observations, current conditions, forecasts | | `precision` | integer `0` through `2` | `0` | observations, current conditions, forecasts |
| `tz` or `TZ` | timezone selector | UTC/no conversion | forecasts, discussions, weather stories | | `tz` or `TZ` | timezone selector | UTC/no conversion | forecasts, discussions, weather stories, outlooks |
`units`, `format`, and `precision` values are normalized case-insensitively `units`, `format`, and `precision` values are normalized case-insensitively
where applicable. `units=metric` returns metric field names; `units=us` returns where applicable. `units=metric` returns metric field names; `units=us` returns
US-customary field names for unit-bearing payloads. Alerts, discussions, and US-customary field names for unit-bearing payloads. Alerts, discussions, and
weather stories accept `units` but their current payload fields are not weather stories accept `units` but their current payload fields are not
materially changed by it. materially changed by it. Outlooks also accept `units` without changing payload
values or field names.
`precision` controls numeric rounding. The default `0` rounds to whole numbers. `precision` controls numeric rounding. The default `0` rounds to whole numbers.
`precision` is rejected on alerts, discussions, and weather stories. `precision` is rejected on alerts, discussions, weather stories, and outlooks.
Timezone selectors accepted by `tz` / `TZ`: Timezone selectors accepted by `tz` / `TZ`:
@@ -220,6 +221,151 @@ Example:
GET /alerts/active?format=text GET /alerts/active?format=text
``` ```
### Convective Outlooks
```http
GET /outlooks/convective
GET /outlooks/convective/active
GET /outlooks/convective/location
```
Returns the latest SPC convective outlook run reconstructed from
weatherfeeder-owned Postgres tables.
Route behavior:
- `/outlooks/convective` returns the latest run with all stored outlook
polygons unless user filters are supplied.
- `/outlooks/convective/active` adds an active-time filter using the server's
current UTC time. Outlooks are active when `validFrom <= now < validTo`.
- `/outlooks/convective/location` adds the same active-time filter and
`containsLocation=true`.
When no latest run exists, `data` is null. When a run exists but filters match
no outlooks, `data` remains an object and `outlooks` is an empty array.
Query parameters:
| Parameter | Supported on | Values |
| --- | --- | --- |
| `format`, `units`, `tz` / `TZ` | all outlook routes | shared rules above |
| `day` | all outlook routes | `1`, `2`, or `3` |
| `outlookType` | all outlook routes | `categorical`, `tornado`, `hail`, or `wind` |
| `containsLocation` | `/outlooks/convective`, `/outlooks/convective/active` | boolean |
`outlookType` values are normalized case-insensitively. `containsLocation` is
rejected on `/outlooks/convective/location` because that route always applies
`containsLocation=true`. `precision` and unknown parameters are rejected.
Run `data` fields:
| Field | Type | Notes |
| --- | --- | --- |
| `locationId`, `locationName` | string | optional |
| `latitude`, `longitude` | number | optional |
| `asOf` | RFC3339 datetime | required when `data` is not null |
| `issuedAt` | RFC3339 datetime | optional |
| `outlooks` | array | ordered outlook polygons, possibly empty |
Outlook fields:
| Field | Type | Notes |
| --- | --- | --- |
| `id`, `provider`, `product`, `outlookType`, `label` | string | required when an outlook is present |
| `day` | integer | SPC outlook day |
| `labelText`, `forecaster`, `headline`, `summary`, `discussion` | string | optional |
| `severityRank` | integer | optional |
| `validFrom`, `validTo`, `issuedAt`, `expiresAt` | RFC3339 datetime | required when an outlook is present |
| `sourceUrl`, `imageUrl` | string | optional |
| `containsLocation` | boolean | whether the outlook polygon contains the configured location |
| `geometry` | GeoJSON | stored outlook geometry |
GeoJSON coordinates use standard GeoJSON coordinate order: longitude, then
latitude. Timezone conversion applies to run `asOf`, run `issuedAt`, and each
outlook's `validFrom`, `validTo`, `issuedAt`, and `expiresAt`. Active filtering
compares instants and is not changed by the presentation timezone.
Examples:
```http
GET /outlooks/convective?day=1&outlookType=categorical
GET /outlooks/convective/location?format=text&tz=CDT
```
Example JSON response:
```json
{
"data": {
"locationId": "stl",
"locationName": "St. Louis",
"asOf": "2026-06-11T18:00:00Z",
"issuedAt": "2026-06-11T17:00:00Z",
"outlooks": [
{
"id": "spc-day1-cat-slight",
"provider": "spc",
"product": "convective",
"day": 1,
"outlookType": "categorical",
"label": "SLGT",
"labelText": "Slight Risk",
"severityRank": 5,
"validFrom": "2026-06-11T18:00:00Z",
"validTo": "2026-06-12T12:00:00Z",
"issuedAt": "2026-06-11T17:00:00Z",
"expiresAt": "2026-06-12T12:00:00Z",
"containsLocation": true,
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-91.0, 38.0],
[-90.0, 38.0],
[-90.0, 39.0],
[-91.0, 38.0]
]
]
}
}
]
}
}
```
Example location-filtered JSON response:
```json
{
"data": {
"locationId": "stl",
"asOf": "2026-06-11T18:00:00Z",
"outlooks": [
{
"id": "spc-day1-tor-2pct",
"provider": "spc",
"product": "convective",
"day": 1,
"outlookType": "tornado",
"label": "2%",
"validFrom": "2026-06-11T18:00:00Z",
"validTo": "2026-06-12T12:00:00Z",
"issuedAt": "2026-06-11T17:00:00Z",
"expiresAt": "2026-06-12T12:00:00Z",
"containsLocation": true,
"geometry": {
"type": "Point",
"coordinates": [-90.2, 38.6]
}
}
]
}
}
```
Text format uses the shared convective outlook template for all three outlook
routes and renders a no-data message when `data` is null.
### Forecasts ### Forecasts
```http ```http

View File

@@ -37,6 +37,7 @@ Postgres owns persistence, backup, restore, and availability.
| Forecast discussion | `forecast_discussions`, `forecast_discussion_key_messages` | | Forecast discussion | `forecast_discussions`, `forecast_discussion_key_messages` |
| Weather story run | `weather_story_runs`, `weather_stories` | | Weather story run | `weather_story_runs`, `weather_stories` |
| Latest weather story | `weather_stories` | | Latest weather story | `weather_stories` |
| Convective outlook run | `outlook_runs`, `outlooks` |
## Latest Row Selection ## Latest Row Selection
@@ -52,6 +53,7 @@ Latest parent resources use these ordering rules:
- weather story runs: `as_of DESC, event_emitted_at DESC`; - weather story runs: `as_of DESC, event_emitted_at DESC`;
- latest individual weather story: `updated_at DESC, as_of DESC, - latest individual weather story: `updated_at DESC, as_of DESC,
story_order ASC, story_index ASC`. story_order ASC, story_index ASC`.
- convective outlook runs: `as_of DESC, event_emitted_at DESC`.
Current conditions aggregate `observations` rows where `observed_at` is inside Current conditions aggregate `observations` rows where `observed_at` is inside
the application-provided observation window. the application-provided observation window.
@@ -65,7 +67,8 @@ Child rows are loaded separately and attached in stored order:
- alert references: `alert_index ASC, reference_index ASC`; - alert references: `alert_index ASC, reference_index ASC`;
- forecast periods: `period_index ASC`; - forecast periods: `period_index ASC`;
- forecast discussion key messages: `message_index ASC`; - forecast discussion key messages: `message_index ASC`;
- weather stories for a run: `story_index ASC`. - weather stories for a run: `story_index ASC`;
- outlooks for a run: `outlook_index ASC`.
## Columns Read ## Columns Read
@@ -148,6 +151,21 @@ routes.
`description`, `alt_text`, `priority`, `story_order`, `download_url`, `description`, `alt_text`, `priority`, `story_order`, `download_url`,
`run_event_id`, and `as_of`. `run_event_id`, and `as_of`.
### `outlook_runs`
`event_id`, `location_id`, `location_name`, `latitude`, `longitude`, `as_of`,
`issued_at`, and `event_emitted_at`.
### `outlooks`
`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`, and `run_event_id`.
`geometry_json` is copied into response GeoJSON without parsing or
reserializing. It must contain valid JSON.
## Nullability and Time Assumptions ## Nullability and Time Assumptions
The repository scans nullable columns with `sql.Null*` types and maps them to The repository scans nullable columns with `sql.Null*` types and maps them to

View File

@@ -8,7 +8,8 @@ This document describes the internal HTTP adapter under
The HTTP adapter turns feedapi route definitions into calls on the application The HTTP adapter turns feedapi route definitions into calls on the application
service boundary. It owns route registration, query binding, request validation, service boundary. It owns route registration, query binding, request validation,
forecast day-slice filtering, response envelopes, and template names. forecast day-slice filtering, outlook active/location filter construction,
response envelopes, and template names.
## Inputs and Outputs ## Inputs and Outputs
@@ -33,7 +34,8 @@ The adapter may:
- bind and validate query parameters; - bind and validate query parameters;
- call the `Service` interface; - call the `Service` interface;
- choose the presenter function for an endpoint; - choose the presenter function for an endpoint;
- filter forecast copies for `/today` and `/tomorrow`. - filter forecast copies for `/today` and `/tomorrow`;
- construct outlook active/location filters.
The adapter must not: The adapter must not:
@@ -59,9 +61,9 @@ declared in endpoint definitions, but `templates.base_dir` is loaded by feedapi.
## State ## State
The adapter has no durable state. `forecastNow` is package-level state only to The adapter has no durable state. `forecastNow` and `outlookNow` are
make forecast day filtering testable. Do not add request caches or cross-request package-level state only to make time-dependent endpoint tests deterministic.
mutable state here. Do not add request caches or cross-request mutable state here.
## Route Registry ## Route Registry
@@ -71,6 +73,7 @@ mutable state here.
- active alerts; - active alerts;
- current conditions; - current conditions;
- weather stories; - weather stories;
- convective outlooks;
- forecast discussions; - forecast discussions;
- hourly and narrative forecasts. - hourly and narrative forecasts.
@@ -80,19 +83,25 @@ middleware, and error normalization after definitions are registered.
## Query Binding ## Query Binding
There are three binder shapes: Binder shapes include:
- `bindQuery`: `format` and `units`; - `bindQuery`: `format` and `units`;
- `bindPrecisionQuery`: `format`, `units`, and `precision`; - `bindPrecisionQuery`: `format`, `units`, and `precision`;
- `bindForecastPrecisionQuery`: `format`, `units`, `precision`, and timezone; - `bindForecastPrecisionQuery`: `format`, `units`, `precision`, and timezone;
- `bindTimezoneQuery`: `format`, `units`, and timezone. - `bindTimezoneQuery`: `format`, `units`, and timezone.
- outlook binders: `format`, `units`, timezone, and outlook filters.
All binders use feedapi binding helpers with `RejectUnknown: true`. Supported All binders use feedapi binding helpers with `RejectUnknown: true`. Supported
common query values are lowercased and trimmed before binding where applicable. common query values are lowercased and trimmed before binding where applicable.
`precision` defaults to `0` and must be between `0` and `2`. Timezone parsing is `precision` defaults to `0` and must be between `0` and `2`. Timezone parsing is
available only through binders used by forecast, discussion, and weather story available only through binders used by forecast, discussion, weather story, and
routes. outlook routes.
Outlook routes accept `day`, `outlookType`, and, except for
`/outlooks/convective/location`, `containsLocation`. The location route always
adds `containsLocation=true` after binding and rejects an explicit
`containsLocation` query value.
## Timezone Parsing ## Timezone Parsing
@@ -122,6 +131,19 @@ Filtering behavior:
The package variable `forecastNow` exists so endpoint tests can make day-slice The package variable `forecastNow` exists so endpoint tests can make day-slice
behavior deterministic. behavior deterministic.
## Outlook Filters
Outlook route filters are built at the HTTP boundary and passed to the
application service:
- `/outlooks/convective` uses only user-supplied filters;
- `/outlooks/convective/active` adds `ActiveAt=outlookNow().UTC()`;
- `/outlooks/convective/location` adds the same active timestamp and
`ContainsLocation=true`.
The package variable `outlookNow` exists so endpoint tests can make active and
location filtering deterministic.
## Failure Behavior ## Failure Behavior
Binder failures become feedapi invalid-parameter responses. Handler service Binder failures become feedapi invalid-parameter responses. Handler service

View File

@@ -21,8 +21,8 @@ Inputs:
Outputs: Outputs:
- latest observation, forecast, discussion, weather story, alert, and current - latest observation, forecast, discussion, weather story, alert, convective
conditions read models; outlook, and current conditions read models;
- nil data with nil error when the latest resource does not exist; - nil data with nil error when the latest resource does not exist;
- contextual errors for query, scan, iteration, and JSON decode failures. - contextual errors for query, scan, iteration, and JSON decode failures.
@@ -89,6 +89,8 @@ successful responses with `data: null`.
- `LatestWeatherStoryRun`: latest row from `weather_story_runs`, then child - `LatestWeatherStoryRun`: latest row from `weather_story_runs`, then child
`weather_stories`. `weather_stories`.
- `LatestWeatherStory`: latest individual row from `weather_stories`. - `LatestWeatherStory`: latest individual row from `weather_stories`.
- `LatestConvectiveOutlookRun`: latest row from `outlook_runs`, then child
`outlooks`.
Latest parent rows are selected by descending weather timestamp and Latest parent rows are selected by descending weather timestamp and
`event_emitted_at` where that tie-breaker is available in the query. `event_emitted_at` where that tie-breaker is available in the query.
@@ -102,7 +104,8 @@ Child queries preserve stored order:
- alert references by `alert_index`, then `reference_index`; - alert references by `alert_index`, then `reference_index`;
- forecast periods by `period_index`; - forecast periods by `period_index`;
- discussion key messages by `message_index`; - discussion key messages by `message_index`;
- weather stories by `story_index`. - weather stories by `story_index`;
- outlooks by `outlook_index`.
Alert references are attached after both alert and reference rows are loaded. Alert references are attached after both alert and reference rows are loaded.
References are grouped by alert index and attached to their corresponding alert. References are grouped by alert index and attached to their corresponding alert.
@@ -126,6 +129,10 @@ Observation present-weather rows store raw JSON text. Empty or null text maps
to an empty present-weather value. Invalid JSON returns a contextual decode to an empty present-weather value. Invalid JSON returns a contextual decode
error with the weather index. error with the weather index.
Outlook rows store `geometry_json` as compact GeoJSON text. The repository
validates and copies the JSON bytes into `json.RawMessage` without parsing or
reserializing the geometry.
## Failure Behavior ## Failure Behavior
Repository methods wrap failures with operation context, for example: Repository methods wrap failures with operation context, for example:

View File

@@ -93,9 +93,9 @@ Latitude and longitude are copied but not rounded by forecast presenters.
timezone is supplied. Without a timezone, timestamps are preserved as returned timezone is supplied. Without a timezone, timestamps are preserved as returned
by the repository. by the repository.
Timezone conversion is applied by forecast, discussion, and weather story Timezone conversion is applied by forecast, discussion, weather story, and
presenters. Observations and current conditions do not currently receive outlook presenters. Observations and current conditions do not currently
timezone input from their routes. receive timezone input from their routes.
## Optional Fields and Copy Semantics ## Optional Fields and Copy Semantics
@@ -118,6 +118,9 @@ response envelope so renderers can produce `data: null`.
- Discussions: full or focused payload shapes, section copy, key-message copy, - Discussions: full or focused payload shapes, section copy, key-message copy,
timezone conversion. timezone conversion.
- Weather stories: run/story copy and timezone conversion. - Weather stories: run/story copy and timezone conversion.
- Convective outlooks: canonical model copy, pointer and geometry copy, and
timezone conversion. `units` is accepted by routes but ignored by the
presenter because outlook fields are not unit-bearing.
## Templates ## Templates

View File

@@ -11,6 +11,13 @@ Accept: application/json
### Active alerts as text ### Active alerts as text
GET {{baseUrl}}/alerts/active?format=text GET {{baseUrl}}/alerts/active?format=text
### Convective outlook day 1 categorical risk
GET {{baseUrl}}/outlooks/convective?day=1&outlookType=categorical
Accept: application/json
### Active convective outlooks for the configured location as text
GET {{baseUrl}}/outlooks/convective/location?format=text&tz=CDT
### Hourly forecast in US units ### Hourly forecast in US units
GET {{baseUrl}}/forecast/hourly?units=us&precision=1&tz=Chicago GET {{baseUrl}}/forecast/hourly?units=us&precision=1&tz=Chicago
Accept: application/json Accept: application/json