diff --git a/docs/superpowers/plans/2026-07-09-library-snapshot-instant-startup.md b/docs/superpowers/plans/2026-07-09-library-snapshot-instant-startup.md new file mode 100644 index 0000000..c996cd2 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-library-snapshot-instant-startup.md @@ -0,0 +1,479 @@ +# Persistent Library Snapshot — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the desktop GUI render the last-known library instantly at launch from a persisted snapshot, then background-rescan and swap in the fresh result. + +**Architecture:** Serialize the scanned `Vec` to a JSON blob in a new one-row-per-root SQLite table. On desktop launch, load and seed `self.library` before spawning the (unchanged) background scan thread. The scan stays authoritative and swaps in truth via the existing `library_load_rx` drain, so the snapshot is a display optimization that can only be briefly stale, never wrong. + +**Tech Stack:** Rust, rusqlite (r2d2 pool), serde / serde_json (already dependencies), eframe/egui. + +## Global Constraints + +- Desktop GUI only. Do **not** touch `web.rs` startup or the web in-memory snapshot machinery. +- Snapshot writes and loads are **error-swallowing / non-fatal** — a failure just means no fresh snapshot next launch (same tolerance as `info_cache_put_many`). +- The background scan runs unchanged and remains authoritative. +- New DB schema goes in `init_schema()` as idempotent `CREATE TABLE IF NOT EXISTS` (no migration framework in this repo). +- Never commit `cookies.txt`, `config.toml`, or `catacomb.db` (gitignored). +- Commits are SSH-signed: `export SSH_AUTH_SOCK=/tmp/luna-ssh-agent.sock` before `git commit`. End commit messages with `Co-Authored-By: Claude Opus 4.8 `. +- Spec: `docs/superpowers/specs/2026-07-09-library-snapshot-instant-startup-design.md`. + +--- + +### Task 1: DB snapshot persistence layer + +Make the library structs serializable and add save/load to the DB, fully tested headlessly. Derives are scaffolding for the persistence API, so they live in this task. + +**Files:** +- Modify: `src/library.rs` — add `Serialize, Deserialize` derives to `Subtitle`, `Video`, `Playlist`, `ChannelMeta`, `Channel`; add `#[serde(default)]` to `Video` and `Channel` fields for forward-compat. +- Modify: `src/database.rs` — add `library_snapshot` table to `init_schema()`; add `save_library_snapshot` + `load_library_snapshot`; add unit tests. + +**Interfaces:** +- Produces: + - `Database::save_library_snapshot(&self, root: &std::path::Path, library: &[crate::library::Channel])` — serializes to JSON, `INSERT OR REPLACE` into `library_snapshot`. Error-swallowing (returns `()`). + - `Database::load_library_snapshot(&self, root: &std::path::Path) -> Option>` — `None` on missing row **or** deserialize failure. + +- [ ] **Step 1: Add serde derives to the library structs** + +In `src/library.rs`, confirm the serde import at the top of the file (add if absent): + +```rust +use serde::{Deserialize, Serialize}; +``` + +Change each derive line as follows: + +```rust +// Subtitle +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Subtitle { +``` + +```rust +// Playlist +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Playlist { +``` + +```rust +// ChannelMeta +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ChannelMeta { +``` + +For `Video`, add the derives **and** `#[serde(default)]` on every field so old snapshots missing a future field still deserialize: + +```rust +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Video { + #[serde(default)] + pub id: String, + #[serde(default)] + pub title: String, + #[serde(default)] + #[allow(dead_code)] + pub stem: String, + #[serde(default)] + pub video_path: Option, + #[serde(default)] + pub thumb_path: Option, + #[serde(default)] + pub description_path: Option, + #[serde(default)] + pub info_path: Option, + #[serde(default)] + pub subtitles: Vec, + #[serde(default)] + pub has_live_chat: bool, + #[serde(default)] + pub duration_secs: Option, + #[serde(default)] + pub has_chapters: bool, + #[serde(default)] + pub file_size: Option, + #[serde(default)] + pub mtime_unix: Option, + #[serde(default)] + pub upload_date: Option, +} +``` + +For `Channel`, add the derives and `#[serde(default)]` on every field (keep the existing doc comments): + +```rust +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Channel { + #[serde(default)] + pub name: String, + #[serde(default)] + pub path: PathBuf, + #[serde(default)] + pub platform: Platform, + #[serde(default)] + pub source_url: Option, + #[serde(default)] + pub videos: Vec