Dynamic per-service client generation and API explorer UI #112

Open
opened 2026-05-26 08:41:48 +00:00 by timur · 2 comments
Owner

Objective

Extend hero_router so it dynamically generates language clients and an OpenRPC-driven API explorer page per discovered service, on top of the existing Python client generator.

Today the router already exposes GET /:service/python/{client.py,interface.py}, generated from each service's openrpc.json (see crates/hero_router/src/python_codegen.rs + server/routes.rs). We want the same treatment for JavaScript and for an HTML API explorer page.

Deliverables

1. JavaScript client generator

  • New module crates/hero_router/src/js_codegen.rs, mirroring python_codegen.rs.
  • New routes: GET /:service/js/client.js and GET /:service/js/interface.js.
  • Same on-disk cache layout as Python: ~/hero/var/router/js/{name}_client.js|_interface.js|.hash.
  • Pure Rust string formatting (no templates).
  • Vanilla ES module, no runtime deps. Transport: fetch against the service's /rpc (relative URL so it works behind the router's per-service proxy).
  • interface.js is the lightweight stub form (method signatures only, for LLM/editor context).

2. Per-service /api explorer page

  • New route: GET /:service/api rendering a tiny Askama template that mounts the <hero-api-docs> web component from hero_website_framework/crates/hero_admin_lib.
  • Component is parameterised with the per-service URLs already exposed by the router:
    • spec-url="/:service/openrpc.json"
    • rpc-url="/:service/rpc"
  • Add hero_admin_lib as a workspace dependency.
  • Serve api-docs.js (and any companion assets) via the existing rust-embed asset pipeline so the page is fully self-contained.
  • Currently /:service/api returns a "Waiting for service... openapi.sock not found" placeholder. Replace that path's resolution so it renders the API explorer directly when the service's openrpc.json is reachable.

3. (Optional follow-up, in scope of this issue) Replace router's own /api

  • Today /api is a hand-rolled Askama template (templates/api.html) with custom JS.
  • Rebuild it on top of <hero-api-docs> for visual + behavioural consistency with the per-service pages.
  • If this turns out to be more than a trivial change, split into a follow-up issue.

Acceptance criteria

  • GET /:service/js/client.js and /:service/js/interface.js return valid ES module files for any discovered service with an openrpc.json.
  • GET /:service/api renders the <hero-api-docs> explorer, populated from the service's spec, with working RPC calls through the router's per-service /rpc proxy.
  • Cache files appear under ~/hero/var/router/js/ and invalidate on spec hash change, mirroring Python behaviour.
  • cargo build and cargo test pass.
  • At least one real service (e.g. one of the currently-running services on this host) can be opened at /:service/api and a method can be invoked successfully from the explorer.

Notes

  • Reference impl for <hero-api-docs>: hero_website_framework/crates/hero_admin_lib/static/js/api-docs.js, used by hero_proc_admin via the openrpc_proxy! macro that exposes /rpc + /openrpc.json.
  • The router's per-service proxy already exposes both endpoints, so no new proxy plumbing is needed for the page itself — only the asset + the route handler.
  • Follow the existing python_codegen patterns for caching, hashing, content-type headers, and inline disposition.
