Skip to content

dotbot/console-web: unified web console for control + orchestration - #279

Merged
geonnave merged 62 commits into
DotBots:developfrom
geonnave:unified-web-ui
Aug 25, 2026
Merged

dotbot/console-web: unified web console for control + orchestration#279
geonnave merged 62 commits into
DotBots:developfrom
geonnave:unified-web-ui

Conversation

@geonnave

@geonnave geonnave commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Adds dotbot/console-web, a map-first web console that merges the PyDotBot control plane and swarmit orchestration into one UI: Map/List/Grid views with shared selection, a footer control dock (drive pad, LED, waypoint missions), a testbed rail (firmware flashing with per-device SSE progress, start/stop, a streaming event console, missions), and an inspector reporting what each bot is running and why it last rebooted. React 18 + Vite + TS, no UI framework, every color through design tokens, dark and light themes. Ported from a Claude Design prototype.

The controller serves the built console at /console, side by side with the classic /PyDotBot frontend, and proxies /swarmit/* to the swarmit server through a streaming httpx route so SSE stays live and production is same-origin exactly like the vite dev proxy. The swarmit endpoint is configurable: [run.controller] swarmit_url in dotbot.toml, --swarmit-url, or DOTBOT_SWARMIT_URL; the default http://localhost:8001 matches swarmit serve. The config resolver learned dotted nested sections for this.

One change reaches outside the console

Device addresses are now rendered uppercase everywhere, through a single dotbot.addr_to_hex() helper that is the only place a 64-bit address becomes a string. This is not cosmetic. The address is the join key between the two data planes; swarmit spells it uppercase and binascii.hexlify returns lowercase, so at the bench one real robot arrived under two keys and drew as two bots, neither record complete. DOTBOT_ADDRESS_DEFAULT and GATEWAY_ADDRESS_DEFAULT were already written uppercase, so this makes the codebase agree with itself - but anything comparing addresses case-sensitively against controller output will see the difference.

Also new outside the console: GET /controller/connection, reporting adapter, connection URI, swarm id and gateway address so the console can show which broker and which swarm it is driving. The response model is curated rather than a dump of settings, and a test asserts the MQTT credentials never appear in it.

Things a reviewer should know

  • The console renders strings swarmit computes - reset cause, fault name, severity tier, battery percentage, LH2 summary - instead of re-deriving them, so it cannot disagree with dotbot swarm status about the same robot. Those fields arrive with swarmit/testbed: serve computed status strings and a per-device battery reading swarmit#160, which should land first.
  • Crashed and hung are separate tiers, deliberately. A sandbox app has no clean exit: finishing normally means starving the deadman, so a healthy app that ran to completion latches WatchdogTimeout. Without the split, a normal fleet shows red everywhere.
  • There is no Reset button. swarmit's /reset is dead end to end (wrong payload shape, and the firmware trigger is commented out). A button that does nothing is worse than an absent one.
  • The controller does not push mode/status transitions over WS, so the console runs a 3 s refetch to catch AUTO->MANUAL arrival flips. Pushing those notifications is a future improvement here.
  • swarmit's /flash/stream progress reaches only the requesting client; multi-client progress belongs in /events or /status (to file upstream).
  • codecov.yml now gates on the Python project coverage and reports the two web apps informationally, so a frontend-heavy change does not fail against Python's project target.
  • The console is known to be comfortable to roughly 100 robots and to degrade above 200. The causes are measured and are mostly payload shape on both servers rather than console structure; they are a separate change set, not a blocker for this one.
  • The docs sweep (README quickstart, doc/cli/run.md for --swarmit-url, doc/reference/configuration.md, a console section) is deliberately not in the diff yet and lands before merge, after review of the functional change.

Validation

End-to-end against dotbot run simulator plus the fake swarmit server under dotbot/console-web/dev/ (all views and themes, waypoint missions, a full flash and start cycle, and the production build served from the controller). Then a bench session against real hardware - one DotBot v3 on a live MQTT broker with swarmit serve alongside - which is what found the address-casing join bug, the battery scale, and the fact that the closed-loop drive pad was unusable on a real robot. Automated: vitest for the pure logic (state merge, mission tail-grouping, SSE parsing, inspector formatting), pytest for config resolution, the proxy and the new route, a console CI job mirroring the frontend one, and sdist/wheel builds inspected to confirm the built console ships.

The dev harness implements the real swarmit HTTP contract, so pointing the console at a real swarmit server is a URL change.

Internal context for DotBots org members: rendered tracker at https://claude.ai/code/artifact/11e2969b-9d34-44a7-80c2-b63215b7d56f, source at https://github.com/DotBots/dotbot-workspace/blob/main/plans/unified-web-ui/implementation.html, board item DBK-127. Both are private to the org; everything a reviewer needs is above.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 17.57093% with 3312 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.06%. Comparing base (295e453) to head (55715c0).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
dotbot/console-web/src/MapView.tsx 0.00% 465 Missing and 1 partial ⚠️
dotbot/console-web/src/Footer.tsx 0.00% 463 Missing and 1 partial ⚠️
dotbot/console-web/src/App.tsx 0.00% 453 Missing and 1 partial ⚠️
dotbot/console-web/src/TestbedRail.tsx 11.08% 393 Missing ⚠️
dotbot/console-web/src/ListView.tsx 0.00% 197 Missing and 1 partial ⚠️
dotbot/console-web/src/viewChrome.tsx 12.73% 185 Missing ⚠️
dotbot/console-web/src/Inspector.tsx 26.22% 166 Missing ⚠️
dotbot/console-web/src/FirmwareSection.tsx 19.20% 143 Missing ⚠️
dotbot/console-web/dev/fake_swarmit_server.py 29.37% 125 Missing ⚠️
dotbot/console-web/src/Joystick.tsx 15.71% 118 Missing ⚠️
... and 17 more

❗ There is a different number of reports uploaded between BASE (295e453) and HEAD (55715c0). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (295e453) HEAD (55715c0)
4 0
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##           develop     #279       +/-   ##
============================================
- Coverage    83.55%   66.06%   -17.49%     
============================================
  Files          121      142       +21     
  Lines        11266    15171     +3905     
  Branches       570      691      +121     
============================================
+ Hits          9413    10023      +610     
- Misses        1850     5135     +3285     
- Partials         3       13       +10     
Flag Coverage Δ
console 11.41% <11.41%> (?)
frontend 97.66% <ø> (?)
python 76.35% <68.43%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dotbot/__init__.py 100.00% <100.00%> (ø)
dotbot/cli/gateway.py 100.00% <100.00%> (ø)
dotbot/config.py 98.08% <100.00%> (+0.06%) ⬆️
dotbot/console-web/src/types.ts 100.00% <100.00%> (ø)
dotbot/controller_app.py 97.00% <100.00%> (+0.09%) ⬆️
dotbot/models.py 100.00% <100.00%> (ø)
dotbot/tests/conftest.py 100.00% <100.00%> (ø)
dotbot/tests/test_config.py 100.00% <100.00%> (ø)
dotbot/tests/test_controller.py 100.00% <100.00%> (ø)
dotbot/tests/test_controller_app.py 100.00% <100.00%> (ø)
... and 29 more

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

geonnave and others added 27 commits August 19, 2026 13:57
…yphs

Adopts the design's north-up y convention (screen top = arena max-y),
which deliberately diverges from dotbot/frontend's y-down map - keep
that in mind when comparing positions across the two UIs.

AI-assisted: Claude Fable 5
AI-assisted: Claude Fable 5
…troller conventions

The controller reports direction as 0 = north (+y), positive counter-
clockwise, so the north-up display must negate it; the pad's steering
sign is verified against the simulator (pad-right = clockwise arc) but
the classic frontend used the opposite sign on real hardware - validate
on a real DotBot before trusting teleop in the field.

AI-assisted: Claude Fable 5
…should go)

The pad closes the heading loop itself at 10 Hz (P on heading error,
alignment-scaled throttle with a floor that keeps the motion-derived
heading observable). Replaces body-relative throttle/yaw teleop, which
felt erratic; verified convergent against the simulator.

AI-assisted: Claude Fable 5
AI-assisted: Claude Fable 5
AI-assisted: Claude Fable 5
AI-assisted: Claude Fable 5
The bot marker used a fixed CSS transition (left/top .2s linear).
Position updates arrive at whatever rate the source reports them
(~20Hz in simulation, sparser and irregular from real LH2 hardware),
so the fixed 200ms duration is either too short relative to the gap
between updates (the bot glides for 200ms then holds still until the
next update, producing visible stutter) or too long (a fast update
interrupts the transition already in flight). The old frontend has
no transition at all and snaps instantly, which is the same defect
in its more visible form.

Add useSmoothPositions: a small per-bot animation state driven by
requestAnimationFrame, using the previously observed update interval
as the next animation's duration (clamped to [60ms, 600ms]) instead
of a constant. A jump larger than 35% of the arena diagonal in one
update is treated as a teleport and rendered instantly rather than
animated across the whole arena. MapView now reads bot render
position from this hook instead of the raw prop, and the CSS
transition is removed (redundant with per-frame interpolation).

Verified: vitest (7 new cases covering first-sight snap, in-flight
retarget, duration clamping, teleport detection), tsc build, eslint,
and a manual pass against the running simulator + vite dev stack.

AI-assisted: Claude Sonnet 5
A curated view rather than the settings object: the same settings carry
mqtt_username and mqtt_password, and this route answers any browser that
can reach the controller.

AI-assisted: Claude Opus 5
…ng them

Needs a swarmit serving reset_cause, fault_name, image_state_name,
image_result_name and info.lh2_summary; against an older one those
fields read as unknown rather than being recomputed here, which is what
kept the two vocabularies drifting apart.

AI-assisted: Claude Opus 5
Reset could not work from here: swarmit's /reset takes a locations map
rather than a device list, so the call 422s, and the netcore's handler
stores the position but never triggers the SoC reset - the line is
commented out. The action type is narrowed so it cannot come back by
accident.

AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
The simulated bot's address is uppercase but the rx path compared it to
hex()[2:], which is lowercase and unpadded, so the gate was never true
and every move and waypoint frame was dropped: commands returned 200 and
the bot did not move, with nothing logged. The uplink index lookup missed
the same way and fell back to bot 0, silently routing every frame through
the wrong network mode.

AI-assisted: Claude Opus 5
swarmitAction resolved on any status, so a 502 from an absent swarmit
server raised a "Start sent" toast; the map drew swarmit's (0, 0) no-fix
sentinel as an arena corner, which puts an uncalibrated fleet in one pile
that reads as real; the file input kept its value, so re-picking a rebuilt
image fired no change event and flashed the bytes captured the first time;
and a stale queue-clear timer could leave a job without its address, which
throws where nothing catches it.

AI-assisted: Claude Opus 5
The flag is hand-typed, so it is normalised where it is stored rather
than at the REST boundary, which would be a compat shim. An address that
matches nothing now warns with the address instead of logging at info and
exiting silently.

AI-assisted: Claude Opus 5
The classic frontend stays mounted at /PyDotBot: it is the only place the
qrkey demo, the REST demo and the SailBot views exist, so this changes
which UI opens rather than retiring one. The path is chosen from what is
actually built, so a source checkout with no console still opens the
classic UI instead of a 404.

AI-assisted: Claude Opus 5
Two controller tests drive the full run loop with headless defaulting to
false, so every local run put two tabs on the developer's screen. They now
set it, and an autouse fixture neutralises webbrowser.open for the whole
suite so a future test cannot reintroduce this.

AI-assisted: Claude Opus 5
Breaking: the REST/WS API was served on 0.0.0.0 with no authentication,
and /swarmit/* now forwards to the swarmit server, so a wide bind also
republished an otherwise-local swarmit at the controller's reachability.
--controller-http-host restores the old behaviour and warns when it is
used.

AI-assisted: Claude Opus 5
@geonnave
geonnave merged commit ec66763 into DotBots:develop Aug 25, 2026
16 checks passed
@geonnave
geonnave deleted the unified-web-ui branch August 25, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants