Repository navigation
feat(ui): RTDL tree view and Studio service tabs - #15
Conversation
…ation, and performance optimizations
…sistent RTDL execution tree (option A)
Replace the two-tab cockpit with a compact sidebar: the current goal keeps its panel, the RTDL forest and the event log switch through a small menu, and nodes open in a detail modal instead of an inline pane. Long execution trees are pannable and zoomable, open centered in their panel even when they first render while hidden, and keep the user's view across executor polls. The latest completed plan stays on screen instead of flickering away, and ambient status lines fold into an expandable activity run so the robot reply stays the only bubble in the transcript. Also route legacy status rows through the i18n reverse table, re-render an open node popup on a language switch, localize the Studio service tab titles, and keep the configured host value out of innerHTML.
- Move 're' import to module level in app.py - Remove decorative separator lines for consistency - Initialize studioDiscoveredServices as empty array - Remove unused data-studio-service attributes from HTML - Fix potential bug where default services could show when none exist
- Studio: 工坊 → 控制台 - Integrated Service Cockpit: 一体化微服务座舱 → 集成服务面板 - Scene Service: Scene 感知套件 → 场景服务 - Robonix Hub: 全景门户 → Robonix Hub - Fleet Hub: 车队门户 → 机群中心 - Audio Server: 声卡诊断 → 音频服务 - Audio Reverse Bridge: 音频反向桥 → 音频桥接 - Mapping Service: SLAM 建图服务 → 建图服务 Remove overly elaborate terminology and use straightforward translations.
The panel already reserved a minmax(0, 1fr) row for the execution table, but every box below it was content-sized, so the SVG viewport collapsed to the tree's drawing height and the tree sat in a thin strip at the top of the panel. Give the chain definite heights: the table becomes a flex column, the plan tree and plan record take the remaining space, and the tree card row is minmax(0, 1fr). The SVG keeps its own width/height (fitView scales and centers it), so a shallow tree is no longer shrunk to fit the old 160px-tall strip and a wide one still pans and zooms as before.
.goal-preview was grouped with .map-stage and .camera-feed, so it inherited the image-stage rules (position: relative, height: calc(100% - 35px), overflow: hidden). That rule is for a cover-fit image; the goal preview is the one place in the app that uses the class and it only ever holds the text goal card, so the card was cut in half: two of the four call fields and the whole args block were invisible, and the panel's own overflow: auto never got a chance to scroll because the child clipped first. Drop .goal-preview from that rule and delete the dead .goal-preview img rule, so the box is content-sized again. Raise the panel cap to 340px so a typical card (source pill, call name, four fields, args) fits without scrolling; a longer contract id or args block now scrolls inside the panel instead of being cut off, and the scrollbars stay hidden as everywhere else.
Studio: - The Hub fallback card shares the studio-empty-state class, so a stale card counted as "an empty state already exists" and the real one was never built: blank viewport, no retry button. Exclude the card from the lookup and manage it in both branches. - Fullscreen targeted the viewport, which hid the header holding the button just clicked plus the service tabs. Target the shell instead. - A forced scan stacked one probe per click against every candidate port, and a hung robot host left the badge spinning with no request left to fail. Abort the superseded scan, add an 8s timeout, and let only the newest scan own the shared state. - The four shipped tabs are static markup. An empty discovery result cleared them and returned early, so a language switch left the Studio header with no service tabs at all. Restore the saved markup and re-translate it. - iframe.src reads back the browser-normalized URL, so a robotHost typed with capitals never compared equal and the frame reloaded on every return to that service, flashing the scrim. Track the URL we asked for in a dataset attribute, and keep the reload button in step with it. - Discovered tabs carried no role/aria-selected/aria-controls, and the viewport had no tabpanel role or label. - The audio tab's tooltip key was "Audio Device Service" while the catalog holds "Audio Device Server". RTDL: - A plan that scrolls off the 80-record cap left its ResizeObserver watching a detached viewport, and the view-state map grew one entry per plan ever drawn. Prune both on every render. - A plan literally id'd "tree" collided with the planless tree's key at the same index, so the two trees shared one pan/zoom state. i18n: expose applyDocument on window.RobonixI18N so a re-rendered strip can be re-translated; drop ambiguous values from the reverse map instead of letting the last key win; remove 13 unused keys.
styles.css: remove rules for markup that no longer exists -- the map stage and camera feed, the RTDL snapshot tree and history launcher, and the execution detail panel. 174 lines; brace balance re-checked. app.py: hoist urllib imports to module level and drop the desc field the frontend never read. Also reject extra_ports outside 1-65535: urllib does not reject them, and 99999 wraps to 34463 and answers, so the scan would report a service on an unrelated port behind a URL nobody can open.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
enkerewpo
left a comment
There was a problem hiding this comment.
Thanks. Both parts are useful: the RTDL tree (fill the panel, drag to pan, cursor-anchored zoom, per-plan view state) and Studio, which gives the client one place to open the robot's web UIs (Foxglove, RTAB-Map and so on). A few changes before merge (details inline):
- Naming. Port 4880 is Robonix Console, not "Robonix Hub" or "Robonix Fleet Center", and product names are not translated (机群中心). "Studio" translated as 控制台 also collides with Robonix Console; keep "Studio" or pick another Chinese name.
/api/studio/servicesprobes any host the browser sends with an unbounded port list. Restrict it to the configured robot host and capextra_ports.- Discovery. A fixed port list plus page-title guessing ("hub" matches "github", "arm" matches "alarm") mislabels and misses services. We will add a directory to Atlas where packages declare their web pages, WebSockets and streams themselves (syswonder/robonix#292); Studio should then read that directory instead of scanning. Until that lands, keep the scan but use whole-word matching and drop the hard-coded default tabs shown when nothing is found.
- Console listens on 127.0.0.1 by default, so a client on another machine will not reach 4880; handle that tab accordingly (it already opens in a new tab because of X-Frame-Options).
- The GZip middleware is unrelated to both features; please move it out. Title: keep it to 50 characters, e.g.
feat(ui): RTDL tree view and Studio service tabs; squash bd4865b (not a Conventional Commit).
| return {"name": "Scene Service", "icon": "👁️"} | ||
| if port == 8091 or "mapping" in tl or "slam" in tl: | ||
| return {"name": "SLAM Mapping", "icon": "🗺️"} | ||
| if port == 4880 or "hub" in tl: |
There was a problem hiding this comment.
Title substrings mislabel services: "hub" matches "github", "arm" matches "alarm". This scan is a stopgap until the Atlas directory (syswonder/robonix#292); meanwhile match whole words.
|
|
||
| @app.get("/api/studio/services") | ||
| async def discover_studio_services( | ||
| robot_host: str = "127.0.0.1", |
There was a problem hiding this comment.
This endpoint probes any robot_host the browser sends and an unbounded extra_ports list, so the client can be used to scan arbitrary hosts. Restrict the host to the configured robot host and cap extra_ports.
| <span class="tab-title" data-i18n="SLAM Mapping">SLAM Mapping</span> | ||
| <span class="tab-port">8091</span> | ||
| </button> | ||
| <button type="button" class="studio-service-tab" role="tab" aria-selected="false" aria-controls="studioViewport" title="Robonix Fleet Center (Port 4880)" data-i18n-title="Robonix Fleet Center (Port 4880)"> |
There was a problem hiding this comment.
"Robonix Fleet Center" — this is Robonix Console. There is no fleet center.
| "Compute node telemetry from Vitals": "来自 Vitals 的计算节点遥测", | ||
| "Audio": "音频", | ||
| "Settings": "设置", | ||
| "Studio": "控制台", |
There was a problem hiding this comment.
"Studio" → "控制台" collides with Robonix Console. Keep "Studio" or choose a different Chinese name.
| "Audio Server": "音频服务", | ||
| "Scene Perception Service (Port 50107)": "场景感知服务(端口 50107)", | ||
| "RTAB-Map Mapping (Port 8091)": "RTAB-Map 建图(端口 8091)", | ||
| "Robonix Fleet Center (Port 4880)": "Robonix 机群中心(端口 4880)", |
There was a problem hiding this comment.
机群中心 → Robonix Console (product names are not translated).
| CANDIDATE_STUDIO_PORTS = [ | ||
| 50107, # Scene Perception Service | ||
| 8091, # RTAB-Map SLAM Mapping | ||
| 4880, # Robonix Hub |
There was a problem hiding this comment.
Port 4880 is Robonix Console (the robot-side console), not Robonix Hub.
Address PR review on the Studio service console: - Port 4880 is Robonix Console, not "Robonix Hub" or "Fleet Center". Rename it across the probe table, title inference, the iframe fallback card, and the i18n strings. Product names are no longer translated, and "Studio" is "工作室" so it no longer collides with the Console's name. - Restrict /api/studio/services: reject any robot_host that is not a single bare host (scheme, path, separators that could smuggle a second target) and the cloud metadata addresses, and cap extra_ports at 8 so the endpoint can no longer be used as an arbitrary port scanner. - Match service titles on whole words, so "github" no longer reads as the Console and "alarm" no longer reads as an arm. - Drop the four hard-coded default tabs; the strip is built from the scan and an empty result shows the empty state. - Note on the Console card that a loopback-bound Console is only reachable from the robot itself. - Remove the unrelated GZip middleware. Add tests for the Console rename, whole-word matching, host rejection, and the extra_ports cap.
|
Thanks for the review. Pushed
On |
Problem
The sidebar's service panel was a hand-built linear pipeline (perception →
mapping → planning → execution) that held its place even when the robot ran
none of those services, and the RTDL tree it framed was pinned to a small
fixed height, so the behavior tree rendered as a thumbnail. There was also no
way to reach the robot's own web tools (Foxglove, RTAB-Map, the Hub) from the
client.
Changes
RTDL execution tree
minmax(0, 1fr)+min-height: 0) instead of being squeezed into a fixed height.view is stored per
planId, so the periodic re-render keeps the user's zoomand position.
the view-state map no longer grows one entry per plan ever drawn.
collide with the planless tree at the same index.
Studio service console
/api/studio/services: probes the candidate ports concurrently andinfers each service's name and icon from the title it returns.
X-Frame-Options: DENY, so it falls back to a card that opens it in a newtab.
so a hung robot host no longer leaves the badge spinning with no request
left to fail.
clear them, so switching language left the Studio header with no tabs.
Layout
content scrolls horizontally with the scrollbar hidden, per the repo's
existing convention.
Known limitations
bd4865b("Make the RTDL tree viewport fill the sidebar panel") does notfollow Conventional Commits. It is already pushed history, so correcting it
would need a force-push; left as is.
capability manifest from the robot.
Validation
vitals.bundle.jsrebuild is byte-identical