bug: deselecting a background file does not clear stale tag #69

Open
opened 2026-05-15 13:16:26 +00:00 by casper-stevens · 3 comments
Member

Problem

When a file is added to content/background/, it is automatically included in the global context selection and all slides become stale (correct). However, deselecting that file in the UI does not clear the stale tag.

Root Cause

Context selection (selectedBackgroundFolders, selectedBackgroundRootFiles, etc.) is stored entirely in localStorage. compute_context_fingerprint in the Rust lib hashes all files in content/background/ unconditionally — it has no access to the localStorage selection state.

This means:

  • Adding a file → directory changes → hash changes → stale fires
  • Deselecting a file → localStorage changes only → directory unchanged → hash unchanged → stale never clears

The code even acknowledges this in a comment:

"per-slide UI selection (ADR-0002) is not reachable from lib code, so we include the whole directory — file-byte changes invalidate, pure selection changes do not."

Fix

Move context selection out of localStorage and into server-side metadata (metadata.toml). Then compute_context_fingerprint can read the selection and hash only the selected files.

This also fixes the inverse false-positive: adding a file but not selecting it would no longer mark slides stale.

## Problem When a file is added to `content/background/`, it is automatically included in the global context selection and all slides become stale (correct). However, deselecting that file in the UI does **not** clear the stale tag. ## Root Cause Context selection (`selectedBackgroundFolders`, `selectedBackgroundRootFiles`, etc.) is stored entirely in localStorage. `compute_context_fingerprint` in the Rust lib hashes **all** files in `content/background/` unconditionally — it has no access to the localStorage selection state. This means: - Adding a file → directory changes → hash changes → stale fires ✅ - Deselecting a file → localStorage changes only → directory unchanged → hash unchanged → stale never clears ❌ The code even acknowledges this in a comment: > *"per-slide UI selection (ADR-0002) is not reachable from lib code, so we include the whole directory — file-byte changes invalidate, pure selection changes do not."* ## Fix Move context selection out of localStorage and into server-side metadata (`metadata.toml`). Then `compute_context_fingerprint` can read the selection and hash only the selected files. This also fixes the inverse false-positive: adding a file but not selecting it would no longer mark slides stale.
Author
Member

Implementation Spec for Issue #69

Objective

Move the global context selection (selectedBackgroundFolders, selectedBackgroundRootFiles, selectedBackgroundFolderFiles) from browser localStorage into metadata.toml so that compute_context_fingerprint can hash only the selected files. This fixes both the described bug (deselecting a file never clears the stale tag) and the inverse false-positive (adding a file but not selecting it marks all slides stale).


Requirements

  • metadata.toml must store a SelectionSet-shaped context_selection field in its [global] section.
  • compute_context_fingerprint must accept an optional SelectionSet and hash only the selected files when provided; fall back to the current whole-directory hash when None.
  • Two new RPC methods — deck.getContextSelection and deck.setContextSelection — expose the persisted selection to the frontend.
  • dashboard.js must call deck.getContextSelection when loading a deck (instead of reading from localStorage), and call deck.setContextSelection whenever the selection changes.
  • slide_edit.js must read the global context from the server-side selection rather than from localStorage (per-slide overlay stays localStorage-resident).
  • Legacy decks with no context_selection in metadata.toml are treated as "all files selected" — no regression.
  • When all files are selected, null is stored (field omitted by skip_serializing_if) to keep metadata.toml clean.
  • Per-slide context overlay (per_slide) remains in localStorage — moving it is out of scope.

Files to Modify

File Change
crates/hero_slides_lib/src/hashing.rs Add context_selection: Option<SelectionSet> to GlobalMeta; make compute_context_fingerprint selection-aware; update DeckInputsContext::for_deck and for_linked_slide
crates/hero_slides_lib/src/lib.rs Re-export load_context_selection and save_context_selection; update compute_context_fingerprint re-export
crates/hero_slides_server/src/rpc.rs Add deck.getContextSelection and deck.setContextSelection handlers
crates/hero_slides_server/openrpc.json Document the two new methods
crates/hero_slides_admin/static/js/dashboard.js Replace global localStorage read/write with server RPC calls; keep per-slide localStorage
crates/hero_slides_admin/static/js/slide_edit.js Load global selection from server in init(); write only per-slide data to localStorage

Implementation Plan

