Skip to content

Fleet lab: swarm coordination sandbox + teaching tool#2

Open
publu wants to merge 121 commits into
mainfrom
feat/fleet-lab
Open

Fleet lab: swarm coordination sandbox + teaching tool#2
publu wants to merge 121 commits into
mainfrom
feat/fleet-lab

Conversation

@publu

@publu publu commented Jun 15, 2026

Copy link
Copy Markdown
Owner

A multi-quadruped /fleet sandbox reachable from the robot picker — built to teach how a swarm covers ground over an imperfect radio, not just demo it.

Fleet sandbox (/fleet)

  • 4 strategies under real limits (radio range, airtime, onboard memory, inbox depth, one-goal-at-a-time, lossy links): lone wolves, gossip, claim-and-yield, one commander.
  • Hover-to-inspect any robot: tiles it sensed firsthand (filled) vs. only heard from peers (outlined), with a live knowledge/inbox/data card.
  • Base station + data points: discover data and relay it home with greedy geographic routing (multi-hop / data-mule). The "main server" / sink.
  • Environments: open / scattered obstacles / building with line-of-sight-blocking walls.
  • CONCEPTS panel (expandable) answering in-UI: delivered vs dropped, overlapping searches, optimisation, "is this libp2p?", swarm intelligence/stigmergy — plus tooltips on every metric.
  • "Your algorithm": a live JS strategy editor + a Generate button that asks the local runtime's LLM to draft a policy (POST /api/fleet/strategy), runnable on the spot.

Python twin

  • roborun/swarm/ — the same comms model, strategies, base relay and data points as runnable Python (python -m roborun.swarm); ships with the package.

Also

  • ROS card "Allow network scan to load robots" — trips the browser's local-network permission and lists every rosbridge robot found as its own one-click view.
  • ROS-connected robots reuse the exact sim deck; the EYES camera docks into the layout instead of floating.
  • Replaced the last native confirm() (deploy-to-robot) with the styled in-app modal.
  • vercel.json (cleanUrls) so /fleet resolves on the static Vercel build.

Verification

  • 106/106 tests pass; JS validated; pages boot in Chrome with no console errors.
  • Sim, hover, base relay, environments, custom-strategy run, ROS scan button, and both new endpoints (/api/fleet/strategy, /api/sources/scan) all checked.

Note: a pre-existing uncommitted Vercel-analytics change in roborun/web/runtime-base.js was intentionally left out of this PR.

🤖 Generated with Claude Code

publu and others added 30 commits June 12, 2026 18:42
…elf-deadlock

- rosbridge.py: a websocket recv timeout on a quiet socket (a sim robot
  with no /tf chatter) was treated as a disconnect, causing an infinite
  reconnect loop that never replays subscriptions. Timeouts now continue.
- ros_camera.py: state() called is_active() while already holding the
  same non-reentrant lock, freezing every behavior's see() on first use.
  The freshness check is now inlined.
…oes live

The arena/deck pages already degrade gracefully on a static host, but
silently: relative /api fetches 404 and the page stays in demo mode even
when a live roborun is one port away.

runtime-base.js (loaded first by both pages) wraps fetch: /api calls
resolve same-origin first, then probe http://127.0.0.1:8765. A badge
shows the mode; in demo mode it keeps probing, so starting roborun
upgrades the open page to the live cockpit with one click.

Server side: Access-Control-Allow-Origin on all responses (deduped out
of do_OPTIONS — doubled CORS headers are rejected by browsers) plus
Access-Control-Allow-Private-Network for Chrome PNA preflights.
The whole thesis is switch from sim to robot without changes — so the
arena, not a separate deck, is what a connected robot looks like. When
/api/ros/health reports a robot, the same arena page enters robot mode:

- pose/heading/altitude come from the telemetry handle (SIM_SPEC contract)
  and drive the same bot body; the level hides, the accumulated lidar
  cloud is the map, the minimap and telemetry panels read as before
- an EYES panel shows /api/camera/stream — the same pixels robot.see()
  runs YOLO on
- WASD publishes real cmd_vel through /api/ros/move (now with linear_z
  for drones); behaviors keep running server-side, untouched
- pushState is gated off: feeding the arena backend while a robot is
  connected would flip get_arena().is_active() and silently reroute
  robot.see()/move() from hardware to the browser sim

Plus two host-fallback fixes with the same root cause: _get_ros_client
and RosTelemetry._try_subscribe demanded a profile robotIp and went dead
without one, even while a live connection existed — both now ride the
already-connected client. This was why robot_type never resolved (and
why drone cmd_vel fell back to /cmd_vel).
The camera stream was last-writer-wins: webcam and robot camera both
wrote /tmp/roborun_frame.jpg, so the EYES panel showed your desk while
claiming to be the robot. Each pipeline now writes its own file and
/api/camera/stream takes ?source=robot|webcam|auto (auto: fresh robot
frames outrank the webcam).

