Commit Graph
100 Commits
Author SHA1 Message Date
peter 079c8182b4 Merge: promote dev to prod 2026-06-16 02:16:33 +02:00
peter f6efe44e37 chore(release): v0.4.0 — navigation overhaul, glass, account switch
Bump VERSION to 0.4.0 and add the 0.4.0 release-notes entry (Epic N: left nav sidebar,
multi-account switch, Settings page, in-app back, frosted-glass refresh).
2026-06-16 02:16:21 +02:00
peter 15716dd578 fix(auth): always seed the active account into the switch list
Sessions created before multi-session existed had no account_ids, so adding a second
account left the first one (e.g. the long-lived session) absent from the switcher.
current_user now ensures the active user_id is always present in account_ids, so the
switch list is never missing the account you're using.
2026-06-16 02:12:59 +02:00
peter 2d8c47b04e Merge feature/n3-multi-session: account switch without re-auth (Epic N, N3) 2026-06-16 02:05:38 +02:00
peter 583e003c23 feat(auth): N3 — server-side multi-session account switch
Track every account that completes OAuth in a browser session (session 'account_ids',
~14-day cookie), so the user can switch between them without another Google round-trip.
New GET /api/me/accounts (switchable accounts, active flagged) and POST /api/me/switch
(guarded: target must be in the session list — proof it signed in here — and re-checked
against is_allowed in case the invite was revoked). Logout now signs out the active
account and falls back to the most recent remaining one, else clears the session. UI: the
account popover lists the other signed-in accounts (click to switch, reloads) plus an
'Add another account' action (-> /auth/login, which uses prompt=select_account).
Trilingual. Third step of Epic N.
2026-06-16 02:05:38 +02:00
peter 8c076f7a75 feat(ui): brighten dark-mode glass so the frost is visible over dark UI
Dark glass looked nearly flat because the blurred backdrop is itself dark. Add a
brightness lift to backdrop-filter in dark mode for .glass/.glass-menu so the frosted
sheen reads even over dark surfaces (over colourful content it just looks richer).
Perf-mode still disables the blur (later rule wins).
2026-06-16 01:57:41 +02:00
peter ed2d76f92d fix(nav): remove backdrop div so the account popover frosts
The full-screen transparent dismiss backdrop sat directly behind the popover and acted as
a compositing layer, so the popover's backdrop-filter sampled it (empty) instead of the
page — making the glass look fully solid. Drop the backdrop and dismiss via document
mousedown/Escape listeners (like the other menus). Now backdrop-filter samples real
content and the frost shows.
2026-06-16 01:54:01 +02:00
peter a18924bf97 fix(nav): restore frosted glass on the portaled account popover
Now that the popover is portaled to body its backdrop-filter actually samples the content
behind it, so the near-opaque glass-menu (needed only as a workaround while blur was
trapped) made it look solid. Switch it back to the frosty .glass (78%); the working blur
softens the background so text stays readable without the bleed-through. Trapped header
menus (language/notifications) keep glass-menu.
2026-06-16 01:50:01 +02:00
peter 5bb85b039a fix(nav): portal account popover to body (stacking + click-through)
The nav's .glass backdrop-filter makes it a containing block for fixed descendants and a
stacking context, so the account popover's fixed inset-0 backdrop only covered the nav (not
the viewport) and the popover sat below the main content — controls behind it stayed
clickable. Portal the popover + its dismiss backdrop to <body> with fixed coords computed
from the trigger rect, so it's truly top-most (z-50) and blocks clicks underneath.
2026-06-16 01:46:32 +02:00
peter 74491991d7 fix(ui): opaque .glass-menu for floating popovers (readability)
Floating menus hover over undimmed content (no backdrop scrim like dialogs), so the
frosted .glass (78%) let the content bleed through and hurt readability. Add a near-opaque
.glass-menu (surface 92%, keeps blur) and use it for the account, language, notification
and add-to-playlist popovers. Dialogs/chrome keep the frostier .glass (they sit over a dark
scrim / the ambient bg). perf-mode disables its blur too.
2026-06-16 01:39:56 +02:00
peter 0adc1104a3 fix(ui): restore glass opacity in dark mode (account popover readability)
The dark-mode experiment made .glass globally more translucent (surface 66%), which
also hit content-overlapping surfaces like the account popover — text became hard to
read over the feed behind it. Drop that override so .glass is back to the readable 78%
everywhere; keep the richer dark ambient (harmless to readability). Docked chrome loses
the marginal extra frost (which was barely noticeable anyway).
2026-06-16 01:34:25 +02:00
peter 7b2d4dfb78 Merge feature/n2-back-history: in-app Back/Forward navigation (Epic N, N2) 2026-06-16 01:27:49 +02:00
peter 250c1da214 feat(nav): N2 — browser Back navigates in-app, not to OAuth
Push an in-app history entry on each page switch (page rides in history.state, URL stays
clean — filters still never go in the URL) and sync the page from history.state on
popstate. The initial entry is stamped after the strip-params effect (which nulls
history.state). Now the mouse/browser Back button steps through visited pages instead of
jumping straight to the Google consent/redirect. Second step of Epic N.
2026-06-16 01:27:49 +02:00
peter 36c7a649ab feat(ui): stronger ambient + more translucent glass in dark mode
In dark mode the faint accent pools left the docked frosted chrome (nav/header) looking
flat — no content behind it to refract, unlike dialogs over the feed. Boost the dark-mode
ambient pools and make .glass a bit more translucent there so the chrome reads as glass
near the accent pools. Light mode unchanged. The full app-wide dark effect still needs
Phase 2 (ambient content/mosaic), deferred.
2026-06-16 01:20:40 +02:00
peter 67e518f7ef Merge feature/glassmorphism-phase1: frosted glass on nav, header, dialogs 2026-06-16 01:11:50 +02:00
peter d46be00d47 feat(ui): glassmorphism phase 1 — frost the app chrome
Apply the real frosted .glass surface (blur) to the few large chrome surfaces — the nav
sidebar, the header and all Modal-based dialogs (was glass-card / plain surfaces) — and
enrich the ambient backdrop a touch (three soft accent pools) so the glass has more to
refract app-wide. Popovers (notifications, language, account, add-to-playlist) were
already glass. Bulk feed cards stay glass-card (no per-card blur) for performance; the
existing perf-mode still disables blur. Phase 2 (ambient thumbnail mosaic / bg image +
toggle) deferred to end-of-project polish.
2026-06-16 01:11:50 +02:00
peter 2d54167afd Merge feature/n1b-settings-page: Settings as a left-nav page (Design B) 2026-06-16 01:05:05 +02:00
peter d30dc7f760 feat(nav): Settings as a page module (Design B)
Move Settings out of the right-side overlay into a left-nav page (page='settings'),
so it opens where you're already looking — no cross-screen mouse travel. The Settings
rail item now sets the page (with active highlight) instead of opening a dialog; the
panel is refactored to an in-flow .glass card (keeps the frosted look over the ambient
backdrop), with the page title shown in the header. Removed the overlay + Esc/backdrop
close path.
2026-06-16 01:05:05 +02:00
peter 7b1fc02c68 Merge feature/n1-nav-shell: collapsible left navigation (Epic N, N1) 2026-06-16 00:42:23 +02:00
peter 4a80ea5aa5 feat(nav): N1 — collapsible left nav sidebar (Design C)
Move the modules (Feed/Channels/Playlists/Stats) out of the avatar dropdown into a
persistent left sidebar with icon+label entries; collapses to a thin icon rail
(persisted in localStorage). Account actions (About, Sign out, admin badge) move to a
popover at the sidebar bottom; Settings is a rail entry. Header is now a contextual top
bar (sync status, feed scope + search or page title, language, notifications) — logo and
account menu removed from it. New nav.json strings (HU/EN/DE). First step of Epic N.
2026-06-16 00:42:23 +02:00
peter 424e820276 Merge: promote dev to prod 2026-06-16 00:17:10 +02:00
peter 3143d1c1db chore(release): v0.3.0 — playlists + YouTube two-way sync
Bump VERSION to 0.3.0 and add the 0.3.0 release-notes entry (local playlists, watch
later as a playlist, YouTube mirror + two-way sync, sort + undo/redo, rail sorting).
2026-06-16 00:16:59 +02:00
peter 993e6ba7d5 fix(playlists): dirty lagged one edit (flush before fingerprinting)
The session has autoflush off, so recompute_dirty's query read the last-committed
state, not the current request's pending item/order edits — so the first reorder
after a sync compared against itself and missed dirty, and only the next edit showed
it. Flush pending changes before computing the current fingerprint. Verified: a swap
now flips dirty immediately, and reverting to the original clears it.
2026-06-16 00:11:58 +02:00
peter 7a547d40fe Merge feature/playlist-fingerprint-dirty: fingerprint-derived dirty state 2026-06-15 23:00:56 +02:00
peter 6330ac3184 feat(playlists): derive dirty from a synced-state fingerprint
Store a SHA-256 of each playlist's name + ordered video ids as last synced from /
pushed to YouTube (migration 0013, backfilled for already-clean linked playlists).
'dirty' is now recomputed on every edit by comparing the current fingerprint to that
baseline instead of being stickily set true. So manually restoring the original order
(or undo back to it) clears dirty on its own — the Reset to YouTube button disappears
and no YouTube read is needed. Baseline is set on read-sync, repull (reset), and a
clean push; a missing baseline counts as dirty (unknown YT state). Verified the
migration's fingerprint matches the app's runtime fingerprint exactly.
2026-06-15 23:00:56 +02:00
peter a2ad555ef3 fix(playlists): show Reset/Unsynced right after a reorder
A reorder only invalidated the sidebar query, so detail.dirty stayed stale and the
Reset to YouTube button + Unsynced indicator appeared only after F5. Also invalidate
the detail query on reorder; the membership guard keeps the refetch from wiping the
undo history on a pure reorder.
2026-06-15 22:44:57 +02:00
peter f6e662ff14 Merge improvement/playlist-header-polish: tidy playlist detail header 2026-06-15 22:40:21 +02:00
peter 17bbb05593 polish(playlists): compact header actions + open-on-YouTube link
The detail header overflowed the items column. Make the secondary actions icon-only
with tooltips (Sync/Export, Reset to YouTube, Delete) keeping only Play all labeled;
replace the long 'Unsynced changes' / 'Synced from YouTube' chips with a compact
accent 'Unsynced' dot (tooltip explains). Add an open-on-YouTube external-link button
next to the name + edit pencil (tooltip), shown for any YouTube-linked playlist.
2026-06-15 22:40:21 +02:00
peter 55d476c0b7 Merge feature/playlist-revert-youtube: reset playlist to YouTube state 2026-06-15 22:28:16 +02:00
peter 55833d8f72 feat(playlists): reset a playlist to its YouTube state (discard local edits)
The in-session undo/redo is lost when switching playlists, leaving no way to undo
local edits to a YouTube-linked list afterwards. Add a per-playlist 'Reset to YouTube'
action (shown when a linked playlist is dirty) that force-repulls that single playlist
from YouTube, replacing its items/order/name and clearing dirty — even though the bulk
read-sync skips dirty playlists. Backend: repull_playlist() + POST /api/playlists/
{id}/revert-youtube (read-scope gated). Confirm dialog (destructive). Trilingual.
2026-06-15 22:28:16 +02:00
peter 380ad39ad4 fix(playlists): don't drop the restored selection during initial load
The auto-select effect ran while the playlist query was still loading (empty list),
nulling the localStorage-restored selection so F5 fell back to the first playlist.
Wait for the first load (listQuery.data) before adjusting the selection.
2026-06-15 22:19:05 +02:00
peter c1d43d8961 Merge improvement/playlist-rail-sort: rail sorting + persisted selection 2026-06-15 22:13:39 +02:00
peter a00779a1c9 feat(playlists): rail sorting, consolidated item sort, persist selection
1) Fix: the selected playlist is now persisted (localStorage) and scrolled into view,
   so F5 keeps it instead of jumping to the first one.
