# Session handoff — Catacomb > Working notes for continuing this work in another session/agent (Copilot, Codex, etc.). > Updated 2026-07-09. Delete or update freely; this is not a tracked spec. ## RESOLVED (2026-07-09): GUI launches with empty library until manual Rescan **Root cause (confirmed by reproduction against the real library):** not a data or render bug — the cold-cache startup scan takes **~64 s** (vs ~96 ms warm) on the LUKS+btrfs+zstd `/mnt/InannaBeloved` volume, dominated by the directory walk + `std::fs::metadata` stat of ~11k sidecar/video files. `info_cache` only saves JSON *parsing*, not the stats, so it can't shorten a cold scan. The library did populate on its own after ~64 s (the drain delivers all 89 channels), but the content area looked empty during the wait, so the user hit ⟳ Rescan — which appeared to fix it only because the OS cache was warm by then. The old "database is locked" log line is a separate benign symptom (the web server on :8081 contends on the same FTS write; `sync_search_index` failing is caught). **Fixes shipped this session (committed, on `main`):** - `843fee5` **Scanning state** — `video_list` shows a centered spinner + "Scanning library…" while the initial scan is in flight (`library_load_rx.is_some() && library.is_empty()`), so a cold scan no longer looks broken. Verified live. - `5a55c15` + `3e6bcfb` **Persistent library snapshot (instant startup)** — the scanned `Vec` is serialized to a JSON blob in a new `library_snapshot(root, json, saved_at)` table after every scan; desktop `App::new()` seeds `self.library` from it before spawning the scan thread, so a warm launch renders the full library **within ~2 s** (status "N videos (refreshing…)") while the authoritative rescan runs and swaps in. Deleting the snapshot row falls back to the scanning spinner. Spec + plan in `docs/superpowers/specs|plans/2026-07-09-library-snapshot-*`. Serde derives added to `Subtitle/Video/Playlist/ChannelMeta/Channel` (`#[serde(default)]` for forward-compat). Original investigation notes (kept for reference): - **Launch environment**: the daily GUI is the packaged `/usr/bin/catacomb` (pacman pkg `catacomb d4bed01-1`, i.e. includes the perf pass), launched with **CWD = /home/luna**, which has its own `/home/luna/config.toml` pointing at `/mnt/InannaBeloved/youtube-backup/` (real 73 MB DB). Config is correct — rescan works against the right library. - **Code path**: `app.rs::new()` spawns the `catacomb-libscan` thread (scan → apply options/folders → `sync_search_index` → `send(library)` → `request_repaint`); `update()` drains `library_load_rx` (~app.rs:4855). Small scratch libraries populate fine (verified live during the layout-fix work), so the mechanism works — this is timing/contention-dependent on the big real library. - **Smoking gun in the user's own launch** (`journalctl --user`, pid 609582, launched 03:39:37): `search index sync failed: database is locked` at 03:40:54 — i.e. the scan took ~77 s and then the FTS sync hit SQLITE_BUSY **despite `busy_timeout = 5000`** (present and correct in `database.rs`). So some writer held the DB write lock > 5 s during startup. Prime suspect: the dedup fingerprint pass (`dedup_enabled = true` in the user's config) or another batched writer running concurrently with the libscan thread. - **Open question**: `sync_search_index` failing is handled (eprintln, continue) and `send(library)` comes after it, so that error alone should NOT leave the library empty. Either the scan itself returns empty/partial under the same lock contention (check `scan_channels_with_cache`'s info_cache read/write error handling — the perf pass batched those writes), or the send/drain has a separate failure mode. `try_recv().ok()` at app.rs:4855 swallows a Disconnected (dead thread) silently — worth making that surface an error status. - **Repro gotcha**: don't launch `/usr/bin/catacomb` under the XWayland screenshot recipe — the wgpu backend panics with `Surface::configure: Invalid surface` (panic=abort → SIGABRT). That crash is environmental, NOT this bug. The repo `target/release` binary renders fine under XWayland. - **Next steps**: (1) read `scan_channels_with_cache` + batched-write error handling for "return empty on DB error" paths; (2) instrument or add a status line when the libscan thread dies/errors; (3) reproduce with the real library — run the GUI on Wayland natively (no XWayland forcing) with stderr captured, or add temporary eprintln timing to the libscan thread; (4) check what else writes to the DB in the first ~90 s (fingerprint sync runs at startup? scheduler?). - The user's live instance (pid 609582) was left running; a stray `--version`-launched instance from the investigation was killed. ## Feature in flight: in-UI federation remote editor (brainstorming stage) Roadmap 3.5 follow-up chosen as next feature. Brainstorming (superpowers) started; decision so far: **editor lives in Settings, in both UIs** (web Settings modal section + desktop Settings screen section) — matches every other config value and stays discoverable when no remotes exist (nav/sidebar hide remotes when the list is empty). Still to design: API shape (peers are positional `/api/remotes/:id`; WebState.remotes is an immutable `Vec>` built at startup — needs to become mutable or rebuildable), live-apply semantics, whether to add a test-connection button, and whether GET settings should echo peer passwords back to the browser. No spec written yet. ## Shipped this session (2026-07-09): narrow-window layout fix — committed `2c9031b`, pushed - `23ed80a` (desktop row virtualization, below) is committed + pushed. - **Narrow-window layout fix** (the follow-up noted below, now RESOLVED): the real root cause was NOT the top bar shoving the central panel — the sort bar's `right_to_left` chip group inside `video_list`'s toolbar row overflows LEFT when the panel is narrow, and egui advances the parent's vertical cursor from the overflowed rect, shifting every row below it under the sidebar. Fix in `src/app.rs`: - `video_list`: sort chips are defined once in `SORT_CHIPS` (visual order); `sort_chip_row_width(ui)` measures the group via font metrics and the RTL right-aligned layout is used only when it fits (`sort_inline`), otherwise the chips render on their own `horizontal_wrapped` row below. Wide-window look unchanged. - Top bar: `ui.horizontal` → `ui.horizontal_wrapped` (nav buttons wrap to a second line instead of going off-screen); the right-aligned status label is width-guarded the same way with a truncating-label fallback (an overflowing RTL layout spills left over the buttons). - `SortMode` now derives `Copy`. - Verified by live GUI screenshots at 1000×700 and 1400×800 (scratchpad `final2-1000.png` / `final-1400.png`); 134+12 tests pass. - Screenshot-harness gotcha: egui renders on events; after `xdotool windowsize` you must `windowactivate` + `windowraise` + a couple of `mousemove --window` nudges or the capture races a stale frame (the "empty second toolbar line" ghost). ## Previous work (2026-07-08): desktop row virtualization — committed as `23ed80a` - `main` was at `d4bed01` (perf pass phase 1: WAL pragmas, prepare_cached, batched scan writes, sort_by_cached_key, panic=abort) — committed + pushed. - **`src/app.rs`: desktop row virtualization** (the follow-up the perf spec deferred; spec at `docs/superpowers/specs/2026-07-07-runtime-performance-pass-design.md`): - List/Card/Grid all render through `ScrollArea::show_rows` — only the visible row range is laid out per frame. - Row heights are enforced by fixed-size cells (`allocate_ui_with_layout` + `set_min_size`), built from `row_text_block_height(ui)` + density-scaled thumb height. Titles truncate to one line (`wrap_mode = Truncate`, full title on hover) — required for the fixed lattice. - List separator is a painted hline inside the cell (the old `ui.separator()` added unpredictable height). Grid is a manual horizontal run of `cols` fixed cells per lattice row (egui::Grid can't be windowed); `cols` is computed before `show_rows` from available width. - **Verified (live GUI, screenshots in session scratchpad):** - List @ 2000 — windowed rendering, uniform lattice, both-direction scroll, stable-sort interleave, selection + details pane; @ 24 — exact bottom termination, single-line ellipsis truncation. - Card — uniform fixed-height cards, single-line truncation, hover ring. - Grid — 2 cols @1400px window, 3 cols @1500px, uniform lattice, no clip, truncated titles, full button rows. `cols` computed to fit (conservative; never overcommits/clips off-screen). - `cargo build --release` clean; `cargo test --release` = 134 unit + 12 integration, 0 failures. - **Known pre-existing issue — FIXED in the 2026-07-09 session (see top):** at the ~1000px window minimum width the central panel content underlapped the sidebar. Root cause was the sort bar's overflowing `right_to_left` group, not the top header as originally guessed. - **GUI screenshot recipe on this box (Wayland/KWin):** winit defaults to a native Wayland surface that xdotool/`import` can't see. Launch with `env -u WAYLAND_DISPLAY WINIT_UNIX_BACKEND=x11 DISPLAY=:0` to force XWayland, then `xdotool search --pid`, `windowsize`, and `import -window out.png` all work. The window has a hard min size ~1000px (xdotool can't shrink below it). Foreground `sleep` is blocked by the harness Bash tool — put launch+settle+capture in a script file and run that. `spectacle -b -a` was flaky here; `import -window ` is reliable. ## TL;DR — where things are - Project: **Catacomb** (crate/binary `catacomb`) — one Rust binary that is **both** an egui desktop GUI and an axum web server wrapping `yt-dlp`. See [CLAUDE.md](CLAUDE.md) for the authoritative architecture; [ROADMAP.md](ROADMAP.md) for the plan. - Branch: `main`. Last commit `c12de9c` (command palette); `5f95bdb` and earlier are pushed, **`c12de9c` is committed but NOT yet pushed**. Remote: `https://codeberg.org/anassaeneroi/catacomb.git`. - **Local checkout dir was renamed** `~/code/youtube-backup` → **`~/code/catacomb`** (code dir only; the library/backup dir at `/mnt/InannaBeloved/youtube-backup` is unchanged). Run the server with the right CWD, e.g. `env -C ~/code/catacomb ~/code/catacomb/target/release/catacomb --web 8081`. - Only `HANDOFF.md` is uncommitted now. - The project was **renamed yt-offline → Catacomb** (crate, binary, data paths with migration, UI, docs, repo URLs). See "The rename" below. - A dev web server is usually run on **:8081** against the user's real library. It gets reaped between turns in this sandbox — just relaunch it (see gotchas). ## How to build / run / test ```bash cargo build --release # ~1.5 min (opt-level=3 + thin LTO) → target/release/catacomb cargo test --release # 128 unit + 11 integration (tests/api.rs); no network ./target/release/catacomb --web 8081 # headless web server (what the user uses) ./target/release/catacomb # desktop GUI (default) ``` ### CRITICAL gotchas 1. **The web SPA is one big embedded file** — `src/web_ui/index.html`, baked in at compile time via `include_str!`. **Editing it requires a `cargo build`** to take effect. A JS syntax error there will NOT be caught by `cargo build` (the HTML is just a string), so after every edit: ```bash awk '/