Met Office's daily series can include a stray leftover entry for
today/yesterday that the old exact-match skip set (today, +1, +2)
didn't catch. It sorted to the front of the 4-day window and showed
up as an extra day ahead of the real day+3..+6 (live symptom: kiosk
bottom row showing Sun, Thu, Fri, Sat instead of Thu, Fri, Sat, Sun).
Now filters on "date key > cutoff" (day+2) so any stray day before
the window is excluded regardless of why it appeared, and explicitly
sorts before slicing so the window is chronological even if the
provider's array isn't. Applied to both the Met Office and Open-Meteo
daily parsers for consistency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Spot is now confirmed subscribed to hourly/three-hourly too (settings'
met_office test matrix), so those parsers are genuinely exercised for the
first time -- previously a wrong field name would silently produce an
empty points array while still reporting the tier as a 'met_office'
success. assertParsed() throws when raw entries came back but none parsed
a valid temp_c, so a field-name mismatch now correctly falls back to
Open-Meteo (and logs an error) instead of quietly showing an empty today
strip.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Forecast key -> hourly was tried and confirmed wrong (settings test route
reported 'not subscribed'). New fetchWithKeys() tries Spot then Forecast
for EVERY tier (hourly, three-hourly, daily) and keeps whichever
succeeds, rather than assigning a specific key to a specific endpoint
based on an unverified guess about Met Office's product catalog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was: one Promise.all across all 3 Met Office endpoints, any single failure
threw and discarded ALL of it, falling back to Open-Meteo entirely -- even
though only the daily endpoint has ever been confirmed to work (only Spot
key configured; hourly/three-hourly may need the separate Forecast
subscription settings just gained a field for). So the display was very
possibly running on Open-Meteo for everything despite a working Met Office
key, silently.
Now: hourly, three-hourly and daily are fetched independently
(Promise.allSettled). hourly/three-hourly try met_office's new
forecast_api_key first (falling back to api_key if no separate Forecast
key is set); daily keeps using api_key (Spot), matching what's actually
confirmed live. Only the tiers that fail fall back to Open-Meteo -- and
Open-Meteo itself is now fetched lazily, once, only if at least one tier
actually needs it.
status.last_provider is now {hourly, threeHourly, daily} instead of one
flat string, so the admin page shows exactly which source served which
tier. Admin.tsx / types.ts updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend now stores hourly points for every day the provider returns (not
just today) -- for Open-Meteo this is free, the hourly= block already
covers the full forecast_days=8 window in the one call, today's strip just
discarded everything past today. Met Office's hourly product window isn't
verified this far out (only daily has been exercised live so far); an
empty result for a given date just means "no hourly detail", handled as a
graceful fallback, not an error.
New GET /api/public/forecast/hourly?date=YYYY-MM-DD backs the modal. The
main /api/public/forecast payload's `hourly` field is now its own scoped
query (today only) rather than filtering the much bigger stored set in JS.
Frontend: tapping any "Coming up" card (tomorrow/day-after or days 4-7)
opens DayModal with that day's hourly points, falling back to the
already-known 3-hourly points (or the single daily summary) if the
provider doesn't have hourly detail that far out. Auto-closes after 20s of
no interaction (pointerdown/keydown/wheel/scroll all reset the timer), with
a depleting progress bar so the countdown isn't a surprise. Extracted
PointMeta/AstroRow and the date/format helpers out of Display.tsx into
shared modules since DayModal needs them too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wrong direction on the previous fix -- hiding the number lost real
information. Better fix: when no hour's icon is rain-classified but there's
still a real (if low) numeric precip_prob somewhere in the day, the
headline now says 'Mostly dry, slim chance of rain' instead of a flat 'Dry
today' that silently disagreed with a 5% sitting on an hour card. Rain% is
shown unconditionally again everywhere (hourly cards, next-2-days cards,
the Now hero line).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CREATE TABLE IF NOT EXISTS is a no-op against a table that already exists —
it doesn't add columns introduced after the first deploy. The live LXC's
forecast_points was created before wind_dir_deg existed, so every insert
in lib/weather-fetch.js has been failing with 'column wind_dir_deg of
relation forecast_points does not exist' since that feature landed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither Met Office's Global Spot API nor Open-Meteo return a narrative text
summary -- both are purely numeric/coded. New lib/summary.js derives one
rule-based from today's already-fetched hourly points (icon + precip_prob),
no extra API call, no LLM:
- Headline: "Scattered showers today", "Rain likely later", "Heavy rain
today", "Dry today", etc. -- compares rain distribution across the whole
day vs specifically the hours still ahead of "now".
- Note: "Thunderstorms possible", appended when a thunder icon shows up.
- Umbrella nudge: shown when there's a real rain chance now or later today.
Computed fresh per-request (not cached at fetch time), since "later" means
later than whoever's looking at the screen right now, not later than the
last hourly refresh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New `config` table + routes/config.js: GET /api/public/config (no auth,
Display.tsx needs it with no session) and PUT /api/admin/config (authed +
'settings' cap). A blank title clears the override and falls back to the
build-time VITE_HOTEL_NAME, so an unconfigured install still shows
something sensible. Admin.tsx gets a "Display title" field above the
existing status card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Today's hourly cards and the next-2-days 3-hourly cards now always show
rain chance% and wind (compass direction + mph), not just rain% above a
20% threshold. New wind_dir_deg column on forecast_points (Met Office
windDirectionFrom10m / Open-Meteo winddirection_10m).
- New forecast_astro table: sunrise/sunset for today + next 2 days, shown
next to each day's section label. No extra API calls needed — both
providers' daily responses already carry sunrise/sunset for those dates,
just weren't being extracted before.
- Today's hourly strip now sits in its own bordered block (wx-today-block)
so it reads as a distinct group instead of blending into the cards below.
- "Next 2 days" no longer has its own separate heading — tomorrow and the
day after now sit under the single "Coming up" label, same as days 4-7.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Backend: Fastify service polling Met Office DataHub (Global Spot: hourly,
three-hourly, daily endpoints) with Open-Meteo fallback, hourly refresh.
Reuses settings' hotel_location/met_office integrations (same pattern as
hvac's lib/weather.js) rather than its own credential copy.
- Three display tiers stored in one forecast_points table: today (hourly),
next 2 days (3-hourly), days 4-7 (plain daily) - 7 days total.
- Public /api/public/forecast (no auth) feeds the reception kiosk display;
/api/internal/forecast (WEATHER_SECRET bearer) lets other apps read the
stored forecast instead of polling providers themselves; /api/admin/*
(normal JWT + settings cap) is the staff status/refresh page.
- Frontend: root / is the public full-screen Display (no AuthGate at all -
first genuinely public app in the stack); /admin is the authed status
page. apps.base_path is /weather/admin so the portal card opens the
admin view directly, precedented by forecasting's history sub-route.
- LXC 131 / 10.10.10.131, registered in auth/src/db.js, push-to-forgejo.sh,
and stack-init/install-stack.sh (deploy_weather + --only dispatch + full
install chain).
- Typechecks and builds clean; not yet deployed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>