PITWALL: the strategy surfaces, in web technology

Two windows, PITWALL · DATA and PITWALL · AGENTS, rendering React against the arcade's live broadcast. They replaced the two PySide6 windows the arcade dashboard (legacy) page describes. Both stacks ran side by side through the migration so every ported panel could be compared against the window it replaced while that window still existed; the Qt pair was later retired, and the comparison baseline survives as committed screenshots under documents/dev_docs/migration/pitwall/.

Desktop surface, web technology

The distinction matters and it was a deliberate decision.

What renders inside those windows is React, ECharts and CSS in a real Chromium engine. What surrounds them is a native OS window, because the host process is Python and pywebview embeds the browser engine in one.

The alternative (a page in a browser, served by a relay) was designed first and rejected for a concrete reason: a browser cannot open a raw TCP socket, so a page would have needed a FastAPI WebSocket relay in front of the arcade's broadcast, which makes the backend a runtime dependency of a surface that otherwise has none. A Python host opens the socket itself.

That said, the same pages are also served over loopback. The host already holds the socket and the payload, so handing that payload to a browser costs a small HTTP server rather than a relay:

INFO __main__: Also serving the same windows at
      http://127.0.0.1:62042/data.html and http://127.0.0.1:62042/agents.html

The URL is printed on startup. It binds the loopback interface only (the broadcast carries a whole race's live state and there is no authentication), and it exists for devtools, a second screen, and anything that is not this desktop.

Launching

Nothing extra to run. f1-arcade --strategy (or python -m src.arcade.main … --strategy) spawns it.

The first launch of any given race builds its replay telemetry, which takes minutes, so the menu reports the stages while a worker thread does the work. f1-prefetch --year 2025 runs that same preparation for a whole season ahead of time, and skips the rounds already cached.

To develop against an already-running arcade:

python -m src.pitwall

The UI is a Vite project under src/pitwall/ui/. It ships as built static files, so a source checkout needs npm install && npm run build in that directory once before the windows have anything to show, and the entry point says so rather than opening blank.

The two windows

PITWALL · DATA

A status strip across the top, then two columns: the all-cars world on the left and the own-car world on the right. That is the zoning a real pit wall uses (the two live on physically different surfaces there), and it is also the only arrangement that fits, because four stacked bands need 908 px of a window that has 790.

Left: the timing tower, twenty rows of P · # · DRV · GAP · INT · S1 · S2 · S3 · LAST · ST · TYRE · STOPS, with the sector colour code every timing screen uses: purple for fastest of the session, green for a driver's own best, amber for slower than his own. Under it, the bests: S1, S2, S3 and Lap ranked across the field with their percentage off the leader, and the theoretical lap those three sectors recombine into.

Right: a tab strip over three panels, because the column has 825 px of width to give and sharing it costs one of them too much: with the track ring still mounted, the pace grid's columns narrow enough that 1,101 of 1,140 cells clip. The panels take turns instead.

TRACES, the tab open by default, is the own car's lap as four locked-axis traces against distance: Δ Time, Speed, Brake, Throttle, ported field by field from the Qt telemetry panel, plus a shared vertical cursor marking where the car is on the lap. Beside it, a schematic track ring placing the whole field by lap fraction, and the radio feed: race control messages and driver radio in one list, newest line on top, the header naming the total so the cut at the bottom is never silent. The feed carries the whole field rather than only the pinned car, a rival's line marked BROADCAST, the same tag the rival's traces carry. A real team receives the public broadcast radio feed too, so showing it is fidelity rather than a privilege the window grants itself.

RACE PACE is a grid, one column per driver and one row per lap: how quick each lap was. RACE TRACE is a chart of the same laps read the other way, where everyone is relative to a reference over the whole race: one cut down it at a given lap gives every gap in the field at that moment, a question the grid cannot answer.

Splitting the two across tabs rather than stacking them repeats the column's own arithmetic: a grid squeezed to half this height stops showing enough laps to read as a history, and a trace squeezed the same way, to roughly 300 px, stops resolving the gaps it exists to show.

Shipping both, with the grid one click from the default tab, follows what the pit-wall research behind this window found and what it could not settle. Across seven sources and six photographs of a real SBG/Catapult RaceX client, not one race-trace line chart appears on a wall, while the literature names the gapper plot the central strategy tool, and the pace grid takes up more screen area than anything else in the photographs. The photographs cover the in-session view, so they cannot rule out a trace living on an analysis tab; what they establish is which of the two a wall keeps in front of it during a race. RACE TRACE stays because the question it answers has no other tab that can answer it.

Four details that are not cosmetic:

One consequence of taking the order from the replay and the seconds from the timing table: they disagree by one place at about 0.7% of line crossings, both times within a tenth of a second of each other. When they do, the interval cell shows a dash rather than a negative number. It is two measurements of the same moment, not an error in either.

PITWALL · AGENTS

Four strata, top to bottom: header, the decision band, the agent grid, status bar. The band answers one question per module across the line the eye lands on anyway, left to right in the order a reader asks them, which is what the orchestrator is doing, why, on what evidence, and what happens next.

The Qt lineage ends at the layout, and only at the layout. The host formats nothing on the React side: every headline, body line and colour is produced by src/pitwall/agent_formatters.py, the Qt window's own formatting code, and agents_view/builder.py states that as its first invariant. Chart colours come from src/arcade/palette, which the replay uses too. The Qt window itself is gone, so the rule now buys a single source for the strings rather than agreement between two surfaces. What the port inherited and then dropped is the geometry: a header strip over a 540 / 740 horizontal split, decision in the left column and a 3x2 card grid in the right. That split made the decision a peer of the agent grid, two territories with no reading order, and put the most important content on the window in the same column as a reasoning panel measured at 1.9% ink.

The grid places its cards by name rather than in reading order, because three of the six want a shape reading order does not give them:

pace   tire   side
radio  exit   rag

PIT EXIT is the one output with no Qt counterpart. It answers the question a pit wall asks before every stop: box on this lap, and what position does the car rejoin in, with which cars either side. It reads P1 → P3 under an if we box now header, the car ahead and the car behind named with the gap to each. The number is the one the projection layer is graded on, 86.1% within one position over 552 real green-flag stops of 2025, because the card computes at the same two-lap horizon the ground truth measures rather than at the five-lap one the strategy scoring uses.

The card is a hypothetical and says so in its own header, which is what keeps it readable on a STAY_OUT lap. Suppressing it there was designed and rejected: it would idle the card on exactly the laps somebody is asking whether to box, which is when the readout earns its space. It carries no identity colour either. The live call wears ACCENT one card up, and a branch that is not happening must not.

RADIO gave up the second of its two columns to make room, so the bottom row runs three cards wide. That is not cosmetic. At one column a radio transcript no longer fits on one line and the body wraps rather than clips, so the height comes out of the charts above it, which is why agent_formatters.BODY_LINE_LIMIT budgets the lines.

Before the first view arrives the window renders what the Qt window shows at startup rather than a spinner. The scenario scores are the one field that no longer follows Qt: they read -- where it painted 0%, because before the first tick nothing has been simulated and 0% is a measurement.

How it is wired

arcade process                    pitwall process
  pyglet replay                     ArcadeStreamClient  (ONE socket)
  TelemetryStreamServer  ──TCP──▶     └─ latest payload slot
  127.0.0.1:9998                          │
                                          ├─ window: DATA    ┐ js_api
                                          ├─ window: AGENTS  ┘ get_tick(since_seq)
                                          └─ loopback HTTP    /api/tick

One client, however many consumers. Both windows and any browser tab read through the same get_tick(since_seq), and the sequence is what makes them agree: against a blind latest-payload slot, two pollers on independent 10 Hz timers were measured reading a different frame on 58% of polls.

Closing one window does not blind the other. The client belongs to the host, not to a window; a window closing only decrements a count, and the last one out tears the client down.

Related reading