feat(server): align JSON-RPC dispatcher with openrpc.json (param shim + helpers) #50

Merged
mik-tf merged 1 commit from development_mik into development 2026-05-10 02:38:18 +00:00
Owner

Closes hero_slides#49.

TL;DR

The openrpc.json schema, generated client, and JS dashboard all use the new {collection, deck, slide} API shape; the dispatcher in crates/hero_slides_server/src/rpc.rs was still on the legacy {deck_path, slide_name, root_path} shape. 46 of 122 methods silently failed in the UI — the JS swallowed every error in try{…}catch{toast(…)} so the operator only ever saw side-effects (counter not incrementing, dropdown showing duplicate keys, slide editor not loading).

This PR adds a single legacy_param_shim that runs before match req.method.as_str() and translates the public schema-shape params into the legacy keys handlers expect. Zero per-handler edits. Smoke harness drift went from 46 → 0 PARAM_MISMATCH.

What's in here

1. Request preprocessor: legacy_param_shim

Public shape (openrpc.json) Legacy key injected Resolver
{collection, deck} deck_path state.registry.resolve_deck() with <root>/<deck> fallback for not-yet-existing decks (deck.delete / deck.rename)
{src_collection, src_deck} src_deck_path same (slide.copyTo)
{dst_collection, dst_deck} dst_deck_path same (slide.copyTo)
{collection} root_path + parent_path state.registry.lookup_root() (template.*, deck.create)
{slide} slide_name passthrough rename
{slides} slide_names passthrough (bulk ops)
{to} to_position passthrough (slide.move)
{filename}{file} bidirectional bg.uploadFile mismatch
bg.* {deck_path, folder, file} synthesised path <deck_path>/backgrounds/<folder>/<file>

Already-present legacy keys are preserved (no overwrite). Resolver errors are silent so the handler returns its own clearer error if neither shape is provided.

2. handle_deck_list shape fix

  • Records now include deck_name (matches DeckSummary schema) so the JS deck dropdown gets uniquely-keyed options. Pre-fix: every option was ${col}::undefined, breaking deck switching + URL-hash auto-select.
  • Now accepts an optional collection filter param the JS sends in loadDecks().

3. Async helpers for direct use

  • param_deck_path(params, state, method) — async, returns PathBuf. Use when a handler wants the resolver directly rather than going through the shim.
  • param_collection_root(params, state, method) — async, returns PathBuf.
  • param_slide(params, method) — sync, mirrors param_string("slide", Some("slide_name"), method).

4. scripts/smoke_openrpc.py

Python stdlib only (zero deps). Reads openrpc.json, calls every advertised method with minimal valid params over the rpc.sock, and classifies the response:

  • OK — server returned a result.
  • METHOD_MISSING-32000 "Method not found".
  • PARAM_MISMATCH-32000 "Missing 'X' parameter" (schema drift class).
  • RUNTIME_ERROR — server accepted the call but business logic failed (still proves dispatch is wired).

Skips destructive methods by default; --include-destructive re-registers the fixture between calls so prior mutations don't break later ones.

Verification

Live against http://127.0.0.1:9988/hero_slides/admin after deploying this binary:

Smoke harness:

Pre-fix:  ✓ 18  ! 18  ≠ 46  · 40  ? 0
Post-fix: ✓ 40  ! 15  ≠  0  · 35  ? 0   (90 non-destructive methods)

The 15 remaining METHOD_MISSING are entirely undispatched features (deckjobs.*, wizard.*, slide.setLink / clearLink / setImageModel / getStaleness / revertToLastGenerated / resolveContext, bg.extractTheme, deck.staleness, folder.pick) — separate scope; these need new handlers, not just shape fixes.

Hero Browser MCP UI smoke (1440×900 headless, dark theme, console interceptor installed):

  • Open Example Deck → Collections counter 0→1, sidebar reactive ✓
  • Click into hero_slides_examples → 2 deck cards render with state badges ✓
  • Click into hero_slides_intro → all 6 slide thumbnails render with PNG previews ✓
  • Click Edit on a slide → editor loads with markdown content + preview pane + Save/Save & Quit/Save & Generate + image-model selector ✓
  • console.messages = [] at every stage ✓

API smoke: all 5 collection.*, deck.list/get/getTheme/saveTheme/listThemes, slide.list/getContent/saveContent/setHidden/move/insert/delete/duplicate, template.list/get, bg.createFolder etc. — each manually round-tripped clean.

Out of scope

  • METHOD_MISSING (15 methods) — undispatched features, separate sweep. Filed as part of #49's catalogue; sub-issue can be split.
  • "Generate All" AI round-trip — chain proven (aibroker reachable, request goes upstream and is rejected with 400 "property 'image_config' is unsupported") but the local aibroker doesn't have gemini-3.1-flash-image-preview registered (only Qwen / wan2.7 image models via Alibaba). Routing falls back to a Qwen model that doesn't accept Gemini's image_config. Configuration gap, not code bug. Either register Gemini in aibroker's models.add or change the dashboard's default image-model-basic meta to a Qwen model. Will note on the issue close.