2) Consolidate the in-detail sort: one key select (Title/Duration/Channel) + an
   asc/desc direction toggle (was separate A-Z / Z-A / shortest / longest options);
   add Channel as a sort key. Direction also orders the channel groups when grouping.
3) Left rail sorting: by name / item count / total length, asc/desc, plus an
   'unsynced first' toggle that floats playlists with unpushed edits to the top.
   Backend: list/summary now return total_duration_seconds (grouped sum). The rail
   also shows each playlist's total length. Sort prefs persist to localStorage.
2026-06-15 22:13:32 +02:00
peter 156e10235e Merge feature/s4e-playlist-sort-undo: playlist sort + reusable undo/redo 2026-06-15 21:52:35 +02:00
peter 40c0cbfc5a feat(playlists): sort + group-by-channel with reusable undo/redo
Add a sort toolbar to the Playlists page: Title A-Z/Z-A, shortest/longest first,
and a 'group by channel' toggle that groups items by channel (A-Z) and applies the
chosen sort within each group. Applying a sort reorders the items and persists via
the existing reorder API.

The item order is now an undoable value: drag, sort and grouping all go through it,
so every change is reversible via undo/redo buttons and Ctrl/Cmd+Z / Ctrl+Y
(Ctrl+Shift+Z). Built on two reusable pieces, not playlist-specific:
- useUndoable<T> — a generic past/present/future snapshot hook (set/undo/redo/reset
  + canUndo/canRedo), ref-backed so callbacks are stable and never see a stale snapshot;
  onApply runs the side effect (here: persist order).
