live-demo
Everything between "the app's smoke test is green" and "a stranger on the
website is driving the robot." This skill owns the demo architecture that
robium.org/demos/nav-trial runs in production: a control page whose
Start/Stop buttons manage private, per-visitor simulator instances on
Cloud Run, a session gateway inside the app container, and a viewer
handoff. Every command, flag, and gotcha here was verified live
(2026-07-13, nav-trial demo) — this is a distillation of a real deployment,
not a design sketch.
When to use this skill
- Publishing any robium app as an interactive web demo, and every design
choice inside that: flow shape, page anatomy, instance lifecycle,
visualizer, budget, and the Cloud Run deployment.
- Debugging a deployed demo (instance won't boot, viewer can't connect,
status endpoint misroutes).
- Cross-references — go to the sibling skill instead when the question is:
- foxglove_bridge setup, layouts, or MCAP for development use →
foxglove (this skill consumes its bridge; the demo-specific parts —
session gateway, deep-link handoff — live here).
- Container/compose patterns for the app itself →
integration.
- Whether the app is done enough to demo →
testing (smoke test
green is this skill's entry bar).
- Env reproducibility of the app →
environments.
Key directives
- Delegation posture: embed. The demo architecture (gateway contract,
Cloud Run tuning for sims, viewer decision table) exists nowhere
upstream in one place — it was derived by deployment. Bridge mechanics
are
foxglove's; everything demo-shaped is embedded here.
- A demo is a product surface: the smoke test extends to it. The app's
demo scenario gets its own gated smoke (
make demo-smoke): WebSocket
handshake through the gateway, /start claim, /status reaches
ready, intruder session rejected (409/403), one scripted goal
succeeds, /shutdown kills the container. Ship no demo without it.
- Scale-to-zero is non-negotiable; per-session cost is explicit.
min-instances=0 always. State the per-session cost implication of the
billing mode you pick (see references/cloud-run-tuning.md) — never let
an "idle" demo bill silently.
- One visitor, one instance, enforced in the gateway. The session
UUID claims an instance; a live tunnel is never shareable; an idle claim
is takeable (reload semantics). Never rely on Cloud Run routing alone
for isolation.
- Be honest on the page. Cold-boot time (30–90 s, with self-restart
on unlucky boots), session caps, and busy states are stated in the
demo page's terminal — a demo that pretends to be instant reads as
broken the moment it isn't.
Quick start
The proven path (mission-control flow, Foxglove deep-link viewer):
- App side — add a
demo scenario to the app: one launch = sim +
nav/policy stack + foxglove_bridge on an internal port + an
auto-init node (set initial pose / whatever makes the app immediately
drivable) + the session gateway owning $PORT. Copy
examples/demo_gateway.py (verified) and adapt the two constants.
- Deploy — Cloud Build the image, then (values verified for a
Gazebo+Nav2 stack):
gcloud run deploy demo-<app> --image=<image> \
--region=us-central1 --port=8765 \
--concurrency=4 --session-affinity \
--min-instances=0 --max-instances=5 --timeout=1800 \
--cpu=8 --memory=8Gi --cpu-boost --no-cpu-throttling \
--execution-environment=gen2 \
--set-env-vars=GZ_RELAY=127.0.0.1,GZ_IP=127.0.0.1,FASTDDS_BUILTIN_TRANSPORTS=UDPv4 \
--command=/entrypoint.sh --args="ros2,launch,<pkg>,demo.launch.py" \
--allow-unauthenticated --quiet
Map a same-site subdomain (e.g. demo.yourdomain.org) — required
for the affinity cookie to work from your site's pages (see gotchas).
- Site side — a
/demos/<app> page with the mission-control
anatomy (see references/demo-page.md): Start/Stop buttons, terminal
majority, fleet budget line, viewer button gated on ready.
- Gate it — demo smoke locally, then the same probes against the
live URL; only then link it from the homepage proof/apps card
("Try the live demo →").
Usage patterns
Choose the demo flow. Three shapes, in order of proven-ness:
| Flow |
What the visitor gets |
When |
| Mission-control page (proven) |
Start/Stop instance buttons, live boot terminal, fleet count, viewer opens on ready |
Default. Honest about boot time; visitors see the machinery (which is the pitch for infra products). |
| Deep-link only |
One "open in Foxglove" link; connection cold-boots the instance |
Minimal page work; boot happens behind the viewer's "connecting" spinner; needs the connection itself to hold CPU (request-based billing). |
| Embedded viewer (self-hosted) |
Viewer iframe in the page, no login |
Best UX on paper; costs a self-hosted viewer build and iframe/browser-storage complexity. Deferred by robium.org after trying it — revisit deliberately. |
Choose the visualizer (verified facts, 2026-07-13):
| Option |
Login |
Layout preload |
Embeddable |
Verdict |
app.foxglove.dev deep link (/~/view?ds=foxglove-websocket&ds.url=<wss>) |
Required |
✗ (visitor imports the layout file once — link it on the page) |
✗ (x-frame-options: DENY) |
Default: zero hosting, robotics users have accounts |
| Self-hosted Lichtblick (open-source Foxglove fork, MPL) |
None |
✓ (globalThis.LICHTBLICK_SUITE_DEFAULT_LAYOUT build hook) |
✓ (you control headers) |
Frictionless but you own the build + browser-storage kiosk-wipe; new-tab use is solid, iframe needs care |
| Foxglove embed SDK (embed.foxglove.dev) |
Org members only |
✓ |
✓ |
Paid tier + viewers must belong to your Foxglove org — internal portals, not public demos |
Instance lifecycle contract (the gateway, examples/demo_gateway.py):
POST /start?session=U claims · GET /status?session=U → JSON
(claimed/ready/rtf/nodes/uptime_s/remaining_s/fleet{running,budget}/log[]),
409 for foreign sessions · ws upgrade tunnels to the bridge, one live
tunnel max (second → 503) · POST /shutdown?session=U → SIGINT PID 1 →
container exits · idle claims takeable · page sends a pagehide
sendBeacon shutdown. Full rationale in references/gateway-pattern.md.
Show the demo where the proof lives. The homepage apps/proof card for
the app gets a primary "Try the live demo →" button to /demos/<app>;
the demo page carries the reproduction story (the brief that produced the
app) so the demo sells the plugin, not just the robot.
Fleet/budget display. Budget = --max-instances, stated statically
on the page. Live count comes from Cloud Monitoring's
run.googleapis.com/container/instance_count queried by the gateway
(metadata-server token + roles/monitoring.viewer on the runtime service
account), folded into /status and cached ~30 s. Never fetch fleet
status on page load — that request cold-boots a billable instance per
drive-by visitor; show it only while a session runs.
Platform gotchas
All verified in production, 2026-07-13 (details + fixes in
references/cloud-run-tuning.md):
- Gazebo discovery needs help on Cloud Run (no multicast):
GZ_RELAY=127.0.0.1 + GZ_IP=127.0.0.1, and even then the unicast
relay loses a sticky per-boot race ~half the time — ship a boot
watchdog (no sim data within ~120 s → SIGINT PID 1 → fresh instance
on the client's reconnect).
Connection: close on every hand-rolled HTTP response — Cloud
Run's proxy pools keep-alive connections; closing without declaring it
= edge 503 "malformed response" (invisible in local testing).
- SIGINT, not SIGTERM, stops
ros2 launch as PID 1 — the kernel
drops unhandled signals to PID 1 and launch installs no SIGTERM
handler.
- Session affinity cookies are SameSite-Lax — they never flow to a
*.run.app host from your site (cross-site). Map a subdomain of the
site's domain to the service and use credentials:'include' +
exact-origin CORS, or affinity silently does nothing.
- Request-based billing throttles CPU between requests — a sim boot
with no held connection freezes. Start-button flows need
--no-cpu-throttling (cost: idle-retention after sessions,
≈$0.20–0.40/session at 8 vCPU); connection-driven flows can stay
request-based since the ws holds CPU.
- FastDDS shared memory misbehaves in Cloud Run —
FASTDDS_BUILTIN_TRANSPORTS=UDPv4 silences open_and_lock_file errors.
- WebSocket probes need
curl --http1.1 against https Cloud Run
(h2 negotiation breaks the upgrade), and foxglove_bridge ≥3.x expects
subprotocol foxglove.sdk.v1 (the classic foxglove.websocket.v1
gets a misleading 400).
Customization
- Different app: the gateway and page are app-agnostic; the app-side
work is the
demo launch (stack + bridge + auto-init making it
instantly drivable) and picking what "ready" means (nav: initial pose
set + RTF measured; a policy demo might be "model loaded + env reset").
- Different budget/size:
--max-instances (budget), --cpu/--memory
(a Gazebo+Nav2 stack wanted 8 vCPU for RTF ≈ 1; measure with the
DEMO READY rtf= log line), --timeout (session cap).
- Non-ROS demos: the gateway pattern (claim/status/shutdown +
ws tunnel) works for any ws-speaking backend; swap the bridge for your
stream and the status file for your readiness signal.
References
references/gateway-pattern.md — the session gateway: endpoint
contract, claim semantics, tunnel guard, shutdown, watchdog.
references/cloud-run-tuning.md — flags, billing modes and their
costs, gz/FastDDS env fixes, same-site affinity, fleet monitoring IAM.
references/demo-page.md — mission-control page anatomy, session JS
flow, honesty copy, where demo links go on the site.
examples/demo_gateway.py — the production gateway (status: verified
2026-07-13, nav-trial live demo on robium.org).
- Upstream: Cloud Run docs,
Foxglove deep links,
gz-transport relay,
Lichtblick. Sibling
skills:
foxglove (bridge/viewer), integration (containers),
testing (the entry bar), environments (app env).
Changelog
- 1.0.0 (2026-07-13): created from the nav-trial live-demo deployment on
robium.org — mission-control flow, session gateway, Cloud Run tuning,
and visualizer decision table, all production-verified same day.
1---2name: live-demo-23description: Turn a working robium app into a public, interactive web demo: a mission-control demo page (start/stop instance buttons, live boot terminal, fleet budget), per-visitor simulator instances on Cloud Run (scale-to-zero), and a visualizer handoff (Foxglove deep link or self-hosted viewer). Use when: 'live demo', 'demo page', 'let visitors drive the robot', 'try it live on the website', 'demo instance start/stop', 'host the sim for the demo', choosing the demo visualizer, or budgeting/deploying demo backends. Load after an app passes its smoke test (testing) — a demo hosts a finished app. Pairs with foxglove (bridge/viewer mechanics) and integration (container patterns). Not for: developer-facing visualization during a build (foxglove/rviz2) or general website building.4---56# live-demo78Everything between "the app's smoke test is green" and "a stranger on the9website is driving the robot." This skill owns the demo architecture that10robium.org/demos/nav-trial runs in production: a control page whose11Start/Stop buttons manage private, per-visitor simulator instances on12Cloud Run, a session gateway inside the app container, and a viewer13handoff. Every command, flag, and gotcha here was verified live14(2026-07-13, nav-trial demo) — this is a distillation of a real deployment,15not a design sketch.1617## When to use this skill1819- Publishing any robium app as an interactive web demo, and every design20 choice inside that: flow shape, page anatomy, instance lifecycle,21 visualizer, budget, and the Cloud Run deployment.22- Debugging a deployed demo (instance won't boot, viewer can't connect,23 status endpoint misroutes).24- Cross-references — go to the sibling skill instead when the question is:25 - foxglove_bridge setup, layouts, or MCAP for *development* use →26 `foxglove` (this skill consumes its bridge; the demo-specific parts —27 session gateway, deep-link handoff — live here).28 - Container/compose patterns for the app itself → `integration`.29 - Whether the app is *done* enough to demo → `testing` (smoke test30 green is this skill's entry bar).31 - Env reproducibility of the app → `environments`.3233## Key directives3435- **Delegation posture: embed.** The demo architecture (gateway contract,36 Cloud Run tuning for sims, viewer decision table) exists nowhere37 upstream in one place — it was derived by deployment. Bridge mechanics38 are `foxglove`'s; everything demo-shaped is embedded here.39- **A demo is a product surface: the smoke test extends to it.** The app's40 demo scenario gets its own gated smoke (`make demo-smoke`): WebSocket41 handshake through the gateway, `/start` claim, `/status` reaches42 `ready`, intruder session rejected (409/403), one scripted goal43 succeeds, `/shutdown` kills the container. Ship no demo without it.44- **Scale-to-zero is non-negotiable; per-session cost is explicit.**45 `min-instances=0` always. State the per-session cost implication of the46 billing mode you pick (see `references/cloud-run-tuning.md`) — never let47 an "idle" demo bill silently.48- **One visitor, one instance, enforced in the gateway.** The session49 UUID claims an instance; a live tunnel is never shareable; an idle claim50 is takeable (reload semantics). Never rely on Cloud Run routing alone51 for isolation.52- **Be honest on the page.** Cold-boot time (30–90 s, with self-restart53 on unlucky boots), session caps, and busy states are stated in the54 demo page's terminal — a demo that pretends to be instant reads as55 broken the moment it isn't.5657## Quick start5859The proven path (mission-control flow, Foxglove deep-link viewer):60611. **App side** — add a `demo` scenario to the app: one launch = sim +62 nav/policy stack + `foxglove_bridge` on an internal port + an63 auto-init node (set initial pose / whatever makes the app immediately64 drivable) + the session gateway owning `$PORT`. Copy65 `examples/demo_gateway.py` (verified) and adapt the two constants.662. **Deploy** — Cloud Build the image, then (values verified for a67 Gazebo+Nav2 stack):68 ```bash69 gcloud run deploy demo-<app> --image=<image> \70 --region=us-central1 --port=8765 \71 --concurrency=4 --session-affinity \72 --min-instances=0 --max-instances=5 --timeout=1800 \73 --cpu=8 --memory=8Gi --cpu-boost --no-cpu-throttling \74 --execution-environment=gen2 \75 --set-env-vars=GZ_RELAY=127.0.0.1,GZ_IP=127.0.0.1,FASTDDS_BUILTIN_TRANSPORTS=UDPv4 \76 --command=/entrypoint.sh --args="ros2,launch,<pkg>,demo.launch.py" \77 --allow-unauthenticated --quiet78 ```79 Map a **same-site subdomain** (e.g. demo.yourdomain.org) — required80 for the affinity cookie to work from your site's pages (see gotchas).813. **Site side** — a `/demos/<app>` page with the mission-control82 anatomy (see `references/demo-page.md`): Start/Stop buttons, terminal83 majority, fleet budget line, viewer button gated on `ready`.844. **Gate it** — demo smoke locally, then the same probes against the85 live URL; only then link it from the homepage proof/apps card86 ("Try the live demo →").8788## Usage patterns8990**Choose the demo flow.** Three shapes, in order of proven-ness:9192| Flow | What the visitor gets | When |93| --- | --- | --- |94| **Mission-control page** (proven) | Start/Stop instance buttons, live boot terminal, fleet count, viewer opens on ready | Default. Honest about boot time; visitors see the machinery (which *is* the pitch for infra products). |95| Deep-link only | One "open in Foxglove" link; connection cold-boots the instance | Minimal page work; boot happens behind the viewer's "connecting" spinner; needs the connection itself to hold CPU (request-based billing). |96| Embedded viewer (self-hosted) | Viewer iframe in the page, no login | Best UX on paper; costs a self-hosted viewer build and iframe/browser-storage complexity. Deferred by robium.org after trying it — revisit deliberately. |9798**Choose the visualizer** (verified facts, 2026-07-13):99100| Option | Login | Layout preload | Embeddable | Verdict |101| --- | --- | --- | --- | --- |102| app.foxglove.dev deep link (`/~/view?ds=foxglove-websocket&ds.url=<wss>`) | Required | ✗ (visitor imports the layout file once — link it on the page) | ✗ (`x-frame-options: DENY`) | Default: zero hosting, robotics users have accounts |103| Self-hosted Lichtblick (open-source Foxglove fork, MPL) | None | ✓ (`globalThis.LICHTBLICK_SUITE_DEFAULT_LAYOUT` build hook) | ✓ (you control headers) | Frictionless but you own the build + browser-storage kiosk-wipe; new-tab use is solid, iframe needs care |104| Foxglove embed SDK (embed.foxglove.dev) | Org members only | ✓ | ✓ | Paid tier + viewers must belong to your Foxglove org — internal portals, not public demos |105106**Instance lifecycle contract** (the gateway, `examples/demo_gateway.py`):107`POST /start?session=U` claims · `GET /status?session=U` → JSON108(`claimed/ready/rtf/nodes/uptime_s/remaining_s/fleet{running,budget}/log[]`),109409 for foreign sessions · ws upgrade tunnels to the bridge, one live110tunnel max (second → 503) · `POST /shutdown?session=U` → SIGINT PID 1 →111container exits · idle claims takeable · page sends a `pagehide`112sendBeacon shutdown. Full rationale in `references/gateway-pattern.md`.113114**Show the demo where the proof lives.** The homepage apps/proof card for115the app gets a primary "Try the live demo →" button to `/demos/<app>`;116the demo page carries the reproduction story (the brief that produced the117app) so the demo sells the plugin, not just the robot.118119**Fleet/budget display.** Budget = `--max-instances`, stated statically120on the page. Live count comes from Cloud Monitoring's121`run.googleapis.com/container/instance_count` queried *by the gateway*122(metadata-server token + `roles/monitoring.viewer` on the runtime service123account), folded into `/status` and cached ~30 s. Never fetch fleet124status on page load — that request cold-boots a billable instance per125drive-by visitor; show it only while a session runs.126127## Platform gotchas128129All verified in production, 2026-07-13 (details + fixes in130`references/cloud-run-tuning.md`):131132- **Gazebo discovery needs help on Cloud Run** (no multicast):133 `GZ_RELAY=127.0.0.1` + `GZ_IP=127.0.0.1`, and even then the unicast134 relay loses a *sticky per-boot race* ~half the time — ship a **boot135 watchdog** (no sim data within ~120 s → SIGINT PID 1 → fresh instance136 on the client's reconnect).137- **`Connection: close` on every hand-rolled HTTP response** — Cloud138 Run's proxy pools keep-alive connections; closing without declaring it139 = edge 503 "malformed response" (invisible in local testing).140- **SIGINT, not SIGTERM, stops `ros2 launch` as PID 1** — the kernel141 drops unhandled signals to PID 1 and launch installs no SIGTERM142 handler.143- **Session affinity cookies are SameSite-Lax** — they never flow to a144 `*.run.app` host from your site (cross-site). Map a subdomain of the145 site's domain to the service and use `credentials:'include'` +146 exact-origin CORS, or affinity silently does nothing.147- **Request-based billing throttles CPU between requests** — a sim boot148 with no held connection freezes. Start-button flows need149 `--no-cpu-throttling` (cost: idle-retention after sessions,150 ≈$0.20–0.40/session at 8 vCPU); connection-driven flows can stay151 request-based since the ws holds CPU.152- **FastDDS shared memory misbehaves in Cloud Run** —153 `FASTDDS_BUILTIN_TRANSPORTS=UDPv4` silences `open_and_lock_file` errors.154- **WebSocket probes need `curl --http1.1`** against https Cloud Run155 (h2 negotiation breaks the upgrade), and `foxglove_bridge ≥3.x` expects156 subprotocol `foxglove.sdk.v1` (the classic `foxglove.websocket.v1`157 gets a misleading 400).158159## Customization160161- **Different app:** the gateway and page are app-agnostic; the app-side162 work is the `demo` launch (stack + bridge + auto-init making it163 instantly drivable) and picking what "ready" means (nav: initial pose164 set + RTF measured; a policy demo might be "model loaded + env reset").165- **Different budget/size:** `--max-instances` (budget), `--cpu/--memory`166 (a Gazebo+Nav2 stack wanted 8 vCPU for RTF ≈ 1; measure with the167 `DEMO READY rtf=` log line), `--timeout` (session cap).168- **Non-ROS demos:** the gateway pattern (claim/status/shutdown +169 ws tunnel) works for any ws-speaking backend; swap the bridge for your170 stream and the status file for your readiness signal.171172## References173174- `references/gateway-pattern.md` — the session gateway: endpoint175 contract, claim semantics, tunnel guard, shutdown, watchdog.176- `references/cloud-run-tuning.md` — flags, billing modes and their177 costs, gz/FastDDS env fixes, same-site affinity, fleet monitoring IAM.178- `references/demo-page.md` — mission-control page anatomy, session JS179 flow, honesty copy, where demo links go on the site.180- `examples/demo_gateway.py` — the production gateway (status: verified181 2026-07-13, nav-trial live demo on robium.org).182- Upstream: [Cloud Run docs](https://cloud.google.com/run/docs),183 [Foxglove deep links](https://docs.foxglove.dev/docs/visualization/shareable-links),184 [gz-transport relay](https://gazebosim.org/api/transport/14/relay.html),185 [Lichtblick](https://github.com/lichtblick-suite/lichtblick). Sibling186 skills: `foxglove` (bridge/viewer), `integration` (containers),187 `testing` (the entry bar), `environments` (app env).188189## Changelog190191<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->192193- 1.0.0 (2026-07-13): created from the nav-trial live-demo deployment on194 robium.org — mission-control flow, session gateway, Cloud Run tuning,195 and visualizer decision table, all production-verified same day.