Multi-Browser Sync¶
Both the simulator UI and the Fleet Dashboard support multiple browsers viewing the same backend simultaneously. State (slider positions, mode selections, fleet card values) stays consistent across browsers via a per-app WebSocket fan-out.
This page documents the mechanism, the reconnect behavior, and the failure modes - useful when you're trying to understand why two windows seemed to disagree.
What's synchronized¶
Simulator UI¶
| Thing | Synchronized? |
|---|---|
| Operating mode selection | Yes - flipping mode in Browser A changes Browser B's selector. |
| Slider positions (altitude, airspeed, heading) | Yes. |
| Airport selection | Yes (with a small heuristic to not stomp on what you're currently typing). |
| Network config target IP / port / protocol | Yes. |
| EFB block toggles, target IPs, sim name | Yes. |
| NMEA sentence selection | Yes (when not running; the panel is disabled when running anyway). |
| Serial device + baud | Yes. |
| Running / stopped state | Yes - Start in A causes B's button to flip to Stop. |
| Output Viewer scroll | No - each browser has its own WebSocket and buffer. Pausing one doesn't pause the other. |
| Theme (light/dark) | No - per-browser, stored in localStorage. |
| Errors / banners | No - errors from a server-side action you initiated show only in your browser. |
Fleet Dashboard¶
| Thing | Synchronized? |
|---|---|
| Card grid contents (per-sim position, altitude, etc.) | Yes - any update on the server is pushed to every browser. |
| Card online/offline status | Yes. |
| Health view toggle | No - per-browser. |
| Theme (light/dark) | No - per-browser. |
| Connection-status badge | Reflects each browser's own WebSocket health. |
In both apps, the rule is: server-side state syncs; per-browser UI preferences don't.
How it works¶
sequenceDiagram
participant A as Browser A
participant B as Browser B
participant Server as FastAPI backend
A->>Server: REST POST /api/config/modes (toggle Rebroadcaster on)
Server->>Server: update in-memory state
Server->>A: 200 OK
Server->>A: WebSocket: status update {modes: {rebroadcaster: true}}
Server->>B: WebSocket: status update {modes: {rebroadcaster: true}}
A->>A: useStatus hook fires; re-render
B->>B: useStatus hook fires; re-render
Step by step:
- Browser A makes a REST call that mutates server state.
- The server updates its in-memory state.
- The server returns 200 to Browser A.
- The server broadcasts the new state over WebSocket to every connected client (including Browser A, which gets a redundant copy - the client de-duplicates harmlessly).
- Each client's
useStatushook fires; React re-renders affected components with the new state.
Typical end-to-end latency on a LAN: <200 ms.
The WebSocket endpoint¶
Both apps expose a WebSocket at /ws. The connection lives for the duration of the browser tab.
| Detail | Simulator | Fleet Dashboard |
|---|---|---|
| Path | /ws |
/ws |
| Subprotocol | none (default) | none |
| Initial message from server | None - waits for client | fleet_state payload (current snapshot) |
| Server -> client broadcast cadence | Event-driven (whenever state changes) plus 1 Hz position updates while running | Once per second |
| Client -> server messages | ping (responded with pong) |
none |
| Server detects client disconnect | When socket closes; client removed from broadcast list | Same |
The simulator's WebSocket carries multiple message types (status_update, nmea_output, position_update). The dashboard's carries one type (fleet_state).
Reconnect logic¶
If the WebSocket drops (network blip, container restart, browser sleep), the client attempts to reconnect.
Simulator UI (frontend/src/hooks/useWebSocket.js)¶
| Detail | Value |
|---|---|
| First reconnect attempt | Almost immediately after onclose |
| Backoff | None - retries on a fixed short interval |
| Max retries | Infinite - the page assumes the container will come back |
| User-visible signal | The OutputViewer falls back to "Waiting for data..." or the value displays freeze |
Fleet Dashboard (dashboard/frontend/src/App.jsx)¶
| Detail | Value |
|---|---|
| First reconnect attempt | 2 s after onclose |
| Backoff | Constant 2 s interval |
| Max retries | Infinite |
| User-visible signal | The connection badge in the header turns red |
The dashboard's reconnect signal is the most visible because the badge is always in view. The simulator UI is less obvious - you have to notice that things stopped updating.
When the WebSocket disconnects¶
| Cause | What you see |
|---|---|
| Container restart | Both browsers' connections drop simultaneously. Reconnect succeeds when the container is back up. State is reset to the env-var defaults; running state is lost (unless auto-start re-arms it). |
| Network blip (Wi-Fi drop) | Only the affected browser's connection drops. Other browsers are unaffected. The blipped browser catches up on reconnect. |
| Browser tab backgrounded | Modern browsers may throttle WebSocket events on inactive tabs. The connection stays open; messages queue. When the tab is foregrounded, the queue drains. |
| Browser closes without logout | The WebSocket closes naturally. The session cookie may persist (depends on browser settings). |
| Reverse proxy timeout | Some reverse proxies (default nginx) close idle WebSockets after 60 s. Add proxy_read_timeout 3600; for long-running connections. |
Subtle gotcha: "the airport picker is fighting me"¶
The simulator's airport picker has special logic to avoid stomping on what you're typing in Browser A when Browser B picks an airport at the same time. Specifically: if userIsEditing is true (you're actively typing), the picker ignores the incoming airportIcao change from the WebSocket.
This means in rare race conditions, two browsers can have different values in the search field for ~1 second while one user is typing. The server-side state is consistent; only the search-field display lags. As soon as you stop typing or pick an airport, the values reconcile.
This is by design. It would be worse to have your half-typed KCRQ get overwritten by another user's KSAN selection mid-keystroke.
Errors and banners¶
API errors (red banners) show only in the browser that initiated the failing call. If Browser A presses Start and the server returns an error, Browser A sees the banner; Browser B doesn't. Browser B sees the running state stay false (because the Start didn't actually succeed) but doesn't know why.
This is intentional - the operator who pressed the button is the one who needs to know what went wrong.
Two browsers, two WebSockets, same content¶
Each browser opens its own WebSocket. The server broadcasts identical messages to all of them. So two browsers downloading the same fleet_state payload is two separate WebSocket sends, doubling outbound bandwidth from the dashboard.
For typical deployments (a handful of operator browsers), this is negligible. For unusual cases (50+ browsers reading the same dashboard), consider putting a WebSocket-aware reverse proxy or message bus in front of it.
Practical tips¶
| Tip | Why |
|---|---|
| Leave the dashboard up on a wall display, then operate from your laptop browser. | The wall display passively sees what you do. No coordination overhead. |
| If two operators need to drive the same simulator, agree on roles first. | Both can technically drive; nothing stops them. But race conditions on slider drags are confusing. |
Open /ws directly with wscat for debugging. |
wscat -c ws://<host>/ws gives you the raw message stream. Handy when figuring out a sync issue. |
| Reload the page after a long absence. | If the browser has been backgrounded for hours, force a fresh connection to be sure you're in sync. |
What's next¶
- Common UI Elements - simulator-side header + WebSocket reconnect behavior.
- Overview (Fleet Dashboard) - dashboard-side header + connection badge.
- API Reference - the REST endpoints that mutate state; every mutation triggers a sync broadcast.