New sources layer answers "what can see and what can move, right now":
- GET /api/sources — webcam on/off, connected robot + camera state, and
  rosbridges discovered on the local /24 (plain TCP probe of :9090,
  cached 60s); POST /api/sources/scan forces a rescan
- arena EYES panel gets a source picker (robot camera / webcam)
- in sim modes the arena shows a chip for any rosbridge found on the
  network — one click connects and reloads into robot mode. A robot on
  your wifi is a source, not a config step.
A connected robot deserves a robot cockpit, not a game with a robot in
it. The screenshot version showed DOG—SANDBOX missions, practice RUNS,
sim crates in the main view, and a sandbox policy editor one click away
from commanding live hardware — the exact path by which player_policy
(forward=0.8 at 10 Hz) flew the test drone to 53 m and 45 m off the map.

In robot mode:
- MISSION/LEVELS/RUNS panels and their toolbar buttons hide (CSS via
  body.robot-mode); loadLevel can no longer resurrect the sim level
- the POLICY panel becomes the robot's behavior editor: it loads the
  source of the behavior actually running (new POST /api/behaviors/read,
  stem-only, no paths), RUN hot-reloads that file, STOP disables it
- deploying to hardware is deliberate: RUN asks for confirmation and
  names the file; the LLM mission compiler gets a be-conservative
  context instead of the level brief
- footer says what WASD really does now: drives the real robot
- room/practice telemetry hides; main camera defaults to chase
…hero

The arena was still a game with a robot crudely piped in — a low-poly dog
floating in a void, the real camera shrunk to a corner, sim crates in
view, the onboarding splash ('pick a robot, pick a task') greeting a live
drone. A connected robot now gets a purpose-built cockpit instead:

- the robot camera fills the stage (full-bleed, cinematic vignette) with
  live YOLO detection boxes overlaid (new /api/robot/detections, normalized)
- a glass identity bar: type glyph, LIVE pulse, host, and ALT/SPEED/HDG
  telemetry chips; OSD reticle + POS/ALT/HDG readouts over the feed
- a tactical minimap (range rings, trail, heading wedge; lidar when present)
- POLICY slides in to edit the *running* behavior; DEPLOY is confirmed
- an event ticker of the robot's live decisions
- the game panels, 3D arena, toolbar, and splash are fully suppressed in
  robot mode; the splash is gated so it never shows over a robot

Camera served as single JPEG frames (/api/camera/frame) polled by the
client — deterministic, unlike an MJPEG <img> that half-paints.

Telemetry hardened against flaky rosapi discovery: /rosapi/topics times
out on this setup, which left type=webcam_only and no pose. Now when
discovery returns empty, trust the type roborun connect saved and
subscribe to the standard topics blind (a subscribe to a not-yet-seen
topic is harmless and flows when it appears).
Answering 'I don't want the drone — give me something else even though ROS
is connected', and making the cockpit a complete shell:

- SOURCE picker: every robot/sim this runtime can reach in one menu — the
  connected robot (live), the browser sim arena, and any rosbridge found on
  the LAN. Pick the sim and the page pins to it (localStorage) without
  disconnecting the robot; a chip offers the way back. Multiple ROS robots
  just appear.
- the policy editor is syntax-highlighted now (a colored <pre> underlay
  behind a transparent textarea — keywords, defs, strings, decorators,
  comments, numbers) instead of plain white-on-black.
- TIMELINE panel (bottom-left, mirroring the tactical map) streams the
  robot's decisions and sightings with timestamps and source-colored dots —
  the same stream+map→timeline surface the sim arena has, so the experience
  is consistent whatever the source is.
- DECK link moves into the top bar.

The cockpit shell — camera stream (hero), tactical map (objects, moving vs
stationary, range rings), timeline, policy — is now one consistent UX; what
fills it is the source.
…urce

Pablo's vision: the sim IS the robot's visual view — what the camera would
see — so there should be one view, not a game UI and a robot UI. The stream,
point map, and timeline are all derived from inputs (camera, cloud, pose)
that both a sim and a ROS robot provide.

The cockpit is now the universal shell, generalized to enterCockpit(src):
- src=robot: the stream is the robot camera (frame-polled); map + telemetry
  from the ROS endpoints.
- src=sim: the stream IS the 3D arena render (POV camera, full-screen behind
  the chrome); telemetry from the sim body; the tactical map from the sim's
  world-located sightings + trail; DEPLOY/HOLD run the same policy through
  the game's path; a LEVELS button (sim-only) picks robot + task.

Both render the same HUD, tactical map (objects, range rings, moving vs
stationary), timeline (decisions + sightings), and policy editor — only the
source behind them changes. The game's panel-salad layout and the auto-splash
are retired; LEVELS reopens the picker on demand. body.cockpit + .src-robot/
.src-sim gate the source-specific bits.
…m view

Three issues from using it live:

1. The sim's timeline showed 'frame … · person' — the connected drone's
   camera events. The sim cockpit was reading the shared server event log.
   The sim now keeps its OWN client-side log (simLog): YOLO sightings and
   policy decisions from its own perception, never the server's. The
   tactical map likewise builds objects from the sim's client raycast
   detections (currentDets), not the contaminated server sightings.