Step 1 — Extend GlobalMeta and update compute_context_fingerprint

Files: crates/hero_slides_lib/src/hashing.rs
Dependencies: None

  • Add context_selection: Option<crate::context::SelectionSet> field to GlobalMeta with #[serde(default, skip_serializing_if = "Option::is_none")]
  • Change compute_context_fingerprint(deck_path: &Path) signature to compute_context_fingerprint(deck_path: &Path, selection: Option<&SelectionSet>) -> String
  • When selection is Some, hash only the selected files via a new gather_selected_files helper; when None, hash all files (existing behaviour)
  • Update DeckInputsContext::for_deck: call load_metadata, extract global.context_selection, pass to compute_context_fingerprint
  • Update DeckInputsContext::for_linked_slide similarly for the source deck
  • Update internal test calls to the new two-argument signature

Step 2 — Add load_context_selection / save_context_selection helpers

Files: crates/hero_slides_lib/src/hashing.rs, crates/hero_slides_lib/src/lib.rs
Dependencies: Step 1

  • Add load_context_selection(deck_path) -> Result<Option<SelectionSet>> — reads metadata.toml and returns the field
  • Add save_context_selection(deck_path, Option<&SelectionSet>) -> Result<()> — writes the field back to metadata.toml
  • Export both from lib.rs

Step 3 — Add RPC handlers

Files: crates/hero_slides_server/src/rpc.rs
Dependencies: Step 2

  • Add handle_deck_get_context_selection and handle_deck_set_context_selection async fns
  • Wire both into the dispatch match block
  • The existing legacy_param_shim already handles {collection, deck}deck_path

Step 4 — Document new methods in openrpc.json

Files: crates/hero_slides_server/openrpc.json
Dependencies: Step 3

  • Add deck.getContextSelection and deck.setContextSelection method definitions

Step 5 — Update dashboard.js

Files: crates/hero_slides_admin/static/js/dashboard.js
Dependencies: Step 3

  • Replace _ctxSaveImmediate (global scope) with _ctxSaveToServer() — calls deck.setContextSelection; sends null when all files are selected
  • Replace _ctxLoad (global scope) with _ctxLoadFromServer() — calls deck.getContextSelection
  • Keep localStorage only for per_slide data
  • Remove the expires_at TTL machinery for the global scope

Step 6 — Update slide_edit.js

Files: crates/hero_slides_admin/static/js/slide_edit.js
Dependencies: Step 3

  • Add _serverGlobalSelection module variable + loadServerContextSelection() async loader called from init()
  • readContextSelection: merge _serverGlobalSelection (from server) with per-slide localStorage data
  • writeContextSelection: only persist per_slide to localStorage; global changes never happen in slide_edit.js

Steps 1–2 can be worked on in sequence (Rust lib). Steps 3–4 depend on steps 1–2. Steps 5–6 are independent of each other but both depend on Step 3.


Acceptance Criteria

  • Adding a background file marks slides stale
  • Deselecting a file calls deck.setContextSelection; a subsequent staleness check reports no stale slides from that cause
  • Adding a new file that is not selected does not mark slides stale
  • Selecting a previously unselected file marks slides stale
  • Legacy decks (no context_selection in metadata.toml) behave as "all files selected"
  • metadata.toml for a partial selection contains [global.context_selection]
  • metadata.toml for "all files selected" contains no context_selection key
  • Slide editor shows correct global context chips sourced from server
  • deck.getContextSelection returns null for decks with no saved selection
  • deck.setContextSelection with null resets to "all files"
  • No stale-TTL expiry or TypeError from old localStorage global selection code

Notes

  1. compute_context_fingerprint is only called inside hashing.rs (via for_deck / for_linked_slide) and re-exported from lib.rs — verify no external callers before merging.
  2. deck.setContextSelection writes metadata.toml on every selection change; the 500 ms debounce in dashboard.js is sufficient.
  3. Per-slide overlay stays in localStorage — staleness fingerprint is deck-level, so per-slide overlays do not affect it.
  4. for_linked_slide gains one new load_metadata I/O call on the staleness hot-path; acceptable given linked decks are few.
