Admin UI is broken: hero_books_admin has no /rpc proxy route #98
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_books#98
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?
Problem
The
hero_books_admindashboard UI is broken for all dynamic data. Every admin page fetches data via a JavaScriptrpc()helper that POSTs toBASE_PATH + "/rpc"— but the admin router never registers a/rpcroute, so every call returns 404.Evidence
Templates call
/rpc:crates/hero_books_admin/templates/base.html:111-123defines therpc()helper:fetch(BASE_PATH + "/rpc", { method: "POST", ... }).templates/index.html:41—rpc("libraries.list", {})templates/index.html:55—rpc("libraries.get", { name })templates/collections.html:38—rpc("collections.list", {})templates/collections.html:89—rpc("collections.get", { name })templates/health.html:26—rpc("server.health", {})templates/import.html:52—rpc("importservice.list_namespaces", {})templates/import.html:85—rpc("importservice.start", { request })templates/export.html:108—rpc("collections.exportToDir", params)Admin router does not register
/rpc:crates/hero_books_admin/src/main.rs:244-253registers/,/collections,/health,/.well-known/heroservice.json,/import,/export,/settings— no/rpcroute, no/openrpc.json.The admin home, Libraries, Collections, Import, Export, and Health pages all silently fail to load data in the browser.
Root cause
Violation of the
hero_ui_openrpc_proxyskill: every Hero admin UI should expose a local/rpcendpoint that forwards JSON-RPC requests to the service'srpc.sock.hero_books_uialready has this (crates/hero_books_ui/src/proxy.rs::api_proxy);hero_books_adminwas never given the equivalent.Fix
Add an
/rpcproxy handler tohero_books_adminthat forwards POST bodies to$HERO_SOCKET_DIR/hero_books/rpc.sockand returns the JSON-RPC response verbatim. Mirror the pattern used incrates/hero_books_ui/src/proxy.rs:crates/hero_books_admin/src/proxy.rswith a single async handlerapi_proxy..route("/rpc", post(api_proxy))inmain.rsalongside the existing GET routes.X-Forwarded-Prefix,X-Hero-Context,X-Hero-Claims.Acceptance criteria
POST /rpcon the admin socket forwards tohero_books/rpc.sockand returns the JSON-RPC response.X-Forwarded-Prefixis preserved on the forwarded request (so URL generation in the server still works)./rpcand verifies the round-trip path.Scope
Small — one new module, one new route, tests. No schema or RPC-surface changes. Local to
hero_books_admin.Implementation Spec for Issue #98
Objective
Add a
/rpcproxy route tohero_books_adminthat forwards POST bodies to$HERO_SOCKET_DIR/hero_books/rpc.sock, matching the pattern used inhero_books_ui.Requirements
/rpcroute wired into the admin router.hero_books_lib::default_server_socket_path()withHERO_BOOKS_SOCKETenv override — same resolution ashero_books_ui.X-Forwarded-Prefix,X-Hero-Context,X-Hero-Claimspropagation already lives in admin middleware; proxy forwardsContent-TypeandAuthorizationlike the reference.hero_books_server,hero_books_ui, or other crates.Files to Modify/Create
crates/hero_books_admin/src/proxy.rs— new module, copied fromcrates/hero_books_ui/src/proxy.rswith admin-duplicated items trimmed.crates/hero_books_admin/src/main.rs— addmod proxy;, constructAppState, register/rpcroute, attach state via.with_state(state).crates/hero_books_admin/Cargo.toml— no new dependencies required.Implementation Plan
Step 1: Create
crates/hero_books_admin/src/proxy.rsFiles:
crates/hero_books_admin/src/proxy.rscrates/hero_books_ui/src/proxy.rsverbatim.main.rs:BasePath,HeroRequestContext,base_path_middleware.AppState,proxy_request,api_proxy. Dropimg_proxyandpdf_proxyif present (admin templates have no image/pdf fetches).api_proxyverbatim.Dependencies: none.
Step 2: Wire proxy into admin router
Files:
crates/hero_books_admin/src/main.rsmod proxy;near the top.axum::routing::any,proxy::{AppState, api_proxy},std::sync::Arc.BasePath/HeroRequestContext/base_path_middleware— do not remove or rename.run_serve, before building the router:std::env::var("HERO_BOOKS_SOCKET"), elsehero_books_lib::default_server_socket_path().to_string_lossy().to_string().let state = AppState { server_socket: Arc::new(server_socket) };.Router::new()chain, add.route("/rpc", any(api_proxy))before the.layer(...)calls..layer(cors)line, add.with_state(state).Dependencies: Step 1.
Step 3: Smoke test
Files:
crates/hero_books_admin/tests/rpc_proxy.rs(new) or inline#[cfg(test)]inproxy.rs.Router::new().route("/rpc", any(api_proxy)).with_state(AppState{ server_socket: Arc::new(tmp_sock) }).tower::ServiceExt::oneshotwith aPOST /rpccarrying a JSON-RPC body; assert the body round-trips verbatim and the status is 200.server_socketat a non-existent path, assert status is 502.Dependencies: Step 2.
Step 4: Manual verification
cargo build -p hero_books_adminandcargo clippy -p hero_books_admin -- -D warnings.hero_books --start), load an admin page (/collections), confirm the XHR to/rpcreturns 200 and populates the table.Dependencies: Step 3.
Acceptance Criteria
hero_books/rpc.sockand returns the body verbatimhero_books_server,hero_books_ui, or other cratesNotes
hero_books_uialready uses a hand-rolledproxy.rs. Staying consistent with the sibling crate is the correct call here; migrating both crates to theopenrpc_proxy!macro would be a separate, larger refactor.hyper,hyper-util,http-body-util,tower, andtokioare already inhero_books_admin/Cargo.toml.BasePath/HeroRequestContext/base_path_middlewareare functionally identical to thehero_books_uiversions but live inmain.rs. Leave them in place to minimize diff surface.api_proxyhandles any HTTP method, so registering withany(api_proxy)matches the UI and covers futureGET-style discovery the templates might add.Test Results
Build
cargo build -p hero_books_admin— okClippy (warnings-as-errors)
cargo clippy -p hero_books_admin -- -D warnings— cleancargo clippy -p hero_books_admin --tests -- -D warnings— cleanTests
hero_books_admin— Total: 2, Passed: 2, Failed: 0tests/rpc_proxy.rs::round_trip_ok— oktests/rpc_proxy.rs::upstream_unreachable_returns_502— oksrc/main.rs: 0 tests)hero_books_server(regression gate, unchanged) — Total: 16, Passed: 16, Failed: 0Format
cargo fmt --check -p hero_books_admin— applied (one reformat during this run; final check clean)Notes
SUN_LENon macOS becausestd::env::temp_dir()resolves to/var/folders/..., which exceeds the 104-byte Unix-socket path limit. The test helper was updated to root its per-run directory at/tmpand use a shorterhbadm_prefix. After the fix both integration tests pass and fmt/clippy remain clean.Implementation Summary
The admin
/rpcproxy is wired up on branchdevelopment_fix_admin_rpc_proxy. Every admin page can now round-trip JSON-RPC through the admin socket intohero_books_server.Changes
crates/hero_books_admin/src/proxy.rs(new) — copied verbatim fromcrates/hero_books_ui/src/proxy.rs, withimg_proxy,pdf_proxy, and the items admin already defines (BasePath,HeroRequestContext,base_path_middleware) removed. RetainsAppState,proxy_request,api_proxybyte-identical to the UI version, including the 502-on-upstream-failure behaviour and theContent-Type/Authorizationrequest-header allowlist.crates/hero_books_admin/src/main.rs— addedmod proxy;, imports foraxum::routing::any,proxy::{AppState, api_proxy},std::sync::Arc; resolved the upstream socket (HERO_BOOKS_SOCKETenv, falling back tohero_books_lib::default_server_socket_path()); constructedproxy_state = AppState { server_socket: Arc::new(...) }; merged a small sub-routerRouter::new().route("/rpc", any(api_proxy)).with_state(proxy_state)into the main admin router so admin's ownRouter<()>state stays untouched.crates/hero_books_admin/tests/rpc_proxy.rs(new) — integration test covering both paths.round_trip_okspawns a stub Unix-socket HTTP listener, firesPOST /rpcviatower::ServiceExt::oneshot, and verifies the JSON body round-trips verbatim.upstream_unreachable_returns_502pointsserver_socketat a non-existent path and asserts HTTP 502.crates/hero_books_admin/Cargo.toml— added a[dev-dependencies]section withtower = { version = "0.5", features = ["util"] }so the integration test can calloneshot.macOS note
The integration test roots its per-run tempdir under
/tmprather thanstd::env::temp_dir(). macOS resolves the latter to/var/folders/..., which blows past the 104-byteSUN_LENlimit once you append a subdirectory and a socket filename. A short doc comment in the test explains the constraint.Test Results
cargo build -p hero_books_admin— ok.cargo clippy -p hero_books_admin -- -D warningsand--tests -- -D warnings— clean.hero_books_admintests: 2/2 passed (round_trip_ok,upstream_unreachable_returns_502).hero_books_serverlibrary regression gate: 16/16 passed.cargo fmt --check -p hero_books_admin— clean after initial fmt application.Acceptance criteria
/rpcon the admin socket forwards tohero_books/rpc.sockand returns the body verbatim.X-Forwarded-Prefix,X-Hero-Context,X-Hero-Claimsforwarded (handled unchanged by the existing admin middleware).hero_books_server,hero_books_ui, or other crates.Follow-ups not in this PR
hero_books_uiandhero_books_adminto theopenrpc_proxy!macro (current manualproxy.rsmatches the sibling crate; macro migration is a bigger refactor and out of scope here).architecture.mdand addingdocs/manual/docusaurus.md— remain open as docs-only follow-ups.Pull request opened: #99
This PR implements the changes discussed in this issue.