2. The sim 'rotated like an idiot' — POV put you inside the dog's head as
   its policy turned. The sim hero is now the chase camera: a stable
   3rd-person view of the robot in its world.

3. The policy panel's header and close were hidden behind the top bar and
   it read as an inaccessible wall of code. It's now a framed floating
   panel below the bar — visible BEHAVIOR header, close button, and the
   DEPLOY/STOP bar. The track readout moved to a centered bottom pill so
   it no longer collides with the timeline.
…deck

There were three doors to two UIs: / and /deck served the legacy flight
deck, /arena served the cockpit, and each linked to the other. Confusing
and redundant.

Now /, /deck and /arena all serve the cockpit — the single view. A
connected robot shows its camera/map/timeline; no robot shows the sim; the
SOURCE picker switches between them. The DECK button and cross-links are
gone. deck.html/deck.js stay in the tree but are no longer routed (the
flight recorder still runs server-side; its UI can fold into the cockpit
later if needed).
Serving the same page at three paths still read as three things. Now '/'
is the one canonical URL for the cockpit; /deck and /arena redirect there.
… editor

From live use:
- the 'enter cockpit' chip overlapped the top-bar actions (SOURCE/POLICY/
  HOLD) — moved it below the bar so nothing is blocked.
- the sim immediately span a dog in circles: the starter policy auto-ran.
  The sim now starts PAUSED (simArmed=false → policy holds); DEPLOY arms it,
  STOP/HOLD pauses. Calm on arrival, you choose when it moves.
- the source picker was a cramped dropdown — now a centered modal with a
  backdrop and large source cards (Esc / backdrop / ✕ to close).
- the policy editor clipped long lines and was too narrow to code in — added
  an expand toggle (⤢) that widens it to 80vw with a bigger font.
Deploying to a real robot popped the browser's native confirm ('127.0.0.1
says…') — off-brand and ugly. Now it's an in-app modal matching the cockpit:
amber-bordered glass on a dimmed backdrop, a clear warning, Cancel + green
DEPLOY buttons (Esc/backdrop cancels).
- LEVELS did nothing: cockpit CSS force-hid #start. Now #start.show shows
  in cockpit mode, so the robot+task picker (quadruped/humanoid/drone) opens.
- the sim's lidar wasn't on the map and the 3D cloud sprayed the scene. The
  3D cloud is now hidden in the cockpit; the sim's 36-ray lidar accumulates
  into the 2D tactical map (world frame) — the generated map building up.
- the cryptic HOLD button is now a clear PAUSE/RESUME toggle whose label
  reflects state; PAUSE holds the policy, RESUME runs it. DEPLOY/STOP keep
  it in sync.
Picking a new robot (quadruped/humanoid/drone) from LEVELS rebuilt the sim
but the cockpit identity stayed 'DOG'. pollSimCockpit now refreshes the
type + glyph each tick, so the header reflects the robot you chose.
Two real bugs the audit surfaced:

1. /api/arena/state 500'd on every push when a recording was active: the
   handler reassigned 'h = pose.get("heading")', shadowing the HTTP handler
   'h', so the closing send_json(h, ...) got a float. Renamed to 'hd'.

2. The sim's player_policy is a server behavior. While a sim browser feeds
   /api/arena/state the arena is active and robot.move() drives the arena dog
   (correct). But once that browser closes, the arena goes inactive and the
   still-enabled player_policy falls through to drive the REAL robot. Now the
   sim disables player_policy on beforeunload (keepalive fetch) and the robot
   cockpit disables it on entry — so a closed/abandoned sim can't fly the
   drone.
roborun autostarted the webcam (with its privacy light) on every boot to
avoid a blank first screen. But if a robot is connected, the robot's camera
is the source and the webcam is just an unwanted light on the user's
machine. Autostart now skips the webcam when roborun connect has saved a
robot; it's still available as a manual camera source.
The static site (and any non-runtime page) now opens on the ROBORUN ARENA
menu — the front door to the whole system — instead of dropping straight
into a sim. A robot wired directly into THIS runtime still boots to its
cockpit; everywhere else the menu leads.

The menu gains a fourth card, ROS ROBOT — REAL HARDWARE, alongside the three
sim robots. It shows the complete system whether or not anything is
connected: a live robot (enter its cockpit), any rosbridge found on the LAN
(one-click connect), and an IP field to connect by hand. So 'the same code
drives a real robot' is a thing you can actually click, not just a tagline.

Picks: a sim robot+task enters the sim cockpit; a ROS robot connects and
enters the robot cockpit.
Gazebo and Isaac Sim aren't hardware but speak ROS just the same, so the
real split is browser-sim vs anything-on-rosbridge — not sim vs real. The
card now reads ROS · GAZEBO · ISAAC · HARDWARE: 'connect anything on
rosbridge — a Gazebo or Isaac sim, or a real robot.'
The LAN scan did a plain TCP open of :9090, so any unrelated service on that
port showed up as a 'robot to connect to' — confusing false positives. It now
completes a websocket handshake (what rosbridge actually is); non-websocket
:9090 services are filtered out. Result: only real rosbridge endpoints appear.

