Working UI scaffolds for _admin + _web (closes #98) #103
No reviewers
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_rpc!103
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-98-ui-scaffolds"
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?
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:
rpc2_adapter: registernew+list_full. The hero_rpc2 transport (#97) was missing both arms — the typed SDK couldn’t hit them. Adds them toCRUD_METHODS+ theextract_datano-arg short-circuit. +3 tests, all green.rust_rpc2emitter: 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) toparam_kind = map— the OSchema dispatcher reads named fields, so the jsonrpsee default of positional params would silently fail. +2 tests.<hero-api-docs>now advertises the full surface, and OpenRPC-driven clients (Python / JS / future Rust) can call CRUD via the spec. +1 test.crates/generator/src/build/ui_emit.rs+ extensions toscaffold.rs). Newwith_web()/--no-web(default-on). Per root object the scaffolder writestemplates/<entity>/{list,detail,new}.html(admin) plus{list,detail}.html(web),src/routes/<entity>.rsmodules calling the typed SDK trait, and sharedsrc/{state,error,templates,routes/mod,routes/index}.rsfiles. Admin Cargo deps growaskama+<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 inui_emit.rsheader). All template + route files are scaffolded-once, preserved on re-run. +5 tests.recipe_serverregen + two codegen fixes uncovered during the build.param_kind = map(bare ident, not string — jsonrpsee 0.26 grammar).Display+FromStrimpls on every string-typed enum the Rust struct generator emits (Askama HTML escaper needsT: Displayto render{{ item.field }};<select>form values parse directly viaFromStr).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:
hero_admin_libfor _admin (Axum + rust-embed + components).hero_themeshared CSS for _web (nohero_website_lib/ Tera).#[rpc(server, client)]is the only path; no hand-rolled JSON-RPC.base_path_middleware.hero_<name>_{server,admin,web,sdk,examples}.service_<name>.nu.service.tomlis SoT — scaffolder writes once, codegen never overwrites.What's still in flight
hero_servicetemplate repo regen — that repo's layout is older (crates/hero_service_sdkinstead ofsdk/rust); conflating UI scaffolds with layout modernization would muddy the PR. Tracked as a follow-up.hero_service_scaffold.mdskill doc — separate PR on hero_skills: lhumina_code/hero_skills#279.Test plan
cargo build --manifest-path example/recipe_server/Cargo.tomlcompletes clean (warnings only — preserved files).lab service recipes --start+ openhttp://<admin-port>/→ see dashboard, list, detail, create form round-trip.🤖 Generated with Claude Code
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.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.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._admin+_webthat drive the generated SDK end-to-end #9839879b979205bcb78d48