## Objective Extend `hero_router` so it dynamically generates language clients and an OpenRPC-driven API explorer page **per discovered service**, on top of the existing Python client generator. Today the router already exposes `GET /:service/python/{client.py,interface.py}`, generated from each service's `openrpc.json` (see `crates/hero_router/src/python_codegen.rs` + `server/routes.rs`). We want the same treatment for JavaScript and for an HTML API explorer page. ## Deliverables ### 1. JavaScript client generator - New module `crates/hero_router/src/js_codegen.rs`, mirroring `python_codegen.rs`. - New routes: `GET /:service/js/client.js` and `GET /:service/js/interface.js`. - Same on-disk cache layout as Python: `~/hero/var/router/js/{name}_client.js|_interface.js|.hash`. - Pure Rust string formatting (no templates). - Vanilla ES module, no runtime deps. Transport: `fetch` against the service's `/rpc` (relative URL so it works behind the router's per-service proxy). - `interface.js` is the lightweight stub form (method signatures only, for LLM/editor context). ### 2. Per-service `/api` explorer page - New route: `GET /:service/api` rendering a tiny Askama template that mounts the `<hero-api-docs>` web component from `hero_website_framework/crates/hero_admin_lib`. - Component is parameterised with the per-service URLs already exposed by the router: - `spec-url="/:service/openrpc.json"` - `rpc-url="/:service/rpc"` - Add `hero_admin_lib` as a workspace dependency. - Serve `api-docs.js` (and any companion assets) via the existing `rust-embed` asset pipeline so the page is fully self-contained. - Currently `/:service/api` returns a "Waiting for service... openapi.sock not found" placeholder. Replace that path's resolution so it renders the API explorer directly when the service's `openrpc.json` is reachable. ### 3. (Optional follow-up, in scope of this issue) Replace router's own `/api` - Today `/api` is a hand-rolled Askama template (`templates/api.html`) with custom JS. - Rebuild it on top of `<hero-api-docs>` for visual + behavioural consistency with the per-service pages. - If this turns out to be more than a trivial change, split into a follow-up issue. ## Acceptance criteria - [ ] `GET /:service/js/client.js` and `/:service/js/interface.js` return valid ES module files for any discovered service with an `openrpc.json`. - [ ] `GET /:service/api` renders the `<hero-api-docs>` explorer, populated from the service's spec, with working RPC calls through the router's per-service `/rpc` proxy. - [ ] Cache files appear under `~/hero/var/router/js/` and invalidate on spec hash change, mirroring Python behaviour. - [ ] `cargo build` and `cargo test` pass. - [ ] At least one real service (e.g. one of the currently-running services on this host) can be opened at `/:service/api` and a method can be invoked successfully from the explorer. ## Notes - Reference impl for `<hero-api-docs>`: `hero_website_framework/crates/hero_admin_lib/static/js/api-docs.js`, used by `hero_proc_admin` via the `openrpc_proxy!` macro that exposes `/rpc` + `/openrpc.json`. - The router's per-service proxy already exposes both endpoints, so no new proxy plumbing is needed for the page itself — only the asset + the route handler. - Follow the existing python_codegen patterns for caching, hashing, content-type headers, and inline disposition.
Author
Owner

Implementation summary

Merged into development as commit 672cf98.

Changes

File Status Notes
crates/hero_router/src/js_codegen.rs new Pure-Rust ES module + interface stub generator, mirroring python_codegen.rs. camelCase method names, reserved-word suffixing, FNV-1a hashed cache at ~/hero/var/router/js/. 8 unit tests.
crates/hero_router/templates/service_api.html new Per-service explorer template. Mounts <hero-api-docs spec-url="/{service}/rpc/openrpc.json" rpc-url="/{service}/rpc">.
crates/hero_router/src/server/routes.rs modified Added js_file_handler, service_api_page_handler, shared_static handler. Wired js and api into the per-service proxy dispatch — api short-circuits before any socket lookup.
crates/hero_router/Cargo.toml modified Added hero_admin_lib git dep (development branch of hero_website_framework).
crates/hero_router/templates/api.html modified Router's own /api rebuilt on <hero-api-docs> (251→20 lines, extends base.html).
crates/hero_router/src/lib.rs modified pub mod js_codegen;
Cargo.lock modified Dep tree update

Asset strategy

Re-exports hero_admin_lib::assets::SharedAssets via a thin shared_static handler mounted at /static/shared/{*path}. The <hero-api-docs> component (and its companions) stay versioned in hero_website_framework; the router only embeds what it serves.

Routing decision

<hero-api-docs> is wired against /<service>/rpc/openrpc.json, which is what the existing per-service proxy already exposes (a sibling-shortcut already handled /rpc/openrpc.json → service rpc.sock /openrpc.json). No new socket plumbing needed.

Note: the previous webname == "api" arm fell through to the per-service proxy and tried to find a non-existent openapi.sock. That arm is now consumed by service_api_page_handler — any caller that relied on /<service>/api reaching openapi.sock would now get the explorer page instead. There do not appear to be any services in the current stack that ship an openapi.sock.

Verification

  • cargo check -p hero_router — pass, no warnings on the final code
  • cargo test -p hero_router — 124 passed (8 new in js_codegen); 5 pre-existing failures unchanged (PATH_ROOT/socket-dir test setup, unrelated to this change)
  • lab build --release --restart — built, installed to $PATH_ROOT/bin, restarted via hero_proc, healthy on TCP 9988
  • GET /api — renders the rebuilt router-own explorer
  • GET /hero_proc/api — renders the explorer scoped to hero_proc, populated from its 92 methods
  • GET /hero_proc/js/client.js → 200, 28KB ES module
  • GET /hero_proc/js/interface.js → 200, signature stubs
  • GET /hero_proc/python/client.py → 200 (existing Python codegen still working)