Also: a connected source with no detectable robot type now reads ROSBRIDGE,
not the ugly WEBCAM_ONLY.
The chip is the sim cockpit's affordance to jump back to a connected robot,
but pollNetworkRobots returned early in robot mode without hiding it — so it
leaked into the robot cockpit, telling you to 'enter cockpit' while you were
already in it. It now shows only in the sim cockpit and stays hidden in the
menu and robot cockpit (which have the ROS card / are already there).
A rover that drives on /cmd_vel and senses with a laser scanner — no joints,
no mavros — was falling through to webcam_only. It's now recognized as a
mobile ground robot (quadruped profile: cmd_vel control + lidar + camera), so
its pose, lidar map and camera all light up the cockpit instead of reading as
'WEBCAM ONLY'.
The cockpit map drew accumulated lidar as a scatter of 1.5px dots — noisy and
hard to read. It now bins returns into world cells and fills them (denser hits
= brighter = more confidently a wall), so the swept area reads as solid
structure — the built-up map the old deck's ROBOT MAP showed. Also enlarged
the panel (280px, taller canvas) since the map carries real information.
You preferred the old deck's arrangeable multi-panel layout (and its ROBOT
MAP) over the camera-hero cockpit. So the connected robot now drives the same
deck the sim uses, with real telemetry behind every panel:

- pose places the bot in the 3D scene; the VIEW panels render it from any
  angle (top/chase/orbit), and WASD still grabs the wheel
- lidar feeds integrateLidar -> the occupancy ROBOT MAP and the 3D point
  cloud (the map builds up as it drives)
- a new EYES panel shows the robot camera with YOLO boxes (robot mode only)
- STATUS shows pose/odometer/cmd; POLICY loads and RUN deploys the robot's
  own behavior (confirmed); MISSION becomes the robot identity
- sim-only bits (LEVELS, RUNS) hide in robot mode

Same deck, two sources. The camera-hero cockpit is retired (code dormant);
enterSimCockpit is now a no-op and the sim runs in the deck directly.
A multi-quadruped "/fleet" sandbox reachable from the robot picker, built to
teach how a swarm covers ground over an imperfect radio.

- Coverage sim with 4 strategies (lone wolves, gossip, claim-and-yield,
  one commander) under real limits: radio range, airtime, onboard memory,
  inbox depth, one goal at a time, lossy links.
- Hover any robot to see what it knows: tiles it sensed firsthand (filled)
  vs. only heard from peers (outlined), with a live knowledge card.
- Base station (the "main server") + data points: discover data and relay it
  home with greedy geographic routing (multi-hop / data-mule).
- Environments: open / scattered obstacles / building with LOS-blocking walls.
- CONCEPTS panel (expandable) explaining delivered vs dropped, overlap,
  optimisation, libp2p/gossipsub, stigmergy — plus tooltips on every metric.
- "Your algorithm": live JS strategy editor + a Generate button that asks the
  local LLM to draft a policy (POST /api/fleet/strategy), runnable on the spot.
- roborun/swarm/: the same model + strategies + base relay as runnable Python
  (python -m roborun.swarm), the headless twin; ships with the package.

Also:
- ROS card "Allow network scan to load robots" button — trips the browser's
  local-network permission and lists every rosbridge robot found as its own view.
- ROS-connected robots reuse the exact sim deck; EYES camera docks into the
  layout instead of floating.
