docs: TRACCAR ingest + processor-ws-contract synthesis + auth-mode realignment
Catches up the wiki with several pieces of work accumulated during this session. INGEST: TRACCAR_MAPS_ARCHITECTURE.md - raw/TRACCAR_MAPS_ARCHITECTURE.md (source doc, read-only). - wiki/sources/traccar-maps-architecture.md — TL;DR + key claims + notable quotes + TRM divergences (PostGIS-native GeoJSON, rAF coalescer, Zustand, longer trail, racing sprite set). - wiki/concepts/maps-architecture.md — distilled patterns for the SPA's map subsystem: singleton MapLibre + side-effect-only Map* components + two GeoJSON sources + style-swap mapReady gate + sprite preload + WS- to-map data flow (with rAF coalescer) + geofence editing + camera control trio. - wiki/entities/react-spa.md — corrected the "talks exclusively to Directus" contradiction with [[live-channel-architecture]] (SPA connects to two endpoints — Directus + Processor); locked stack (raw MapLibre over react-map-gl, Zustand over Redux); added Auth section. - wiki/concepts/live-channel-architecture.md — single sentence cross- referencing [[maps-architecture]] for consumer-side throughput discipline. - index.md — Sources + Concepts entries. SYNTHESIS: processor-ws-contract - wiki/synthesis/processor-ws-contract.md — wire-level spec for the live-position WebSocket: endpoint, transport, auth handshake, subscribe/snapshot/streaming/unsubscribe protocol, reconnect, multi- instance behaviour, connection limits, versioning, open questions. Implementation-agnostic; the producer is cookie-name-agnostic so the spec doesn't pin to a specific Directus auth mode. - index.md — Synthesis entry. AUTH-MODE REALIGNMENT (cookie -> session) - SPA implementation surfaced that Directus SDK 'cookie' mode doesn't survive a hard reload cleanly. Switched the SPA to 'session' mode (separate commit in trm/spa). Wiki updates here: - wiki/entities/react-spa.md §Auth pattern — describes session mode (single httpOnly session cookie, no separate access token, no /auth/refresh dance). Added "Mode choice context" note. - wiki/synthesis/processor-ws-contract.md §Auth handshake — emphasises the producer is cookie-name-agnostic; reframed "Cookie refresh while connected" as "Session expiry while connected". Plus all the chronological log.md entries documenting the above plus Phase 1.5 planning, SPA Phase 1 planning, and stage verify+seed work from earlier in the session. Skipped from this commit: .claude/agent-memory/* (user-local agent state, not project content); .gitignore (already-modified by user outside this session's scope). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
-19
@@ -2,8 +2,8 @@
|
||||
title: React SPA
|
||||
type: entity
|
||||
created: 2026-04-30
|
||||
updated: 2026-04-30
|
||||
sources: [gps-tracking-architecture]
|
||||
updated: 2026-05-02
|
||||
sources: [gps-tracking-architecture, traccar-maps-architecture]
|
||||
tags: [service, presentation-plane, frontend]
|
||||
---
|
||||
|
||||
@@ -26,31 +26,50 @@ One application serves multiple user types via role-based routing and conditiona
|
||||
|
||||
## Data access pattern
|
||||
|
||||
The SPA talks **exclusively** to Directus:
|
||||
The SPA talks to **two endpoints**, one per plane (see [[live-channel-architecture]]):
|
||||
|
||||
- REST/GraphQL via `@directus/sdk`.
|
||||
- WebSocket subscriptions via the same SDK.
|
||||
- JWT auth managed by the SDK; refresh handled transparently.
|
||||
- **[[directus]]** — REST/GraphQL via `@directus/sdk`, plus Directus's WebSocket for business-plane events. Auth, schema, all CRUD on entries / vehicles / devices / geofences / etc.
|
||||
- **[[processor]]** — its own WebSocket endpoint, exclusively for the live position firehose. Authenticated by the same Directus-issued credential the SPA already holds; authorization delegated to Directus once at subscribe time.
|
||||
|
||||
**Never** talks to the [[processor]], [[tcp-ingestion]], [[redis-streams]], or [[postgres-timescaledb]] directly. This boundary lets the back-end evolve internally and keeps the security model coherent — every request goes through Directus's permission system.
|
||||
**Never** talks to [[tcp-ingestion]], [[redis-streams]], or [[postgres-timescaledb]] directly. The two-endpoint split exists because Directus's WebSocket subscriptions only fire for writes through its own `ItemsService` — Processor's direct-to-DB position writes are invisible to it. See [[live-channel-architecture]] for why this is the architecturally honest answer rather than a workaround.
|
||||
|
||||
## Recommended stack
|
||||
## Stack
|
||||
|
||||
- **Vite + React + TypeScript**
|
||||
- **TanStack Router** — better TS support than React Router; optional file-based routing
|
||||
- **TanStack Query** — server state, caching, invalidation, optimistic updates
|
||||
- **@directus/sdk** — typed access + real-time
|
||||
- **MapLibre GL + react-map-gl** — open-source WebGL maps, no token needed
|
||||
- **shadcn/ui + Tailwind** — UI primitives
|
||||
- **Zustand** — client-only state (filters, UI prefs)
|
||||
- **react-hook-form + Zod** — forms and validation
|
||||
- **Vite + React + TypeScript** — SPA build, no SSR.
|
||||
- **TanStack Router** — file-based, type-safe routes.
|
||||
- **TanStack Query** — Directus REST: caching, invalidation, refetch on focus.
|
||||
- **`@directus/sdk`** — typed access for REST + Directus's WebSocket.
|
||||
- **MapLibre GL JS** — WebGL map renderer. Used **raw**, not via `react-map-gl` — the declarative wrapper fights the imperative `setData` pattern that's the whole point of the architecture (see [[maps-architecture]]).
|
||||
- **`maplibre-google-maps`** *(optional, runtime-config-gated)* — protocol adapter that lets MapLibre consume Google's official Map Tiles API when an operator-provided API key is present.
|
||||
- **`@mapbox/mapbox-gl-draw`** — geofence editor (polygon / line / trash modes) wrapped to look native to MapLibre.
|
||||
- **Zustand** — high-frequency live state (positions, trails, selection). Granular subscribers; chosen over Redux specifically because Redux's dispatch + selector cascade is the most likely cause of the lag observed in production [[traccar-maps-architecture]] deployments.
|
||||
- **shadcn/ui + Tailwind** — UI primitives.
|
||||
- **react-hook-form + Zod** — forms and validation.
|
||||
|
||||
Covers the spectrum from form-heavy admin screens to real-time map dashboards without architectural changes between them.
|
||||
Covers form-heavy admin screens and real-time map dashboards without architectural changes between them.
|
||||
|
||||
## Auth pattern
|
||||
|
||||
Same-domain session cookie via reverse proxy. One origin serves the SPA, Directus, and Processor's WebSocket endpoint — Vite's `server.proxy` in dev, Traefik (or whatever fronts the deploy stack) in stage/prod.
|
||||
|
||||
- Login uses the Directus SDK in **session mode** (`authentication('session', { credentials: 'include' })`). Directus issues an `httpOnly`/`Secure`/`SameSite=Lax` session cookie; the cookie itself carries the session — there is no separate in-memory access token to manage and no `/auth/refresh` dance.
|
||||
- Reload survives cleanly: the browser still has the cookie, `/users/me` returns the user without any client-side state.
|
||||
- No JavaScript ever reads or writes the cookie (it's `httpOnly`), so XSS cannot exfiltrate it.
|
||||
- WebSocket handshake: same-origin means the browser sends the session cookie automatically with the upgrade request. Processor reads it on the upgrade, validates against Directus's `/users/me`, and uses the resulting user identity for subscription authorization. See [[live-channel-architecture]] and [[processor-ws-contract]].
|
||||
|
||||
This requires the proxy to serve everything under one origin (path-based or single subdomain) — separate subdomains break cookie flow.
|
||||
|
||||
**Mode choice context.** Directus's SDK also supports `'cookie'` mode (refresh cookie + in-memory access token). It works while the SDK is alive in memory but doesn't survive a hard reload cleanly because there's no access token to retry `/users/me` against, and the refresh-then-read sequence is order-sensitive. `'session'` mode collapses that to one credential — the session cookie — and is the right default for an SPA that wants reload-survives behaviour.
|
||||
|
||||
## Real-time rendering
|
||||
|
||||
- **Live maps with many markers**: React reconciler is not the bottleneck — drawing happens in WebGL via MapLibre, which manages features outside React's tree. The React layer manages subscriptions and feeds the map updates.
|
||||
- **High-frequency tabular updates** (live leaderboards, event feeds): split components so high-update areas re-render in isolation; use TanStack Query for live data; memoize at component boundaries that receive frequent updates.
|
||||
The full pattern lives at [[maps-architecture]]; the headlines:
|
||||
|
||||
- **MapLibre is a singleton** held in a module-level variable, attached to a detached `<div>` that React refs mount/unmount per page. WebGL context survives navigation.
|
||||
- **Two GeoJSON sources** for live positions: clustered non-selected, unclustered always-on-top selected. Updates flow through `setData`, not DOM marker manipulation.
|
||||
- **rAF coalescer at the WS boundary.** Incoming position messages buffer per-device; one `requestAnimationFrame` tick flushes the latest snapshot to the Zustand store. Without this, per-message dispatches cascade through selectors and `setData` at every position arrival — the failure mode [[traccar-maps-architecture]] exhibits.
|
||||
- **Per-device bounded ring buffers** for trail history. Default 200 points per device, configurable. The throttle controls visual cadence; trails are never lossy.
|
||||
- **High-frequency tabular updates** (live leaderboards, event feeds) — same Zustand store, separate component subtrees so the map's re-renders don't ripple into the leaderboard and vice versa.
|
||||
|
||||
## Failure mode
|
||||
|
||||
|
||||
Reference in New Issue
Block a user