Working UI scaffolds for _admin + _web (closes #98) #103

Merged
timur merged 5 commits from issue-98-ui-scaffolds into development 2026-05-20 23:57:06 +00:00
Owner

Closes #98.

Summary

Implements the three sub-deliverables from the #98 design comment (signed off in the issue thread), then ships the templates emitter + scaffolder wiring on top.

Per-commit:

  1. rpc2_adapter: register new + list_full. The hero_rpc2 transport (#97) was missing both arms — the typed SDK couldn’t hit them. Adds them to CRUD_METHODS + the extract_data no-arg short-circuit. +3 tests, all green.
  2. rust_rpc2 emitter: CRUD trait methods per root object. Every root object (sid field OR [rootobject] marker) grows seven trait methods (_new / _get / _set / _delete / _list / _list_full / _exists). Also flips every emitted method (CRUD + existing service methods) to param_kind = map — the OSchema dispatcher reads named fields, so the jsonrpsee default of positional params would silently fail. +2 tests.
  3. OpenRPC emitter: CRUD methods per root object. Same shape; <hero-api-docs> now advertises the full surface, and OpenRPC-driven clients (Python / JS / future Rust) can call CRUD via the spec. +1 test.
  4. Templates emitter + scaffolder wiring (crates/generator/src/build/ui_emit.rs + extensions to scaffold.rs). New with_web() / --no-web (default-on). Per root object the scaffolder writes templates/<entity>/{list,detail,new}.html (admin) plus {list,detail}.html (web), src/routes/<entity>.rs modules calling the typed SDK trait, and shared src/{state,error,templates,routes/mod,routes/index}.rs files. Admin Cargo deps grow askama + <name>_sdk + hero_rpc2 + hero_theme. main.rs declares the new submodules + merges per-entity routers. Field-type rendering: enums → <select>, primitive lists → CSV, bool → checkbox, otimedatetime-local, etc. (full table in ui_emit.rs header). All template + route files are scaffolded-once, preserved on re-run. +5 tests.
  5. recipe_server regen + two codegen fixes uncovered during the build.
    • param_kind = map (bare ident, not string — jsonrpsee 0.26 grammar).
    • Display + FromStr impls on every string-typed enum the Rust struct generator emits (Askama HTML escaper needs T: Display to render {{ item.field }}; <select> form values parse directly via FromStr).
    • Admin form converters: all primitives via String + .parse().unwrap_or_default() so the scaffolded route compiles regardless of declared schema widths.

Architectural alignment with META hero_skills#262

Confirmed before coding:

  • Askama default for both _admin and _web.
  • hero_admin_lib for _admin (Axum + rust-embed + components).
  • hero_theme shared CSS for _web (no hero_website_lib / Tera).
  • Typed SDK via #[rpc(server, client)] is the only path; no hand-rolled JSON-RPC.
  • One socket per service; X-Hero-Context header already wired by base_path_middleware.
  • Naming: hero_<name>_{server,admin,web,sdk,examples}.
  • No Makefile / no scripts / no service_<name>.nu.
  • service.toml is SoT — scaffolder writes once, codegen never overwrites.

What's still in flight

  • hero_service template repo regen — that repo's layout is older (crates/hero_service_sdk instead of sdk/rust); conflating UI scaffolds with layout modernization would muddy the PR. Tracked as a follow-up.
  • hero_service_scaffold.md skill doc — separate PR on hero_skills: lhumina_code/hero_skills#279.

Test plan

  • All 141 generator unit tests pass.
  • All 9 rpc2_adapter tests pass.
  • cargo build --manifest-path example/recipe_server/Cargo.toml completes clean (warnings only — preserved files).
  • Browser-smoke: lab service recipes --start + open http://<admin-port>/ → see dashboard, list, detail, create form round-trip.

🤖 Generated with Claude Code

Closes #98. ## Summary Implements the three sub-deliverables from the [#98 design comment](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/98#issuecomment-35152) (signed off in the issue thread), then ships the templates emitter + scaffolder wiring on top. Per-commit: 1. **`rpc2_adapter`: register `new` + `list_full`.** The hero_rpc2 transport (#97) was missing both arms — the typed SDK couldn’t hit them. Adds them to `CRUD_METHODS` + the `extract_data` no-arg short-circuit. +3 tests, all green. 2. **`rust_rpc2` emitter: CRUD trait methods per root object.** Every root object (sid field OR `[rootobject]` marker) grows seven trait methods (`_new` / `_get` / `_set` / `_delete` / `_list` / `_list_full` / `_exists`). Also flips every emitted method (CRUD + existing service methods) to `param_kind = map` — the OSchema dispatcher reads named fields, so the jsonrpsee default of positional params would silently fail. +2 tests. 3. **OpenRPC emitter: CRUD methods per root object.** Same shape; `<hero-api-docs>` now advertises the full surface, and OpenRPC-driven clients (Python / JS / future Rust) can call CRUD via the spec. +1 test. 4. **Templates emitter + scaffolder wiring** (`crates/generator/src/build/ui_emit.rs` + extensions to `scaffold.rs`). New `with_web()` / `--no-web` (default-on). Per root object the scaffolder writes `templates/<entity>/{list,detail,new}.html` (admin) plus `{list,detail}.html` (web), `src/routes/<entity>.rs` modules calling the typed SDK trait, and shared `src/{state,error,templates,routes/mod,routes/index}.rs` files. Admin Cargo deps grow `askama` + `<name>_sdk` + `hero_rpc2` + `hero_theme`. main.rs declares the new submodules + merges per-entity routers. Field-type rendering: enums → `<select>`, primitive lists → CSV, bool → checkbox, `otime` → `datetime-local`, etc. (full table in `ui_emit.rs` header). All template + route files are scaffolded-once, preserved on re-run. +5 tests. 5. **`recipe_server` regen + two codegen fixes uncovered during the build.** - `param_kind = map` (bare ident, not string — jsonrpsee 0.26 grammar). - `Display` + `FromStr` impls on every string-typed enum the Rust struct generator emits (Askama HTML escaper needs `T: Display` to render `{{ item.field }}`; `<select>` form values parse directly via `FromStr`). - Admin form converters: all primitives via `String` + `.parse().unwrap_or_default()` so the scaffolded route compiles regardless of declared schema widths. ## Architectural alignment with [META hero_skills#262](https://forge.ourworld.tf/lhumina_code/hero_skills/issues/262) Confirmed before coding: - ✅ Askama default for both _admin and _web. - ✅ `hero_admin_lib` for _admin (Axum + rust-embed + components). - ✅ `hero_theme` shared CSS for _web (no `hero_website_lib` / Tera). - ✅ Typed SDK via `#[rpc(server, client)]` is the only path; no hand-rolled JSON-RPC. - ✅ One socket per service; X-Hero-Context header already wired by `base_path_middleware`. - ✅ Naming: `hero_<name>_{server,admin,web,sdk,examples}`. - ✅ No Makefile / no scripts / no `service_<name>.nu`. - ✅ `service.toml` is SoT — scaffolder writes once, codegen never overwrites. ## What's still in flight - **`hero_service` template repo regen** — that repo's layout is older (`crates/hero_service_sdk` instead of `sdk/rust`); conflating UI scaffolds with layout modernization would muddy the PR. Tracked as a follow-up. - **`hero_service_scaffold.md` skill doc** — separate PR on hero_skills: [lhumina_code/hero_skills#279](https://forge.ourworld.tf/lhumina_code/hero_skills/pulls/279). ## Test plan - [x] All 141 generator unit tests pass. - [x] All 9 rpc2_adapter tests pass. - [x] `cargo build --manifest-path example/recipe_server/Cargo.toml` completes clean (warnings only — preserved files). - [ ] Browser-smoke: `lab service recipes --start` + open `http://<admin-port>/` → see dashboard, list, detail, create form round-trip. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The hero_rpc2 transport (landed in #97) registers CRUD methods from
`rpc2_adapter::CRUD_METHODS`. That list was `[get, set, delete, find,
exists, list]` — missing `new` and `list_full`. Both are arms in the
OSchema-generated dispatcher (`recipe.new`, `recipe.list_full`) but were
unreachable over the wire after the #97 cutover.

#98's admin templates need `list_full` to render one row per object with
one round-trip, and need `new` to fetch the default-shaped Recipe for the
create form's initial values. Without these the SDK can't drive the UI.

Changes:
- Add `new` + `list_full` to `CRUD_METHODS` so the adapter registers them
  as `<type>.new` / `<type>.list_full` on the RpcModule.
- Extend `extract_data` to short-circuit no-arg methods (`new` / `list` /
  `list_full`) to empty strings — matches the OSIS handler's expectation.
- Pass `data: <obj>` through as a JSON object (not a stringified string)
  so the OSchema `*_from_json` parser sees a real JSON object — what
  jsonrpsee will emit when the generated trait method takes
  `data: Recipe` as a named param.
- Three new tests covering the no-arg short-circuit and the JSON-object
  passthrough.
Today the hero_rpc2 trait emitter only translates user-declared service
methods. The generated `RecipesClient` has zero CRUD entries, so the
typed SDK cannot do `recipe.list_full` / `.get` / `.set` / `.delete` at
all — the consumer would have to drop to raw jsonrpsee.

This adds CRUD trait methods to every root object the schema declares
(detection mirrors the existing rule: `[rootobject]` marker OR a `sid`
field). For each root object the emitter now writes seven methods:

  - `<entity>_new`      → `<entity>.new`
  - `<entity>_get`      → `<entity>.get`       (sid → entity)
  - `<entity>_set`      → `<entity>.set`       (entity → sid)
  - `<entity>_delete`   → `<entity>.delete`    (sid → bool)
  - `<entity>_list`     → `<entity>.list`      (→ Vec<sid>)
  - `<entity>_list_full`→ `<entity>.list_full` (→ Vec<entity>)
  - `<entity>_exists`   → `<entity>.exists`    (sid → bool)

The wire names match the OSchema dispatcher arms in
`osis_server_generated.rs` and the `CRUD_METHODS` list in the
rpc2_adapter (the latter updated in the previous commit to include
`new` + `list_full`).

Also flips every emitted method (both new CRUD and existing service
methods) to `param_kind = "map"`. The OSchema dispatcher reads named
fields with `params.get("<name>")`; jsonrpsee's default positional
shape would have silently failed at runtime on every multi-arg
service method. The compat test that uses positional shape lives in
`hero_rpc2/tests/http_hero_rpc_compat.rs` with a *separate* hand-rolled
trait — unaffected.

Tests:
  - existing `emit_rpc2_trait_translates_service_methods` updated to
    expect `param_kind = "map"`.
  - new `emit_rpc2_trait_emits_crud_methods_per_root_object` covers
    detection (sid-field OR rootobject-marker) and rejects spurious
    CRUD entries on non-root types.

All 132 generator unit tests pass.
Reflects the typed Rust SDK's new CRUD trait methods (previous commit)
in the OpenRPC spec so `<hero-api-docs>` advertises the full surface and
any OpenRPC-driven client (Python, JS, future ad-hoc Rust) can call CRUD
without hand-rolling JSON-RPC.

For every root object (sid field OR `[rootobject]` marker) the generator
now emits seven OpenRPC method entries with proper `$ref` pointers to
the existing `<TypeName>` and `<TypeName>Create` schemas (the latter
already injected per #85). Wire names exactly match the OSchema
dispatcher arms and the Rust SDK trait method names.

New unit test asserts:
  - all seven CRUD methods emitted per root object
  - service methods coexist
  - non-root types don't leak CRUD entries
  - `list_full` items `$ref` the typed object
  - `set` accepts `<TypeName>Create` and returns string sid
  - `exists` returns boolean

133/133 generator lib tests pass.
feat(scaffold): per-root-object UI scaffolds for _admin + _web (#98)
Some checks failed
Test / test (push) Failing after 1m32s
85394d0c87
The first cut of the templates emitter requested by hero_rpc#98. Every
fresh scaffold now produces a *working* admin dashboard + public web
surface — list / detail / create-form per root object — all driving
the generated SDK end-to-end. No more hand-rolled JSON-RPC or "add
screens here" placeholder.

## New module — `crates/generator/src/build/ui_emit.rs`

Pure-function emitter; the scaffolder is the only caller.

- `discover_root_objects(workspace, schemas_dir, domains)` — parses every
  `.oschema` under `schemas/<domain>/`, projects each root object
  (sid field OR `[rootobject]` marker) into a `RootObjectInfo` that
  carries: name, snake/url segment, domain, editable fields (server-managed
  `sid` / `created_at` / `updated_at` filtered), whether a `name: str`
  exists, and a `FieldKind` per field that maps OSchema types to form
  inputs + detail render expressions.
- `admin_{base,index,list,detail,new}_html()` — Askama-friendly Bootstrap
  templates. Per-entity list/detail/new each `{% extends "base.html" %}`.
- `web_{base,index,list,detail}_html()` — read-only public sibling, no
  create / delete affordances, uses the public theme shell.
- `admin_route_module(sdk_crate, domain, root)` — emits a complete
  `src/routes/<entity>.rs` with `pub fn router()`, plus list / detail /
  new / create / delete handlers. **Every CRUD call goes through the
  generated SDK trait** (`{Domain}Client::<entity>_list_full` etc.) —
  the route module asserts this in tests.
- `web_route_module()` — same shape, read-only.

Field-type table (v1, contributor swaps richer rendering by editing):

  | OSchema    | Form input               | Display                    |
  |------------|--------------------------|----------------------------|
  | str        | text input               | {{ value }}                |
  | int / u*   | number input             | {{ value }}                |
  | bool       | checkbox                 | yes / no badge             |
  | otime      | datetime-local input     | as-string                  |
  | enum       | <select> with variants   | text                       |
  | [primitive]| CSV input + helper text  | <ul>                       |
  | nested     | JSON textarea fallback   | <ul> JSON                  |

6 unit tests cover discovery, enum select rendering, CSV list rendering,
the list template, the admin route module trait-only calls, and the
web route module read-only surface.

## Scaffolder wiring — `crates/generator/src/build/scaffold.rs`

- New `generate_web: bool` field (default on), `with_web()` / `without_web()`.
- New `generate_web_crate()` — mirrors `generate_admin_crate`, simpler
  surface (no hero_admin_lib dep, just hero_theme).
- `generate_admin_crate` updated to:
  - Add Cargo deps: `askama`, `<name>_sdk`, `hero_rpc2` (with client +
    uds-http features), `hero_theme`, `serde`, `serde_json`, `tracing`.
  - Emit `templates/base.html`, `templates/index.html`, and per-entity
    `templates/<seg>/{list,detail,new}.html`.
  - Emit `src/state.rs` (AppState + `from_env` connecting to
    `<service>_server/rpc.sock` via `hero_rpc2::Client`).
  - Emit `src/error.rs` (single `AppError` wrapping anyhow).
  - Emit `src/templates.rs` (Askama `#[derive(Template)]` structs +
    re-exports of the root-object types from the SDK).
  - Emit `src/routes/{mod,index}.rs` and per-entity route modules from
    `ui_emit::admin_route_module()`.
  - Rewrite `main.rs` to: declare the new submodules, mount each
    per-entity router via `.merge(routes::<snake>::router())`, mount the
    shared theme at `/lib-static/theme`, and wire `AppState` through
    `.with_state()`.
- All template + route files use **preserve-once** semantics — re-running
  the scaffolder picks up newly added root objects but leaves contributor
  edits to existing files untouched.

Tests updated:

- `test_scaffold_admin_main_uses_hero_admin_lib` — updated signature; now
  also asserts AppState wiring + theme mount.
- `test_scaffold_admin_main_mounts_per_entity_routers` — new — root
  objects discovered at scaffold time produce `.merge(routes::*router())`.
- `test_scaffolder_web_default_on_with_without_toggle` — new — confirms
  `_web` is default-on with the standard with/without toggle.

141/141 generator lib tests pass.
chore(recipe_server): regenerate with #98 UI scaffolds + apply codegen fixes
Some checks failed
Test / test (push) Failing after 2m2s
Test / test (pull_request) Failing after 1m30s
39879b9792
Two parallel changes:

1. **Recipe server regen.** Re-ran the scaffolder against
   `example/recipe_server` to produce the new UI surface. The fresh
   output:
   - `crates/hero_recipes_admin/` — Cargo deps updated (askama, sdk,
     hero_theme, hero_rpc2 client), `src/{state,error,templates}.rs`,
     `src/routes/{mod,index,recipe,collection}.rs`, and per-entity
     templates under `templates/{recipe,collection}/`. All CRUD calls
     resolve to typed SDK methods (`RecipesClient::recipe_list_full`
     etc.); zero raw JSON-RPC.
   - `crates/hero_recipes_web/` — new public read-only sibling. Same
     wiring shape, list/detail only.
   - Old `src/pages/{index,recipes}.html` removed (replaced by the
     Askama templates).
   - `Cargo.toml` workspace members updated to include `_web`.
   - `sdk/rust/src/recipes.rs` regenerated — every root object now
     carries the seven CRUD trait methods + Display/FromStr impls on
     the typed enums.

2. **Two codegen fixes uncovered during the recipe_server build.**

   - **`param_kind = map` (no quotes).** jsonrpsee 0.26's `#[method]`
     attribute parses `param_kind` as a bare identifier, not a string
     literal. Switched the emitter from `"map"` to `map`.
   - **`Display` + `FromStr` on typed enums.** Askama's default HTML
     escaper requires `T: Display` (or `HtmlSafe`) — typed enums
     without an explicit impl couldn't render via `{{ item.field }}`
     in templates. Now every string-typed enum the Rust struct
     generator emits ships an inline Display (writes the lowercase
     serde-renamed variant string — byte-for-byte the same shape
     JSON serialization produces) plus a matching FromStr so form
     `<select>` values parse directly. Applied symmetrically to both
     the regular and wasm-types generators.

   - **Admin form converters use `String` uniformly.** Earlier draft
     emitted `u32`/`u64`/`f64` in the Form-struct fields, which
     mismatched whatever the SDK's actual primitive width was.
     Switched to `String` + `.parse().unwrap_or_default()` so the
     scaffolded route compiles regardless of declared schema widths;
     contributors can swap in proper validation later. Bool stays
     `Option<String>` (checkbox semantics).

Verified end-to-end: `cargo build` on the full
`example/recipe_server/` workspace completes with only warnings from
preserved files. 141/141 generator unit tests pass. 9/9 rpc2_adapter
tests pass.
timur force-pushed issue-98-ui-scaffolds from 39879b9792
Some checks failed
Test / test (push) Failing after 2m2s
Test / test (pull_request) Failing after 1m30s
to 05bcb78d48
Some checks failed
Test / test (pull_request) Failing after 1m46s
Test / test (push) Failing after 2m1s
2026-05-20 23:56:47 +00:00
Compare
timur merged commit 2204eccef7 into development 2026-05-20 23:57:06 +00:00
Sign in to join this conversation.
No reviewers
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_rpc!103
No description provided.