# Implementation Spec for Issue #69 ## Objective Move the global context selection (`selectedBackgroundFolders`, `selectedBackgroundRootFiles`, `selectedBackgroundFolderFiles`) from browser `localStorage` into `metadata.toml` so that `compute_context_fingerprint` can hash only the selected files. This fixes both the described bug (deselecting a file never clears the stale tag) and the inverse false-positive (adding a file but not selecting it marks all slides stale). --- ## Requirements - `metadata.toml` must store a `SelectionSet`-shaped `context_selection` field in its `[global]` section. - `compute_context_fingerprint` must accept an optional `SelectionSet` and hash only the selected files when provided; fall back to the current whole-directory hash when `None`. - Two new RPC methods — `deck.getContextSelection` and `deck.setContextSelection` — expose the persisted selection to the frontend. - `dashboard.js` must call `deck.getContextSelection` when loading a deck (instead of reading from `localStorage`), and call `deck.setContextSelection` whenever the selection changes. - `slide_edit.js` must read the global context from the server-side selection rather than from `localStorage` (per-slide overlay stays localStorage-resident). - Legacy decks with no `context_selection` in `metadata.toml` are treated as "all files selected" — no regression. - When all files are selected, `null` is stored (field omitted by `skip_serializing_if`) to keep `metadata.toml` clean. - Per-slide context overlay (`per_slide`) remains in `localStorage` — moving it is out of scope. --- ## Files to Modify | File | Change | |---|---| | `crates/hero_slides_lib/src/hashing.rs` | Add `context_selection: Option<SelectionSet>` to `GlobalMeta`; make `compute_context_fingerprint` selection-aware; update `DeckInputsContext::for_deck` and `for_linked_slide` | | `crates/hero_slides_lib/src/lib.rs` | Re-export `load_context_selection` and `save_context_selection`; update `compute_context_fingerprint` re-export | | `crates/hero_slides_server/src/rpc.rs` | Add `deck.getContextSelection` and `deck.setContextSelection` handlers | | `crates/hero_slides_server/openrpc.json` | Document the two new methods | | `crates/hero_slides_admin/static/js/dashboard.js` | Replace global localStorage read/write with server RPC calls; keep per-slide localStorage | | `crates/hero_slides_admin/static/js/slide_edit.js` | Load global selection from server in `init()`; write only per-slide data to localStorage | --- ## Implementation Plan ### Step 1 — Extend `GlobalMeta` and update `compute_context_fingerprint` **Files:** `crates/hero_slides_lib/src/hashing.rs` **Dependencies:** None - Add `context_selection: Option<crate::context::SelectionSet>` field to `GlobalMeta` with `#[serde(default, skip_serializing_if = "Option::is_none")]` - Change `compute_context_fingerprint(deck_path: &Path)` signature to `compute_context_fingerprint(deck_path: &Path, selection: Option<&SelectionSet>) -> String` - When `selection` is `Some`, hash only the selected files via a new `gather_selected_files` helper; when `None`, hash all files (existing behaviour) - Update `DeckInputsContext::for_deck`: call `load_metadata`, extract `global.context_selection`, pass to `compute_context_fingerprint` - Update `DeckInputsContext::for_linked_slide` similarly for the source deck - Update internal test calls to the new two-argument signature ### Step 2 — Add `load_context_selection` / `save_context_selection` helpers **Files:** `crates/hero_slides_lib/src/hashing.rs`, `crates/hero_slides_lib/src/lib.rs` **Dependencies:** Step 1 - Add `load_context_selection(deck_path) -> Result<Option<SelectionSet>>` — reads `metadata.toml` and returns the field - Add `save_context_selection(deck_path, Option<&SelectionSet>) -> Result<()>` — writes the field back to `metadata.toml` - Export both from `lib.rs` ### Step 3 — Add RPC handlers **Files:** `crates/hero_slides_server/src/rpc.rs` **Dependencies:** Step 2 - Add `handle_deck_get_context_selection` and `handle_deck_set_context_selection` async fns - Wire both into the dispatch `match` block - The existing `legacy_param_shim` already handles `{collection, deck}` → `deck_path` ### Step 4 — Document new methods in `openrpc.json` **Files:** `crates/hero_slides_server/openrpc.json` **Dependencies:** Step 3 - Add `deck.getContextSelection` and `deck.setContextSelection` method definitions ### Step 5 — Update `dashboard.js` **Files:** `crates/hero_slides_admin/static/js/dashboard.js` **Dependencies:** Step 3 - Replace `_ctxSaveImmediate` (global scope) with `_ctxSaveToServer()` — calls `deck.setContextSelection`; sends `null` when all files are selected - Replace `_ctxLoad` (global scope) with `_ctxLoadFromServer()` — calls `deck.getContextSelection` - Keep `localStorage` only for `per_slide` data - Remove the `expires_at` TTL machinery for the global scope ### Step 6 — Update `slide_edit.js` **Files:** `crates/hero_slides_admin/static/js/slide_edit.js` **Dependencies:** Step 3 - Add `_serverGlobalSelection` module variable + `loadServerContextSelection()` async loader called from `init()` - `readContextSelection`: merge `_serverGlobalSelection` (from server) with per-slide localStorage data - `writeContextSelection`: only persist `per_slide` to localStorage; global changes never happen in `slide_edit.js` Steps 1–2 can be worked on in sequence (Rust lib). Steps 3–4 depend on steps 1–2. Steps 5–6 are independent of each other but both depend on Step 3. --- ## Acceptance Criteria - [ ] Adding a background file marks slides stale - [ ] Deselecting a file calls `deck.setContextSelection`; a subsequent staleness check reports no stale slides from that cause - [ ] Adding a new file that is not selected does not mark slides stale - [ ] Selecting a previously unselected file marks slides stale - [ ] Legacy decks (no `context_selection` in `metadata.toml`) behave as "all files selected" - [ ] `metadata.toml` for a partial selection contains `[global.context_selection]` - [ ] `metadata.toml` for "all files selected" contains no `context_selection` key - [ ] Slide editor shows correct global context chips sourced from server - [ ] `deck.getContextSelection` returns `null` for decks with no saved selection - [ ] `deck.setContextSelection` with `null` resets to "all files" - [ ] No stale-TTL expiry or `TypeError` from old localStorage global selection code --- ## Notes 1. `compute_context_fingerprint` is only called inside `hashing.rs` (via `for_deck` / `for_linked_slide`) and re-exported from `lib.rs` — verify no external callers before merging. 2. `deck.setContextSelection` writes `metadata.toml` on every selection change; the 500 ms debounce in `dashboard.js` is sufficient. 3. Per-slide overlay stays in `localStorage` — staleness fingerprint is deck-level, so per-slide overlays do not affect it. 4. `for_linked_slide` gains one new `load_metadata` I/O call on the staleness hot-path; acceptable given linked decks are few.
Author
Member

