feat(insights): day navigator on the intraday pulse chart; 90-day dense retention with hourly fallback

Adds prev/next-day paging to the intraday pulse "shape of the day" chart
(TileHeader right slot, next disabled at today) so a user can step back
through prior days on the existing date-scoped route.

Raises DENSE_INTRADAY_RETENTION_DAYS from 14 to 90 — a self-hoster's own
storage is the trade-off for three months of full 10-minute-resolution
history instead of two weeks. Days outside the window keep folding to
hourly-mean stats: rows as before; loadIntradayPulse now falls back to
that hourly tier when a day's raw samples are gone, so the navigator
shows a coarser shape instead of an empty chart. The tension-window
read is suppressed on hourly days — it needs per-sample resolution to
stay honest. A new resolution field on the DTO ("tenMin" | "hourly")
drives a small note in the chart.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 14:29:07 +02:00
1 parent 3aa11b0531
commit 4f198ec25f
16 files changed
+354 -32

No files matched your search

+6 -1
View File
@@ -10,7 +10,11 @@
* Cookie OR Bearer auth via `requireAuth()`; `userId` is narrowed from the
* resolved session — never a body field. Gated on the `insights` module. The
* `date` defaults to the user's local today; anything but a `YYYY-MM-DD`
* literal is a 422 via `returnAllZodIssues`.
* literal is a 422 via `returnAllZodIssues`. The frontend day navigator
* pages this same route backward through prior days — a day outside
* `DENSE_INTRADAY_RETENTION_DAYS` (`dense-intraday-retention.ts`) reads back
* at the coarser hourly grain instead of an empty series; the response's
* `resolution` field tells the caller which.
*/
import { z } from "zod";
@@ -54,6 +58,7 @@ export const GET = apiHandler(async (req: Request) => {
buckets: result.series.length,
baseline_source: result.baselineSource,
tension: result.tension !== null,
resolution: result.resolution,
},
});