Scaffolder should emit working UI templates for _admin + _web that drive the generated SDK end-to-end #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_rpc#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?
Context
The parent META called out three things that connect:
hero_admin_lib" — done across hero_website_framework#4. Five components live there now (<hero-api-docs>,<hero-connection-status>, logs/jobs/markdown viewers).hero_web_template" — done in hero_web_template#4 (hero_themecrate).But today the scaffolder's
_admincrate is a placeholder shell — health endpoint, well-known JSON, an<h1>saying "Add screens here." No_webis scaffolded at all. The contributor cloning the template still has to hand-roll all the UI that exercises their service. None of those three META threads close the loop on "a fresh scaffold gives you a working UI out of the box."What this issue does
Make the scaffolder produce functional UI scaffolds for both
_adminand_webthat:reqwest. The samehero_<name>_sdk::Client(or the JS package for browser callers) that downstream consumers use.hero_admin_libfor admin,hero_web_templatefor shared theme + public web. Components like<hero-api-docs>,<hero-connection-status>etc. ship pre-wired..oschema.cargo build && lab service <name> --start→ admin renders a working dashboard,_web(if scaffolded) renders a public surface.Concrete deliverables
_adminscaffold (required)Per-root-object Askama templates emitted under
crates/<name>_admin/templates/:Routes emitted under
src/routes/:_webscaffold (default-on;--no-webto opt out)Public-facing surface — no auth, simpler templates. Per skill table,
_webwas previously "optional"; this issue flips it to default-on so contributors get something runnable.No create/edit on the public surface by default — those stay admin-only.
Field-type rendering
The scaffolder introspects each root object's fields and emits sensible defaults per OSchema type:
str<input type="text">{{ value }}int,u32, …<input type="number">{{ value }}bool<input type="checkbox">{{ value }}otime<input type="datetime-local"><select>with each variantother_sid: strto another root object)<select>populated fromsdk.<other>.list_full()[Type]<fieldset>per sub-fieldAuto-injected fields (
sid,created_at,updated_atper #85/#275) are not in create forms; they're shown as read-only on detail pages.What to do
templates/+src/routes/layout, the per-field-type rendering table (refine the one above), and a snippet of what a real scaffoldedroutes/recipe.rswould look like. Wait for sign-off.crates/generator/src/generate/(mirrors the existingrust_types,rust_server, etc. modules). It writes to agenerated/subfolder per #96 if that lands first; otherwise tocrates/<name>_admin/templates/+src/routes/directly.WorkspaceScaffolderopts into the new emitter (default on); CLI flag--no-webskips the_webcrate.recipe_serverandhero_servicetemplate repo — both rebuilt to show the new UI surface.hero_service_scaffold.mdskill to document the new UI scaffolds.cargo build && lab service recipes --start && open http://localhost:<admin-port>shows working CRUD over the generated SDK.Acceptance
_admin+_webcrates with working UI templates for every root object._adminuseshero_admin_libfor layout + the five Hero web components._webuseshero_web_template'shero_themefor shared CSS._admin→ see dashboard + list page per root object + create form that round-trips to the backend; open_web→ see public list pages.hero_serviceandrecipe_serverboth regenerated and showcasing the new surface.Out of scope
_web— out of scope;_webis read-only by default.Related
hero_themecrate).generated/convention.Design proposal — UI scaffolds for
_admin+_webBefore writing code, here's the proposed shape end-to-end. Please sign off (or redirect) before I touch the emitter.
1. Scope clarification — two SDK gaps surface here
Working through the acceptance criteria ("all RPC calls go through the generated SDK — no hand-rolled JSON wrangling") exposed two preconditions that today aren't met:
rust_rpc2emitter (build/emit/rust_rpc2.rs) only translates theservice.methodlines from.oschema. The#[rpc(server, client)] trait Recipes { … }it produces has zero CRUD entries, so todayRecipesClientcannot dorecipe.list_full/recipe.get/recipe.new/recipe.deleteat all.list_fullwas dropped on the hero_rpc2 cutover.rpc2_adapter::CRUD_METHODS(#97 just landed) is["get", "set", "delete", "find", "exists", "list"].recipe.list_fullis no longer routed —listonly returns SIDs. Until this is fixed the admin index page can't render one row per object with one round trip.Proposed sub-deliverables inside #98 (call out if you'd rather split them):
list_fulltorpc2_adapter::CRUD_METHODS+ the corresponding branch inhandle_rpc_call_with_context. ~10 LOC + a test.build/emit/rust_rpc2.rsso that for every root object the emitted trait grows six methods (<entity>_new,<entity>_get,<entity>_set,<entity>_delete,<entity>_list,<entity>_list_full, plus<entity>_exists) wired to the rpc2 wire names (recipe.new,recipe.get, …). The macro producesRecipesClient::recipe_list_full(&self, ctx)automatically. Identical surface to what the OpenRPC spec emits in (c).schemas/openrpc.rsso every root object adds the same CRUD method set toopenrpc.json. Side benefit:<hero-api-docs>(already embedded by the existing/recipespage) now lists CRUD entries too.If any of these should be its own ticket let me know and I'll spin them out. Everything below assumes (a)(b)(c) land first inside the same PR so the templates can actually compile.
2. Per-root-object file layout
For a schema with root objects
RecipeandCollection, the scaffolder emits:Per-file ownership:
templates/<entity>/*.html,src/routes/<entity>.rs— scaffolded once, preserved on re-run (per the issue body's "scaffolded-once, not regenerated" note).templates/base.html,templates/_macros.html,templates/index.html,src/state.rs,src/templates.rs,src/main.rs,src/routes/mod.rs,src/routes/index.rs— also preserved, but the scaffolder emits them with all known root objects wired up from the first run.Re #96 coordination: the templates are preserved-once anyway, so they don't belong in a
generated/subfolder. The SDK CRUD bits from §1(b)/(c) do belong ingenerated/and will follow whichever layout #96 lands. If #96 lands first → emit there; otherwise → flat. The templates emitter itself is layout-agnostic — only the SDK consumer path changes.3. AppState + dashboard handler — the wiring
(
connect()is the small helper the SDK already gets fromhero_rpc2'sprelude::*— typed over the trait.)4. Field-type rendering table (refined)
str<input type="text" name="…" required>{{ value }}int,u8/16/32/64<input type="number" step="1" min="0">(signed getsmin=""){{ value }}f32,f64<input type="number" step="any">{{ value }}bool<input type="checkbox">{% if v %}yes{% else %}no{% endif %}otime<input type="datetime-local">format_otime(v)filterOTime(UTC) via the shared filter.<select>with one<option>per variantDefault::default().<other>_sid: strfield where the schema knows<other>is a root object)<select>populated fromsdk.<other>.list_full()at render-time; option label =<other>.nameif aname: strfield exists, else the sid<a href="/<other>/{{sid}}">{{name_or_sid}}</a>list_fullcall already used elsewhere.[T]whereTis primitive (str,int, …)<input type="text">+ helper text "comma-separated" + a<button type="button">for add-row (progressive enhancement; works without JS)<ul>{% for x in value %}<li>{{x}}</li>{% endfor %}</ul><button>flow, or comma-split fallback).[T]whereTis a nested struct<textarea>pre-populated with[])<table>one row per element<fieldset>with one input per sub-field, recursive<dl>blockAuto-injected fields (
sid,created_at,updated_at) — never in create forms; read-only on detail page. Matches the OpenRPC<Name>Createcompanion schema thatschemas/openrpc.rsalready emits.Validation beyond OSchema type checks is out of scope (per the issue's "out of scope" section).
5. End-to-end snippet —
routes/recipe.rsWhat the scaffolder writes for the Recipe root object. Every line goes through the generated SDK; zero hand-rolled JSON-RPC; zero
reqwest.Matching
templates/recipe/list.html(Askama):The
_webvariant ofrecipe/list.htmlswaps the admin shell forhero_theme's public navbar and drops the<form action="…/delete">column.6. WorkspaceScaffolder API surface
Mirrors the existing
with_admin/without_adminpair. Default-on for both_adminand_web, per the issue body.CLI: the lab subcommand that drives the scaffolder gets a
--no-webflag →.without_web().--no-adminalready exists.7. Open questions for sign-off
<fieldset>per sub-field, with the JSON-textarea fallback for[NestedStruct]. OK as a v1, or do you want a real nested-form builder?<select>for reference fields — I'm assuming the generator can detect<other>_sid: strand treat it as a foreign key to the<Other>root object. If you'd rather make this explicit via an OSchema annotation, say so now and I'll plumb the marker first.Arc<dyn RecipesClient + Send + Sync>(above) lets tests pass a mock. Alternative isArc<RecipesClient_impl>(whatever jsonrpsee names the client struct) — concrete, faster, no mocks. Either is fine; I'll default to the trait-object shape unless you want concrete.Will start on the implementation as soon as this gets a 👍 or redirect.
Implementation landed in PR #103
All three sub-deliverables from the design comment are in, plus the templates emitter + scaffolder wiring + recipe_server regen.
Per-commit
rpc2_adapter: registernew+list_fullCRUD ops (sub-deliverable a).rust_rpc2emitter: CRUD trait methods per root object (sub-deliverable b). Also flips every emitted method toparam_kind = map— required for the OSchema dispatcher.crates/generator/src/build/ui_emit.rs(700 LOC, 6 tests) + scaffolder getswith_web()/--no-webplus per-entity template + route emission. Admin Cargo deps growaskama+<name>_sdk+hero_rpc2+hero_theme.recipe_serverregen + two codegen fixes uncovered during the build:param_kindbare-ident grammar fix, plusDisplay+FromStrimpls on every string-typed enum so Askama can render{{ item.field }}.What I confirmed before coding
I re-read META #262 per your nudge. Every locked decision lines up with the design comment: Askama default for both binaries;
hero_admin_libfor _admin andhero_themefor _web; typed SDK via#[rpc(server, client)]is the only path; one socket per service; naminghero_<name>_{server,admin,web,sdk}; no Makefile/scripts;service.tomlis SoT (scaffolder writes once, codegen never overwrites).Verified
cargo buildonexample/recipe_server/completes —_adminand_webboth compile through the regenerated typed SDK.Open follow-ups
hero_servicetemplate repo regen — that repo is on the older layout (crates/hero_service_sdk/instead ofsdk/rust/); regenerating it as part of #98 would conflate UI scaffolding with layout modernization. Tracked separately.lab service recipes --startsession; the staticcargo buildalready exercises every code path through to the templates.hero_service_scaffold.mdskill update — PR landed at hero_skills#279.