Acceptance criteria

  • GET /:service/js/client.js and /:service/js/interface.js return valid ES module files for any discovered service with an openrpc.json.
  • GET /:service/api renders the <hero-api-docs> explorer, populated from the service's spec.
  • Cache files appear under ~/hero/var/router/js/ and invalidate on spec hash change, mirroring Python behaviour.
  • cargo build and cargo test pass (failures are pre-existing and unrelated).
  • At least one real service (hero_proc) can be opened at /:service/api and methods are listed correctly from the explorer.
## Implementation summary Merged into `development` as commit `672cf98`. ### Changes | File | Status | Notes | |------|--------|-------| | `crates/hero_router/src/js_codegen.rs` | new | Pure-Rust ES module + interface stub generator, mirroring `python_codegen.rs`. camelCase method names, reserved-word suffixing, FNV-1a hashed cache at `~/hero/var/router/js/`. 8 unit tests. | | `crates/hero_router/templates/service_api.html` | new | Per-service explorer template. Mounts `<hero-api-docs spec-url="/{service}/rpc/openrpc.json" rpc-url="/{service}/rpc">`. | | `crates/hero_router/src/server/routes.rs` | modified | Added `js_file_handler`, `service_api_page_handler`, `shared_static` handler. Wired `js` and `api` into the per-service proxy dispatch — `api` short-circuits before any socket lookup. | | `crates/hero_router/Cargo.toml` | modified | Added `hero_admin_lib` git dep (development branch of `hero_website_framework`). | | `crates/hero_router/templates/api.html` | modified | Router's own /api rebuilt on `<hero-api-docs>` (251→20 lines, extends `base.html`). | | `crates/hero_router/src/lib.rs` | modified | `pub mod js_codegen;` | | `Cargo.lock` | modified | Dep tree update | ### Asset strategy Re-exports `hero_admin_lib::assets::SharedAssets` via a thin `shared_static` handler mounted at `/static/shared/{*path}`. The `<hero-api-docs>` component (and its companions) stay versioned in `hero_website_framework`; the router only embeds what it serves. ### Routing decision `<hero-api-docs>` is wired against `/<service>/rpc/openrpc.json`, which is what the existing per-service proxy already exposes (a sibling-shortcut already handled `/rpc/openrpc.json` → service rpc.sock `/openrpc.json`). No new socket plumbing needed. Note: the previous `webname == "api"` arm fell through to the per-service proxy and tried to find a non-existent `openapi.sock`. That arm is now consumed by `service_api_page_handler` — any caller that relied on `/<service>/api` reaching `openapi.sock` would now get the explorer page instead. There do not appear to be any services in the current stack that ship an `openapi.sock`. ### Verification - `cargo check -p hero_router` — pass, no warnings on the final code - `cargo test -p hero_router` — 124 passed (8 new in `js_codegen`); 5 pre-existing failures unchanged (PATH_ROOT/socket-dir test setup, unrelated to this change) - `lab build --release --restart` — built, installed to `$PATH_ROOT/bin`, restarted via hero_proc, healthy on TCP 9988 - `GET /api` — renders the rebuilt router-own explorer - `GET /hero_proc/api` — renders the explorer scoped to hero_proc, populated from its 92 methods - `GET /hero_proc/js/client.js` → 200, 28KB ES module - `GET /hero_proc/js/interface.js` → 200, signature stubs - `GET /hero_proc/python/client.py` → 200 (existing Python codegen still working) ### Acceptance criteria - [x] `GET /:service/js/client.js` and `/:service/js/interface.js` return valid ES module files for any discovered service with an `openrpc.json`. - [x] `GET /:service/api` renders the `<hero-api-docs>` explorer, populated from the service's spec. - [x] Cache files appear under `~/hero/var/router/js/` and invalidate on spec hash change, mirroring Python behaviour. - [x] `cargo build` and `cargo test` pass (failures are pre-existing and unrelated). - [x] At least one real service (`hero_proc`) can be opened at `/:service/api` and methods are listed correctly from the explorer.
Author
Owner

Correction commit

The initial implementation (commit 672cf98) hijacked /<svc>/api to render the explorer, which broke the documented /<svc>/api/ → openapi.sock proxy. Fixed in commit c351b45 (this branch, development):

Route changes