Refs:

Closes [hero_slides#49](https://forge.ourworld.tf/lhumina_code/hero_slides/issues/49). ## TL;DR The `openrpc.json` schema, generated client, and JS dashboard all use the new `{collection, deck, slide}` API shape; the dispatcher in `crates/hero_slides_server/src/rpc.rs` was still on the legacy `{deck_path, slide_name, root_path}` shape. **46 of 122 methods silently failed** in the UI — the JS swallowed every error in `try{…}catch{toast(…)}` so the operator only ever saw side-effects (counter not incrementing, dropdown showing duplicate keys, slide editor not loading). This PR adds a single `legacy_param_shim` that runs before `match req.method.as_str()` and translates the public schema-shape params into the legacy keys handlers expect. **Zero per-handler edits.** Smoke harness drift went from 46 → 0 PARAM_MISMATCH. ## What's in here ### 1. Request preprocessor: `legacy_param_shim` | Public shape (openrpc.json) | Legacy key injected | Resolver | |---|---|---| | `{collection, deck}` | `deck_path` | `state.registry.resolve_deck()` with `<root>/<deck>` fallback for not-yet-existing decks (deck.delete / deck.rename) | | `{src_collection, src_deck}` | `src_deck_path` | same (slide.copyTo) | | `{dst_collection, dst_deck}` | `dst_deck_path` | same (slide.copyTo) | | `{collection}` | `root_path` + `parent_path` | `state.registry.lookup_root()` (template.*, deck.create) | | `{slide}` | `slide_name` | passthrough rename | | `{slides}` | `slide_names` | passthrough (bulk ops) | | `{to}` | `to_position` | passthrough (slide.move) | | `{filename}` ↔ `{file}` | bidirectional | bg.uploadFile mismatch | | bg.* `{deck_path, folder, file}` | synthesised `path` | `<deck_path>/backgrounds/<folder>/<file>` | Already-present legacy keys are preserved (no overwrite). Resolver errors are silent so the handler returns its own clearer error if neither shape is provided. ### 2. `handle_deck_list` shape fix - Records now include `deck_name` (matches `DeckSummary` schema) so the JS deck dropdown gets uniquely-keyed options. Pre-fix: every option was `${col}::undefined`, breaking deck switching + URL-hash auto-select. - Now accepts an optional `collection` filter param the JS sends in `loadDecks()`. ### 3. Async helpers for direct use - `param_deck_path(params, state, method)` — async, returns `PathBuf`. Use when a handler wants the resolver directly rather than going through the shim. - `param_collection_root(params, state, method)` — async, returns `PathBuf`. - `param_slide(params, method)` — sync, mirrors `param_string("slide", Some("slide_name"), method)`. ### 4. `scripts/smoke_openrpc.py` Python stdlib only (zero deps). Reads `openrpc.json`, calls every advertised method with minimal valid params over the `rpc.sock`, and classifies the response: - **OK** — server returned a result. - **METHOD_MISSING** — `-32000 "Method not found"`. - **PARAM_MISMATCH** — `-32000 "Missing 'X' parameter"` (schema drift class). - **RUNTIME_ERROR** — server accepted the call but business logic failed (still proves dispatch is wired). Skips destructive methods by default; `--include-destructive` re-registers the fixture between calls so prior mutations don't break later ones. ## Verification Live against `http://127.0.0.1:9988/hero_slides/admin` after deploying this binary: **Smoke harness:** ``` Pre-fix: ✓ 18 ! 18 ≠ 46 · 40 ? 0 Post-fix: ✓ 40 ! 15 ≠ 0 · 35 ? 0 (90 non-destructive methods) ``` The 15 remaining METHOD_MISSING are entirely undispatched features (`deckjobs.*`, `wizard.*`, `slide.setLink / clearLink / setImageModel / getStaleness / revertToLastGenerated / resolveContext`, `bg.extractTheme`, `deck.staleness`, `folder.pick`) — separate scope; these need new handlers, not just shape fixes. **Hero Browser MCP UI smoke** (1440×900 headless, dark theme, console interceptor installed): - Open Example Deck → Collections counter 0→1, sidebar reactive ✓ - Click into `hero_slides_examples` → 2 deck cards render with state badges ✓ - Click into `hero_slides_intro` → all 6 slide thumbnails render with PNG previews ✓ - Click `Edit` on a slide → editor loads with markdown content + preview pane + Save/Save & Quit/Save & Generate + image-model selector ✓ - `console.messages = []` at every stage ✓ **API smoke:** all 5 `collection.*`, `deck.list/get/getTheme/saveTheme/listThemes`, `slide.list/getContent/saveContent/setHidden/move/insert/delete/duplicate`, `template.list/get`, `bg.createFolder` etc. — each manually round-tripped clean. ## Out of scope - **METHOD_MISSING (15 methods)** — undispatched features, separate sweep. Filed as part of #49's catalogue; sub-issue can be split. - **"Generate All" AI round-trip** — chain proven (aibroker reachable, request goes upstream and is rejected with `400 "property 'image_config' is unsupported"`) but the local aibroker doesn't have `gemini-3.1-flash-image-preview` registered (only Qwen / wan2.7 image models via Alibaba). Routing falls back to a Qwen model that doesn't accept Gemini's `image_config`. **Configuration gap, not code bug.** Either register Gemini in aibroker's `models.add` or change the dashboard's default `image-model-basic` meta to a Qwen model. Will note on the issue close. Refs: - https://forge.ourworld.tf/lhumina_code/hero_slides/issues/49 - https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/48 - https://forge.ourworld.tf/lhumina_code/home/issues/230
feat(server): align JSON-RPC dispatcher with openrpc.json (param shim + helpers)
Some checks failed
Test / test (push) Failing after 18s
Test / test (pull_request) Failing after 22s
e550878301
Closes the schema-vs-dispatcher drift surfaced during session 86's
[hero_slides#49](#49)
audit: 46 methods declared `{collection, deck, slide}` per openrpc.json
but the dispatcher still expected legacy `{deck_path, slide_name}`,
silently failing every UI flow that depended on them.

## Approach

A single `legacy_param_shim` runs before `match req.method` and
translates the public openrpc.json shape into the legacy keys the
existing handlers read. Reduces ~46 per-method edits to one
centralised translation step.

Translations:
- `{collection, deck}` → `deck_path` (via `state.registry.resolve_deck`,
  with `<root>/<deck>` fallback for not-yet-existing decks).
- `{src_collection, src_deck}` → `src_deck_path` (slide.copyTo).
- `{dst_collection, dst_deck}` → `dst_deck_path` (slide.copyTo).
- `{collection}` → `root_path` + `parent_path` (template.*, deck.create).
- `{slide}` → `slide_name`.
- `{slides}` → `slide_names` (bulk ops).
- `{to}` → `to_position` (slide.move).
- `{filename}` ↔ `{file}` (bg.uploadFile).
- bg.* synthesises a `path` from deck_path + folder + file.

Already-present legacy keys are preserved (won't overwrite); resolver
errors are silent so handlers return their own clearer error if both
shapes are missing.

## Other fixes in this commit

- `handle_deck_list` records now include `deck_name` (matches
  `DeckSummary` schema) so the JS deck dropdown gets uniquely-keyed
  options (was collapsing every option to `${col}::undefined`).
- `handle_deck_list` accepts an optional `collection` filter param
  the JS sends in `loadDecks()`.
- Added three async helpers (`param_deck_path`, `param_collection_root`,
  `param_slide`) for handlers that want to call the resolver directly
  without going through the shim.

## Smoke harness

`scripts/smoke_openrpc.py` (Python stdlib only) reads openrpc.json,
calls every method against the rpc.sock with minimal valid params,
and classifies OK / METHOD_MISSING / PARAM_MISMATCH / RUNTIME_ERROR.

Pre-fix run: 18 OK / 18 METHOD_MISSING / 46 PARAM_MISMATCH / 40
RUNTIME_ERROR.

Post-fix run: 40 OK / 15 METHOD_MISSING / **0 PARAM_MISMATCH** / 35
RUNTIME_ERROR (90 non-destructive methods).

The 15 remaining METHOD_MISSING are entirely undispatched features
(deckjobs.*, wizard.*, slide.setLink/clearLink/setImageModel/
getStaleness/revertToLastGenerated/resolveContext, bg.extractTheme,
deck.staleness, folder.pick) — separate scope from the alignment work
since these need new handlers, not just shape fixes.

## UI verification

Hero Browser MCP smoke against http://127.0.0.1:9988/hero_slides/admin:
- Open Example Deck → Collections counter 0→1.
- Click into hero_slides_examples → 2 deck cards render.
- Click into hero_slides_intro → 6 slide thumbnails render with PNG previews.
- Click into a slide → slide editor loads with markdown content + preview pane.
- console.messages = [] at every stage.

Refs:
- #49
- #48 (collection.* dispatch fix that surfaced this drift)

Signed-off-by: mik-tf
mik-tf merged commit 632603f623 into development 2026-05-10 02:38:18 +00:00
mik-tf deleted branch development_mik 2026-05-10 02:38:18 +00:00
Sign in to join this conversation.
No reviewers
No labels
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_slides!50
No description provided.