15 KiB
Session handoff — Catacomb
Working notes for continuing this work in another session/agent (Copilot, Codex, etc.). Updated 2026-07-08. Delete or update freely; this is not a tracked spec.
Current work (2026-07-09): narrow-window layout fix — DONE, committed
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_leftchip group insidevideo_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 insrc/app.rs:video_list: sort chips are defined once inSORT_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 ownhorizontal_wrappedrow 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). SortModenow derivesCopy.- 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 windowsizeyou mustwindowactivate+windowraise+ a couple ofmousemove --windownudges or the capture races a stale frame (the "empty second toolbar line" ghost).
Previous work (2026-07-08): desktop row virtualization — committed as 23ed80a
mainwas atd4bed01(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 atdocs/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 fromrow_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 ofcolsfixed cells per lattice row (egui::Grid can't be windowed);colsis computed beforeshow_rowsfrom available width.
- List/Card/Grid all render through
- 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.
colscomputed to fit (conservative; never overcommits/clips off-screen). cargo build --releaseclean;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_leftgroup, not the top header as originally guessed. - GUI screenshot recipe on this box (Wayland/KWin): winit defaults to a
native Wayland surface that xdotool/
importcan't see. Launch withenv -u WAYLAND_DISPLAY WINIT_UNIX_BACKEND=x11 DISPLAY=:0to force XWayland, thenxdotool search --pid,windowsize, andimport -window <wid> out.pngall work. The window has a hard min size ~1000px (xdotool can't shrink below it). Foregroundsleepis blocked by the harness Bash tool — put launch+settle+capture in a script file and run that.spectacle -b -awas flaky here;import -window <wid>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 wrappingyt-dlp. See CLAUDE.md for the authoritative architecture; ROADMAP.md for the plan. - Branch:
main. Last commitc12de9c(command palette);5f95bdband earlier are pushed,c12de9cis 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-backupis unchanged). Run the server with the right CWD, e.g.env -C ~/code/catacomb ~/code/catacomb/target/release/catacomb --web 8081. - Only
HANDOFF.mdis 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
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
- The web SPA is one big embedded file —
src/web_ui/index.html, baked in at compile time viainclude_str!. Editing it requires acargo buildto take effect. A JS syntax error there will NOT be caught bycargo build(the HTML is just a string), so after every edit:awk '/<script>/{f=1;next}/<\/script>/{f=0}f' src/web_ui/index.html > /tmp/spa.js && node --check /tmp/spa.jssrc/web_ui/login.htmlis a second, separate embedded page (the login screen). - DB location: the server opens the DB at
channels_root.join("catacomb.db")(web.rs~L3127 andapp.rs~L395), i.e.<backup.directory>/catacomb.db. For this user that's/mnt/InannaBeloved/youtube-backup/catacomb.db(~64 MB, real data — do NOT clobber).config.tomlis read from the process CWD. A stray 0-bytecatacomb.dbmay sit in the repo dir; it's unused — ignore it. - Offline-first: this is a self-hosted, possibly-no-internet archiver.
Never load fonts/CSS/JS from a CDN. UI fonts are embedded as base64 woff2
(SIL OFL) directly in the
<style>block. Keep it that way. - All 10 themes are driven by 7 CSS variables (
--bg --panel --card --accent --text --muted --border) set per.theme-*class. Any new UI must use those vars (and the design-layer tokens below) so every theme inherits it. - Background server reaping: in the sandbox,
run_in_backgroundservers get killed between turns and shell&/nohupis unreliable. Just relaunch./target/release/catacomb --web 8081;pkill -f "web 8081"first. - Verifying UI visually without a browser driver (no puppeteer/playwright):
extract the relevant CSS/JS from
index.htmlinto a/tmp/*.htmlharness with a mock payload and screenshot with headless chromium:chromium --headless=new --no-sandbox --disable-gpu --hide-scrollbars \ --window-size=1100,1300 --virtual-time-budget=3000 \ --screenshot=/tmp/out.png "file:///tmp/harness.html"--virtual-time-budgetcan capture entry animations mid-flight; add a "settle" override (disable transitions, force final state) for a clean final shot.
The rename (yt-offline → Catacomb)
Done and pushed across 404362b (code), 3f61184 (docs), 5f95bdb (repo URLs):
- Crate/binary
catacomb(Cargo.toml, deb/rpm assets, PKGBUILD, package.sh,catacomb.desktop, launch.json, release CI,tests/api.rs→CARGO_BIN_EXE_catacomb). - Data paths: DB
yt-offline.db→catacomb.db; venv~/.local/share/yt-offline→~/.local/share/catacomb.migrate_legacy_paths()inmain.rsadopts the old names on first run (renames DB + WAL/SHM sidecars + venv dir; best-effort, no-op once migrated). Keep this function's OLD-name args asyt-offline— a blunt rename sed will wrongly make both sidescatacomband break migration (happened once; watch for it). - Display: window/tray/title/feed/login/SPA wordmarks → "Catacomb". Env
override
YT_OFFLINE_RENDERER→CATACOMB_RENDERER. - Verified: builds, 128+11 tests pass, scratch-dir migration preserved bytes, and the live cutover migrated the real 64 MB DB + venv (server served the library).
- Repo URLs point at
…/anassaeneroi/catacomb(Cargo.toml, PKGBUILD, package.sh).
Settings flow (the easy thing to get wrong)
Adding a setting touches five places (grep an existing one like
dedup_enabled or sponsorblock_mode end-to-end first):
config.rs— field +Default+default_with_dir().download_options.rs—Option<…>per-channel override (None = use global).downloader.rs— resolver merging global+override + apubfield onDownloader.- Both UIs:
app.rs(egui Settings + channel-options dialog) ANDweb_ui/index.html(Settings modal + channel dialog) ANDweb.rsSettingsPayload(GET reads config, POST writes config + pushes to liveDownloader). - Seed the
Downloaderfield at construction AND on settings-save, in BOTHapp.rsandweb.rs.
What this session shipped (committed, newest first)
5f95bdbrepo URLs → renamed Codeberg repo.3f61184/404362bRename → Catacomb (see above).6d2261bLogin page reskin —src/web_ui/login.html: serif wordmark + recording dot, aurora + grain, accent focus ring; system-serif (offline-safe).2f95e7fUnified cinematic video player. Replaced native<video controls>with ONE custom player for both direct + transcode. Custom scrubber (played/buffered/chapter-ticks/hover tooltip, drag-to-seek via pointer events, commits on release), speed popover (0.5–2×), persistent volume/speed/captions (localStorageplPrefs), CC toggle, PiP, fullscreen, auto-hiding controls + center flash, expanded keyboard (space/k j/l ←→ ↑↓ m c p f<>0–9). All seeking routes throughplayerSeek/effTime(transcode reload-at-offset intact). CSS prefix.pl-*; JS aroundplayVideo/updateVctrl/plBindScrubber.816da05Persist login sessions in SQLite —sessions(token, issued_at)table; map switchedInstant→u64; insert/delete/clear + rehydrate at startup. Fixes "restart logs everyone out".dd48e89api() 401 → reload to login instead of a cryptic "error" toast.615c088Maintenance modal → "diagnostics console" (CSS.mx-*): pulsing health verdict + status tiles + instrument panels. Presentation only; handler ids/classes untouched.6f61821Stats modal → "observatory" dashboard (CSS.sx-*): count-up metrics, self-drawing SVG area chart, growing histogram, ranked leaderboard.c8cb700"Cinematheque" reskin: embedded Instrument Serif + Hanken Grotesk (base64 OFL), accent aurora + film-grain, cinematic card hover, recording dot.983864fmacOS osxcross packaging;207013eWindows shippable .zip + CI;8b25787library sorting (download-date + grouped options).
Design-layer conventions (web UI)
- CSS is appended near the end of the
<style>block, AFTER the functional rules, so equal-specificity overrides win by source order. Prefixes:.sx-*= stats observatory,.mx-*= maintenance console,.pl-*= player. - Reveal animations gate behind a class added post-layout (e.g.
.sx-go) so they play once per open, not on re-render. All honorprefers-reduced-motion(global rule at the end of<style>). - Fonts:
--font-display(Instrument Serif),--font-body(Hanken Grotesk). Extra tokens:--radius,--radius-sm,--ring,--glow,--shadow,--hair.
Command-palette search overlay — SHIPPED (c12de9c)
openSearch() is now a .cp-* command palette (CSS .cp-*; JS openSearch,
runCommandSearch, renderCommandPalette, cpKeydown, updateSelected,
scrollIntoSelected, cpSelect, cpOpenRecent, cpClearRecents,
closeCommandPalette, ftsSnippet). Blurred backdrop, centered input, results
grouped by channel with highlighted FTS snippets (char(2)/char(3) → <mark>
via ftsSnippet), ↑↓ nav, Enter to open (cpSelect→selectVideo), Esc to
close, recents + quick-actions in localStorage['cp-recents']. Debounced
queries to /api/search?limit=60&q=… with a cpSeq guard against stale
responses. Upload date is looked up client-side via findVideo (SearchHit has
no date). Triggers: 🔍 header button + f hotkey. Verified by build + headless
harness screenshot. Built into the running :8081 binary.
Suggested next steps (pick up here)
- Push —
c12de9c(command palette) is committed but not yet pushed. - Any rough edges from real daily use (the player + palette especially — both verified via harness screenshots, not live click-through).
- A fresh web-UI surface to reskin/upgrade, or a roadmap item below.
Roadmap "surpass" items still open (see ROADMAP.md §3):
- 3.1 macOS binary — osxcross scaffolding done (
scripts/package.sh mac); needs the toolchain + SDK installed, then verify. Windows already ships. - 3.2 Android client (big), 3.8 plugin/scripting hooks (architectural).
- Phase 4 blue-sky: AI transcript summarisation (FTS transcript index already exists), TV-mode layout, multi-user accounts.
- Federation follow-up (3.5): in-UI "add remote" editor (peers are config-only).
Watch-outs / open questions
- The video player's seek/speed/captions: verified in real daily use as of 2026-07-09 (user confirmed) — retired as a watch-out.
- After the rename + session-persistence work, the user's existing browser cookie predates both, so they must log in once more; logins after that survive restarts. A download password may or may not be set (toggled during testing) — that's user-driven, not a bug.
- The user's local
config.tomlsource_urlstill points at the old…/yt-offlinerepo (gitignored, user-specific) — change it in Settings if the UI "source" link should match the renamed repo. - Don't commit
cookies.txt,config.toml, orcatacomb.db(all gitignored; contain creds / the Argon2 password hash).