DEV Community

Ivan Annovazzi
Ivan Annovazzi

Posted on

Moveet: incidents, recording & replay, fleet management, dispatch flows, and 500+ tests

A few months ago I shipped the first version of Moveet — an open-source real-time vehicle fleet simulator that runs vehicles on actual road networks using A* pathfinding, streams positions over WebSocket, and renders everything in a custom D3 SVG map without touching Leaflet or Mapbox.

Since then a lot has landed. Here's what's new.


What shipped

🚦 Fleet management

Vehicles can now be grouped into named, colour-coded fleets. You create a fleet, assign vehicles to it, and the UI colours routes and markers per fleet. Useful when you want to simulate multiple operators or companies sharing the same road network.

REST API:

POST   /fleets
GET    /fleets
DELETE /fleets/:id
POST   /fleets/:id/assign
POST   /fleets/:id/unassign
Enter fullscreen mode Exit fullscreen mode

Fleet events are broadcast over WebSocket in real time (fleet:created, fleet:deleted, fleet:assigned).


⚠️ Incidents and dynamic rerouting

You can now drop a road incident anywhere on the network. Every vehicle currently routing through the affected segment gets rerouted live — A* reruns from their current position, routing around the blocked edge.

POST   /incidents          # create incident, triggers rerouting
GET    /incidents          # list active
DELETE /incidents/:id      # clear it, vehicles return to normal paths
POST   /incidents/random   # random incident for testing
Enter fullscreen mode Exit fullscreen mode

Incident markers appear on the map. The vehicle:rerouted WebSocket event fires for each affected vehicle.


🎬 Session recording and replay

Every simulation session can be recorded to a timestamped NDJSON file. The format is a header line followed by one event per line — direction assignments, vehicle snapshots, incidents, everything.

POST   /recording/start
POST   /recording/stop
GET    /recordings
Enter fullscreen mode Exit fullscreen mode

Replay loads any recording file and plays it back with full control:

POST   /replay/start    { "file": "path/to/recording.ndjson" }
POST   /replay/pause
POST   /replay/resume
POST   /replay/stop
POST   /replay/seek     { "timestamp": 12000 }
POST   /replay/speed    { "speed": 2 }
GET    /replay/status
Enter fullscreen mode Exit fullscreen mode

Playback supports 1×, 2×, and 4× speed. The UI has an interpolated progress bar that advances smoothly between server ticks, so it doesn't jump every 500 ms.


🖥 UI redesign: icon rail + panel sidebar

The controls were previously a floating overlay. They're now an icon rail on the left edge — a vertical strip of icon buttons that each toggle a panel.

Panels:

Icon Panel
�� Vehicles — list, filter, select
🗂 Fleets — create, assign, colour
⚠️ Incidents — active list with badge count
Recordings — start/stop/browse/replay
👁 Visibility — toggle map layers
Speed — simulation speed controls
⚙️ Adapter — hot-swap source/sink plugins

The bottom dock holds live simulation controls (play/stop/reset/record) and collapses to a replay transport bar during playback.

All panel components are built from a shared set of primitives (PanelShell, PanelHeader, PanelSection) and a unified theme token set, so every panel looks consistent without per-component ad-hoc styling.


✅ Test coverage: from 410 to 502 tests

The simulator test suite grew significantly. New files added:

File What it covers
rateLimiter.test.ts Window enforcement, 429 responses, per-IP tracking, cleanup interval
helpers.test.ts calculateBearing, interpolatePosition, calculateDistance, nonCircularRouteEdges, estimateRouteDuration
serializer.test.ts serializeVehicle with and without fleet assignment
config.test.ts verifyConfig — missing file, port range, speed ordering, all numeric constraints
SimulationController.test.ts Full lifecycle: start, stop, getStatus, setOptions, getVehicles, getInterval, all replay methods

The pattern for testing SimulationController is worth noting: construction stubs VehicleManager.prototype.setRandomDestination to prevent A* calls on the tiny test-network fixture, then restores it. Replay tests write a minimal NDJSON file to a temp dir before each test. Everything cleans up in afterEach.


📡 WebSocket: 100 ms batching + backpressure

Vehicle position updates are now batched at 100 ms before broadcast. If a client's write buffer is backed up, the broadcaster skips it rather than growing an unbounded queue. This makes the simulator usable with 50–100 vehicles without the connection degrading.

All event types the simulator broadcasts:

vehicles          – batched position array (100ms window)
status            – simulation state (running / ready / interval)
options           – current StartOptions
heatzones         – HeatZoneFeature[]
direction         – active dispatch assignment
waypoint:reached  – vehicle reached a waypoint
route:completed   – vehicle completed its full route
reset             – simulation was reset
fleet:created / fleet:deleted / fleet:assigned
incident:created / incident:cleared
vehicle:rerouted  – live A* reroute triggered
Enter fullscreen mode Exit fullscreen mode

🔧 CI improvements

The GitHub Actions workflow previously ran npm ci three times in parallel — once per job. It now has a dedicated setup job that installs once and caches node_modules keyed on package-lock.json. Lint, test, and build jobs restore from cache. The build job also caches .turbo/ with a per-commit key and branch-level restore keys.


Architecture diagram (updated)

┌──────────────────────────────────────┐
│  apps/ui                             │
│  React 19 · D3 7 · Vite · TS 5.8    │
│  Mercator SVG map, 1×–15× zoom       │
│  Icon rail · Panel sidebar · Dock    │
└──────────────┬───────────────────────┘
               │ REST + WebSocket
               ▼
┌──────────────────────────────────────┐
│  apps/simulator                      │
│  Express 4 · ws 8 · Turf.js 7       │
│  GeoJSON graph · A* · LRU cache      │
│  WS broadcaster · 100ms batching     │
└──────────────┬───────────────────────┘
               │ GET /vehicles · POST /sync
               ▼
┌──────────────────────────────────────┐
│  apps/adapter (optional)             │
│  Source plugins: static / graphql /  │
│  rest / mysql / postgres             │
│  Sink plugins: console / graphql /   │
│  rest / redpanda / redis / webhook   │
└──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Try it

curl -O https://raw.githubusercontent.com/ivannovazzi/moveet/main/docker-compose.ghcr.yml
docker compose -f docker-compose.ghcr.yml up
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:5012. No config, no API keys.

Repo: github.com/ivannovazzi/moveet

Happy to answer questions about the A* implementation, the D3 renderer, or the recording format in the comments.

Top comments (0)