feat: task 2.9 connection status + per-device staleness fade — Phase 2 done
- src/live/last-seen.ts: formatLastSeen(ts, now) — "now" / "Ns ago" /
"Nm ago" / "HH:MM".
- src/live/use-staleness.ts: useStalenessTick(intervalMs=1000) hook
re-renders subscribers every interval with the current epoch ms.
- src/ui/components/connection-chip.tsx: <ConnectionChip /> bottom-left,
three states (connected / connecting+reconnecting / disconnected),
aria-live polite + role status.
- src/map/layers/map-positions.tsx: every FeatureProps now carries
staleSec; both symbol layers interpolate icon-opacity (and text-opacity
on the non-selected layer) on staleSec — 0-60s full opacity,
5min faded, 30min very faded. Update effect rebuilds features at the
staleness tick rate (1Hz).
- src/routes/_authed/monitor.tsx: renders <ConnectionChip />.
- src/live/index.ts: re-exports formatLastSeen + useStalenessTick.
Strategy A (faded marker via interpolation) chosen over Strategy B
(separate warning-badge layer) per the task's open-question
resolution; simpler and easier to extend later.
Deviations:
1. stalenessTick is its own hook in src/live/use-staleness.ts rather
than a property on the position store — keeps the store clean of
UI-driven re-render concerns; the hook is reusable.
2. <DeviceLastSeen deviceId> standalone component skipped — the SPA
doesn't have a sidebar yet (Phase 3.4); the per-marker fade IS the
indicator for v1.
Bundle: main 396KB / 121KB gz — small bump from 2.8.
🎉 Phase 2 — Live monitoring map — complete. All 9 tasks shipped.
End-to-end: login -> /monitor -> event auto-selects -> snapshot
positions render -> live updates flow via WS through the rAF
coalescer -> staleness fades stale markers -> connection chip
surfaces WS state. Dogfood-blocking work for Rally Albania 2026 done.
This commit is contained in:
@@ -192,7 +192,7 @@ Click toggles `mapFollow`. Selecting a device with follow off still pans to it o
|
||||
## Done
|
||||
|
||||
- **`src/map/core/map-pref-store.ts`** — added `mapFollow: boolean` (default `true`) + `setMapFollow`. Persisted via the existing `trm-map-prefs` zustand+persist store.
|
||||
- **`src/map/core/camera/default-camera.tsx`** — `<MapDefaultCamera />`. Watches `activeEventId` + `latestByDevice`. Uses a `fittedFor` ref to ensure the camera only fits *once* per event-id change. Single-device events centre + zoom to 12; multi-device events `fitBounds` with padding capped at `min(canvas * 0.1, 80)`.
|
||||
- **`src/map/core/camera/default-camera.tsx`** — `<MapDefaultCamera />`. Watches `activeEventId` + `latestByDevice`. Uses a `fittedFor` ref to ensure the camera only fits _once_ per event-id change. Single-device events centre + zoom to 12; multi-device events `fitBounds` with padding capped at `min(canvas * 0.1, 80)`.
|
||||
- **`src/map/core/camera/selected-device.tsx`** — `<MapSelectedDevice />`. Two effects:
|
||||
- Selection / position effect: on selection change, always pan + zoom to ≥ 14. On subsequent position updates with `mapFollow` on, smooth pan only (no zoom change).
|
||||
- Manual-pan listener: hooks `map.on('movestart', ...)`. Distinguishes user gestures from programmatic `easeTo`s via `originalEvent`-truthy check; flips `mapFollow` to false when the user grabs the map.
|
||||
@@ -207,6 +207,7 @@ Click toggles `mapFollow`. Selecting a device with follow off still pans to it o
|
||||
- `<MapDefaultCamera>` clears `fittedFor.current = null` when `activeEventId` becomes null (e.g. on logout / event clear). Without that, switching back to the same event later wouldn't re-fit. Spec didn't call this out; added it as a small correctness fix.
|
||||
|
||||
**Smoke check (`pnpm dev`):**
|
||||
|
||||
- Pick the seeded Rally Albania event → camera fits all snapshot positions on first appearance.
|
||||
- Click a device → camera pans + zooms in.
|
||||
- With Follow on, push a synthetic position for the selected device → camera smoothly pans to follow it.
|
||||
|
||||
@@ -147,4 +147,29 @@ useEffect(() => {
|
||||
|
||||
## Done
|
||||
|
||||
(Filled in when the task lands.)
|
||||
- **`src/live/last-seen.ts`** — `formatLastSeen(ts, now)` returns `"now"` / `"Ns ago"` / `"Nm ago"` / wall-clock `"HH:MM"` based on age. Used by both the connection chip and Phase 3.4's per-device detail panel.
|
||||
- **`src/live/use-staleness.ts`** — `useStalenessTick(intervalMs = 1000)` hook. Re-renders subscribers every `intervalMs` with the current epoch ms. The position store doesn't change on its own once positions stop arriving; this keeps `staleSec` fresh on every `<MapPositions>` feature so the icon-opacity fade reflects "device went silent" without waiting for a new position.
|
||||
- **`src/ui/components/connection-chip.tsx`** — `<ConnectionChip />` floating bottom-left. Three states: `connected` (green dot + "Live"), `connecting`/`reconnecting` (amber pulsing dot + status word), `disconnected` (red dot + "Offline · last live HH:MM"). `aria-live="polite"` so screen readers announce transitions; `role="status"`.
|
||||
- **`src/map/layers/map-positions.tsx`** updated:
|
||||
- Adds `staleSec` to every `FeatureProps`; computed as `Math.floor((now - position.ts) / 1000)`.
|
||||
- Both non-selected and selected symbol layers gain `'icon-opacity'` interpolations on `staleSec` (0–60 s full opacity, 5 min faded, 30 min very faded). Non-selected layer also fades `text-opacity` so the device label ages along with the marker.
|
||||
- Update effect now reads `useStalenessTick(1000)` and includes it in dependencies — feature collections rebuild every second so opacity stays fresh.
|
||||
- **`src/routes/_authed/monitor.tsx`** — renders `<ConnectionChip />` inside `<MapView>`.
|
||||
- **`src/live/index.ts`** — re-exports `formatLastSeen` and `useStalenessTick`.
|
||||
|
||||
**Strategy A picked from the spec's open question**: faded marker via `icon-opacity` interpolation, no separate "warning" badge. Rationale per spec: simpler, fewer layers, cleaner visually. If it's not legible enough after dogfood feedback, layer B (a dedicated overlay sprite) is a single new symbol layer addition.
|
||||
|
||||
**Deviations from spec:**
|
||||
|
||||
1. Spec sketched a `stalenessTick` mutation inside the position store's actions. Pulled it out into a dedicated `useStalenessTick` hook in `src/live/use-staleness.ts` instead — keeps the position store free of UI-driven re-render concerns, and the hook is reusable in any component that needs an N-Hz tick (per-device detail panel in Phase 3.4 will).
|
||||
2. Spec referenced a per-device `<DeviceLastSeen deviceId>` component for use inside a sidebar / detail panel. Skipped — the SPA doesn't have a sidebar yet (Phase 3.4 territory), and the per-marker fade is the per-device indicator for v1. The `formatLastSeen` utility is exported and ready for that component when 3.4 lands.
|
||||
|
||||
**Smoke check (`pnpm dev`):**
|
||||
- `/monitor` shows the connection chip bottom-left. While connecting the chip pulses amber. Once the WS opens, it flips to a green dot + "Live". Disconnect the network → flips to amber + "Reconnecting…". Stay disconnected past `STALE_CONNECTION_MS` (60 s) → red dot + "Offline · last live …".
|
||||
- Push a synthetic position via the console, wait > 60 s without further updates → marker visibly fades. Wait > 5 min → fades further. Each tier matches the interpolation breakpoints.
|
||||
- The visible fade is also gradual *within* each tier because the interpolation is `linear`.
|
||||
- Hard refresh while disconnected: chip starts as "Connecting…" (no `lastConnectedAt` yet), then either flips to "Live" or "Reconnecting…" based on whether the WS opens.
|
||||
|
||||
**Bundle:** main 396KB / 121KB gz — small bump from 2.8.
|
||||
|
||||
Landed in `PENDING_SHA`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 2 — Live monitoring map
|
||||
|
||||
**Status:** ⬜ Not started — depends on [[processor]] Phase 1.5 landing (already shipped).
|
||||
**Status:** 🟩 Done — all 9 tasks shipped.
|
||||
|
||||
The dogfood-day deliverable. After Phase 2, an operator opens the SPA, picks the active event, and watches the field move on a real-time map. Inherits the architecture documented in `docs/wiki/concepts/maps-architecture.md` from `docs/wiki/sources/traccar-maps-architecture.md`, with the deliberate divergences (rAF coalescer, Zustand, longer trail default, racing sprite set, native PostGIS GeoJSON) baked in from day one.
|
||||
|
||||
@@ -56,7 +56,7 @@ When Phase 2 is done:
|
||||
| 2.6 | [MapTrails (bounded ring buffer, polyline rendering)](./06-map-trails.md) | 🟩 |
|
||||
| 2.7 | [Event picker (subscription driver)](./07-event-picker.md) | 🟩 |
|
||||
| 2.8 | [Camera control trio](./08-camera-trio.md) | 🟩 |
|
||||
| 2.9 | [Connection status + per-device last-seen indicators](./09-connection-status.md) | ⬜ |
|
||||
| 2.9 | [Connection status + per-device last-seen indicators](./09-connection-status.md) | 🟩 |
|
||||
|
||||
## Files modified
|
||||
|
||||
|
||||
Reference in New Issue
Block a user