Test Results

  • Total: 0
  • Passed: 0
  • Failed: 0 (build failed before tests could run)

Build Failure

The test run did not complete — compilation failed in the hero_slides_examples crate with 4 errors:

Error E0433 — unresolved module tokio (2 occurrences)

crates/hero_slides_examples/examples/health.rs:8 and crates/hero_slides_examples/examples/basic_usage.rs:8 both use #[tokio::main] but tokio is not declared as a dependency in that crate.

Error E0752 — async main not allowed (2 occurrences)

Because tokio is missing, the async fn main() functions in both example files are rejected by the compiler.

Root cause: hero_slides_examples is missing tokio in its Cargo.toml dependencies.

Fix: Add tokio = { version = "1", features = ["full"] } (or at minimum ["macros", "rt-multi-thread"]) to crates/hero_slides_examples/Cargo.toml.

## Test Results - Total: 0 - Passed: 0 - Failed: 0 (build failed before tests could run) ## Build Failure The test run did not complete — compilation failed in the `hero_slides_examples` crate with 4 errors: **Error E0433 — unresolved module `tokio`** (2 occurrences) `crates/hero_slides_examples/examples/health.rs:8` and `crates/hero_slides_examples/examples/basic_usage.rs:8` both use `#[tokio::main]` but `tokio` is not declared as a dependency in that crate. **Error E0752 — async main not allowed** (2 occurrences) Because `tokio` is missing, the `async fn main()` functions in both example files are rejected by the compiler. **Root cause:** `hero_slides_examples` is missing `tokio` in its `Cargo.toml` dependencies. **Fix:** Add `tokio = { version = "1", features = ["full"] }` (or at minimum `["macros", "rt-multi-thread"]`) to `crates/hero_slides_examples/Cargo.toml`.
Author
Member

Implementation Summary

Changes Made