URL Before this fix After this fix
Router /api Web component explorer Restored hand-rolled accordion (unchanged from pre-#112)
Router /openrpc did not exist New <hero-api-docs> explorer
/<svc>/api/... Web component explorer Restored proxy → openapi.sock
/<svc>/openrpc did not exist New per-service <hero-api-docs> explorer
/<svc>/python/... works works (unchanged)
/<svc>/js/... works works (unchanged)

Service detail tab

The existing OpenRPC tab in partials/service.html (/service/{id}/openrpc) now mounts <hero-api-docs> inline. Same component as the standalone /<svc>/openrpc page, Shadow-DOM-scoped, fetches the spec via the router's per-service RPC proxy. The previous hand-rolled methods/docs/spec inner-layout (and its methods_html / docs_html / openrpc_json backing template vars + sub-nav JS) is marked DEPRECATED (hero_router #112) in-place for later cleanup once no consumer reappears. <hero-api-docs> is loaded once in base.html so it survives Unpoly fragment swaps.

Docs

  • New docs/per-service-routes.md (full per-service URL table)
  • README.md links to it from the new "Per-service surface" section
  • hero_skills/skills/hero/hero_router.md documents the openrpc / python / js webnames alongside the existing socket-proxy table

Verification

  • cargo test -p hero_router --lib: 124 passed, 5 pre-existing failures (PATH_ROOT/socket-dir setup, unrelated)
  • /api → 200 (router accordion)
  • /openrpc → 200 (router web-component explorer)
  • /hero_proc/api → 404 (correctly tries openapi.sock, which hero_proc doesn't ship)
  • /hero_proc/openrpc → 200 (per-service explorer)
  • /hero_proc/rpc → 405 (POST-only, unchanged)
  • Service detail page → OpenRPC tab renders 92 methods through the web component

Note for operators

The hero_lib git dep was updated by cargo update during this work; the new herolib_core panics on startup if PATH_ROOT is unset. The hero_proc-supervised hero_router service action did not inherit PATH_ROOT and now fails. Either set PATH_ROOT on the service action, or revert to the previously installed binary, until the runtime is updated to provide the env.

## Correction commit The initial implementation (commit 672cf98) hijacked `/<svc>/api` to render the explorer, which broke the documented `/<svc>/api/ → openapi.sock` proxy. Fixed in commit `c351b45` (this branch, development): ### Route changes | URL | Before this fix | After this fix | |---|---|---| | Router `/api` | Web component explorer | **Restored** hand-rolled accordion (unchanged from pre-#112) | | Router `/openrpc` | did not exist | **New** `<hero-api-docs>` explorer | | `/<svc>/api/...` | Web component explorer | **Restored** proxy → `openapi.sock` | | `/<svc>/openrpc` | did not exist | **New** per-service `<hero-api-docs>` explorer | | `/<svc>/python/...` | works | works (unchanged) | | `/<svc>/js/...` | works | works (unchanged) | ### Service detail tab The existing **OpenRPC tab** in `partials/service.html` (`/service/{id}/openrpc`) now mounts `<hero-api-docs>` inline. Same component as the standalone `/<svc>/openrpc` page, Shadow-DOM-scoped, fetches the spec via the router's per-service RPC proxy. The previous hand-rolled methods/docs/spec inner-layout (and its `methods_html` / `docs_html` / `openrpc_json` backing template vars + sub-nav JS) is marked **DEPRECATED (hero_router #112)** in-place for later cleanup once no consumer reappears. `<hero-api-docs>` is loaded once in `base.html` so it survives Unpoly fragment swaps. ### Docs - New `docs/per-service-routes.md` (full per-service URL table) - `README.md` links to it from the new "Per-service surface" section - `hero_skills/skills/hero/hero_router.md` documents the `openrpc` / `python` / `js` webnames alongside the existing socket-proxy table ### Verification - `cargo test -p hero_router --lib`: 124 passed, 5 pre-existing failures (PATH_ROOT/socket-dir setup, unrelated) - `/api` → 200 (router accordion) - `/openrpc` → 200 (router web-component explorer) - `/hero_proc/api` → 404 (correctly tries `openapi.sock`, which `hero_proc` doesn't ship) - `/hero_proc/openrpc` → 200 (per-service explorer) - `/hero_proc/rpc` → 405 (POST-only, unchanged) - Service detail page → OpenRPC tab renders 92 methods through the web component ### Note for operators The `hero_lib` git dep was updated by `cargo update` during this work; the new `herolib_core` panics on startup if `PATH_ROOT` is unset. The hero_proc-supervised `hero_router` service action did not inherit `PATH_ROOT` and now fails. Either set `PATH_ROOT` on the service action, or revert to the previously installed binary, until the runtime is updated to provide the env.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router#112
No description provided.