- Replaced the last native confirm() (deploy-to-robot) with the styled modal.
- vercel.json (cleanUrls) so /fleet resolves on the static deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A freshly opened page (even when a robot is connected on the local runtime)
now lands on the picker instead of auto-entering the robot deck — the menu is
the front door. Auto-enter only when a robot is explicitly pinned. In the deck,
keep a visible '⊞ MENU' button so there's always a way back to select something.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ROS-parity change made EYES a layout-managed panel, so once the robot deck
opened it the saved layout carried it into later sim sessions — where there's
no robot camera, leaving it black. Guard it in applyLayout: the EYES panel and
its toolbar toggle only show when MODE === 'robot'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each robot now tracks everKnown (lifetime, never evicted) alongside its
memory-capped current set. Hover splits it into IN MEMORY NOW (sensed vs heard,
out of the memory cap) and EVER KNOWN (cells discovered, and how many it has
forgotten because memory filled). The map overlay adds a faint third tier for
the forgotten footprint, under the bright in-memory cells. Mirrored in
roborun/swarm (Robot.ever_known + forgotten()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l, record-everything channels, headless sim, SimBackend/rl_env/gz, retention, perf benches

Implements the buildable items across the six private build specs, each with tests
(167 passing):
- scenario_defs: seed + run_matrix (A/B+sweeps) + regression_gate
- routes/scenarios.py + web/scenarios.html: the Scenarios board (suites, pass-rate, runs)
- recorder.close: seal now, anchor async (seal 9.9ms vs up to 30s)
- spatial_memory.recall(by=clip|label|near|time): unified retrieval
- recorder /cmd /telemetry /gps /cloud channels + post-clamp tap in behaviors.move
- simulator headless render gate (full SPS); retention.py GC
- sim_backend.py (sense MjData via the handle), rl_env.py (reset/step/obs), gz.py
- r2sync list_keys(start_after) cursor; scripts/bench_seal.py + bench_clip.py
publu added 30 commits June 17, 2026 14:32
- ros_telemetry subscribes /gps/fix,/fix,/navsat/fix (NavSatFix) and folds GPS
  into the run's MCAP via write_gps + pushes to the telemetry bus.
- RunRecorder.channels() exposes written topics; record/stop captures them before
  close and finalize() writes the channel list into the run manifest, so the
  browser/search know a run's modalities without parsing the tape.
- isaac.py mirrors gz.py: detect_world (live-stage discovery via the ROS bridge),
  IsaacClock (lockstep pause/step/reset for determinism), level_to_prims (RoboRun
  level -> USD stage prims, pure/testable), IsaacRunner (degrades to a dry-run
  plan offline). Isaac is now 'available' (real driver), not 'planned'.
- backends.py probes isaac.detect_world like gz; capability matrix gains photoreal.
- tests: registry has 6 backends with valid status; SimBackend satisfies the
  handle contract; gz+isaac degrade cleanly offline with deterministic plans.
- spatial.py: transform_point (sensor-local -> env world frame), sensor_pose
  (fixed-camera extrinsics vs robot pose), camera_frustum (view cone geometry),
  cluster_tracks (merge same-label detections within radius into env-frame object
  tracks with running-mean centroid + count + first/last seen).
- SpatialMemoryStore.object_tracks(): tracks scoped to the active project index.
- /api/spatial: the environment's spatial picture — object tracks + registered
  camera placements/frusta in one frame.
- /browser draws a top-down spatial map: tracked objects (sized by count) +
  camera frusta, auto-scaled to the env frame. Detections + cameras + lidar now
  read as one picture of 'what is where'. 5 tests.
/run is now a true multi-panel telemetry viewer: play/pause + scrub drive a
shared playhead across the time-series panels (velocity, clearance), the camera
frame, and a synced event log that highlights + scrolls to the event at the
current time. Every panel moves on one clock (spec 02 P2).
…tors

- worlds.py: a multi-floor warehouse generator — grid of rooms per floor, aisle
  walls with doorways, detectable items, and ELEVATORS that stack adjacent floors
  at a shared shaft. Deterministic by seed. /api/worlds/warehouse.
- /fleet-sim + fleet-sim.js: a real N-robot sim (1-40 robots) in ONE shared
  warehouse across up to 4 floors. Robots navigate room-to-room, ride elevators
  to change floors, and detect items — all on one shared clock. Live HUD: robots,
  found/total, coverage %, elapsed; per-floor bot counts; play/pause/speed.
  Shows it's collecting jointly into the active project's environment (P3 data
  side already wired). Scales to tens of robots at interactive rate.
- linked from cockpit VIEWS + browser nav.
This is spec 04: launch fleets that collect data together from a space large
enough — a warehouse with elevators.
…pecs

Bump for the platform feature set (projects/environments, data standard, telemetry
browser, backends incl. Isaac, fleet sim with warehouse+elevators, spatial
perception+tracks, unified recall, project/env data isolation, standard env +
camera registration). Built + twine-checked; ready to publish.
…ch-style)

The platform spine existed but the UX didn't reflect it. Now it's a real product:

- shell.js + ui.css: ONE persistent chrome on every dashboard page — left sidebar
  (Home / Sims&Robots / Cockpit / Data{Browser,Scenarios,Timeline,Search,Analytics}
  / Fleet / Projects) + top bar with the PROJECT/ENVIRONMENT SWITCHER as the spine
  (switch → everything re-scopes) + live/go-live badge. Auto-wraps each page's
  content; pages dropped their bespoke navs. Retires project-chip.js.
- home.html at /: the Antioch-style console — active scope hero, environments grid
  (open → launch), data KPIs, recent runs, quick actions. Clean go-live state on
  the static host.
- setup.html at /setup: guided robot/sim setup — project → backend (live
  availability) → robot+task or warehouse fleet → Launch (creates env, sets active,
  routes to /sim?level= / /fleet-sim / connect). Replaces the tutorial-wall modal.
- Cockpit demoted to a workspace at /sim (/ is the dashboard now); its start modal
  is a slim level-switcher with links back to Dashboard/Setup. build_site index =
  home.html; vercel.json rewrites /sim → arena.html so the playable demo still
  works. Data is divided by project/environment everywhere, not muddled.

277 passed, 1 skipped.
One agent per route, each scoped to only its page's files, reusing the shared
ui.css tokens/components + the app shell — no shared-file edits, no collisions.

