Replace the crypt-archway icon with a goth coffin whose play wedge is
punched out as negative space, in the same default palette. Regenerate
all derived PNGs (web icon-512/192, apple-touch-icon, and the 256px
icon.png used for the desktop window/tray + Linux/AppImage/macOS packaging).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d9sNr6GAtXoVpLJsnh19L
The web UI kept its own hardcoded theme list (JS THEMES array + .theme-*
CSS blocks) separate from the desktop theme::THEMES, and had drifted: the
newer palettes (cemetery-moss, vampire, witching-hour, cyberpunk, synthwave,
vaporwave, gruvbox, tokyo-night, paper, honey, candlelight) were never
mirrored into the SPA, so the web only offered 10 of them.
Port those palettes to CSS variables and add them to the picker. A new
unit test (web_ui_offers_every_desktop_theme) pins the web list to
theme::THEMES so this can't silently drift again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-level lazy nav in the Remotes screen (channels → paged videos),
on-demand Play via mpv (HLS-only noted), one-click Archive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-level lazy nav (channels → paged videos), on-demand Play (HLS-only
disabled), one-click Archive to the local library.
Channel handles/names are peer-controlled, so they're rendered via esc()'d
data-* attributes with JS-wired click handlers rather than interpolated into
inline onclick JS (a handle containing a single quote would otherwise break
out of the attribute — stored XSS from a hostile peer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET channels / channel videos (paged) / video media (204 = HLS-only);
POST archive routes watch_url through the shared downloader (lands in Other).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pt_channels/pt_channel_videos/pt_video_media/pt_watch_url dispatch to the
PeerTubeClient; Err for a catacomb remote. RemoteChannelInfo now Serialize.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
URL-keyed write-only password merge; whole-list replace rebuilds the live
RwLock client list; POST /api/remotes/test checks reachability per kind. GET
/api/remotes now reports kind + has_password (never plaintext). Integration
test covers the PUT/GET round-trip, removal, and password preservation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both front-ends hold Vec<Arc<RemoteClientKind>> (web behind RwLock). Catacomb
browse unchanged; PeerTube remotes report a phase-3 stopgap when browsed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the phase-1 backend client: password-grant OAuth with refresh-token
renewal (falling back to a full re-grant), anonymous public browsing when no
creds are set, list_channels, paginated channel_videos, and on-demand
video_media (direct MP4, None for HLS-only). Wires the tested pure mapping fns
to the API. The client is dead-code-allowed until the editor (phase 2) and
browse UI (phase 3) construct it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed self.library from load_library_snapshot before spawning the scan thread,
so a warm launch renders the library immediately (status '… refreshing…')
instead of a blank/scanning window. Write the snapshot after every scan (the
startup thread and rescan). The background scan is unchanged and still swaps
in authoritative data; the snapshot only ever shows briefly-stale state.
Verified live against the real 5574-video library: second launch renders the
full list within 2s (well under the ~64s cold scan) with the refreshing
status; deleting the snapshot row falls back to the scanning spinner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add serde derives to the library structs and a library_snapshot table with
save_library_snapshot/load_library_snapshot. Load returns None on a missing
row or unparsable JSON (with #[serde(default)] fields tolerating struct
evolution). Groundwork for desktop instant startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop GUI's startup library scan runs on a background thread; on a
cold filesystem cache a large library on encrypted/compressed storage takes
~60s to walk + stat, during which the content area rendered an empty video
list. That looked like a broken/empty library and prompted a needless Rescan
(which only appeared to help because the OS cache was warm by then).
Render a centered spinner + "Scanning library…" while the initial scan is
in flight (library_load_rx still open and library empty). The scan/drain
pipeline is unchanged; this is display-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At the ~1000px window minimum, the toolbar's right-aligned sort group
ran out of row width and overflowed leftward past the panel edge; egui
then advances the vertical cursor from the overflowed rect, so every
row below (view toggle, the whole list) shifted left and was clipped
under the sidebar.
The sort chips now right-align only when a font-metrics measurement
says they fit, and otherwise drop to their own wrapped row. The top
bar likewise wraps instead of pushing nav buttons off-screen, with the
right-aligned status label width-guarded the same way (truncating
fallback).
Verified via live GUI screenshots at 1000x700 and 1400x800; the wide
layout is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render List/Card/Grid through ScrollArea::show_rows so only the visible
row range is laid out per frame. Fixed-size cells (allocate_ui_with_layout
+ set_min_size) enforce a uniform lattice; titles truncate to one line with
the full title on hover. List separators are painted hlines inside the
cell; Grid is a manual horizontal run of fixed cells per lattice row with
cols computed from available width before show_rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executes docs/superpowers/specs/2026-07-07-runtime-performance-pass-design.md:
- database.rs: every pooled connection now runs WAL + synchronous=NORMAL
+ busy_timeout=5000 + foreign_keys=ON via with_init (in-memory pools
get the latter two). Fixes the SQLITE_BUSY failure mode under the
parallel scanner and makes FK cascades work on every connection, not
just whichever one last ran the old one-shot pragma in delete_folder.
- database.rs: prepare_cached for the per-video info_cache queries; new
info_cache_put_many bulk upsert (single transaction) replaces the
per-row autocommitted info_cache_put, with a round-trip unit test.
- library.rs: enrich_with_cache collects cache misses and flushes them
once per channel; title sort uses sort_by_cached_key.
- app.rs: Title/ChannelAsc sorts use sort_by_cached_key instead of
allocating two lowercased strings per comparison.
- Cargo.toml: panic=abort in release (crash-hook still fires; Cargo
forces unwind for test harnesses). PACKAGING.md documents the local
RUSTFLAGS target-cpu=native opt-in; repo stays portable.
- .gitignore: catacomb.db-wal / catacomb.db-shm sidecars.
Verified: 146 tests pass (integration suite exercises the pragmas
end-to-end), x86_64-pc-windows-gnu check green, live smoke shows
journal_mode=wal + batched cache writes landing correct rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mobile fixes (all in the embedded SPA):
- Move the mobile media queries to the bottom of the stylesheet — the
cinematheque design layer was appended after them, so its base rules
(header padding, video heights) silently overrode the phone layout by
source order. A comment now pins the ordering invariant.
- The design layer's uiDrop entry animation (fill-mode:both) permanently
overrode the sidebar's translateX(-100%), leaving the drawer stuck open
over the content on phones; the mobile block now disables it.
- Lower the single-column breakpoint 380px -> 350px: it was catching
360/375px phones and blowing every card up to full width.
- viewport-fit=cover so the env(safe-area-inset-*) paddings actually
resolve on iPhones; notch-safe padding on header/content/modals.
- 16px inputs on small screens (kills iOS Safari's focus auto-zoom),
same for the login page's password field.
- Mobile rules for the design-layer components that had none: full-height
command-palette sheet, clamped stats numerals, coarse-pointer touch
targets for the custom player (taller scrub track, always-visible
thumb, no volume slider).
- CSP font-src now allows data: — the embedded base64 woff2 fonts were
being blocked and silently fell back to system faces.
PWA:
- manifest.webmanifest, minimal service worker, and a new Catacomb arch
icon set (SVG source + rendered PNGs incl. maskable + apple-touch),
all include_str!/include_bytes!-embedded like the HTML.
- sw.js only intercepts GET navigations to "/" (network-first, cached
offline fallback) and the static assets; /api, /ws, /files,
/music-files, /feed are never touched, and it's served no-store so
binary upgrades keep propagating.
- Routes + auth_middleware allowlist so the browser can fetch the
statics pre-login; theme-color meta tracks the active theme's panel.
- tests/api.rs covers the new endpoints incl. the ungated-when-password
invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the old search modal with a keyboard-driven command palette: blurred
backdrop, centered input, results grouped by channel with highlighted FTS
snippets (char(2)/char(3) → <mark>), ↑↓ navigation, Enter to open, Esc to
close, recent searches + quick actions (Downloads/Stats/Health/Shortcuts) in
localStorage. Debounced queries to /api/search with a sequence guard so stale
responses don't clobber newer ones. Upload date is looked up client-side from
the loaded library (SearchHit has no date field). Opened by the 🔍 header
button and the `f` hotkey.
Verified: node --check passes, release builds clean, and a headless harness
screenshot confirms grouped results, highlighted matches, and selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prose + commands updated to the new name: brand reads "Catacomb", while
binary/path/package references are the lowercase `catacomb` (commands, deb/rpm
names, ~/.local/share/catacomb, catacomb.db, catacomb.desktop). Codeberg repo
URLs left pointing at the existing repo. Also folds in the pending doc edits
from earlier in the session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The login page was the last surface still on the old flat style. Give it the
same identity as the rest of the UI: serif wordmark with the glowing crimson
"recording" dot, an italic "your private archive" tagline, accent aurora +
film-grain atmosphere on a gradient card, a themed focus ring and accent
button, and a soft entrance. Login JS is unchanged; honours reduced-motion.
Standalone page, so the display font is a system serif stack (no CDN, no
duplicating the SPA's embedded woff2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the browser's native <video controls> (used for direct /files/
playback) with one custom player for both direct and transcode streams, so
the chrome matches the reskin and gains features it never had. All seeking
still routes through playerSeek/effTime, so the transcode reload-at-offset
path is unchanged.
- Custom scrubber: accent played fill + buffered fill + chapter tick markers
+ a hover time tooltip; click/drag to seek (pointer events, works in both
modes — commits the seek on release so transcode only reloads once).
- Playback speed popover (0.5–2×) and persistent volume + speed + captions
across videos (localStorage).
- Captions (CC) toggle for overlaid <track> subtitles (off by default; the
searchable 📄 transcript pane is unchanged), PiP, fullscreen.
- Auto-hiding controls + cursor after idle while playing; a center play/pause
flash; gradient scrim.
- Expanded keyboard: space/k, j/l ±10, ←/→ ±5, ↑/↓ volume, m, c, p, f,
< > speed, 0–9 seek-to-percent — now active for direct videos too (they
previously relied on native controls).
Verified: node --check passes, release builds clean, live 8081 serves it, and
a headless screenshot of the control chrome (extracted CSS) renders correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sessions were an in-memory HashMap seeded empty at startup, so every restart
or upgrade invalidated every browser's cookie (the cause of the recent
"settings save → error" report: a restart out from under a logged-in tab).
Mirror sessions to a new `sessions(token, issued_at)` table:
- store issued-at as a UNIX timestamp (the map switches from Instant→u64 so it
can round-trip the DB; is_authed prunes by wall-clock age vs SESSION_TTL),
- insert on login, delete on logout, clear on password change,
- rehydrate the map at startup via load_sessions(), which also prunes rows
past the TTL. DB writes are best-effort — a failure just means that token
won't survive a restart, not that login breaks.
Verified end-to-end: set a password, log in, restart the server against the
same DB, and the original cookie still authenticates (200) while a cookieless
request is still 401. 128 unit + 11 integration tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sessions are in-memory, so a server restart (or a session TTL expiry) leaves
a still-open SPA tab holding a stale cookie: cached views keep rendering, but
every mutating call 401s and surfaced only as an "authentication required"
toast (e.g. saving Settings) — a confusing dead end.
Make api() treat 401 as "session gone": flash a notice and location.reload(),
which the server answers with the login page. No reload loop (the login page
isn't the SPA), and authed sessions are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reskin the Library-health modal into a systems-diagnostics readout, sibling
to the stats observatory and sharing its sx-* section language. Presentation
only — every id/class/handler the logic depends on (dup-chk, sim-chk, at-chk,
dedup-area, autotag-area, the async polling) is untouched.
- A pulsing health verdict ("Healthy" / "N items to review") whose count and
status colour update live as the async subsystems resolve.
- Four status tiles (Duplicate IDs, Missing assets, Similar content, Unfiled
groups) with health-coloured dots + top rules; sim/at start pending and are
filled by their render functions.
- Sections become instrument panels: duplicate/similar groups as cards with
KEEP/REMOVE pills, missing-asset rows, a gradient dedup progress bar, and
auto-tag groups with confidence dots. Modal widened to 920px.
- Status colour flows through --mx-c off a data-status attribute, scoped to
.mx-body so it can't leak onto other nodes; pulse honours reduced-motion.
Verified: JS passes node --check, release builds clean, live 8081 serves it,
and a headless screenshot of a harness running the extracted CSS+JS against a
mock report confirms the verdict math, tiles, pills and panels all render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild the Library statistics modal from plain tiles + flat bars + tables
into a real data dashboard, driven by the same /api/stats payload and the
theme variables (so all 10 themes inherit it).
- Metric cards with giant serif numbers that count up from zero on open
(size/runtime spin up through their units), an accent top-rule, and a
glowing hero tile.
- A self-drawing SVG area chart for downloads-per-week: Catmull-Rom-smoothed
line that animates in via stroke-dashoffset, a gradient area fill,
gridlines, a date axis, and hover dots with tooltips.
- A growing-column histogram for videos-by-upload-year with value labels.
- A ranked channel leaderboard with animated meter fills, gold/silver/bronze
ranks, and a Size/Count segmented toggle that re-animates on switch.
- Reveal animations gate behind .sx-go (added post-layout) so they play once
per open, not on re-render; honours prefers-reduced-motion; modal widened
to 1040px.
Verified: JS passes node --check, release builds clean, the live 8081 server
serves it, and a headless screenshot of a harness running the *extracted*
dashboard CSS+JS against a mock payload confirms the chart math, count-up,
histogram and leaderboard all render correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A presentational redesign of the web SPA, appended as a design layer so it
overrides by source order while every colour still flows through the existing
7 per-theme CSS variables — so all 10 themes inherit it and no element
id/class the JS relies on changes.
- Typography: embed Instrument Serif (display) + Hanken Grotesk (body) as
base64 woff2 (SIL OFL) — offline-safe, no CDN, no privacy leak. Serif
wordmark/headers/empty-state; clean grotesque for UI.
- Identity: a glowing crimson "recording" dot before the wordmark; a soft
accent aurora + fine SVG film-grain fixed behind content (negative z, so
it never sits under text).
- Depth & motion: translucent blurred masthead with a hairline accent
underglow; cinematic card hover (lift + accent ring + thumbnail zoom +
shadow); one-time staggered shell reveal; themed focus rings; thin themed
scrollbars; restrained button hover (no full crimson flood). Honours
prefers-reduced-motion.
Verified: JS still passes node --check, release builds clean, and a headless
chromium screenshot of the live 8081 server confirms it renders (fonts +
atmosphere + masthead) without breaking the existing layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tree already links a working x86_64-pc-windows-gnu exe; turn that into
a shipped artifact:
- scripts/package.sh gains a `win` target (and `all` picks it up when the
mingw toolchain is present) that cross-compiles and zips the exe +
LICENSE + a README listing the yt-dlp/ffmpeg/mpv PATH deps.
- Forgejo release workflow installs mingw-w64 + zip + the rust target, so
a tag push produces yt-offline-<ver>-x86_64-windows.zip from the same
Linux container — no Windows runner needed.
- main.rs: attach_windows_console() (cfg(windows), windows-sys
AttachConsole) reattaches a release build to the launching terminal so
--web/CLI output is visible, while a double-click stays windowless
(release sets windows_subsystem = "windows").
- ROADMAP 3.1 updated: Windows ships; macOS deferred (needs a Mac runner).
Native Linux build + 128 unit / 11 integration tests still green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add "download date" sorting (file mtime, distinct from upload date) plus a
broader set of options. Web select is now grouped via <optgroup>; desktop
gains SortMode::DownloadDesc/DownloadAsc/ChannelAsc with matching arms and
toolbar entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The startup channel scan used available_parallelism() (every core), which
could briefly peg the whole box on a cold/large library. Cap it at cores-1
so the machine stays responsive; the scan is disk-I/O-bound and runs as a
background task, so one fewer thread costs little. Mirrors the headroom the
fingerprint pass and thumbnail pool already leave.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Patch progress/speed in place on each tick instead of rebuilding the whole
modal body, so open per-job logs no longer collapse and the bar doesn't
flicker while downloading. Full rebuild only on a structural change
(job added/removed, state or retryability changed, queue length changed).
- "↻ Retry all failed" header button that re-issues every retryable failed
job (re-snapshots between retries since indices shift).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>