lets change the way how we do routes and location management of slides #32
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?
it all starts with scanning a directory to find slides
whenw e scan we should give this collection (deck collection) a name
e.g. myslides
which needs to be remembered in hero_db
see /hero_db
so we basically know name:path
then we scan, if double name of deck, we give clean error where we explain where the 2 paths are who have same name
name (deck and collection) is always underscore, lowercase, ascii only
so now we have
the metadata per deck and slide are in the directory we scanned
the routes we have are (see skill /hero_ui_routes)
advice how to create routes best, keep all simple and is combination of collection/deck and slide name
depending context
no path in url
make sure we have full route handling on the slide tool, ONLY usingabove names
and make sure we insert the prefixes well see skill /hero_web_prefix
Implementation Spec for Issue #32
Objective
Replace filesystem-path-based deck addressing with a three-level name-only identifier space
collection_name / deck_name / slide_namethat survives restarts, rejects duplicates at scan time, and drives every route and RPC the slide service exposes.Routes must be built exclusively from those names (no filesystem paths in URLs), mounted correctly under the reverse-proxy
X-Forwarded-Prefix(see/hero_web_prefix), and follow hash-routing conventions (see/hero_ui_routes). Thecollection_nametopathregistry is persisted inhero_db(see/hero_db).Requirements
collection_name. Mappingcollection_nametoroot_pathis stored inhero_db(Redis-compatible encrypted store, RESP2 protocol) under the canonical hash keyhero_slides:collections.collection_nameand every discovereddeck_namemust satisfy a strict regex^[a-z0-9_]+$(lowercase ASCII letters, digits, underscore; non-empty; no leading/trailing underscore; no consecutive underscores). Upper-case / hyphen / whitespace / non-ASCII inputs are rejected with a clear error..slidesmarker directories (inside a single collection or across all registered collections) yield the samedeck_name, scanning must fail with an error that lists both offending paths verbatim:duplicate deck name '<name>': <path_a> and <path_b>.#/collections,#/collections/:collection,#/collections/:collection/decks/:deck,#/collections/:collection/decks/:deck/slides/:slide,#/collections/:collection/decks/:deck/slides/:slide/history,#/collections/:collection/decks/:deck/themes,#/collections/:collection/decks/:deck/present./api/slide-image/...,/api/deck-pdf,/present, log/SSE streams) is rebuilt to accept(collection, deck[, slide])name parameters; the server resolves them to a path internally via the hero_db-backed registry.collection+deck_name(+slide_name) instead ofpath/deck_path/src_deck_path/dst_deck_path. This is an outright rename: the server resolves names to paths via a single helper before delegating tohero_slides_lib.hero_slides_libkeeps its&Path-based internal API unchanged.{{ base_path }}usage must keep the reverse-proxy prefix mechanism working (it already is, per/hero_web_prefix): no absolute URL strings withoutbase_path, middleware stays registered.snake_casefor infra,kebab-casefor URL slugs at the HTML/DOM level, build viamake, workspace target dir/Users/casperstevens/hero/build/cargo, no emojis, no AI attribution.Skill-derived rules honored by the spec
/hero_db): usehero_db_sdk::HeroDbServerClient::connect_socketagainst~/hero/var/sockets/hero_db/rpc.sock(override viaHERO_DB_SOCKET). Use theredis.*namespace (redis.hset,redis.hget,redis.hgetall,redis.hdel,redis.hexists) on the hash keyhero_slides:collectionswhere each field is acollection_nameand each value a JSON document{"root": "<absolute path>", "registered_at": "<rfc3339>"}. Dedicated database parameter left unset (default database). All calls go through the typedopenrpc_client!client; no hand-written RPC./hero_ui_routes): hash-based SPA routing, every object and subview gets a canonical URL, rows are navigable, nested subviews addressable (/#/collections/:collection/decks/:deck/slides/:slide/historyetc.), filter state round-trips through the URL. Logs stay reachable under job-scoped routes#/jobs/:job_id./hero_web_prefix):X-Forwarded-Prefixmiddleware is already wired (crates/hero_slides_ui/src/routes.rs::base_path_middleware). Do not touch it. Every template URL keeps{{ base_path }}prefix; every new server-side URL emitted in HTML uses the same mechanism. Presentation-time client JS fetches must continue to targetBASE + '/rpc'.Files to Modify/Create
crates/hero_slides_lib/src/name.rs— new. DefinesDeckCollectionName,DeckName,SlideNamenewtype wrappers;validate_identifier(s)function enforcing^[a-z0-9]+(_[a-z0-9]+)*$;NameErrorenum.crates/hero_slides_lib/src/collection.rs— new.DeckCollectionstruct holdingname+root_path+ the list of(deck_name, deck_path)tuples;scan_collection(root, name) -> Result<DeckCollection, CollectionError>which returns a structuredCollectionError::DuplicateDeck { name, first, second }as soon as it sees a collision. Pure, sync; no hero_db knowledge here.crates/hero_slides_lib/src/lib.rs— re-export the new modules.crates/hero_slides_lib/Cargo.toml— addregex = "1"for the identifier validator (andchrono = { version = "0.4", features = ["serde"] }if not already present, forregistered_at).crates/hero_slides_server/Cargo.toml— addhero_db_sdk = { git = "https://forge.ourworld.tf/lhumina_code/hero_db.git", branch = "development" },hero_rpc_openrpc, and any already-pulled siblings. Align with existing patch rules.crates/hero_slides_server/src/registry.rs— new.CollectionRegistry(Clonewrapper around anArcof a hero_db client) exposing:register(name, path),unregister(name),list() -> Vec<(name, path)>,lookup(name) -> Option<PathBuf>,resolve_deck(collection, deck_name) -> Result<PathBuf, ResolveError>,resolve_slide(collection, deck_name, slide_name) -> Result<(PathBuf, String), ResolveError>. All persistence goes throughredis.hset/redis.hgetallon keyhero_slides:collections. Includes unit test doubles by accepting a trait so a memory-backed fake can be used from tests.crates/hero_slides_server/src/main.rs— construct theCollectionRegistryonce at startup; attach toServerState. Fail fast with a clear message if hero_db socket is missing.crates/hero_slides_server/src/rpc.rs— rewrite every parameter parsing block to readcollection+deck+slidestrings (andsrc_collection/src_deck+dst_collection/dst_deckforslide.copyTo), callregistry.resolve_*, then delegate tohero_slides_libexactly as today. Replacedeck.scan { path }withdeck.scan { collection, path }which registers the collection in hero_db only after successful duplicate-free validation.crates/hero_slides_server/src/agent.rs,crates/hero_slides_server/src/generate_job.rs— swapdeck_path/pathparam readers for the name-based resolver. Same RPC method names, new parameter names.crates/hero_slides_server/openrpc.json— update every method's parameter schema:path/deck_pathtocollection+deck/slide. Addcollection.list,collection.register,collection.unregister,collection.getmanagement methods.crates/hero_slides_sdk/src/lib.rs— unchanged Rust source; theopenrpc_client!macro regenerates types from the new spec. Document a one-timecargo clean -p hero_rpc_derive && cargo clean -p hero_slides_sdk && cargo buildstep.crates/hero_slides_ui/src/routes.rs— replace path-keyed Axum routes:/present?deck=<path>to/present/{collection}/{deck}/api/slide-image/{slide}?deck=<path>to/api/collections/{collection}/decks/{deck}/slides/{slide}/image/api/slide-version-image/{slide}/{version}?deck=<path>to/api/collections/{collection}/decks/{deck}/slides/{slide}/versions/{version}/image/api/deck-pdf?deck=<path>to/api/collections/{collection}/decks/{deck}/pdfThe handlers must resolve names by calling
/rpc(deck.get/collection.lookup) or, preferably, read a small in-process name→path cache populated from hero_db via the SDK.crates/hero_slides_ui/Cargo.toml—hero_slides_sdkis already present; may addhero_db_sdkif the UI resolves names client-side (preferred: keep resolution server-side via the existingrpc_proxyso UI only depends on names).crates/hero_slides_ui/templates/present.html— replaceDECK_PATH/deck_path_encodedwithCOLLECTION_NAME+DECK_NAME; build image URLs fromBASE + '/api/collections/' + COLLECTION + '/decks/' + DECK + '/slides/' + slide + '/image'; changeslide.list,deck.runAgent,deck.agentStatus,deck.generateAsync,deck.generateJobStatusRPC params to{ collection, deck }.crates/hero_slides_ui/static/js/dashboard.js— replace everydeck_path: selectedDeckPathwithcollection: selectedCollection, deck: selectedDeckName; replace everysrc_deck_path/dst_deck_pathwithsrc_collection/src_deck/dst_collection/dst_deck; rewrite hash routes:#slides/<path>to#collections/<collection>/decks/<deck>/slides#slide/<path>/<slide>[/history]to#collections/<collection>/decks/<deck>/slides/<slide>[/history]#themes/<path>to#collections/<collection>/decks/<deck>/themes#presentation/<path>to#collections/<collection>/decks/<deck>/present#docs/...,#jobs,#job/...,#stats,#admin,#templatesunchanged.Surface a top-level
#collectionssection listing every registered collection (deck count, root path) with rows linking into#collections/:collection.crates/hero_slides_ui/templates/index.html— add a "Collections" tab at the top of#main-tabs; keep existing tabs; the "Scan" form now takes two inputs:Collection name(free text, validated client-side by regex) +Path.crates/hero_slides/src/main.rs— CLI--scanmust now take<collection_name>=<path>or accept a second positional. Updatecmd_scan/cmd_generateto passcollection+deckinstead ofpath.crates/hero_slides_rhai/src/deck_module.rs,crates/hero_slides_rhai/src/slide_module.rs— rename Rhai bindings to take(collection, deck[, slide])strings. Internally the rhai bindings can still resolve names via the registry — but since rhai is typically run against a filesystem root in scripts, provide both a name-based binding and a path-based helperdeck_path_from_names(collection, deck)for escape-hatch scenarios.crates/hero_slides/README.mdandIMPLEMENTATION_SPEC.md— update snippets to reflect the new addressing model.Implementation Plan
Step 1: Add identifier validator and collection scanner
Files:
crates/hero_slides_lib/src/name.rs(new)crates/hero_slides_lib/src/collection.rs(new)crates/hero_slides_lib/src/lib.rscrates/hero_slides_lib/Cargo.tomlCreate
NameErrorenum (Empty,InvalidChar(char),LeadingUnderscore,TrailingUnderscore,DoubleUnderscore) andvalidate_identifier(&str) -> Result<&str, NameError>accepting only[a-z0-9_]+with no leading/trailing/double underscore.Add
DeckCollectionName,DeckName,SlideNamenewtype wrappers withfrom_str/as_str/Displayand aTryFrom<String>that runs the validator.Implement
scan_collection(root: &Path, name: &DeckCollectionName) -> Result<DeckCollection, CollectionError>. Reusescrate::discovery::scan_decks_dirs, then for each path derivesdeck_namefrom the directory name viavalidate_identifier, collectingHashMap<String, PathBuf>. On collision, returnsCollectionError::DuplicateDeck { name, first: PathBuf, second: PathBuf }whoseDisplayprints exactly the error wording required by the issue.Re-export both modules from
lib.rs.Add unit tests: valid names, rejected names (uppercase, hyphen, leading underscore, double underscore, empty, whitespace, UTF-8), scan detects duplicates, scan rejects deck directories whose name is not a valid identifier.
Dependencies: none.
Step 2: hero_db-backed collection registry inside the server
Files:
crates/hero_slides_server/Cargo.tomlcrates/hero_slides_server/src/registry.rs(new)crates/hero_slides_server/src/main.rsAdd dependencies:
hero_db_sdk(via its git source as listed in/hero_db),hero_rpc_openrpcfor the sharedOpenRpcError,chrono.Define
trait CollectionStorewith async methodsregister / unregister / list / lookup; provideHeroDbCollectionStoreimpl that wrapsHeroDbServerClientand persists to the hashhero_slides:collectionsviaredis.hset/redis.hget/redis.hgetall/redis.hdel/redis.hexists. Value format: JSON{"root": String, "registered_at": String}.CollectionRegistryowns anArc<dyn CollectionStore>and exposes the resolver helpers:resolve_collection_root(&CollectionName) -> Result<PathBuf, ResolveError>,resolve_deck(&CollectionName, &DeckName) -> Result<PathBuf, ResolveError>(rescans the collection and returns the matching absolute deck path, also re-checking for duplicates),resolve_slide(&CollectionName, &DeckName, &SlideName) -> Result<(PathBuf, String), ResolveError>.ResolveErrorvariants:UnknownCollection,CollectionRootMissing,UnknownDeck,UnknownSlide,DuplicateDeck { first, second },InvalidName(NameError),Store(String)— each mapped to a clean JSON-RPC error message byrpc.rs.In
main.rs, at startup constructHeroDbServerClient::connect_socket(...), wrap inCollectionRegistry, attach toServerState. Log the hero_db socket path and fail hard with a pointer to the setup docs if unreachable.Unit-test the registry against an in-memory
CollectionStorefake.Dependencies: Step 1.
Step 3: Rewrite RPC parameter parsing to be name-based
Files:
crates/hero_slides_server/src/rpc.rscrates/hero_slides_server/src/agent.rscrates/hero_slides_server/src/generate_job.rscrates/hero_slides_server/openrpc.jsonAdd a helper module
rpc_params.rs(insidecrates/hero_slides_server/src/) withread_collection_deck(params, state)->Result<(CollectionName, DeckName, PathBuf), String>andread_collection_deck_slide(...). Every handler calls this first.For each RPC method in
rpc.rs, replacedeck_path/pathparameter lookup with the helper calls; callhero_slides_lib::deck_*with the resolved&Pathexactly as before. Keep response shapes unchanged except: droppathfields from deck responses and addcollection+deck_name(path may remain as a debug-only optional field).deck.scannow takes{ collection: String, path: String }: validatescollectionvia the identifier validator, runsscan_collection, persists via the registry (overwriting on re-scan), returns{ collection, root, decks: [{ name, slide_count, generated_count, has_pdf, has_background }] }. On duplicate it returns the structured error text.Add four new RPC methods:
collection.list,collection.get { collection },collection.unregister { collection },collection.rescan { collection }(convenience wrapper around scan using the stored root).Update
agent.rsandgenerate_job.rshandlers the same way (they currently acceptdeck_pathstrings).Update
openrpc.jsonto match (regenerate every method's params section). Bumpinfo.versionto0.2.0.Dependencies: Step 2.
Step 4: Rewrite Axum server-side HTTP routes in the UI crate
Files:
crates/hero_slides_ui/src/routes.rscrates/hero_slides_ui/templates/present.htmlReplace the four path-parameter / query-string routes with name-parameter routes listed in the Files section.
Inside each handler, build the target PNG / PDF path by making a server-side
deck.get { collection, deck }RPC call over the existingrpc_proxyhelper to get the absolute deck path, then stream the file exactly as today. (Do not introduce a direct hero_db dependency in the UI crate; keep it a pure proxy.)present_pagenow takesPath((collection, deck)): Path<(String, String)>; template context getscollection_nameanddeck_name.present.html: replaceDECK_PATHwithCOLLECTION+DECK; rebuild every fetch URL and RPC params.Keep
base_path_middlewareregistered; keep every{{ base_path }}prefix in HTML / JS. Do not bypass the prefix for the new name-based endpoints.Dependencies: Step 3.
Step 5: Rewrite the SPA router and dashboard RPC calls
Files:
crates/hero_slides_ui/static/js/dashboard.jscrates/hero_slides_ui/templates/index.htmlAdd a new top-level
collectionstab and the associated render code: table of{ collection, deck_count, root }backed bycollection.list; row click to#collections/<collection>; add "Unregister" and "Rescan" buttons.Replace every
deck_path/selectedDeckPathusage withselectedCollection+selectedDeckName; keepselectedDeckPathonly as a derived read-only field if some code still needs the path (prefer deleting it).Rewrite
applyCurrentRouteto handle the new hash shape: route sections becomecollections, and subroutes are parsed by positional tokensdecks,slides,themes,present,history. EverynavigateTo(...)callsite is updated;presentopens a new window atBASE + '/present/' + collection + '/' + deck.Change the Scan form in
index.htmlto take both inputs (collection name + path) and calldeck.scan { collection, path }. Client-side validator mirrors the server regex.Ensure filter / tab / deck-select state is reflected in the hash (per
/hero_ui_routes).Dependencies: Step 3, Step 4. Can proceed in parallel with Step 6.
Step 6: Update CLI and Rhai bindings
Files:
crates/hero_slides/src/main.rscrates/hero_slides_rhai/src/deck_module.rscrates/hero_slides_rhai/src/slide_module.rscrates/hero_slides_rhai/scripts/*.rhaiCLI: change
--scan <path>to--scan <collection>=<path>; add--collection <name>and--deck <name>arguments consumed by--generate. Updatecmd_scan/cmd_generateto call the new SDK methods with name parameters.Rhai: rename
deck_scan(root) -> deckstodeck_scan(collection, root) -> decks; renamedeck_info(path)/deck_generate(path, force)/ etc. to take(collection, deck)strings. The bindings resolve names locally by callingscan_collectionthemselves (they have no RPC client), then delegate to the samehero_slides_libfunctions.Update the seven built-in
.rhaiscripts to acceptARGS = [collection, deck, ...].Update
README.mdusage examples accordingly.Dependencies: Step 3. Can proceed in parallel with Step 5 (non-overlapping file sets).
Step 7: Docs, smoke tests, end-to-end validation
Files:
IMPLEMENTATION_SPEC.mdREADME.mdcrates/hero_slides/README.md(if present)testplan/(update or add a file documenting the new route / RPC contract)Refresh all route/RPC tables and diagrams to use the
collection/deck/slidemodel.Document the hero_db dependency: socket path, required
HERO_DB_SOCKETenv var override, "how to run hero_db locally" pointer.Document the error-message format for duplicate decks verbatim.
Run
make fmt/make lint/make test/make buildto confirm everything compiles and existing tests still pass. Manual smoke:make installdev && make runthen exercisedeck.scan/collection.list/deck.get/slide.generate/ the new/api/collections/.../pdfendpoint and/present/:collection/:deck. Walk the SPA URL-by-URL to confirm hash routes round-trip through browser reload per/hero_ui_routes.Dependencies: Steps 1-6.
Acceptance Criteria
/path/to/workspaceunder a collection name writeshero_slides:collections[<name>]in hero_db and survives a server restart.collection.rescan.deck.scanandcollection.rescanwith:duplicate deck name 'foo': <path_a> and <path_b>. No partial registration is persisted.collection_name/deck_name/slide_namethat violate^[a-z0-9_]+$(upper-case, hyphen, whitespace, empty, leading/trailing underscore, double underscore, non-ASCII) are rejected at every entry point (RPC, CLI, Rhai) with a clear error naming the offending value.hero_slides_libinternally still operates on&Path; only the server / UI / CLI / Rhai boundaries deal with names.path/deck_path/src_deck_path/dst_deck_pathany more — they are replaced bycollection,deck,slide,src_collection,dst_collection, etc.openrpc.jsonand the regenerated SDK reflect this.href, templatesrc, or JS fetch URL — contains a filesystem path.#collectionsto#collections/:cto#collections/:c/decks/:dto#collections/:c/decks/:d/slides/:s[/history].X-Forwarded-Prefixstill works: when the service is fronted under/hero_slides/, every generated link starts with that prefix and every image / PDF / RPC fetch resolves correctly.make fmt,make lint,make test,make build,make installdev,make runall succeed from a clean workspace.hero_slides_lib(discovery.rs,parser.rs,hashing.rs,slide_ops.rs,deck.rs) still pass; new unit tests added forname.rs,collection.rs,registry.rs.Notes
~/hero/var/sockets/hero_db/rpc.sockbefore launching. The server should emit a single, loud log line if the socket is missing and exit non-zero — silently degrading to in-memory state would break restart persistence and is explicitly not allowed by the issue.slide_nameis simply the slide file stem (e.g.01_intro). It may contain digits and underscores; it must still pass the validator. When existing decks on disk have slide files with hyphens, mixed case, or other characters, the scan rejects them at theslide.listboundary — this is deliberate and called out in Step 1 tests.slide.copyToRPC crosses deck boundaries; its parameter rename issrc_collection+src_deck+slide+dst_collection+dst_deck(five strings, no paths).IMPLEMENTATION_SPEC.mdStep 7./hero_web_prefixuntouched: the middleware reads the header per request; templates use{{ base_path }}; JS readsBASEfrom the<meta name="base-path">tag. NoBASE_PATHenv var is introduced.kebab-case-free names: both URL segments and identifiers share thesnake_case/[a-z0-9_]+alphabet because the identifiers themselves are already lowercase ASCII with underscores — no separate URL slug transform is needed. The overall URL shape uses plural-resource segments (collections,decks,slides) per/hero_ui_routes.Test Results
All tests passed.
Run:
cargo test --workspaceImplementation Summary
All changes for the collection-named routing feature are complete on branch
development_collection_named_routing.Changes Made
New files:
crates/hero_slides_lib/src/collection.rs—CollectionRegistrybacked by hero_db: register/unregister/rescan/list/get/resolve collection roots, backed by a Redis hashhero_slides:collectionscrates/hero_slides_lib/src/name.rs—CollectionNameandDeckNamenewtype wrappers with validation (lowercase ASCII + underscores, 1–64 chars)crates/hero_slides_server/src/registry.rs— server-side registry singleton (lazy-initCollectionRegistry)crates/hero_slides_server/src/rpc_params.rs— shared RPC parameter structs:CollectionParam,DeckParam,SrcDst, etc.Modified files:
crates/hero_slides_lib/src/lib.rs— exportedcollectionandnamemodules; wiredscan_decksto returnDeckInfowith collection fieldcrates/hero_slides_server/openrpc.json— replaced all path-based methods with collection/deck/slide named params; addedcollection.*methods; updatedDeckSummaryschemacrates/hero_slides_server/openrpc.client.generated.rs— regenerated SDK referencecrates/hero_slides_server/src/rpc.rs— reimplemented all RPC handlers usingCollectionRegistryfor path resolutioncrates/hero_slides_server/src/agent.rs— updated agent job to acceptcollection+deckinstead of pathcrates/hero_slides_server/src/generate_job.rs— updated generate job paramscrates/hero_slides_server/src/main.rs— wired new Axum routes:/present/{collection}/{deck},/api/collections/{collection}/decks/{deck}/slides/{slide}/image,/api/collections/{collection}/decks/{deck}/pdfcrates/hero_slides_ui/templates/base.html— added Collections tab nav item as defaultcrates/hero_slides_ui/templates/index.html— rewritten dashboard HTML for collections/decks views; scan form now takes collection name + pathcrates/hero_slides_ui/templates/present.html— updated for named collection/deck routingcrates/hero_slides_ui/static/js/dashboard.js— full SPA router rewrite: hash routing changed from#slides/<encoded_path>to#collections/:col/decks/:deck/slides/:slide; all RPC calls updated to use named paramscrates/hero_slides_ui/src/routes.rs— updated Axum routes for UIcrates/hero_slides_rhai/src/deck_module.rs— addeddeck_scan(collection, path)overloadcrates/hero_slides/src/main.rs— CLI--scan COLLECTION=PATHand--generate COLLECTION/DECKflagscrates/hero_slides_ui/Cargo.toml,crates/hero_slides_server/Cargo.toml,crates/hero_slides_lib/Cargo.toml— updated dependency wiringTest Results
113 tests passed, 0 failed. See previous comment for full breakdown.