Tap a Coming up card -> hourly-detail modal, 20s idle auto-close
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>
This commit is contained in:
parent
c6be990f6b
commit
13fb30d9a3
10 changed files with 370 additions and 75 deletions
|
|
@ -65,8 +65,13 @@ async function fetchFromMetOffice(lat, lon, apiKey) {
|
|||
|
||||
const today = londonDateKey(new Date().toISOString())
|
||||
|
||||
// Store every hour the endpoint returns, not just today's — the "tap a Coming up
|
||||
// card" hourly-detail modal needs this for tomorrow/day-after too (see
|
||||
// routes/forecast.js's /forecast/hourly). Met Office's hourly product window
|
||||
// isn't documented/verified here (unlike Open-Meteo, which we know covers the
|
||||
// full forecast_days=8 range) — whatever it actually returns just becomes
|
||||
// whatever range the modal can show; no assumption made about how far it goes.
|
||||
const hourlyPoints = hourly
|
||||
.filter(p => londonDateKey(p.time) === today)
|
||||
.map(p => ({
|
||||
resolution: 'hourly',
|
||||
ts: p.time,
|
||||
|
|
@ -153,8 +158,11 @@ async function fetchFromOpenMeteo(lat, lon) {
|
|||
windDir: data.hourly.winddirection_10m?.[i] ?? null,
|
||||
}))
|
||||
|
||||
// Store every hour across the full forecast_days=8 window, not just today — the
|
||||
// "tap a Coming up card" hourly-detail modal needs tomorrow/day-after (and, where
|
||||
// available, days 4-7) too. This is free: the hourly= block already covers all 8
|
||||
// days in this one call, today's strip just used to discard everything past today.
|
||||
const hourlyPoints = hourlyRows
|
||||
.filter(r => r.dateKey === today)
|
||||
.map(r => ({
|
||||
resolution: 'hourly',
|
||||
ts: r.time,
|
||||
|
|
|
|||
|
|
@ -16,15 +16,25 @@ function rowToPoint(r) {
|
|||
}
|
||||
}
|
||||
|
||||
// forecast_points' 'hourly' resolution now holds every day the provider returned
|
||||
// (see lib/weather-fetch.js — needed for the per-day hourly modal below), not just
|
||||
// today, so the main payload's `hourly` field is scoped to today with its own query
|
||||
// rather than filtering a much bigger already-fetched set in JS.
|
||||
async function buildPayload() {
|
||||
const { rows: hourlyToday } = await pool.query(
|
||||
`SELECT * FROM forecast_points
|
||||
WHERE resolution = 'hourly'
|
||||
AND (ts AT TIME ZONE 'Europe/London')::date = (NOW() AT TIME ZONE 'Europe/London')::date
|
||||
ORDER BY ts ASC`
|
||||
)
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM forecast_points ORDER BY ts ASC`
|
||||
`SELECT * FROM forecast_points WHERE resolution IN ('3hourly', 'daily') ORDER BY ts ASC`
|
||||
)
|
||||
const { rows: astroRows } = await pool.query(
|
||||
`SELECT date, sunrise, sunset FROM forecast_astro ORDER BY date ASC`
|
||||
)
|
||||
const status = await getStatus()
|
||||
const hourly = rows.filter(r => r.resolution === 'hourly').map(rowToPoint)
|
||||
const hourly = hourlyToday.map(rowToPoint)
|
||||
// "Later" is relative to whoever's looking at the screen right now, not to when
|
||||
// the hourly data was last fetched — compute this fresh per-request, not at
|
||||
// refresh time (see lib/summary.js).
|
||||
|
|
@ -44,12 +54,37 @@ async function buildPayload() {
|
|||
}
|
||||
}
|
||||
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||
|
||||
// Public, unauthenticated — this is the whole point of the app: a reception lobby
|
||||
// display can't reasonably sit behind a login screen. No PII, no write actions, just
|
||||
// read-only forecast data, so it deliberately skips requireAuth entirely (see
|
||||
// project_weather memory's access-model decision).
|
||||
export async function publicForecastRoutes(app) {
|
||||
app.get('/api/public/forecast', async () => buildPayload())
|
||||
|
||||
// GET /api/public/forecast/hourly?date=YYYY-MM-DD — backs the "tap a Coming up
|
||||
// card" modal. Whether a date beyond tomorrow/day-after actually has hourly rows
|
||||
// depends entirely on how far the active provider's hourly product reaches
|
||||
// (confirmed for Open-Meteo: the full forecast_days=8 window; NOT verified for Met
|
||||
// Office — see lib/weather-fetch.js) — an empty `points` array just means "no
|
||||
// hourly detail for this day", the frontend falls back to whatever coarser data
|
||||
// it already has (3-hourly or the daily summary) rather than treating it as an
|
||||
// error.
|
||||
app.get('/api/public/forecast/hourly', async (req, reply) => {
|
||||
const date = req.query?.date
|
||||
if (typeof date !== 'string' || !DATE_RE.test(date)) {
|
||||
return reply.status(400).send({ error: 'date must be YYYY-MM-DD' })
|
||||
}
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM forecast_points
|
||||
WHERE resolution = 'hourly'
|
||||
AND (ts AT TIME ZONE 'Europe/London')::date = $1::date
|
||||
ORDER BY ts ASC`,
|
||||
[date]
|
||||
)
|
||||
return { date, points: rows.map(rowToPoint) }
|
||||
})
|
||||
}
|
||||
|
||||
// Service-to-service — same bearer-secret convention as settings' /internal/*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue