default scan directory #12
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?
look if $CODEROOT/ourworld_slides exist if yes auto scan this one, if nothing scanned yet
CODEROOT comes from the secrets
use skill /hero_proc_secrets to get the CODEROOT
remember the last scan in our hero_db
see skill /hero_db
so when we start again we check, what was last scan, if dir exists, load that one
if that dir does not exist, remove the last scan and check $CODEROOT/ourworld_slides exist exists
Implementation Spec — Issue #12: Default Scan Directory
Objective
On server startup,
hero_slides_servershould automatically resolve a default scan directory and perform an initialdeck.scanwithout any user interaction. The logic uses two persistence mechanisms:CODEROOT(the code workspace root).The fallback directory is
$CODEROOT/ourworld_slides. On each startup the server checks the remembered path first; if it still exists the scan proceeds immediately; if it is gone the stored entry is cleared and the fallback path is tried instead.Requirements
hero_slides_servermust attempt an automatic scan before accepting user-initiated requests.secret.getwith keycoderoot(context"core") to obtainCODEROOT.CODEROOT, the fallback candidate is$CODEROOT/ourworld_slides.hero_slides:last_scan_pathfrom hero_db (Redis) to recall the previously successful scan root.last_scan_pathexists in Redis AND that directory is present on disk → scan that path.last_scan_pathexists in Redis BUT that directory is missing on disk → delete the Redis key, then try the CODEROOT fallback.last_scan_pathin Redis → try the CODEROOT fallback.$CODEROOT/ourworld_slidesis a directory → scan it and save the path to Redis.hero_slides:last_scan_pathin Redis.warnlevel and silently skipped; the server must not fail to start.Files to Modify / Create
crates/hero_slides_server/src/startup.rs— Create: Containsauto_scan_on_startup()and all helpers for secret fetching, Redis persistence, and path resolution.crates/hero_slides_server/src/main.rs— Modify: Addmod startup, makesocket_base_dir()pub(crate), spawn background auto-scan task after binding socket.crates/hero_slides_server/Cargo.toml— Modify: Addredis = { version = "0.27", features = ["tokio-comp", "aio"] }.Implementation Plan
Step 1 — Add Redis dependency to Cargo.toml
Files:
crates/hero_slides_server/Cargo.tomlDependencies: none
Add
redis = { version = "0.27", features = ["tokio-comp", "aio"] }to[dependencies].Step 2 — Create
startup.rsFiles:
crates/hero_slides_server/src/startup.rsDependencies: Step 1
Create module with:
fetch_coderoot() -> Option<String>— connects to hero_proc, callssecret.getwith key"coderoot", context"core".redis_socket_path() -> PathBuf— returns$HERO_DB_SOCKETorsocket_base_dir()/hero_db/redis.sock.read_last_scan_path() -> Option<String>— GEThero_slides:last_scan_pathfrom Redis.save_last_scan_path(path: &str)— SEThero_slides:last_scan_pathin Redis.delete_last_scan_path()— DELhero_slides:last_scan_pathfrom Redis.do_scan(root: &Path, state: &Arc<ServerState>) -> bool— callshero_slides_lib::scan_decks(), upserts results into state.pub async fn auto_scan_on_startup(state: Arc<ServerState>)— orchestrates the full decision tree.Step 3 — Wire into
main.rsFiles:
crates/hero_slides_server/src/main.rsDependencies: Step 2
mod startup;fn socket_base_dir()topub(crate) fn socket_base_dir()tokio::spawn(async move { startup::auto_scan_on_startup(state_clone).await; })after axum serve setup.Acceptance Criteria
$CODEROOT/ourworld_slidesexisting, decks appear indeck.listwithout user interaction.deck.scanvia UI/RPC still works after startup.cargo buildsucceeds with no new warnings.Notes
redis+unix:///absolute/path/to/socket?db=0coderoot(lowercase), context"core"tokio::spawntask. Requests that arrive before scan completes see an empty deck list — acceptable.Implementation Spec for Issue #12
Objective
At
hero_slides_serverstartup, if the in-memory deck list is empty, auto-populate it by scanning a deck root discovered through this cascade: (1) last successfully scanned directory remembered inhero_db, if it still exists on disk; otherwise (2)$CODEROOT/ourworld_slides, whereCODEROOTis read fromhero_proc's secret store. Persist the scanned path back intohero_dbso the next startup picks the same root.Requirements
CODEROOTfromhero_procsecrets (contextcore, keycoderoot). Do not read$CODEROOTfrom the process environment.hero_db(not rawrediscrate) as the persistence layer forlast_scan_path.handle_deck_scan(hero_slides_lib::scan_decks+ upsert intoServerState::decks) — do not duplicate logic.hero_procorhero_dbare unreachable.last_scan_pathno longer exists on disk, delete the key fromhero_dband fall back to$CODEROOT/ourworld_slides.$CODEROOT/ourworld_slidesalso does not exist (orCODEROOTcannot be read), log a warning and leave the deck list empty — the user can still triggerdeck.scanmanually.hero_procorhero_dbmust log a warning and continue; it must never crash the server.Files to Modify/Create
crates/hero_slides_server/Cargo.toml— addhero_db_sdkdependency (git, branchdevelopment), matching existinghero_proc_sdkstyle. No rawrediscrate.crates/hero_slides_server/src/main.rs— declare newmod startup;, makesocket_base_dir()visible to the module (pub(crate)or move intostartup), spawn the auto-scan task after the Unix socket is bound (so startup is not blocked by network I/O against hero_proc/hero_db).crates/hero_slides_server/src/startup.rs— NEW — all auto-scan logic:fetch_coderoot,hero_db_socket_path,read_last_scan_path,save_last_scan_path,delete_last_scan_path,do_scan,pub async fn auto_scan_on_startup(state: Arc<ServerState>).Implementation Plan
Step 1: Add
hero_db_sdkdependency tohero_slides_serverFiles:
crates/hero_slides_server/Cargo.tomlhero_proc_sdkline:hero_db_sdk = { git = "https://forge.ourworld.tf/lhumina_code/hero_db.git", branch = "development" }redisdependency.hero_db_sdkre-exports a RESP2HeroDbClient(hero_db_sdk::hero_db_client::HeroDbClient) built onredisinternally, which is the supported pattern.Dependencies: none.
Step 2: Create
crates/hero_slides_server/src/startup.rsFiles:
crates/hero_slides_server/src/startup.rs(new)Module contents:
pub async fn auto_scan_on_startup(state: Arc<ServerState>)— orchestrator, spawned frommain. Steps:state.decks.read().await.len() > 0, return early (idempotent).read_last_scan_path(). IfSome(path)andPathBuf::from(&path).is_dir(), calldo_scan(path, &state).awaitand return. If the directory doesn't exist, calldelete_last_scan_path().awaitand continue to fallback.fetch_coderoot().await; if it returns a non-empty value, computecandidate = PathBuf::from(coderoot).join("ourworld_slides")and, ifcandidate.is_dir(), calldo_scan(candidate.to_string_lossy().to_string(), &state).await.tracing::warn!with context and return.async fn fetch_coderoot() -> Option<String>hero_proc_sdk::socket::default_path()(existing pattern inagent.rsandgenerate_job.rs).HeroProcRPCAPIClient::connect_socket(&socket_path.to_string_lossy()).await. On error,warn!and returnNone.client.secret_get(SecretGetInput { key: "coderoot".into(), context: Some("core".into()) }).await. OnOk, returnSome(output.value)(unless empty). OnErr, warn +None.fn hero_db_socket_path() -> PathBuf$HERO_DB_SOCKET→$HERO_SOCKET_DIR/hero_db/resp.sock→$HOME/hero/var/sockets/hero_db/resp.sock.resp.sock(RESP2 data API), NOTrpc.sock.async fn open_hero_db_client() -> Option<HeroDbClient>hero_db_socket_path(). If missing, warn and returnNone.HeroDbClient::new_unix(path, "")intokio::task::spawn_blocking(blocking under the hood).async fn read_last_scan_path() -> Option<String>hero_slides:last_scan_path.client.get(key)insidespawn_blocking.async fn save_last_scan_path(path: &str)client.set(key, path)insidespawn_blocking, fire-and-log.async fn delete_last_scan_path()client.del(key)insidespawn_blocking.async fn do_scan(root: String, state: &ServerState) -> boolPathBuf::from(&root).is_dir(); if not, warn + returnfalse.hero_slides_lib::scan_decks(&root_pb)insidetokio::task::spawn_blocking(mirroringrpc.rshandle_deck_scan).false— do NOT save to hero_db.DeckInfointostate.decksusing the shared helper from Step 3 (or duplicate the loop fromhandle_deck_scan).save_last_scan_path(&root).await, loginfo!, returntrue.Dependencies: Step 1.
Step 3: Extract a shared deck-upsert helper (optional, recommended)
Files:
crates/hero_slides_server/src/rpc.rspub(crate) async fn upsert_decks(state: &ServerState, found: Vec<hero_slides_lib::DeckInfo>) -> Vec<DeckInfo>nearhandle_deck_scan.handle_deck_scanto call it.do_scanduplicates the upsert loop (acceptable).Dependencies: none.
Step 4: Wire auto-scan into
main.rsFiles:
crates/hero_slides_server/src/main.rsmod startup;next to the existing module declarations.fn socket_base_dir()topub(crate) fn socket_base_dir().UnixListener::bindand the permissions block, BEFOREaxum::serve(...), spawn: This way the socket is reachable before the (possibly slow) scan completes.Dependencies: Step 2.
Step 5: Manual verification
cargo build -p hero_slides_server.hero_procrunning andcoderootsecret set, andhero_dbrunning at$HOME/hero/var/sockets/hero_db/resp.sock: start the server, confirminfo!logs"auto-scan: loaded N decks from ..."anddeck.listreturns populated decks.hero_slides:last_scan_pathis set:redis-cli -s ~/hero/var/sockets/hero_db/resp.sock GET hero_slides:last_scan_path.hero_procand/orhero_db, restart — confirm the server still binds and serves.Dependencies: Steps 1–4.
Acceptance Criteria
crates/hero_slides_server/Cargo.tomldepends onhero_db_sdk(no barerediscrate).crates/hero_slides_server/src/startup.rsexposespub async fn auto_scan_on_startup(state: Arc<ServerState>).main.rsdeclaresmod startup;, spawns the task after bind and before serve, andsocket_base_dirispub(crate).CODEROOTviahero_proc_sdk::HeroProcRPCAPIClient::secret_get(SecretGetInput { key: "coderoot", context: Some("core") })— not fromstd::env.hero_db_sdk::hero_db_client::HeroDbClientat socket$HERO_SOCKET_DIR/hero_db/resp.sock, keyhero_slides:last_scan_path.is_dir(); missing directory → key deleted, fallback attempted.$CODEROOT/ourworld_slidesfallback only runs when no valid remembered path exists.state.decksis empty (idempotent across accidental re-runs).hero_procorhero_dblogs a warning and is non-fatal — server startup never blocks or aborts.deck.scanRPC behaviour is unchanged for callers.cargo build -p hero_slides_serversucceeds.Notes
Divergences from the earlier proposal that this spec corrects:
hero_db_sdk, not the barerediscrate.hero_db_sdk::hero_db_client::HeroDbClientwrapsrediswith the correctnew_unix(path, secret)and matches how other Hero services talk to hero_db.redis+unix:///absolute/path/to/socket?db=0. Corrected:HeroDbClient::new_unixbuilds the URL internally; callers pass the socket path and secret directly.redis_socket_path. Corrected: canonical name ishero_db_socket_pathand it points atresp.sock(RESP2 data API), NOTrpc.sock(which is the JSON-RPC management API).secret.getwithcontext: "core". Corrected: generatedSecretGetInputis{ key: String, context: Option<String> }— passSome("core".into()).scan_decksreturnsResult<...>. Corrected:hero_slides_lib::scan_decks(root: &Path) -> Vec<hero_slides_lib::DeckInfo>returns an empty Vec on IO errors internally.Other notes:
ServerState.decksis atokio::sync::RwLock<Vec<rpc::DeckInfo>>(notlib::DeckInfo) —do_scanmust convert via the existingimpl From<hero_slides_lib::DeckInfo> for rpc::DeckInfoinrpc.rs.secret = "". If a future change requires auth, fetch it the same wayCODEROOTis fetched.hero_db_sdkuses the blockingrediscrate; allHeroDbClientcalls (including construction) MUST be wrapped intokio::task::spawn_blocking.Arc<ServerState>by clone — no extra synchronization needed.Build & Test Results
cargo build -p hero_slides_server
PASS (exit 0). No warnings or errors.
cargo test -p hero_slides_server
No unit tests are defined in the
hero_slides_servercrate itself (it is a thinmain.rsbinary). All relevant logic lives inhero_slides_liband is exercised by the workspace run below.cargo test (workspace)
test_generate_single_slide_ai— requires live AI provider)Breakdown:
hero_slides_libunit tests: 68 passed, 0 failed, 0 ignoredhero_slides_libintegration tests: 13 passed, 0 failed, 1 ignoredhero_slides_libdoctests: 2 passedhero_slides_rhaidoctests: 1 passedhero_slides,hero_slides_server,hero_slides_ui,hero_slides_sdk,hero_slides_rhai: 0 tests (binary / stub crates)Notes
No new unit tests were added for the
auto_scan_on_startuppath — the logic requireshero_proc+hero_dbrunning and a populatedHERO_SLIDES_ROOT, so it is best verified via the manual integration steps listed in the spec. The existingdiscovery::scan_decksbehaviour it delegates to is already covered bydiscovery::tests::test_scan_decks_dirsand the integration-leveltest_deck_scan_finds_created_decks/test_deck_scan_empty_dircases, all green.Implementation Summary
Auto-scan on server startup is now wired into
hero_slides_server. If the in-memory deck list is empty at boot, the server resolves a deck root viahero_db(remembered last scan) with a fallback to$CODEROOT/ourworld_slides(CODEROOT fromhero_procsecrets), runs the existing scan pipeline, and persists the scanned root back tohero_dbfor next time.Files changed
crates/hero_slides_server/Cargo.toml— addedhero_db_sdkdependency (git, branchdevelopment), matching the existinghero_proc_sdkstyle. No barerediscrate.crates/hero_slides_server/src/rpc.rs— extracted a sharedpub(crate) async fn upsert_decks(state: &ServerState, found: Vec<hero_slides_lib::DeckInfo>) -> Vec<DeckInfo>helper;handle_deck_scannow delegates upsertion to it. External RPC behaviour unchanged.crates/hero_slides_server/src/startup.rs— NEW. Contains all auto-scan logic behind a single public entry pointpub async fn auto_scan_on_startup(state: Arc<ServerState>). Private helpers handle CODEROOT lookup viahero_proc_sdk::HeroProcRPCAPIClient::secret_get,hero_dbconnectivity viahero_db_sdk::hero_db_client::HeroDbClient(all blocking calls wrapped intokio::task::spawn_blocking), and the full decision cascade. Redis key used:hero_slides:last_scan_path.crates/hero_slides_server/src/main.rs— declaredmod startup;, promotedsocket_base_dir()topub(crate)(reused bystartup::hero_db_socket_path), and spawnedstartup::auto_scan_on_startup(state.clone())as a detached task after the Unix socket binds but beforeaxum::serve, so startup never blocks on hero_proc / hero_db.Key design choices
hero_db_sdkinstead of the rawrediscrate — workspace convention.$HERO_DB_SOCKET→$HERO_SOCKET_DIR/hero_db/resp.sock→$HOME/hero/var/sockets/hero_db/resp.sock(RESP2 data socket, NOTrpc.sock).state.decksis empty, so manualdeck.scancalls always take precedence and the task is idempotent across restarts.warnlevel and exits cleanly — the server never fails to start because of optional auto-scan dependencies.HeroDbClientmethods are&mut self, so each helper constructs its own client per operation (cheap; single Unix socket).hero_slides_lib::scan_decksis synchronous and returnsVec<DeckInfo>(empty on errors) — all calls wrapped inspawn_blocking.Build & Test
cargo build -p hero_slides_server: PASS (no warnings)cargo test -p hero_slides_server: PASS (0 tests — binary crate)cargo test(workspace): 84 passed, 0 failed, 1 ignored (test_generate_single_slide_ai— pre-existing; requires live AI provider)No new unit tests were added. The auto-scan logic depends on a running
hero_procandhero_db, so it is best validated via the manual verification steps listed in the spec (running the server with CODEROOT set, restarting to confirm the remembered path is reused, removing the remembered directory to confirm fallback, killing hero_proc / hero_db to confirm graceful degradation).Notes
ServerState.decksistokio::sync::RwLock<Vec<rpc::DeckInfo>>; the scan helper converts via the existingimpl From<hero_slides_lib::DeckInfo> for rpc::DeckInfoinrpc.rs.hero_dbclient is opened withsecret = "". If hero_db auth is added later, the secret can be fetched the same wayCODEROOTis.SecretGetInputpassescontext: Some("core".into())explicitly, matching the issue brief, though the hero_proc server defaults to"core"whenNone.