The 'Play on your server' tile now shows the actual server logo (same Plex/
Jellyfin art as the header server toggle) on a dark tile with the green owned
glow, instead of a generic play glyph. Falls back to the play glyph if the logo
fails to load.
The 'Where to Watch' section is now actionable:
- For an OWNED title it leads with a 'Play on Plex/Jellyfin' tile (green, play
glyph) that deep-links straight to the item on your server — Plex via the
app.plex.tv web app (machineIdentifier fetched once + cached), Jellyfin via its
web detail page. Built in engine.item_extras from the row's server_source +
server_id and the shared media-server config (same source poster.py uses).
- Streaming providers (TMDB/JustWatch) are now clickable → the where-to-watch
page, with a hover lift.
Owned-only: preview (tmdb) items have no library row so they get no server tile.
Seam tests cover the Jellyfin + Plex link building and the unowned no-link case.
240 video-suite tests pass.
Adds an ownership filter (All / In Library / Missing) next to the kind tabs on
the person page. Both the Known For rail and the full filmography respect it, so
you can see 'what of this person's work do I actually have' (or what's missing).
- Each credit already carries library_id (owned), so filtering is client-side.
- Contextual counts: the kind tabs count within the current ownership filter and
vice-versa, so the numbers always match what you'll see.
- 'In Library' filter glows green (matches the owned ribbon); empty states for
'you have everything' / 'nothing owned yet'.
Person hero glow-up:
- Cinematic ambient — blurred portrait + an accent colour mesh + vignette, masked
to fade into the page (was a flat wash).
- Portrait gets a slowly-rotating accent gradient ring (masked donut, GPU
transform) and a gentle float; an accent ring + glow frame.
- An accent role tagline ('ACTOR' / 'DIRECTOR' …) above a gradient-filled name,
plus a credits-count chip. Honors prefers-reduced-motion.
Performance:
- Long filmography grids use content-visibility:auto + contain-intrinsic-size so
the browser skips off-screen cards (cheap scroll), and skip replaying the
entrance animation as cards recycle.
- Hero layers are static (painted once); only two tiny composited transforms
animate. Posters/photos stay on small TMDB sizes + lazy-load; trending cached;
search debounced + request-sequenced.
Pure visual/perf layer — same data + isolation. Shell/JS tests pass.
Smart back (mirrors music's artist-detail): the top-left back button now
remembers where you actually came from, many layers deep. It keeps an origin
stack ({page} or {detail title}) and stamps each history entry with its layer
depth, so:
- the label is dynamic — '← Back to Search' / '← Back to The Bear' / '← Back to
<person>' — instead of a hardcoded 'Library'/'Back';
- backing out of the first layer returns to the page you started from (Search,
Watchlist, wherever), not always the Library;
- browser Back and our button both unwind the chain one layer at a time, in sync.
Fixes: search → person → back → movie used to mislabel as 'Library' and dump you
in the library.
Next level:
- Search isn't a blank box when idle — a 'Trending this week' rail (TMDB
trending, owned/preview annotated). Returns when you clear the query.
- Person page gets a 'Known For' hero rail (top titles by popularity) above a
full filmography now sorted chronologically (newest first).
Backend: TMDBClient.trending + engine.trending (+library annotation), route
/api/video/trending. Isolated; 237 video-suite tests pass.
Matches the show-detail page's vibe (--vd-accent-rgb glows, full-bleed, rise/
fade entrances) instead of the plain library shell.
Search:
- Cinematic hero — big title, ambient accent glow, a large glowing search bar
that lights up on focus.
- Results are premium 2:3 poster cards: hover lift + accent glow, poster zoom,
gradient overlay, a play affordance, owned/preview ribbon + rating chip.
People render as circular portrait cards. Grouped rows with accent-pill counts.
- Polished empty/hint states with a floating icon.
Person:
- Full-bleed cinematic hero with an ambient blurred-portrait backdrop (per-person
color), a glowing circular portrait, oversized name, meta as glass chips.
- Bio with Read more/less; filmography as the same premium poster cards with
premium pill tabs (All / Movies / TV).
Pure visual layer — same data hooks, routing and isolation. Shell/JS isolation
tests still pass.
Search any movie / show / person (TMDB multi-search) entirely in-app. Results
that you already own link straight to the library detail; the rest open a
TMDB-backed 'preview' detail that reuses the exact same Netflix billboard UI
(direct image URLs, nothing owned/enriched). Everything resolves back into
SoulSync — no external links on un-owned titles.
- Search page (video-search.js): debounced /api/video/search, grouped
movies/shows/people cards (reuses .library-artist-card) with owned/preview
ribbons. People open the person page.
- Source-agnostic detail (video-detail.js): loads from /api/video/detail
(library) or /api/video/tmdb (preview); art helpers pick proxy vs direct URLs;
tmdb shows lazy-load episodes per season; owned-via-tmdb-url auto-redirects to
the library detail.
- 'More Like This' now drills in-app (tmdb detail, redirects if owned); cast/crew
link to a new in-app person page (bio + filmography, each credit owned/preview).
Library credits now carry tmdb_id so owned-item cast is clickable too.
- Backend: TMDBClient.search/full_detail/person (+ shared _parse_extras);
engine.search/tmdb_detail/tmdb_season/person_detail; db.library_id_for_tmdb;
routes /search, /tmdb/<kind>/<id>, /tmdb/show/<id>/season/<n>, /person/<id>.
Isolated (one-way): video-only files, no music imports, music shell untouched.
Seam tests: search/full_detail parsing, tmdb_detail assemble+redirect, search +
person library annotation, library_id_for_tmdb, route registration, shell/JS
isolation. 234 video-suite tests pass.
OMDb now has the same setup as TMDB/TVDB: a yellow dashboard orb (★ glyph) that
spins/idles in the worker-orb animation, an entry in Manage Workers (Ratings
coverage cards, pause/resume, retry, search), and a BACKGROUND ratings pass.
- Worker 'ratings mode' (is_ratings): instead of a match queue it pulls
ratings_next() (library items with an imdb_id and ratings_synced=0), fetches
IMDb/RT/Metacritic, applies + marks synced. So the whole library gets ratings,
not just titles you open (schema v7: ratings_synced).
- enrichment_breakdown/unmatched/retry get an 'omdb' branch (coverage =
ratings-filled, not matched). build_clients includes omdb; the lazy on-view
backfill uses the omdb worker's client.
- Dashboard orb + Manage Workers entry (★ glyph fallback where there's no logo),
yellow accent.
Seam tests: omdb worker rates the queue (ratings mode), ratings breakdown.
Next-level: real critic/audience scores beyond the TMDB star. OMDb (free key,
keyed by the imdb_id we already capture) returns IMDb / RT / Metacritic.
- OMDBClient (ratings + test); built as a non-worker 'ratings_client' on the
engine. _backfill_ratings runs in both lazy detail refreshes (overwrites, since
ratings are dynamic). schema v6: imdb_rating / rt_rating / metacritic on
movies + shows; show/movie payloads return them.
- Billboard renders branded rating badges (IMDb yellow, RT tomato/splat by
fresh/rotten, Metacritic green/yellow/red by score). Lazy refresh also triggers
when an imdb_id exists but ratings are missing.
- OMDb API-key frame in Settings (parity with TMDB/TVDB) + config GET/POST +
/enrichment/omdb/test.
Seam tests: OMDb parse, engine ratings backfill, apply_ratings + payload, config
includes omdb.
Phase 4: dynamic extras fetched LIVE per view (providers change, so not cached)
via GET /detail/<kind>/<id>/extras → engine.item_extras → TMDB
(videos + watch/providers + similar in one call).
- Trailer: a '▶ Trailer' action that opens an in-app YouTube modal embed (Esc /
click-away to close).
- Where to Watch: provider logos for the region (JustWatch via TMDB).
- More Like This: a poster row of similar titles linking out to TMDB.
Both movie + show pages; all keyless (same TMDB key).
Seam tests: extras parse (trailer priority, provider/similar shape), item_extras
gating on tmdb_id, route registered, markup hooks. (RT/Metacritic via OMDb needs
its own key — offered separately.)
Phase 3: the stylized transparent title logo replaces the text title in the
billboard (the big Plex/Netflix 'premium' jump). Sourced from TMDB images
(append_to_response=images, include_image_language=en,null) in the same detail
call — no Fanart key needed.
- schema v4: movies.logo_url / shows.logo_url (idempotent migration).
- TMDB client picks an English logo (then language-neutral, then any); enrichment
backfills logo_url gap-only; show/movie payloads return 'logo'.
- Billboard shows the logo img (with graceful fallback to the text title on error
/ when absent; title kept visually-hidden for a11y). Lazy on-view refresh now
also triggers when the logo is missing, so existing libraries fill it in.
Seam tests: English-logo pick, backfill + payload, schema.
- Movie cards in the library now drill into a movie-detail page (both kinds use
the same open-detail event / video-side navigation).
- New video-movie-detail subpage reuses the .vd-* hooks; video-detail.js is now
kind-aware (root() targets the active page by kind, billboard/links/actions
branch on movie vs show). Flat layout: billboard + a details strip (released /
runtime / studio / status / critic score / quality) + the shared Cast & Crew row.
- Lazy on-view backfill for movies too: engine.refresh_movie_art re-fetches TMDB
(cast/genres/backdrop/ratings) when missing, regardless of match status, via
POST /detail/movie/<id>/refresh-art. movie_match_info added.
Seam tests: movie refresh backfills cast/genres, movie_match_info, route
registered, movie subpage markup, cards clickable for both kinds.
Last capture piece, schema v3 (new people + credits tables; CREATE IF NOT EXISTS
migrates existing DBs on restart, no wipe):
- people deduped by tmdb_id; credits link to exactly one movie OR show (separate
nullable FKs + CHECK, no polymorphic id) with department/job/character/order.
- TMDB client appends credits to the detail call (free) and parses cast (name,
character, photo, billing order) + headline crew (directors/writers/creators).
- enrichment_apply backfills cast/crew gap-only (never clobbers); show/movie
detail return cast + crew. Populates on view via the existing lazy refresh-art.
- Cast & Crew section on the detail page: grouped crew line + a horizontal
cast row with circular TMDB headshots, names, characters (accent hover).
Seam tests: TMDB credit parse (+ job filtering, created_by), backfill + people
dedup across titles, gap-only no-clobber, payload shape.
Brings the video modal to parity with music's:
- 'Process first everywhere' control (Movies/Shows/Auto) in the topbar — a global
setting that pins which kind every worker processes first. enrichment_next takes
a priority kind; the worker reads enrichment_priority each loop; GET/POST
/api/video/enrichment/priority persists it. Reuses music's .em-global styling.
- Needs-matching bar now has a live count, status filter (All unmatched / Not
found / Pending) and a debounced search (reusing .em-select / .em-search),
matching the music modal. Episode view stays read-only.
- Live glow (scoped to #vem-overlay): pulsing running dot, accent glow on the
selected worker row + active process-first/kind, and a pulsing 'now processing'
chip in the worker accent. Music's shared .em-* styles untouched.
Seam tests: priority pins kind in enrichment_next + worker honors the setting,
priority endpoint GET/POST + validation, modal feature markup pinned.
Captures the richer metadata the media server already exposes (schema v2;
idempotent migrations + CREATE IF NOT EXISTS, so an existing DB upgrades on
restart with no wipe):
- movies: tagline, rating (audience), rating_critic; shows: tagline, rating,
first/last air date; episodes: still_url + rating.
- Genres as a normalised many-to-many (genres + movie_genres/show_genres link
tables — no comma-blob), deduped, replace-on-upsert.
- Plex (.genres/.tagline/.audienceRating/.rating/.thumb) + Jellyfin (Genres/
Taglines/CommunityRating/CriticRating/Premiere+EndDate/episode Primary) both
extract them; episode stills served via /api/video/poster/episode/<id>.
- Detail payloads return genres/tagline/rating/air-dates + per-episode has_still;
the billboard shows a tagline, ★ score, genre chips, and episode rows render
REAL stills (no more orange placeholder once scanned).
Seam tests for genre dedup/replace, show+episode capture, episode still ref.
Cast/crew (people + credits) is the next phase.
Season selection is now switchable via a view toggle (persisted): poster RAIL
(scrollable season cards w/ coverage), TIMELINE band (segments sized by episode
count, filled by owned), TABS (pills), and the LIST dropdown. All drive the same
selection; episodes fade in on change.
- Watchlist button is now REAL: toggles shows.monitored via POST /api/video/monitor
(set_monitored), reflects 'In Watchlist' state. show_detail returns monitored.
- 'Get Missing' + a 'Missing only' toolbar toggle filter the episode list to
unowned episodes (actual downloading is the future acquisition subsystem).
Seam tests for the monitor endpoint + bad-input guards; shell hooks updated.
- Action buttons now reuse the exact artist-detail classes
(.library-artist-watchlist-btn + .discog-download-btn/.discog-btn-compact with
shimmer) instead of bespoke vd-btn styling — identical to the music hero. Wired
by data-attr (music binds those by id, so no hijack).
- IMDb/TMDB/TVDB source links now render as .artist-hero-badge chips (logo + short
text fallback, TVDB inverted), matching the artist hero's #artist-hero-badges
treatment instead of the off-standard text tags.
Per feedback, this drops the Spotify/artist-page parallel entirely and goes
Netflix:
- Full-bleed billboard (edge-to-edge — breaks out of the host's 40px padding),
big backdrop with Ken-Burns drift + layered scrims, oversized title, a Netflix
meta row (owned% · year · rating · seasons · runtime · status), 3-line synopsis,
and action buttons (Watchlist / Get Missing / external links).
- Per-show accent colour sampled from the poster (canvas) → drives the primary
button glow, status, episode hover — the SoulSync 'vibe', per title.
- Custom season dropdown + rich episode rows (index, 16:9 thumb w/ hover play,
title · runtime, 2-line synopsis, Owned/Missing) that fade/stagger in on season
change.
- Backdrop falls back to a cover-cropped poster; episode stills + genres + cast
arrive with the 'capture everything' phase. Watchlist/Get-Missing are visual
pending their endpoints. Shell tests updated.
Addresses the 'feels basic' feedback:
- Hero is now a contained glass card with the backdrop blurred INSIDE it +
gradient overlay (same treatment as the music artist hero) — no more bare
gaps around the top/sides. Bigger poster, accent external-link chips
(IMDb/TMDB/TVDB), refined badges + stat tiles.
- Seasons are a poster-art card grid (season = album) with coverage rings/bars
and hover-lift, selecting one renders its episodes below (episode = track) —
episode overviews now shown. Mirrors the artist album-grid -> tracklist.
- Scan now captures real per-season posters (Plex sh.seasons() thumbs / Jellyfin
/Seasons Primary), served via get_art_ref('season') + /api/video/poster/season.
Falls back to the show poster until a re-scan populates them.
Seam tests for the season art ref; shell markup tests still green.
Drill-in from a show card: full-bleed backdrop + poster + title/badges/overview +
stat tiles, then seasons->episodes as collapsible accordions with owned/missing
state and per-season coverage bars (season = album, episode = track — inspired by
the music artist page, premium vibe).
- video-detail.js (isolated IIFE) renders from /api/video/detail/show/<id>;
backdrop/poster via the proxy.
- Show cards dispatch soulsync:video-open-detail; video-side.js navigates to the
(non-nav) detail subpage; back button reuses data-video-goto.
- Movies stay non-clickable until the movie-detail page lands next.
- .vd-* CSS scoped to the detail page; music untouched. Shell + isolation tests.
- Paused enrich buttons now get music's exact amber/yellow treatment (gradient,
border, glow, hover) and an amber tooltip status — was just a flat opacity dim.
- Manage Workers button reuses music's .em-manage-btn* classes verbatim, so the
logo sits in the same gradient icon-circle with glow and the pill matches
pixel-for-pixel. Still wired by data-attribute (no inline handler), and music's
orbs/handler can't touch it (scoped to #dashboard-page). Dropped the old
bespoke .video-manage-workers-* CSS.
Root cause both buttons looked black: --ve-accent was space-separated
(1 180 228) but used in rgba(var(--ve-accent), a) -> invalid 'rgba(1 180 228, a)'
so the color silently failed. Switched --ve-accent to comma-separated (matching
music's --accent-rgb) and fixed all fallbacks -> the accent + glow now render.
- TMDB: vibey light blue (56,189,248); TVDB: purple (168,85,247).
- TVDB brand mark inverted everywhere (dashboard button, modal rail, settings
frame) so the dark logo reads on the dark UI.
The buttons were flat and the reused .tooltip-content rendered in music's
default purple. Now everything is driven by --ve-accent (on the container so the
tooltip inherits it):
- buttons mirror music's .musicbrainz-button — always-on accent glow, accent
gradient/scale on hover, bigger glow when active; logo 24px @0.85 opacity with
drop-shadow; spinner uses the accent.
- tooltip-content border/glow/arrow/header + the status value are tinted with
the service accent (TMDB blue / TVDB green), overriding music's purple default
scoped to the video tooltip only. Music untouched.
- Logos (the URLs you gave) everywhere the services are listed: dashboard
buttons, Manage-Workers modal rail, and the settings API-config frames —
matching how music shows enrichment-service logos.
- Dashboard hover tooltips now reuse music's shared .tooltip-content/-header/
-body/-status/-current/-progress classes + the same positioning, so they look
identical to the music enrichment tooltips (Status / current item / Progress)
instead of my bespoke style. Music CSS untouched (shared classes, reused).
The dashboard 'Manage Workers' button now opens a video enrichment modal that
reuses music's global .em-* modal CSS (identical look) but is entirely its own,
isolated JS: own #vem-overlay, event-delegated (no inline handlers, no music
function calls), targets /api/video/enrichment, shows only TMDB/TVDB with
movie/show coverage.
- Rail of workers (status dot + coverage), panel with pause/resume, per-kind
coverage cards (matched/not-found/pending segmented bars), and a paged
unmatched browser with retry (item + retry-failed).
- Polls the selected worker every 3s. The few invented sub-classes are styled
scoped to #vem-overlay so music is never affected. 87 tests green.
Brings the worker buttons back onto the video dashboard header as real, live
controls — isolated (own CSS classes + own JS + /api/video/enrichment), music
untouched.
- TMDB/TVDB round buttons with per-service accent, a spinner that spins while
the worker runs, and a hover tooltip (status / current item / progress).
- video-enrichment.js polls /api/video/enrichment/<svc>/status (only on the
video side, so the video engine isn't spun up on the music side); click
toggles pause/resume. Manage Workers button fires soulsync:video-open-workers
for the modal (Phase 3).
86 tests green.
Handles big libraries (your ~8500 movies) like music does instead of rendering
everything at once.
- DB: sort_title populated article-aware on upsert ('The Matrix' files under M);
query_library(kind, search, letter, sort, status, page, limit) does all
filtering/sorting/paging in SQL and returns music's pagination shape
{page,total_pages,total_count,has_prev,has_next} + badge fields (resolution,
owned/episode counts).
- GET /api/video/library now takes those params (per kind) instead of dumping
everything.
- Library page: 75/page with ← Previous / Page X of Y / Next → (music's exact
controls/classes), Sort (Title/Year/Recently Added) + Owned/Wanted filter,
server-side search + A–Z. Cards gain a resolution chip (4K/1080p/…) and the
owned/wanted meta. Still not clickable.
124 tests green.
The Library tab is entirely music-specific today (file-org templates, music
library paths, post-processing, conversion, listening stats), so hide all of it
on the video side via one rule. Future video library settings (root folders,
video naming) just need data-video-only to remain visible. Music side untouched.
Rebuilt the Library page to reuse the music library's exact look — no
reinvention, just new data:
- Same classes: .library-container, .library-artist-card grid, .alphabet-
selector, .library-search-input, loading/empty states. Movies/Shows tab pill
is the only video-specific bit.
- Real posters via a server-side proxy: GET /api/video/poster/<kind>/<id>
streams the Plex/Jellyfin artwork (token stays server-side); cards fall back
to an emoji on miss. list_movies/list_shows now expose has_poster (no raw
server paths leaked).
- Client-side search + A-Z letter filter (article-aware) over the loaded set;
cards are divs (not clickable yet, per request). Scan button in the header
reuses the shared scan controller and reloads on done.
110 tests green.
On the video side the API Configuration section (Spotify/Tidal/Deezer/etc.) is
all music — hidden now (group marked data-music-only). In its place, a video API
Configuration group (data-video-only) with disabled TMDB + TVDB placeholders for
the metadata sources we'll likely use. Music side unchanged.
Video side was showing both the Music Library selector and the new Movies/TV
selectors. The music-library picker is irrelevant there (the Movies/TV mapping
replaces it), so hide it on the video side — music side is unchanged.
Right next to music's 'Music Library' selector, the video side now shows
'Movies Library' + 'TV Shows Library' dropdowns (data-video-only, hidden on the
music side). video-settings.js populates them from /api/video/libraries when
Settings opens on the video side and saves the choice back; the scanner then
reads only those libraries. Isolated IIFE, data-attr wired. 83 tests green.
Video Settings was a 'coming soon' placeholder. Now it reuses the actual
#settings-page, shown identically for now (no hiding of music-only bits yet) —
the foundation for adding the Movies/TV library mapping next.
- video-side.js: SHARED_PAGES maps video-settings -> the music 'settings' page;
showPage sets body[data-video-page] and triggers the shared loadPageData
loader (same init music navigation uses) instead of a video subpage.
- CSS reveals #settings-page (and hides the video host) when
data-side=video + data-video-page=video-settings; id selector outranks the
blanket music-page hide. 81 tests green.
The visuals were off because I'd invented CSS/markup instead of reusing the
shared design system. Fixed to match music exactly:
- Dashboard Library card now uses music's full markup — header icon, Refresh/
Deep Scan buttons WITH their icons, and stat rows with icons (movies/shows/
episodes/disk). Same .library-status-* classes, no custom CSS.
- Tools 'Library Scan' card now mirrors the music Database Updater: a mode
dropdown (Incremental/Full Refresh/Deep Scan) + one Scan button inside
.tool-card-controls + the standard progress bar. Styling comes for free from
the generic music classes.
- Dropped bespoke .video-tool-btn/.video-scan-controls CSS and folded the
separate video-tools.js into the shared video-scan.js (one fewer file). JS
stays isolated only because it must hit /api/video + update video DOM.
110 tests green.
- New Tools page (video nav + subpage, mirrors music tools styling): a Library
Scan tool card with Incremental / Full Refresh / Deep Scan buttons + a live
status line. Room for more maintenance jobs later.
- Dashboard Library card now has Refresh (full) + Deep Scan buttons, like the
music dashboard.
- Shared video-scan.js controller: one place triggers + polls scans for all
surfaces (wires any [data-video-scan-mode]/[data-video-scan]); emits
soulsync:video-scan-progress/done. Library/Tools/Dashboard just listen — no
duplicated fetch/poll. video-library.js refactored onto it; dashboard reloads
stats on scan-done.
- All isolated IIFEs, data-attr wired (no inline onclick). video-tools added to
the nav (13 pages). 110 tests green.
- GET /api/video/library -> {movies, shows} from video.db (VideoDatabase.
list_movies/list_shows; shows carry episode_count + owned_count).
- Library page (video-library subpage, isolated video-library.js): tabbed
Movies/Shows grid of poster cards, count, empty-state. A 'Scan Library'
button POSTs /api/video/scan/request then polls /api/video/scan/status,
showing live phase/counts, and refreshes the grid when done.
- Reuses the music dashboard-header chrome (icon title, sweep hidden) + the
watchlist-button styling for the scan button; video-card grid styles added.
- All data-attr wired (no inline onclick); module is an isolated IIFE that
listens for soulsync:video-page-shown. 105 tests green.
Now: video.db -> scanner -> /api/video -> live dashboard + Library page, all
isolated from music. Scanner adapters await live Plex/Jellyfin validation.
Pointless until the real enrichment workers exist. Header keeps the icon
title, subtitle, Watchlist/Wishlist quick-nav and (hidden) sweep; the worker
button row will land later, matching music.
The video dashboard header now mirrors music's: icon + shimmer title,
subtitle, the Watchlist/Wishlist quick-nav (top-right), and the action-button
row. Differences, all isolated:
- Sweep band kept in markup but hidden on the video side (no animation for
now; meta-source-driven equivalent may return later).
- Quick-nav reuses .watchlist-button/.wishlist-button styling but carries NO
music IDs (no duplicate IDs, no music-JS binding) — navigates to the video
Watchlist/Wishlist pages via data-video-goto.
- header-actions holds disabled TMDB/TVDB/Trakt/OMDb placeholder chips
(.video-meta-button) standing in for music's enrichment buttons until the
video meta sources are wired.
No inline onclick; 75 tests green.
Real first video page, reusing music's .dash-grid/.dash-card CSS for an
identical look — but every value is driven by isolated video JS, no music
code referenced.
Sections mirror the music dashboard, adapted:
- Service Status: Media Server / Download Client / Metadata Source
- System Stats: swaps 'Active Syncs' -> 'Disk Usage'; keeps download/speed/
uptime/memory
- Library: Movies / Shows / Episodes / Disk Size
- Recent Syncs -> Recent Downloads (empty state for now)
- Quick Actions: Add Movie/Show, Watchlist, Downloads (navigate via
data-video-goto)
- Recent Activity
- No enrichment section, no header sweep animation (per plan)
Mechanics:
- #video-page-host now holds .video-subpage sections; controller toggles one
at a time and falls back to #video-placeholder-slot for unbuilt pages.
- video-side.js dispatches soulsync:video-page-shown; video-dashboard.js (new
isolated IIFE) listens and applies a zeroed STUB until video.db exists.
Single seam to swap for a real /api/video/dashboard fetch later.
- All wiring via data-attrs + addEventListener; no inline onclick (keeps the
script-split integrity contract intact). 73 tests green.
- Subtitle on the video side is now 'Video Manager' (was 'Video Sync & Manager').
Music keeps 'Music Sync & Manager' — sync fits music, not video.
- Toggle is now a proper animated pill: a gradient thumb slides under the active
side (CSS-driven off body[data-side], spring easing), each side has a small
icon (music note / film), inset track. Still data-attr wired, no inline onclick.
First slice of the video side, on the experimental branch. Purely additive and
fully isolated from music:
- A Music | Video toggle in the sidebar header; clicking flips body[data-side]
(remembered in localStorage). The shared shell (logo, user, Support, Version)
stays; only the nav set + subtitle swap.
- A second sidebar nav (.video-nav) with the video pages — Dashboard, Search,
Discover, Library, Calendar, Import, Settings, Issues, Help & Docs — shown via
CSS off body[data-side]. Service Status is hidden on the video side.
- A placeholder content host; real video pages land later.
Isolation contract held: index.html is +51/-0 (no music markup changed), music
JS/CSS untouched, nothing in music references the controller. The controller
(webui/static/video/video-side.js) is a self-contained IIFE wired purely via
addEventListener (no globals, no inline onclick) — so it can't affect music and
doesn't trip the script-split-integrity contract.
Tests: 6 video-shell structural/isolation tests + 64 script-integrity green.