- / (home): layered hero w/ live status, scannable env cards (backend/mode/runs/
  last-activity), richer KPI tiles, status-dotted recent runs, strong offline state
- /setup: real numbered wizard — gutter badges, ✓ selected states, backend caps,
  always-visible live summary + sticky Launch, responsive
- /browser: dropped redundant scope banner, legible spatial map w/ legend + axes,
  scannable run rows, tidy backend matrix
- /scenarios: bigger donuts, avg-duration KPI, tag facet, aligned dotted timelines,
  sticky-header runs table w/ Dur column
- /search: controls grouped into a console card (prominent query + collapsible
  filters), polished result cards, dropped redundant :root overrides
- /timeline: card-framed two-pane, outcome dots, clean selected state, responsive
- /analytics: shared .kpi tiles, gradient bars + peak-marked sparkline, fleet grid
- /projects: status-bar active scope, scannable project cards, contained camera
  tagging, strong empty state
- /run: two-column synced player, grid/axis-labeled charts, 3-col event log;
  run-detail.js keeps renderRunRecord signature (timeline still works)
- /fleet (swarm lab): rebuilt full-viewport overlay into a shell-friendly 3-column
  dashboard (ResizeObserver + screenToWorld fix), shared tokens
- /fleet-sim: KPIs as .kpi tiles, legend, richer canvas (rooms/walls/labeled items/
  elevators/robot trails, HiDPI)

Verified: all 11 node --check clean; headless sweep = shell wraps every route,
11 nav items, zero orphaned content, zero console errors. Suite green.
The 11-route optimization moved nav into the centralized app shell and titles
into the shell top bar, so the old assertions (per-page header text, per-page
nav hrefs) were obsolete. Now: assert each dashboard loads shell.js + keeps the
element ID its JS drives, and that shell.js carries the links to every route.
The /sim 'pick a robot & task' modal was hardcoded old-style hex (green-on-black,
.22em letter-spacing) and clashed with the rest of the UI. Reskinned every modal
class (start-card, bot-card, ros-card, fleet-card + controls) onto the ui.css
tokens (--panel/--line/--fg/--fg-2/--fg-dim/--accent/--blue/--r) so the cockpit's
launcher matches the dashboard. Also removed the local fake test projects
(warehouse-pilot) so the project switcher reads 'scratch' again.
Was a long single-page scroll of all 4 steps. Now: a stepper (Project →
Backend → What to run → Environment) showing ONE step at a time, with Back/Next
and Launch only on the final step. Click a completed dot to jump back. Each step
gets a short lead line so it's digestible. All IDs/logic preserved; verified
headlessly — clean step transitions, zero errors.
…nify store path

The user's point: dashboards should fill because you ran rapier, not from seeds.

- /api/arena/state now indexes the sim's detections into the spatial store
  (throttled ~1 Hz, scoped to the active project/environment) — so just playing
  in rapier populates /search, /api/spatial (tracks) and /analytics with REAL,
  env-frame-positioned data. Previously detections only persisted if you started
  a recording and sealed it; the dashboards sat empty (which is why I'd seeded
  fakes). Verified: a sim detection is instantly searchable at its world pose.
- Fixed a real bug: the searchable index used a CWD-relative '.roborun' while the
  recorder used '~/.roborun', so data split across two dirs. Unified the store to
  the same root as recorder.runs_root (honoring ROBORUN_STATE_DIR).
- Wiped the 268MB of fake test observations + test runs → clean slate that only
  fills from real activity.
Replaces the 2D kinematic toy with an ACTUAL Rapier world (same engine as the
cockpit): a warehouse of fixed-cuboid walls/rooms, N kinematic-capsule robots
driven by a character controller so they collide with walls AND each other,
navigating room to room. Top-down render of the true physics positions + trails.

Every item a robot detects POSTs to the new /api/fleet/observe, which indexes it
into the active project/environment store — so a fleet run fills /search and the
spatial map with real, multi-robot, world-positioned data. Verified headlessly:
Rapier WASM loads, robots physically move, coverage climbs, and the store grows
from fleet detections. Old 2D fleet-sim.js removed.
Stacked floors in one Rapier world (6m apart in Y, so physically independent),
each a ground slab + walls. N robots start on floor 0; ~25% of targets are the
central elevator shaft — reaching it transitions a robot to the adjacent floor
(brief ride, then teleport to the shaft on the destination floor). One top-down
canvas per floor renders the true physics positions + per-floor bot counts.
Detections across all floors POST to /api/fleet/observe into the active env.

Verified headlessly: 3 floor canvases, robots ride the elevator (8/0/0 → 5/3/0
across floors), coverage climbs, store fills. floors/robots/speed controls live.
…ing + premature summary

- /sim was a full-screen cockpit with no obvious way back (the shell can't wrap
  the immersive 3D view). Added a fixed top-left ← Dashboard button.
