Skip to content

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_status computation (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). When anchor=latest:
  • Compute duration = end_us − start_us from the (defaulted) request.
  • Find the device's most recent ingest session (max(started_at); use closed_at if present, else started_at, as the anchor end L_us). Reuse / add a small ingest_session repo query.
  • If L_us exists and L_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.
  • anchor absent → byte-for-byte current behavior (default-safe).

2. Sensor cards — AllDevicesSensorsSection.tsx

  • Pass anchor: 'latest' on each per-device getDeviceTimeseries query.
  • 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=latest and render using the response's start_us/end_us rather than the locally-computed now window.
  • 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, if sessions_24h == 0 (no data in the last 24 h), recompute the anchor: anchor_now = max(started_at) for the facility's sessions; set since_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).
  • anchor param 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 as now-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 that anchor=latest is 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.