App dashboard: anchor stale-device views to latest available data
- Date: 2026-06-08
- Surface:
app.xylolabs.com(frontend-app/) + backend (crates/xylolabs-server) - Trigger: KOSPO facility has 2 devices with no data since 2026-06-03; the
dashboard, recent timeline, and daily report all show blank because every
view anchors its window to
Date.now().
Goal
When a device stops reporting, the operator app should keep showing that device's most recent real data as if it were the current state — populated sensor cards, a populated recent timeline, and a normal-looking daily report — rather than empty widgets. Real data only; no fabricated points or shifted timestamps. Staleness is de-emphasized (a subtle caption), not hidden entirely (owner choice: "최신값을 현재값처럼, staleness 비강조").
Non-goals / guardrails (integrity)
- Do NOT synthesize data points or advance timestamps to fake live ingestion.
- Do NOT change the underlying
is_online/health_statuscomputation (600 s threshold) or the alert engine. A real outage must still flag in admin views and still fire alerts. This change only affects how the operator dashboard presents the latest readings. - No change to the admin app (
frontend/).
Core concept: anchor-to-latest with auto-fallback
A single reusable idea applied to every surface: when the requested window
[now − duration, now] contains no data, fall back to the most recent window
that does — [L − duration, L], where L is the device's latest available
data time. Live devices are unaffected (L ≈ now). Fallback is automatic
whenever the window is empty (not gated on the offline flag).
Changes by surface
1. Backend — GET /v1/devices/{id}/timeseries?anchor=latest
crates/xylolabs-server/src/routes/device_timeline.rs
- Add optional query param
anchor(DeviceTimeseriesQuery). Whenanchor=latest: - Compute
duration = end_us − start_usfrom the (defaulted) request. - Find the device's most recent ingest session
(
max(started_at); useclosed_atif present, elsestarted_at, as the anchor endL_us). Reuse / add a smallingest_sessionrepo query. - If
L_usexists andL_us < end_us(i.e. the now-window is in the future relative to the latest data), shift:end_us = L_us,start_us = L_us − duration. Otherwise keep the request as-is (live device → no change; device with zero sessions → unchanged → empty). - Run the existing pipeline over the (possibly shifted) window. The response
already returns
start_us/end_us, so clients render what they receive. anchorabsent → byte-for-byte current behavior (default-safe).
2. Sensor cards — AllDevicesSensorsSection.tsx
- Pass
anchor: 'latest'on each per-devicegetDeviceTimeseriesquery. - The latest point per stream is rendered as the current value (existing behavior), so the widget stays populated instead of hiding (the P728 hide-when-empty path now rarely triggers).
- Add a subtle, muted "as of
<latest ts>" caption per device/series when the latest point is older than the live threshold. Low contrast; no OFFLINE badge.
3. Timelines — MultiDeviceTimelineChart.tsx, DeviceTimelineView.tsx, DeviceDetailPage
- Pass
anchor=latestand render using the response'sstart_us/end_usrather than the locally-computednowwindow. - Multi-device chart: share one x-axis spanning
[min(start_us), max(end_us)]across the returned per-device windows so all lines are visible on a common axis (for KOSPO both anchor to ~Jun 3, so they align).
4. Daily report — crates/xylolabs-server/src/routes/daily_report.rs
- In
generate_report, after the 24h aggregate query, ifsessions_24h == 0(no data in the last 24 h), recompute the anchor:anchor_now = max(started_at)for the facility's sessions; setsince_24h = anchor_now − 24h,start_of_day = anchor_now.date(), and re-run the aggregates over the anchored window. Build the narrative from the anchored data, dated to the anchored day (honest date). - The report already omits connectivity status (P727), so it reads as a normal daily report. Cache stays per-facility/24h.
Data flow
DashboardContext (now-relative range) → components request
anchor=latest → backend shifts the window to the latest data when the
now-window is empty → response carries the effective start_us/end_us →
components render the returned window + latest values, with a muted "as of"
caption.
Error handling
- Device with zero sessions ever: window unchanged → empty → existing hide/skeleton behavior (correct; nothing to show).
anchorparam malformed/absent: treated as normal now-window (no behavior change).- Report fallback finds no sessions at all: behaves as today (zeros).
Testing
- Backend: unit/integration test for
anchor=latest— a device whose only data is older than 24 h returns a non-empty window anchored to that data; a live device returns the same asnow-window; a device with no sessions returns empty. - Backend: daily-report fallback test — facility with no last-24h sessions but older data produces a non-empty report anchored to the latest day.
- Frontend:
tsc+vite build; component test thatanchor=latestis sent and the response window is used; the "as of" caption renders for stale data. - Browser (Playwright): KOSPO facility dashboard renders populated sensor cards
- timeline for the 2 stale devices, zero JS errors, desktop + mobile.
Out of scope
- Changing the offline/health/alert logic.
- Admin app (
frontend/). - Any data fabrication or timestamp shifting.