- UndoToolbar — undo/redo buttons + keyboard shortcuts, plain props so any undo
  source can use it.

Undo history is reset only when the item set actually changes (different playlist or
add/remove), so a refetch confirming our own reorder doesn't wipe it; membership
changes (remove) clear history since they aren't reorder-undoable. Trilingual.
2026-06-15 21:52:28 +02:00
peter 1ddeac57cd Merge improvement/s4d-editable-mirrors: editable YouTube playlist mirrors 2026-06-15 21:40:20 +02:00
peter 9acea11010 feat(playlists): make YouTube-mirrored playlists editable + syncable
Mirrors (source='youtube') were read-only; per the original two-way design they can
now be edited locally (add/remove/reorder/rename) and pushed back. The read-sync
skips a dirty mirror so it won't clobber unpushed edits; a successful push clears
dirty and lets the mirror refresh. _mark_dirty now covers any YouTube-linked list
(exported local or mirror), and rename marks dirty too. push/delete no longer reject
mirrors. Push reconciles the title as well (cheap playlists.list compare, then
playlists.update only if changed) so a local rename doesn't revert on the next pull.
Frontend: editable = not-built-in (was also excluding mirrors); rows/reorder enabled
for all owned lists; AddToPlaylist popover lists mirrors too (with a YT marker); the
origin chip now reads 'edits sync back'. Trilingual.
2026-06-15 21:40:13 +02:00
peter 49d226126c Merge feature/s4d-youtube-playlist-push: YouTube playlist write-back 2026-06-15 21:23:31 +02:00
peter 8911e9dc89 feat(playlists): Sync to YouTube UI + delete-on-YouTube choice
Editable local playlists get an Export/Sync to YouTube button (write-scope gated):
it fetches a dry-run plan, shows a confirm with the change counts, quota estimate
and divergence warning, then pushes. An 'unsynced changes' badge and an accented
YouTube icon mark linked playlists with local edits. Deleting a linked playlist
offers 'delete on YouTube too' vs 'here only'. Trilingual strings (HU/EN/DE).
2026-06-15 21:23:13 +02:00
peter b150337c87 feat(playlists): push local playlists to YouTube (export + diff sync)
Local 'user' playlists (not Watch later, not YouTube mirrors) can now be pushed
to YouTube: create a new playlist on first push, else reconcile membership + order
so YouTube matches local (local wins). plan_push computes a dry-run estimate
(inserts/deletes/reorders + quota units + divergence) read from the live YouTube
state; push refuses if the estimate exceeds the remaining daily quota so a big
playlist can't strand half-pushed. Reorder uses an insertion-sort move count that
matches what executes. Edits to a linked playlist set dirty; a successful push
clears it. DELETE accepts on_youtube to also delete the playlist on YouTube. The
read-sync now skips YouTube playlists already owned by a local export, avoiding
duplicate read-only mirrors.
2026-06-15 21:23:02 +02:00
peter b7bf6cc6f9 feat(playlists): YouTube write API client methods
Add OAuth-only write helpers to YouTubeClient (each 50 quota units):
create_playlist, add_playlist_item, move_playlist_item, delete_playlist_item,
delete_playlist, plus iter_playlist_items_with_ids for diffing. A shared _write
helper records quota and raises YouTubeError on non-2xx.
2026-06-15 21:22:51 +02:00
peter 35831dc3ce Merge feature/s4c-youtube-playlist-pull: YouTube playlist read-sync
S4c (read direction, YT -> local):
- feat(playlists): mirror users' YouTube playlists into source=youtube playlists,
  ingesting catalog-missing videos; manual /api/playlists/sync-youtube + scheduler job