- /setup: removed the redundant per-card step number (the top stepper already
  numbers steps — it read '1 ... 1') and stopped showing the full launch chain
  (dog · dog-sandbox on rapier → …) on earlier steps, which made it look like
  choices were already made. Earlier steps now show 'Step X of 4'; the full
  'Ready to launch — …' summary appears only on the final step.
…apier physics)

Keep them SEPARATE, not muddled. Doing rapier shows zero coordination-algorithm UI;
the algorithm work lives in its own place.

- Swarm Lab (/fleet) = layer 1: coordination algorithms — how a swarm decides where
  to spread & search, abstract, no physics. Reframed title + cross-link to Fleet Sim.
- Fleet Sim (/fleet-sim) = layer 2: real Rapier physics — bodies, collisions,
  multi-floor + elevators, joint data. Scope line reframed + cross-link to Swarm Lab.
  No strategy/algorithm controls here.
- Sidebar FLEET group reordered to read as the two layers (Swarm Lab → Fleet Sim).
- Setup's fleet path still routes to the physics sim with no coordination UI.
Setup made you pick a robot+task, then the cockpit's detectMode() called
showStart() unconditionally — reopening 'PICK A ROBOT & TASK' on top of the
already-loaded level. Now showStart() is skipped when /sim?level= is present
(you already chose); fresh /sim with no level still shows the picker. Verified
end-to-end: Setup→Launch → /sim?level=dog-sandbox, no picker; bare /sim, picker.
The default was semantic (CLIP) search — but sim/fleet detections are stored
label-only (no image → no embedding), so 'by meaning' returned 0 after a ~2s
CLIP call (looked like it hung). Default to 'by object' (label): instant and it
actually finds the data the platform generates. 'by meaning' stays available for
camera/real data that has CLIP embeddings (tooltip notes the requirement).
…side/connect

Picking Gazebo/Isaac/MuJoCo then launching dropped you into the in-browser Rapier
cockpit as if it were that engine. Clarified the backend step: Rapier plays in the
browser; MuJoCo/MJX/Gazebo/Isaac run server-side/externally (you connect to a
running sim); real robot connects over ROS.
1) Don't auto-run the rapier fleet: it now starts PAUSED (▶ play); robots are
   placed but still until you press play. Fixed the elapsed timer to count sim
   time (only while playing), not wall-clock — so paused actually reads as paused.
2) Fleet (and its algorithm) is SEPARATE: removed 'Warehouse Fleet' from the
   single-robot 'What to run' picker in Setup — it doesn't belong next to
   Quadruped/Humanoid/Drone. Setup is single-robot/real only; a small link points
   to Fleet Sim for swarms. Cleaned the dead S.fleet code + launch branch. The
   coordination algorithm stays in the Swarm Lab (separate page), as before.
The fleet ran the real 3D Rapier world but only rendered flat 2D top-down
canvases — jarring next to the 3D cockpit. Render it in actual 3D instead:

- three.js scene of the stacked multi-floor warehouse: per-floor grids + slabs,
  room walls as semi-transparent boxes, items as cylinders that light up green
  on detection, the elevator drawn as a translucent shaft spanning floors.
- Robots are 3D bodies (capsule + heading nose) at true Rapier positions;
  hidden while riding the elevator. Per-floor bot counts overlaid on the stage.
- Manual orbit camera (drag to rotate, scroll to zoom, gentle auto-rotate idle),
  framed to centre the whole stack. Lights tuned so robots/walls read clearly.
- Easier to find: labelled '3D' in the Home hero actions and the sidebar
  ('Fleet Sim · 3D'), distinct from the Swarm Lab (the coordination algorithm).

window.__fleet exposes {S,V} for headless tests. Verified under headless WebGL:
131 static meshes + 8 robot meshes, robots move on play, items detect, 0 errors.
The 'white page' was never a blank/broken page — it was white leaking below
the content. Pages whose <style> forgot body{background} (analytics, timeline,
search, fleet) defaulted to a white body, and the shell's content column is
transparent, so any short page showed white under the panels. This is the same
'white page under timeline' reported earlier.

Fix it once, structurally: ui.css now sets html/body + .app-shell + .app-col to
var(--bg), so no page can leak white regardless of whether it sets its own body
background or how tall its content is. Verified: analytics/timeline/search/fleet
all render rgb(10,13,11) top-to-bottom.
New self-contained package `strata/` — one engine, two data models, on any
S3-compatible endpoint (S3 / MinIO / R2 / B2 / local). Object storage is the
source of truth; local RAM/NVMe is only cache. No external DB or lock service:
serializable writes come from a CAS'd, numbered manifest log written straight to
the bucket (PUT If-None-Match), the same design object-store table formats and
serverless vector stores use.

ReductStore feature parity (time-series blobs):
  buckets/entries/records (µs timestamp + labels + content-type + blob), write by
  timestamp + batched writes, read latest/at-ts, time-range + label queries,
  each_n/each_s downsampling, FIFO quota eviction, write-block sealing by
  size/records/age, bearer-token auth w/ per-bucket permissions, filtered +
  checkpointed replication, REST API.

