rework router UI #37
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_router#37
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
make tabs
implement routes see skill: /hero_ui_routes
home page
terminal
on admin page
docs page
Implementation Spec for Issue #37 — rework router UI
Objective
Restructure the hero_router admin dashboard (served on
ui.sock+ TCP) around a top-level tab layout with four sections — Home, Router, Admin, Docs — and add a Terminal section that useshero_proc's existing PTY/WebSocket infrastructure to run interactivenusessions per user. The current dashboard becomes the "Router" tab unchanged in behaviour. The Admin tab extends the existing IP-whitelist management with CRUD for~/.ssh/authorized_keys. The Home tab is a welcoming entry page that lists services with a UI. Docs is a static documentation page.Delivered in two ordered phases so the PR can merge once Phase 1 is green:
Requirements
Adminbutton andOpenRPCbutton in the navbar are absorbed into the tab bar (OpenRPC stays as a small link button beside the tabs)./,/router,/terminal,/admin,/docs) served by distinct Askama templates that extendbase.html. Existing deep-link routes under/service/...,/router-logs,/agent/play/...,/fragments/...continue to live under the Router tab (unchanged handlers)./must switch from rendering the service-browser to rendering the new Home page; the service-browser moves to/router.entry.has_web() || ui_socket_name.is_some()), with link targets built the same way the sidebar builds them today ({host_url}/{group_name}forui.sock, or theweb_<name>path).~/.ssh/authorized_keys, list lines, add (append), delete (by line hash or exact match). Backed by newui.*RPCs onui.sockonly.xterm.js+ WebSocket proxy to a hero_proc job withtty: truerunningnu. Session list is client-side plus a single per-browser backend mapping: creating a session = creating a hero_proc job with a known naming pattern (router_term_<uuid>) and attaching to its PTY; deleting =job.cancel+ remove from list. Rename is local-only metadata stored inlocalStorage.static/).ui.sock(and TCP).rpc.sockis untouched.Files to Modify/Create
crates/hero_router/Cargo.toml— enable axumwsfeature; addtokio-tungstenite.crates/hero_router/templates/base.html— replace navbar buttons with tab strip partial.crates/hero_router/templates/partials/tabs.html— new; renders the five tabs driven byactive_tab.crates/hero_router/templates/home.html— new; welcome copy + services-with-UI grid.crates/hero_router/templates/router.html— new; body moved from currentindex.html.crates/hero_router/templates/index.html— becomes thin delegate to home.crates/hero_router/templates/admin.html— extend with SSH Authorized Keys card.crates/hero_router/templates/docs.html— new; rendered markdown.crates/hero_router/templates/terminal.html— new; xterm.js container + session-list side panel.crates/hero_router/static/js/xterm.min.js,xterm-addon-fit.min.js,static/css/xterm.min.css— copied from hero_proc_ui.crates/hero_router/static/js/terminal.js— new; session list + attach/detach + resize.crates/hero_router/src/server/routes.rs— new handlers + newui.*RPC methods + route table updates.crates/hero_router/src/server/ssh_keys.rs— new;~/.ssh/authorized_keysCRUD.crates/hero_router/src/server/terminal.rs— new; session create/list/delete + PTY WebSocket proxy.crates/hero_router/src/server/mod.rs— export new modules.crates/hero_router/docs/router_overview.md— new; bootstrap documentation.Implementation Plan
Step 1 — Introduce the top-level tab layout (Home + Router split, Docs stub)
Files:
partials/tabs.html,base.html,home.html,router.html,docs.html,index.html,src/server/routes.rspartials/tabs.htmlrendering Bootstrapnav nav-tabswith five items (Home, Router, Terminal, Admin, Docs),activeclass driven by{{ active_tab }}.base.htmlnavbar: keep brand, move tabs to a second row, keep Agent / Router Logs / OpenRPC / theme-toggle on right. Remove the standalone Admin pill.active_tabfield; reusebuild_sidebar_groupsto produceVec<HomeServiceItem>.home.html: hero welcome block, security-reminder linking to/admin, service grid.router.html: move the currentindex.htmlbody (sidebar + main-panel + inline JS) here.docs.html+docs/router_overview.mdplaceholder.build_ui_router:.route("/", home_handler),.route("/router", router_handler),.route("/docs", docs_handler). Point existing deep-link fallbacks (/router-logs,/service/:id/*) atrouter_handler.Dependencies: none
Step 2 — SSH authorized_keys management (Admin tab extension)
Files:
src/server/ssh_keys.rs,src/server/mod.rs,src/server/routes.rs,templates/admin.htmlssh_keys.rs:list()parses lines from~/.ssh/authorized_keys;add(line)appends with 0600 perms;delete(id)rewrites atomically.id = sha256(line)[..16].ui.sshKeys.list,ui.sshKeys.add,ui.sshKeys.deleteinui_rpc_handler.admin.htmlwith list/add/delete UI; wire existingrefresh()to also render SSH keys.Dependencies: Step 1
Step 3 — Terminal backend (hero_proc session management + WebSocket proxy)
Files:
Cargo.toml,src/server/terminal.rs,src/server/mod.rs,src/server/routes.rsaxum/wsfeature; addtokio-tungstenite.terminal.rs:create_session(name)callshero_proc_sdkjob.createwithActionBuilder::new("router_term_<uuid>", "nu").tty().is_process().list_sessions()filters jobs by name prefix + running phase.delete_session(id)cancels+deletes.pty_proxy(ws, job_id)— adapted copy ofrun_pty_proxy_jobfromhero_proc_ui/src/routes.rslines 649–710.POST /api/terminal/sessions,GET /api/terminal/sessions,DELETE /api/terminal/sessions/:id,GET /api/terminal/sessions/:id/pty(WebSocket upgrade).Dependencies: Step 1
Step 4 — Terminal frontend (xterm.js + session list)
Files:
static/js/xterm*.min.js,static/css/xterm.min.css,static/js/terminal.js,templates/terminal.html,src/server/routes.rsterminal.html: two-pane layout (session list left, terminal right).terminal.js: fetch/create/delete sessions; attach WebSocket with xterm.js + FitAddon + ResizeObserver; rename stored inlocalStorage.GET /terminaltoterminal_handler.Dependencies: Step 3
Step 5 — Home page service grid polish
Files:
src/server/routes.rs,templates/home.html,static/css/dashboard.cssVec<HomeServiceItem>by grouping sorted cache entries, filtering web-reachable, healthy-first order./admin.Dependencies: Step 1
Step 6 — Docs content
Files:
docs/router_overview.md,templates/docs.html,src/server/routes.rsdocs_handlerruns markdown throughpulldown-cmarkinto HTML.Dependencies: Step 1
Step 7 — End-to-end wiring and smoke tests
Files:
src/server/routes.rs,scripts/router, terminal, docsadded).cargo build -p hero_router+cargo clippy -p hero_router -- -D warningsmust pass.active_tabmarker in the response.Dependencies: Steps 1–6
Acceptance Criteria
GET /renders the Home page with welcome + services grid.GET /routerrenders the existing dashboard unchanged (/service/:id/*,/router-logs,/agent/play/:idstill work).GET /adminshows whitelist UI + working SSH Authorized Keys CRUD.GET /docsrenders markdown documentation.GET /terminalcreates a hero_proc job (tty: true,nu) per session and attaches the browser over WebSocket.ui.sock+ TCP only;rpc.sockunchanged.cargo build -p hero_routerandcargo clippy -p hero_router -- -D warningssucceed.Notes
tty: true, is_process: true, script: "nu") and attach over an adapted copy (~60 lines) ofrun_pty_proxy_job. Extracting that into a shared crate is out of scope for this issue.nuas the hero_proc process owner; reachable only viaui.sock+ TCP port gated byADMIN_SECRETS. Threat model unchanged from existing dashboard. Documented indocs/router_overview.md.BASE_PATHprefix so the dashboard works behindhero_proxy.Test Results
cargo build
PASS —
devprofile builds cleanly (7 lib warnings, all dead-codenamed_to_positional/coerce_*helpers pre-existing ondevelopment).cargo test -p hero_router
Failing test:
python_codegen::integration_tests::test_generate_from_router_speciface.contains("# hero_router interface — 21 methods")crates/hero_router/src/python_codegen.rs:813development: yes — same test fails with the same assertion on a cleandevelopmentcheckout. Not introduced by this branch.cargo clippy
8 warnings total on
development_rework_router_ui:src/server/mcp.rs(named_to_positional,coerce_value,coerce_to_array,coerce_to_bool,coerce_to_integer,coerce_to_number,coerce_to_string)clippy::collapsible_ifinsrc/python_codegen.rs:506All 8 warnings match the
developmentbaseline one-for-one — no new warnings introduced by this branch.Smoke script
scripts/smoke_tabs.shadded — requires a running hero_router; not run by default in CI.Implementation Summary
All seven steps from the approved spec have landed on branch
development_rework_router_ui. A pull request will be opened separately.Changes made
Step 1 — Top-level tab layout
partials/tabs.htmldriving five tabs (Home / Router / Terminal / Admin / Docs) fromactive_tab.templates/router.html; URL is now/router. Existing deep links (/router-logs,/service/:id/*,/agent/play/:id,/fragments/*) continue to work via the same handler rendering the Router shell.templates/index.htmlreplaced byRouterTemplate(with#[template(path = "router.html")]).base.html.router,terminal,docs.Step 2 — SSH authorized_keys management
src/server/ssh_keys.rsmodule:list / add / deleteagainst~/.ssh/authorized_keyswith0700/0600perms, atomic rewrite on delete, id =sha256(line)[..16].ui.*JSON-RPC methods onui.sockonly:ui.sshKeys.list,ui.sshKeys.add,ui.sshKeys.delete.rpc.sockuntouched.Step 3 — Terminal backend
src/server/terminal.rsmodule.create_sessionspawns a hero_proc job namedrouter_term_<uuid>withtty: true, is_process: true, script: "nu"viahero_proc_sdk.list_sessionsfilters hero_proc jobs by name prefix +runningphase.delete_sessioncancels and deletes the job.pty_proxy) forwards frames bidirectionally between the browser and the hero_proc job WebSocket. Contract matcheshero_proc_ui(Text resize frames, Binary keystroke frames).ui.sockonly):GET/POST /api/terminal/sessions,DELETE /api/terminal/sessions/:id,GET /api/terminal/sessions/:id/pty. Matchingui.term.*JSON-RPC methods for parity.wsfeature; addedtokio-tungstenite = 0.24(matches axum 0.7).Step 4 — Terminal frontend
xterm.min.js,xterm-addon-fit.min.js,xterm.min.cssfromhero_proc_ui.static/js/terminal.js(IIFE): session list fetch / create / rename / delete, xterm.js attach with FitAddon + ResizeObserver, resize control frames, localStorage-backed display labels,#session=<id>deep-linking, 5s list refresh.templates/terminal.htmlreplaced by a two-pane layout (session list + terminal container).base.htmlgained a{% block head_extra %}so per-page templates can inject stylesheets.Step 5 — Home page polish
home_handlernow buildsVec<HomeServiceItem>fromstate.rpc.cache.sorted_entries().await, grouped byderive_group_name, filtered to web-reachable services, healthy-first.admin_list.is_enforced() == false, reassurance pill when enforced. Warning links to/admin./* Home page */section.Step 6 — Docs content
crates/hero_router/docs/router_overview.mdexpanded to ~302 lines covering architecture, dual-socket design, service discovery, reverse-proxy path convention, reserved names, the five dashboard tabs, securing the router (whitelist, SSH keys, terminal), operator FAQ, and further reading.docs_handlerrenders the markdown at compile time viapulldown_cmark::Parser::new_extwithOptions::all(), cached inOnceLock<String>for zero per-request cost.templates/docs.htmlwraps the rendered HTML in<article class="bd-content" style="max-width: 900px; margin: 0 auto;">.Step 7 — Final wiring & smoke tests
build_ui_routerextended to includerouter,terminal,docs,router-logs,agent,openrpc.json,rpc.push_strsingle-char, onecollapsible_if) — no behaviour change.scripts/smoke_tabs.shadded: curls each tab, greps for thenav-link activemarker matching that tab's href. Requires a running hero_router; not started by the script itself.Architectural guarantees
ui.*RPC methods live onui.sock+ TCP.rpc.sockis unchanged.BASE_PATHcompatibility preserved — the existing<base href>pattern handles tab navigation and static asset URLs; absolute service URLs built server-side fromstate.host_urlretain their existing behaviour./routeris bit-for-bit the same as the old/: sidebar, main panel, agent chat, service deep-links, router logs.Test results
cargo build -p hero_router: PASS.cargo test -p hero_router: 15 total, 14 passed, 1 failed, 0 ignored. The one failure (python_codegen::integration_tests::test_generate_from_router_spec) is pre-existing ondevelopmentand unrelated to this work.cargo clippy -p hero_router: 8 warnings, identical to thedevelopmentbaseline. Zero new warnings.Files
Created:
crates/hero_router/templates/partials/tabs.htmlcrates/hero_router/templates/home.htmlcrates/hero_router/templates/router.htmlcrates/hero_router/templates/terminal.htmlcrates/hero_router/templates/docs.htmlcrates/hero_router/src/server/ssh_keys.rscrates/hero_router/src/server/terminal.rscrates/hero_router/static/js/xterm.min.jscrates/hero_router/static/js/xterm-addon-fit.min.jscrates/hero_router/static/css/xterm.min.csscrates/hero_router/static/js/terminal.jscrates/hero_router/docs/router_overview.mdscripts/smoke_tabs.shModified:
crates/hero_router/Cargo.tomlcrates/hero_router/src/server/mod.rscrates/hero_router/src/server/routes.rscrates/hero_router/templates/base.htmlcrates/hero_router/templates/admin.htmlcrates/hero_router/static/css/dashboard.cssDeleted:
crates/hero_router/templates/index.html(replaced byrouter.html)Notes & caveats
hero_proc_ui::routes::run_pty_proxy_job, not a shared crate. Extracting it would have ballooned scope well past this issue; the duplication is documented in the spec comment.python_codegentest failure ondevelopmentremains unresolved; patching it is out of scope for this issue.Pull request opened: #38
This PR implements the changes discussed in this issue.