- feat(playlists): YouTube-mirrored playlists shown read-only with a Sync button
- fix(notifications): success level for success toasts (green check, not accent)
2026-06-15 21:02:49 +02:00
peter 29696c549c fix(notifications): use the success level for success toasts
The 'Synced N playlists' and 'View link copied' toasts used the default info level,
which renders in the accent colour and read as an error/alert. Mark them level:success
so they show the green check — clearly positive and theme-independent.
2026-06-15 19:46:37 +02:00
peter 84b56de21e feat(playlists): YouTube-mirrored playlists in the UI (read-only) + sync button
Show YouTube-sourced playlists with a YouTube icon; they're read-only for now (no
rename/delete/reorder/remove, excluded from the add-to-playlist popover) with a
'synced from YouTube' note — editing comes with the write-back phase. Add a 'Sync from
YouTube' button in the Playlists rail (api.syncYoutubePlaylists) with a result toast.
Trilingual strings.
2026-06-15 19:37:17 +02:00
peter a661d079ee feat(playlists): YouTube playlist read-sync (backend)
Mirror each user's own YouTube playlists into local source='youtube' playlists,
one-way (YT -> local). New client methods iter_my_playlists / iter_my_playlist_video_ids
(OAuth, so private playlists work); sync/playlists.py reconciles the mirror (matching YT
order) and ingests any playlist videos not in the shared catalog yet (with stub channels).
POST /api/playlists/sync-youtube for manual sync (read-scope gated, per-user quota) plus a
scheduler job (playlist_sync_minutes, default 6h) that syncs all read-scope users. YouTube's
Watch Later / History are not API-accessible and are never synced.
2026-06-15 19:37:03 +02:00
peter 9c28a75787 Merge feature/s4b-watch-later-queue: Watch later unification + queue playback
S4b:
- feat(playlists): unify Saved into a built-in Watch later playlist (migration 0012)
- feat(playlists): queue playback in the player (auto-advance + prev/next)
- feat(playlists): live cross-tab queue (track by id, refetch on focus)
- fix(header): keep the sync-status count live (focus refetch + background poll)
2026-06-15 17:04:36 +02:00
peter 4ff0b9705c fix(header): keep the sync-status count live (focus refetch + background poll)
The header 'N without full history' (and sync state) polled every 30s but only while
the tab was focused, and the global refetchOnWindowFocus is off — so after switching
tabs the count looked frozen until a manual reload. Poll in the background too and
refetch on focus so it tracks the scheduler's progress without F5.
2026-06-15 17:00:06 +02:00
peter ddb1890e84 feat(playlists): keep the player queue live across tabs
Track the playing item by id instead of a frozen index, deriving the index from the
live queue so the N / M counter and prev/next neighbours stay correct when the playlist
changes underneath (e.g. an item removed in another tab) without disrupting playback of
the current video. Refetch the playlist list/detail on window focus so such changes flow
in, updating the player's queue length in near-real-time.
2026-06-15 16:27:28 +02:00
peter 4689d2cbbd feat(playlists): queue playback in the player (auto-advance + prev/next)
PlayerModal now accepts an optional queue + startIndex. The active item drives the
player; it recreates per item (reusing the single-video resume / auto-watch / progress
logic), auto-advances to the next item when one ends, and shows Previous / Next controls
with an N / M indicator. The Playlists page passes the playlist as the queue from Play all
or a clicked row. Trilingual previous/next strings.
2026-06-15 16:11:37 +02:00
peter dea740b728 feat(playlists): unify Saved into a built-in Watch later playlist
The old per-video status='saved' becomes membership in a built-in, undeletable
'watch_later' playlist. Migration 0012 moves existing saved videos into each user's
Watch later list and demotes the states to 'new'. The feed/playlist serializers now
expose a 'saved' boolean (EXISTS in watch_later); the card bookmark toggles watch_later
via new /api/playlists/watch-later add/remove endpoints (create-on-demand) instead of
setting a status. Removed the 'saved' show-filter and status. Watch later shows a
localized name and hides rename/delete in the Playlists page and add-to-playlist popover.
2026-06-15 15:33:53 +02:00
peter 47bad6d9ce Merge fix/playlist-detail-invalidation: live playlist detail after popover add 2026-06-15 15:20:28 +02:00
peter ef11c6c605 fix(playlists): refresh playlist detail after add/remove from the feed popover
Adding a video to a playlist via the AddToPlaylist popover invalidated the membership
and the playlist list, but not the playlist *detail* query, so the Playlists page
served stale cached items (the new video only appeared after F5). Invalidate the
["playlist"] detail queries too.
2026-06-15 15:20:28 +02:00
peter a6a0c429d5 Merge feature/s4a-playlists-foundation: local playlists foundation
S4a (local only; YouTube sync in later phases):
- feat(playlists): backend model + migration 0011 + CRUD API
- feat(playlists): Playlists page (rail + detail, drag-reorder, rename, delete,
  remove item, play) + AddToPlaylist popover (cards + player) + nav