crates/hero_slides_lib/src/hashing.rs

  • Added context_selection: Option<SelectionSet> to GlobalMeta (#[serde(default, skip_serializing_if = "Option::is_none")])
  • compute_context_fingerprint now accepts Option<&SelectionSet>: when None, all files are hashed (legacy behaviour); when Some, only the selected files are hashed via a new gather_selected_files helper
  • DeckInputsContext::for_deck and for_linked_slide now read the saved selection from metadata.toml and pass it to compute_context_fingerprint
  • Added load_context_selection and save_context_selection public helpers

crates/hero_slides_lib/src/lib.rs

  • Re-exported load_context_selection and save_context_selection

crates/hero_slides_server/src/rpc.rs

  • Added deck.getContextSelection and deck.setContextSelection RPC handlers

crates/hero_slides_server/openrpc.json

  • Documented both new RPC methods

crates/hero_slides_admin/static/js/dashboard.js

  • _ctxSaveImmediate: global selection now written to server via deck.setContextSelection (sends null when all files selected); localStorage retains only per-slide data
  • loadBgPanel (deck-switch branch): global selection now loaded from server via deck.getContextSelection; per-slide data still read from localStorage

crates/hero_slides_admin/static/js/slide_edit.js

  • Added _serverGlobalSelection module variable and loadServerContextSelection() async loader (called from init())
  • readContextSelection: global portion comes from server cache; per_slide from localStorage
  • writeContextSelection: only persists per-slide data to localStorage

Test Results

  • Total: 25
  • Passed: 25
  • Failed: 0

Tests run against hero_slides_lib and hero_slides_server. A pre-existing build error in hero_slides_examples (missing tokio dependency) prevents cargo test --workspace from completing, but it is unrelated to this fix.

Behaviour after this fix

  • Deselecting a background file calls deck.setContextSelection, which updates metadata.toml. The next staleness check hashes only the selected files, so deselected files no longer cause false stale markers.
  • Adding a new file without selecting it no longer marks slides stale.
  • Legacy decks with no context_selection in metadata.toml behave as before (all files hashed).
## Implementation Summary ### Changes Made **`crates/hero_slides_lib/src/hashing.rs`** - Added `context_selection: Option<SelectionSet>` to `GlobalMeta` (`#[serde(default, skip_serializing_if = "Option::is_none")]`) - `compute_context_fingerprint` now accepts `Option<&SelectionSet>`: when `None`, all files are hashed (legacy behaviour); when `Some`, only the selected files are hashed via a new `gather_selected_files` helper - `DeckInputsContext::for_deck` and `for_linked_slide` now read the saved selection from `metadata.toml` and pass it to `compute_context_fingerprint` - Added `load_context_selection` and `save_context_selection` public helpers **`crates/hero_slides_lib/src/lib.rs`** - Re-exported `load_context_selection` and `save_context_selection` **`crates/hero_slides_server/src/rpc.rs`** - Added `deck.getContextSelection` and `deck.setContextSelection` RPC handlers **`crates/hero_slides_server/openrpc.json`** - Documented both new RPC methods **`crates/hero_slides_admin/static/js/dashboard.js`** - `_ctxSaveImmediate`: global selection now written to server via `deck.setContextSelection` (sends `null` when all files selected); localStorage retains only per-slide data - `loadBgPanel` (deck-switch branch): global selection now loaded from server via `deck.getContextSelection`; per-slide data still read from localStorage **`crates/hero_slides_admin/static/js/slide_edit.js`** - Added `_serverGlobalSelection` module variable and `loadServerContextSelection()` async loader (called from `init()`) - `readContextSelection`: `global` portion comes from server cache; `per_slide` from localStorage - `writeContextSelection`: only persists per-slide data to localStorage ### Test Results - Total: 25 - Passed: 25 - Failed: 0 Tests run against `hero_slides_lib` and `hero_slides_server`. A pre-existing build error in `hero_slides_examples` (missing `tokio` dependency) prevents `cargo test --workspace` from completing, but it is unrelated to this fix. ### Behaviour after this fix - Deselecting a background file calls `deck.setContextSelection`, which updates `metadata.toml`. The next staleness check hashes only the selected files, so deselected files no longer cause false stale markers. - Adding a new file without selecting it no longer marks slides stale. - Legacy decks with no `context_selection` in `metadata.toml` behave as before (all files hashed).
Sign in to join this conversation.
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#69
No description provided.