Skip to content

Station pages reference SnowObs stations by center; the index moves to Payload - #1306

Closed
rchlfryn wants to merge 23 commits into
mainfrom
feat/stations-snowobs-overlay
Closed

rchlfryn wants to merge 23 commits into
mainfrom
feat/stations-snowobs-overlay

Conversation

@rchlfryn

@rchlfryn rchlfryn commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Description

Makes the weather station pages center-aware, moves the /weather/stations index into Payload, gives each station page an editable, ordered list of the SnowObs stations it shows, and gives the Accumulated Precipitation table its own per-center setting. NWAC renders exactly as before.

AvyWeb stores only what SnowObs cannot know: the pages, which stations each one shows and in what order, and which gauges the precip table shows. A station is a (source, stid) reference and nothing more. Names, elevations and coordinates are read live from SnowObs on the public page and in the admin, where the stations field is a table of the page's stations with a search over the center's SnowObs tracking list and an Add button (station/tracking/, which is scoped by the token's client across every source: NWAC 213, SAC 139). There is no local copy of the station list and nothing to sync. See ADR 021 for the reasoning and the API findings behind it.

Everything that assumed NWAC now takes the center: the SnowObs service reads the center's token from its AFP config and sends source as the distinct sources of the stations requested, so a page can mix nwac, snotel and mesowest loggers in one request. Routes 404 for a center with no pages. The /weather/stations index is a normal page under the Weather dropdown, so the URL is unchanged and an editor owns the content. The detail, precipitation and map routes stay native.

Related Issues

Closes #1169. Supersedes #1308 (closed; the sync it added is unnecessary once nothing is mirrored) and the closed #1202. Follow-ups: #1303, #1304.

Key Changes

  • fields/stations: stationsField(), a JSON field of ordered { stid, source } pairs with StationsInput: a table of the chosen stations (live name, id, source, elevation, partner) with drag-to-reorder and a remove button per row, and under it a search over the tracking list with an Add button; the search reads "Name · source" and takes one or several picks. A field can name a required variable: the precip list needs precip_accum_one_hour, so its picker offers only stations reporting it and a listed station that has stopped is marked in its row. A station that has left tracking is marked in its row; when SnowObs is unreachable the table shows stored ids and adding is disabled. validateStations keeps the value to well-formed pairs with no repeats.
  • collections/StationPages: displayName, slug (unique per tenant), archived, stations. hooks/ensureStationsUnique keeps a station on one page per tenant and names the other page on conflict.
  • collections/WeatherStationSettings ("Page Settings" in the admin): one document per center (unique tenant, ADR 016). The Precipitation Table tab holds the stations list; the precip page shows exactly that, top to bottom, dropping a station with no precipitation series and showing "missing" for a gauge that reported none. Station note icons are gone from that table. The Precipitation Columns tab picks which columns the table shows after the station name (services/stations/precipColumns; empty means all). getWeatherStationSettings caches both under the station-pages tag.
  • StationPages/endpoints/trackedStations: GET /api/stationPages/tracked-stations?center=<slug>, signed-in only, proxies station/tracking/ joined with station/data/current/ (so each station carries the variables in its latest observation) with the center's token, cached an hour.
  • services/snowobs/access.ts: token resolution and SnowObsError, free of the Payload config so collection code can import it. snowobs.ts: fetchStationTimeseries(centerSlug, StationRef[], options). stationTracking.ts: the tracking fetch.
  • services/stations/: assemble pages from the array, cache per center, revalidate on change; allStations() maps a bare stid back to its source for the graph-data and CSV routes.
  • services/snowobs/deriveColumns.ts: table columns from the variables the stations report, variable-major.
  • constants/weatherStations.ts, the built-in weather/stations/page.tsx and the stations collection are gone. The seed gives NWAC a stations page instead of the built-in row.

How to test

  1. pnpm seed, then pnpm dev.
  2. Admin → Weather → Station Pages: 32 pages. Open Alpental: three rows with live names, ids, elevations and partners, in table order. The search under the table covers the 213 stations NWAC tracks across sources (try "Nooksack", a SNOTEL site); pick one and press Add; drag the handle to reorder; X removes.
    2a. Admin → Weather → Page Settings: one document, titled with the center name. The Precipitation Table tab lists 52 stations; remove one and reload /weather/stations/accumulated-precipitation; its row is gone. The Precipitation Columns tab: pick 24H, 72H and Elevation only, save, reload; the table has four columns.
  3. Save with a station that is already on another page: the error names that page.
  4. nwac.localhost:3000/weather/stations: the "Weather Data" Payload page, reached from the Weather dropdown.
  5. /weather/stations/alpental and /weather/stations/accumulated-precipitation: as on main. Reorder the rows on Alpental and reload; the columns follow.
  6. sac.localhost:3000/weather/stations/alpental: 404. /weather/stations/map still renders.

Prod: the stations page under Weather in the NWAC admin already exists; point the nav at it and delete the "Weather Data" built-in row.

Screenshots / Demo video

TODO: the Alpental edit form with the add search open, and the settings document.

Migration Explanation

20260918_203650_station_pages creates station_pages (unique (tenant, slug), stations as JSON) and weather_station_settings (unique tenant, with its precip_columns select table), then for the nwac tenant seeds the 32 pages and their station references from migrations/data/nwacStationPages.ts, a settings document whose precip list is the 19 stations with a gauge in page order, and adds both collections to each center's Admin role. Idempotent: a page or settings document that exists is left alone. The .json beside it is Payload's full schema snapshot, as with every migration. The migration-safety bot flags the ON DELETE set null foreign keys and the ALTER TABLE … ADD on payload_locked_documents_rels; those are Payload's standard clauses.

Future enhancements / Questions

  • variable/tracking/ serves per-center units and rounding; it would replace SENSOR_LABELS, UNIT_LABELS and metricUnits.ts (SnowObs says wind's metric unit is m/s; we hardcode km/h).
  • Multi-source fetches were verified with three stations over six hours; worth a check at real page sizes and a season window before a center mixes sources.
  • The spec lists the public token as accepted on the tracking write endpoints. Not probed; a question for Snowbound, along with whether station/tracking/ stays readable with the public token.
  • Station pages: read the display timezone from the center #1303, Station pages for a second center #1304.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YDHvoY5YQsE8iV24NuUqcL

@github-actions

Copy link
Copy Markdown
Contributor

@rchlfryn
rchlfryn added this pull request to stack #1309 September 15, 2026 22:42
@rchlfryn rchlfryn self-assigned this Sep 15, 2026
@rchlfryn
rchlfryn force-pushed the feat/stations-snowobs-overlay branch from 741803f to 11a2f7f Compare September 16, 2026 13:43
@rchlfryn rchlfryn changed the title Move the weather station registry into Payload collections synced from SnowObs Key the weather station pages by center and move the index to Payload Sep 16, 2026
@NWACus NWACus deleted a comment from github-actions Bot Sep 16, 2026
…apshot, and read the station pages from them

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BBfZ9kTcLyGMhsgFwYBiGi
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Migration Safety Check

Found 5 potential issues:

20260918_203650_station_pages.ts

Warning (line 19): DELETE keyword detected - review for data loss

FOREIGN KEY (\`tenant_id\`) REFERENCES \`tenants\`(\`id\`) ON UPDATE no action ON DELETE set null

Warning (line 38): DELETE keyword detected - review for data loss

FOREIGN KEY (\`parent_id\`) REFERENCES \`weather_station_settings\`(\`id\`) ON UPDATE no action ON DELETE cascade

Warning (line 54): DELETE keyword detected - review for data loss

FOREIGN KEY (\`tenant_id\`) REFERENCES \`tenants\`(\`id\`) ON UPDATE no action ON DELETE set null

Warning (line 67): ALTER keyword detected - review for data loss

sql`ALTER TABLE \`payload_locked_documents_rels\` ADD \`station_pages_id\` integer REFERENCES station_pages(id);`,

Warning (line 70): ALTER keyword detected - review for data loss

sql`ALTER TABLE \`payload_locked_documents_rels\` ADD \`weather_station_settings_id\` integer REFERENCES weather_station_settings(id);`,

Review these patterns and add backup/restore logic if needed. See docs/migration-safety.md for guidance.

rchlfryn and others added 3 commits September 16, 2026 11:44
…ream-owned identity

Station page slugs are URLs, so (tenant, slug) is now a unique index alongside
the (tenant, source, stid) one on stations. The SnowObs identity fields were
read-only in the admin but writable through the API; field-level access now
refuses updates, and the sync and seed keep writing through the local API.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The snapshot is the whole SnowObs catalogue, so the migration was creating
seven rows nothing shows: a logger retired in 2019 and six 5-minute duplicates
of hourly stations. The sync never deletes, so they would have sat in the
Stations list for good. Now only the 53 stations on a page become rows.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
…ets a precip columns tab

The stations table now keeps the search visible under the rows with an Add
button, shows the SnowObs id beside the name, and heads the name column
"Name". The per-center settings document is labelled "Page Config", its
precip list is "Stations", and a Precipitation Columns tab chooses which
columns the Accumulated Precipitation table shows after the station name
(trailing windows, last update, latitude, longitude, elevation); an empty
choice shows them all. Migration regenerated for the new select.

Verified on a fresh seed: the admin renders both tabs, Add is disabled until
a station is chosen and appends on click, and choosing 24H, 72H and
Elevation leaves the public table with four columns.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The per-center station settings document is labelled "Page Settings" and
takes its title from the tenant's name through a virtual field, so the edit
view and the list read "Northwest Avalanche Center" rather than "ID: 1". No
schema change: the field is virtual.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
… its height

The search under the stations table takes one or several stations and Add
appends them in the order picked ("Add 2" when more than one). It has an
"Add station" label, sits at 32rem rather than full width, and the button
matches the control's single-line height and stays at the top as chosen
stations wrap.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
rchlfryn and others added 2 commits September 18, 2026 14:16
33 of NWAC's 52 listed stations have no gauge and showed as "missing" rows.
A station whose response carries no precipitation series is now left off;
one whose series is present but empty still shows as missing. The setting's
description says so.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Payload gives a tabs field a top margin unless it is the first field, and the
hidden title input made it second.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
rchlfryn and others added 2 commits September 18, 2026 14:44
…marked, seed lists the 19 gauges

The tracked-stations endpoint now joins station/data/current/ so each station
carries the variables in its latest observation. A stations field can name a
required variable; the precip list needs precip_accum_one_hour, so its picker
offers only stations reporting it and a listed station that has stopped is
noted in its row, next to the "not tracked" note. Picker entries read
"Name · source". The seed's precip list is the 19 loggers with a gauge, in
page order, rather than every logger on a live page.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The flag and legend duplicated what the station page shows; the table reads
cleaner without them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The builder still sorted rows by latitude as the legacy page did, so dragging
a station in the settings changed nothing on the page. Rows now keep the
order requested; the header sort still works on top.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The tab's own text says what the list does; the field's description is now
optional and this one has none.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@rchlfryn

Copy link
Copy Markdown
Collaborator Author

Closing in favour of a stack built from this branch's final tree: #1315 (station pages collection, picker, center-dynamic routes, the station_pages migration) and #1316 on top of it (the index page in Payload, Page Settings for the precipitation table; no migration, handled by hand). The history here stays for reference.

@rchlfryn rchlfryn closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make weather station data pages center-dynamic

1 participant