- feat(ui): reusable app-styled confirm dialog (ConfirmProvider/useConfirm),
  replacing window.confirm
- fixes: popover overflow flip, post-delete refresh/reselect, page persistence
  across reloads, header title on the Playlists page
2026-06-15 15:17:35 +02:00
peter 4ed52cd244 fix(playlists): correctly clear/reselect after deleting a playlist
The post-delete auto-select effect re-picked the just-deleted id from the still-stale
playlists cache, so its detail lingered. Select the next remaining playlist directly on
delete (or null), drop the deleted detail from the cache, and make the effect re-validate
the selection against the current list (clear when empty, reselect when the current id is
gone). Also fix the header title showing "Channel manager" on the Playlists page.
2026-06-15 15:15:56 +02:00
peter 48cb11434d feat(ui): reusable app-styled confirm dialog, replacing window.confirm
Add a promise-based ConfirmProvider + useConfirm() hook (built on the Modal shell,
liquid-glass styling, danger variant, Esc/backdrop = cancel, Enter = confirm) so
confirmations match the app instead of the native browser dialog. Wire it in at the
app root and replace both window.confirm call sites (playlist delete, channel
unsubscribe). Trilingual common.confirm/confirmTitle strings.
2026-06-15 15:06:47 +02:00
peter 50bc35d78f fix(nav): persist the current page across reloads
Since filters/page are no longer mirrored to the URL (de-URL refactor), pressing F5
on the Channels/Playlists/Stats page dropped back to the feed because the initial
page was read only from the (now-absent) ?page= param. Persist page to localStorage
and restore it on load; a share link's ?page= still takes precedence.
2026-06-15 14:59:24 +02:00
peter d7fc237d66 fix(playlists): clear the detail panel after deleting the selected playlist
The detail pane rendered from react-query's cached data, so deleting the selected
playlist emptied the rail but left its stale contents on the right. Gate the detail
view on the current selection so it falls back to the empty state when nothing (or a
just-deleted playlist) is selected.
2026-06-15 14:56:39 +02:00
peter fb5a2f1ed2 fix(playlists): flip the add-to-playlist popover up when it would overflow
The popover was always positioned below its trigger, so on cards near the bottom
of the viewport it ran off-screen. Measure the panel's real height and open it
upward (clamped to the viewport) when there isn't room below; also clamp the
horizontal position.
2026-06-15 14:52:57 +02:00
peter 40a44cdde2 feat(playlists): frontend foundation — page, add-to-playlist, nav
Add the Playlists page (left rail of playlists + detail with drag&drop reorder,
inline rename, one-step delete, remove item, Play all / row-click playback via the
existing PlayerModal) and an AddToPlaylist popover (portaled, multi-toggle +
inline new-playlist) wired into the VideoCard hover overlay and the PlayerModal.
New api client methods + Playlist types, a 'playlists' page route + account-menu
entry, and trilingual strings. Local only — YouTube sync comes in later phases.
2026-06-15 14:45:18 +02:00
peter bf769379cb feat(playlists): backend foundation — model, migration, CRUD API
Add per-user local playlists: Playlist + PlaylistItem models (with forward-looking
kind/source/yt_playlist_id/dirty columns for the later YouTube-sync phases) and
migration 0011. New /api/playlists routes: list (with optional contains=<video_id>
membership flags for the add-to-playlist popover), create, get detail (items
serialized via the feed serializer, with per-user watch state), rename, delete,
add/remove item, and reorder. Watch later (built-in) is protected from deletion.
2026-06-15 14:37:09 +02:00
peter ff1d36238f Merge: promote dev to prod 2026-06-15 13:58:34 +02:00
peter dcb1605cc7 Merge chore/release-0.2.0: version bump + release notes 2026-06-15 13:58:22 +02:00
peter 34c41f7a41 chore(release): 0.2.0 — shared library, dynamic facets, share view
Bump VERSION to 0.2.0 and add release notes covering the Mine/Library shared-feed
scope, dynamic faceted filter chips (live counts, hide non-matching, Any/All topic
match), surprise-me reshuffle, share-view links, and the deep-backfill status
clarity fixes.
2026-06-15 13:58:22 +02:00
peter c69d02d01a Merge feature/s3b-deurl-filter-state: de-URL filter state + share-view link
S3b:
- refactor(filters): stop mirroring filters to the URL; localStorage canonical
- feat(filters): opt-in "Share view" link button (filters/sort/scope)
2026-06-15 12:30:17 +02:00
peter e18be73d46 feat(filters): "Share view" link button in the sidebar
Add a share button next to Clear all that copies a link reproducing the current
filter view (filters, sort, scope) to the clipboard, with a confirmation toast.
This is the opt-in replacement for the old always-on URL mirroring. Trilingual.
2026-06-15 12:29:51 +02:00
peter 3e90fe2e5b refactor(filters): stop mirroring filters into the URL; localStorage is canonical
Filters/sort/search/scope were written to the address bar on every change (a leftover
from sharing reproducible examples), giving two sources of truth. Make localStorage the
single source: drop the automatic syncUrl from setFilters/setPage. A "Share view" link
still hydrates filters on first load, after which the query is stripped from the URL
(stripUrlParams) so it stays clean. syncUrl is replaced by shareUrl (builds the link on
demand); the serializer now also round-trips scope.
2026-06-15 12:29:43 +02:00
peter 581719401e Merge feature/s3-dynamic-facets: dynamic faceted filter chips
S3a:
- feat(feed): /api/facets endpoint for contextual per-tag channel counts
- feat(filters): dynamic chips (live counts, hide non-matching, sort by count)
- feat(filters): prominent topic Any/All match toggle
- fix(feed): conjunctive facet counts in AND mode
2026-06-15 12:25:10 +02:00
peter aa4b069567 feat(filters): make the topic Any/All match toggle prominent
The AND/OR ("Any"/"All") control for topic chips was a single faint corner link
that was easy to miss. Replace it with a labelled segmented control ("Match:
[Any][All]") so the AND option is discoverable. New trilingual 'match' label.
2026-06-15 12:20:17 +02:00
peter 64911e3c4d fix(feed): conjunctive facet counts when topic match mode is AND
In AND ("All") topic mode the facet endpoint still excluded the topic selections
when counting topic chips, so every topic kept its full count and none dropped
out as you narrowed — e.g. picking Comedy left Cooking visible even though no
channel has both. Count topics conjunctively in AND mode (keep the selected
topics applied) so each remaining chip reflects channels that ALSO have all
already-selected topics; non-co-occurring tags fall to zero and hide. OR mode
stays disjunctive. Verified: Comedy selected narrows topic chips 21 -> 6.
2026-06-15 12:20:08 +02:00
peter 5656875a07 feat(filters): sort facet chips by count, then name
Order topic/language chips by their (contextual) count descending, name as the
tiebreaker, so the most-populated tags sit at the top and the smallest counts
fall to the bottom as you scan down.
2026-06-15 12:10:09 +02:00
peter 79f53ccf59 feat(filters): dynamic faceted chips driven by /api/facets
Topic and language chips now show live channel counts for the current filter
context instead of the static global count, and chips that match nothing are
hidden (selected chips stay so they can be cleared). Selecting a channel (or any
filter) drops the now-irrelevant chips and updates the rest. Extract a shared
filterParams() so the feed and facets queries see identical filters; the facets
query is keyed on filters so it refetches as they change. Trilingual empty-state
string when a category has no matching tags.
2026-06-15 12:06:02 +02:00
peter 79e7694b24 feat(feed): /api/facets endpoint for contextual tag counts
Add a facets endpoint that returns per-tag channel counts for the current filter
context (scope, channel, date, content type, search, watch state, and the other
category's tags). Each category is counted with its own selections ignored —
standard drill-down faceting — by a new exclude_tag_category param threaded into
_filtered_query, so selecting one topic doesn't zero out the other topics. Count
is distinct channels with a matching video, keeping the channel-count chip
semantics. Reuses the feed's filter query so both stay in lockstep.
2026-06-15 12:05:53 +02:00
peter bd2e39d6f8 Merge: promote dev to prod 2026-06-15 04:32:05 +02:00
peter 6a11dd1158 Merge improvement/deep-queue-chip-clarity: clearer deep-backfill status
- improvement(channels): distinct "full history coming" label when another user
  queued a channel's full history (vs your own "full history queued")
- improvement(sync): header shows an active "fetching history" state during deep
  backfill instead of a misleading "all synced"
2026-06-15 04:31:56 +02:00
peter cb35b0e86e improvement(sync): reflect deep backfill as active in the header status
The header status only considered recent-sync pending, so it read "all synced"
while the scheduler was still backfilling full history — directly contradicting
the adjacent "N without full history" notice. Add an active "fetching history"
state (spinner) shown when recent sync is done but deep backfill is still
pending, so "all synced" appears only when nothing is pending at all. The admin
pause button now also shows during deep backfill (it's pausable work). Trilingual.
2026-06-15 04:30:27 +02:00
peter 5fba327d24 improvement(channels): distinct label for full-history queued by another user
The channel manager already styled the two deep-backfill-pending states
differently (solid clickable chip when you requested full history vs a faint
outline when another subscriber did), but both used the same "full history
queued" label, so a channel queued by someone else looked identical to one you
queued yourself. Give the by-other case its own label ("full history coming")
so the distinction is legible at a glance, not just on hover. Trilingual.
2026-06-15 04:23:49 +02:00
peter 5db515f644 Merge feature/s2-shared-library: shared-library feed scope
S2 shared-library feed:
- feat(feed): scope=all query path to browse the whole shared catalog
- feat(feed): Mine/Library header toggle + read-scope-less browse path
2026-06-15 04:14:48 +02:00
peter db4a7c0440 feat(feed): shared-library scope toggle in the header
Add a "Mine / Library" segmented toggle (shown on the feed page) that switches
FeedFilters.scope between the user's own subscriptions and the whole shared
catalog. A read-scope-less user (signed in but no YouTube grant) can now browse
and manage the shared library on their own account — the empty "my feed" state
offers a "browse the shared library" shortcut alongside the connect-YouTube CTA.
scope is preserved across "Clear all" (it's a mode, not a filter) and kept out
of the URL state. Trilingual strings (HU/EN/DE) for the toggle and the CTA.
2026-06-15 04:06:22 +02:00
peter e91ded61bb feat(feed): add scope=all to browse the whole shared catalog
The feed query was always scoped to the user's own non-hidden subscriptions
via an INNER JOIN on subscriptions. Add a scope param: scope=my (default)
keeps that behaviour; scope=all LEFT-joins the subscription instead, so every
video in the shared catalog shows while per-channel priority still resolves
for channels the user is subscribed to. Per-user watch state stays private via
the VideoState outer join in both modes. The priority sort is made null-safe
(coalesce to 0) since unsubscribed channels have no subscription row in all-mode.
2026-06-15 04:06:14 +02:00
peter 44ef725a2e Merge feature/s1-filter-quick-wins: reshuffle button + channel counts on chips
S1 filter quick wins:
- feat(filters): channel-count badge on topic/language chips
- feat(feed): reshuffle button for the shuffle ("surprise me") sort
2026-06-15 03:10:40 +02:00
peter 5a96607557 feat(feed): reshuffle button for the "surprise me" sort
The backend shuffle sort already accepts a seed param. Add a circular
reshuffle button next to the sort control (shown only when shuffle is
active) that re-rolls the seed and re-queries the feed; selecting shuffle
also seeds a fresh order instead of the deterministic seed-0 one. The seed
lives in FeedFilters but is intentionally kept out of the URL state.
2026-06-15 03:08:10 +02:00
peter b325893b22 feat(filters): show channel counts on topic and language chips
The per-tag channel_count was already returned by GET /api/tags but only
surfaced in the chip tooltip. Render it as a small count badge on the chip
face so the relative weight of each topic/language is visible at a glance.
2026-06-15 03:08:03 +02:00
peter 6c8b9d6986 fix(version): VERSION-file as source of truth 2026-06-15 02:22:27 +02:00
peter 07f04b8f90 fix(version): read app version from the committed VERSION file
Make APP_VERSION come from the VERSION file (Dockerfile reads it for the SPA build
and ships it for the backend to read at runtime) instead of depending solely on a
deploy-time build-arg. This keeps the version correct even for a plain
`docker compose build` and removes the first-deploy bootstrap gap. git_sha/build_date
stay best-effort build-args.
2026-06-15 02:22:27 +02:00
peter e98e2cbdd5 Release v0.1.0: watch progress, status bar, version/release-notes, i18n (HU/EN/DE) 2026-06-15 02:09:22 +02:00
peter 53708891b3 docs(release): note multilingual UI in v0.1.0 release notes 2026-06-15 02:09:22 +02:00
peter cdc15ad1f9 feat(channels): clearer error when a YouTube action lacks the scope
api: HttpError now carries the server's detail. Channels sync/backfill/unsubscribe
now detect a 403 (no YouTube grant) and show a 'connect your YouTube account'
message with a Connect action that opens the onboarding wizard, instead of a vague
'failed' toast. Translated HU/EN/DE.
2026-06-15 02:02:05 +02:00
peter ea317c0009 feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
peter 941fb7d756 feat(i18n): translate login and app chrome (HU/EN/DE)
Login screen (with a language picker), header, account menu, sync status, About and
Release Notes dialogs, and the version banner are now fully translated in Hungarian,
English and German.
2026-06-15 00:30:34 +02:00
peter 7aa068061d feat(i18n): foundation — react-i18next, language switcher, server-persisted choice
Set up react-i18next with locale files auto-loaded per area (Vite glob), a compact
LanguageSwitcher, and language as a server-persisted preference (preferences.language)
mirrored to localStorage. On first login the default UI language is guessed from the
Google-reported locale (hu/en/de, else English). vite-env.d.ts types the build-time env.
2026-06-15 00:30:34 +02:00
peter 844fed7d2f Merge: release notes, About dialog, and version stamping 2026-06-15 00:07:13 +02:00
peter 82f0936ca7 feat(ui): About dialog, Release Notes, and new-version banner
About (in the account menu) shows frontend/backend/database versions + build.
Release Notes renders per-version highlights with a commit-SHA reference; a
dismissible banner appears once after the running build's version changes and
links into the notes. Adds a reusable Modal shell and the release-notes data
(detailed v0.1.0).
2026-06-15 00:06:57 +02:00
peter a93ab30fb2 feat(version): /api/version + build-time version/commit stamping
Add a VERSION file (0.1.0) and inject APP_VERSION/GIT_SHA/BUILD_DATE as Docker
build-args (both stages; Vite inlines them into the SPA). New public GET
/api/version returns app_version, git_sha, build_date and the Alembic head as the
database revision. deploy.sh and the localdev build pass the args.
2026-06-15 00:06:57 +02:00
peter 5dd3109286 Merge: repo hygiene for public readiness 2026-06-14 23:20:02 +02:00
peter 167ebc74d8 chore(repo): remove internal deployment plan, sanitize infra references
Drop docs/deployment-plan.md (internal planning doc, kept out of the repo) and
replace the personal lab DB IP and host names in the env/compose/deploy docs with
generic placeholders, so the repo is safe to make public later.
2026-06-14 23:20:02 +02:00
peter 21b9e2710e Merge: auto-complete full history when stored >= video_count 2026-06-14 19:01:04 +02:00
peter 75d122de75 fix(backfill): auto-complete full history when stored >= video_count
A channel whose stored uploads already meet or exceed YouTube's advertised
video_count holds its whole history, but backfill_done could stay false forever
when the deep cursor never reached the end (e.g. a small channel that was never
deep-requested, so the demand-driven deep job never ran). Such channels nagged
as 'needs full history' despite having every video. Add reconcile_full_history()
(idempotent, no quota) and run it at the end of each recent/deep backfill cycle
so backfill_done self-heals.
2026-06-14 19:01:04 +02:00
peter 6683720ab8 Merge: header video-count clarity + idle pause hidden 2026-06-14 18:43:11 +02:00
peter 88e3b466b0 Merge: watch progress (resume bar, continue/restart, in-progress filter) 2026-06-14 18:43:11 +02:00
peter b4109909db feat(header): clarify video counts and hide idle pause button
Show 'N yours / M total' in the header (your subscriptions vs. the whole shared
catalog) with a tooltip, backed by a new total_videos field on /sync/my-status.
The admin pause button now only appears when there's sync work to pause; Resume
still shows whenever sync is paused.
2026-06-14 18:42:55 +02:00