turbopuffer-class vectors + full-text:
  namespaces, upsert/delete with schemaless columnar attributes, exact NumPy ANN
  (cosine/euclidean/dot) over a per-namespace live view cached by manifest
  version, pushed-down attribute filters, BM25 full-text ranking, upsert
  shadowing (latest wins) via segment-seq.

Layout: backend (Local/S3/Memory + put_if_absent CAS), manifest (commit log +
checkpoints + optimistic retry), segment (immutable blob + columnar vector),
blobstore, vectors, query (shared filter AST), retention, auth, replication,
server (REST + idle-block ticker), client, cli (`strata serve`). Spec +
parity matrices in docs/STRATA_SPEC.md.

Tests (tests/test_strata.py, 26): both backends + CAS, segment roundtrips,
manifest commit/checkpoint/concurrent-writers (4 threads, 80 commits, 0 lost),
blob write/read/query/downsample/FIFO/durability, vector ANN/metrics/shadow/
delete/BM25/dim-check, auth, filter language, server+client e2e, auth
enforcement (401/403), replication (filtered+idempotent), and the WHOLE engine
running on the S3 backend via a fake S3 client (incl. If-None-Match). Full repo
suite: 286 passed.

Packaging: `strata` console script + [strata] extra (numpy, boto3).
- Compaction: blobs merge small segments into size-bounded blocks; vectors merge
  to one segment and physically drop shadowed + tombstoned rows. Both are
  conflict-free with concurrent writers (blobs key on time; the merged vector
  segment inherits the max sequence it replaces so newer writes still shadow it).
  Exposed as POST .../compact on blobs and vectors + client methods.
- Hybrid search: passing both `vector` and `rank_by` fuses vector ANN and BM25
  via reciprocal-rank fusion (turbopuffer-style).
- Fix: MemoryObjectStore.put_if_absent now locks so it's an atomic CAS — without
  it concurrent commits could both pass the existence check and lose a write
  (surfaced under full-suite load; real S3 If-None-Match is atomic server-side).
  Concurrent-writer test stress-passed 10×.

Strata suite: 31 tests; full repo: 289 passed.
Adds a cached IVF index (k≈√n k-means clusters) for approximate nearest-neighbour
search at scale, alongside the exact default:

- query(..., approx=True, nprobe=N): scores only the nprobe nearest clusters.
  Measured 7.6x faster than exact at 200k vectors; recall is tuned by nprobe
  (higher → recall → 1.0) — the same recall/latency dial turbopuffer exposes.
- Exact stays the default (ground truth, sub-ms to ~50k). When there's no filter
  the approx path uses the IVF candidate set directly (skips a costly intersect).
- IVF index cached per manifest version, rebuilt only when the namespace changes.
- approx/nprobe plumbed through the REST query endpoint + Python client.

Tests: recall >=0.7 at default-ish nprobe and >=9/10 at high nprobe (near-exact).
Strata suite 30; full repo 290 passed.
… /studio

A React/Vite SPA in app/ (builds to roborun/web/studio/) that unifies the
previously-siloed pages behind one shell with source-agnostic panels (LiveSource
/ RunSource), a global scrubber, and project scope.

- Front door: server.py redirects / and the old standalone routes into /studio.
- Live: first-run welcome + CTA, filtered events, framed empty states.
- Sims: thin launcher (Arena/Fleet/Data/Real); in-app behavior editor (hot-reload);
  Real robot = native rosbridge connect; embedded pages strip standalone nav.
- Runs: human-readable list + provenance bar with live verify + reversible tamper demo.
- Search: thumbnails, replayable affordance, honest CLIP gating (per-dim search,
  real embeddings only); drop-zone to import a .mcap and replay it.
- Agent (MCP connect + live activity), native Scenarios + Analytics, guided tour.

Backend fixes: sim/state _drone_ctrl crash; arena no longer silently auto-records;
run_series/frame_at tolerate corrupt/partial MCAPs; arena live obs carry run_id;
/api/run/upload, /api/demo/seed, /api/search/caps. Full audit in docs/STUDIO_AUDIT.md.
…ow, accent-fill)

Imported the official RoboRun Design System (claude.ai/design f84aeaa8) via the
claude_design MCP and re-skinned Studio to its visual language:
- dashboards set in the system monospace stack (dropped IBM Plex webfonts)
- flat layered surfaces + a single soft shadow — removed all glow
- --accent-fill active states, DS radii (10/6/999), 232px sidebar, 1280px cap
- welcome is the one tinted hero (radial accent wash); rr-pulse live dot
Kept the consolidated nav. See docs/STUDIO_AUDIT.md Pass 7.
… flow

The scope chip was hidden for newcomers, so there was no way to create or switch
a project. Now it's always shown (top bar): "+ New project" creates a project +
its default environment and switches to it; existing projects/envs are listed to
switch between; "use scratch" clears scope. Explains scratch plainly.
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.

1 participant