Commit graph

478 commits

Author SHA1 Message Date
npeter83
3d6e278dd8 fix(settings): persist the active tab across reloads
The Settings tab was local React state, so F5 always snapped back to Appearance.
Persist it in localStorage (siftlode.settingsTab), validated against the tab list.
2026-06-17 14:30:56 +02:00
npeter83
9a9d700cf3 Merge improvement/sidebar-chrome-reorg: grouped rail + chrome controls in sidebar + toast relocation 2026-06-17 14:28:39 +02:00
npeter83
d94f41cb56 feat(toast): surface toasts bottom-left by the bell, brighter dark rim
Toasts rose top-right, far from the notification bell which now lives bottom-left.
Anchor them bottom-left inside the content column (clears the sidebar at any width),
newest nearest the bell. Add a ~50% white border in dark mode so they stand out off
the conventional top-right spot.
2026-06-17 14:28:29 +02:00
npeter83
7a5f52a89b feat(nav): group rail modules + move chrome controls into the sidebar
- Split the rail into a content group (Feed/Channels/Playlists) and an admin
  group (Stats/Scheduler) separated by a divider (system group hidden for non-admins).
- Move the language switcher, About and notification bell out of the top header
  into an icon cluster above Settings (horizontal expanded, vertical collapsed).
  Their popovers portal to <body> and anchor right + above the button, escaping the
  nav's backdrop-filter. About is removed from the account popover (now in the cluster).
- LanguageSwitcher/NotificationCenter gain a 'rail' variant for the above.
- Also relocate the Toaster mount into the (now relative) content column.
2026-06-17 14:28:29 +02:00
npeter83
970ef352ec Merge improvement/quick-wins-1: channel YT links + badge checks, playlist input on top, stats bars 2026-06-17 13:55:49 +02:00
npeter83
e0590c2b6c fix(stats): keep daily usage bars always visible
Each day now has a full-height track behind a solid accent fill, so low-usage
days no longer render as a near-invisible sliver that only stood out on hover.
2026-06-17 13:55:43 +02:00
npeter83
b96fe0caba feat(playlists): move the new-playlist input to the top of the rail 2026-06-17 13:55:43 +02:00
npeter83
ccf044ac2c feat(channels): YouTube link on channel name + done checkmarks
- Channel name: middle-click opens the channel's YouTube page in a new tab
  (left-click still opens the in-app detail) plus an explicit external-link icon.
- recent/full sync badges show a check icon when the state is reached.
- New i18n key channels.row.openOnYouTube (HU/EN/DE).
2026-06-17 13:55:43 +02:00
npeter83
fa4b4ac556 Merge bug/watched-state-500-race: fix watched not persisting at end-of-playback 2026-06-17 13:38:55 +02:00
npeter83
a09e0dda0c fix(player): persist watched at end-of-playback (atomic upsert)
The in-app player marks a video watched when it reaches the end, firing
POST /videos/{id}/state at the same instant as a progress checkpoint. The
progress endpoint deletes the 'new' video_states row near the end, so the
concurrent state UPDATE matched 0 rows and raised StaleDataError -> 500. The
watched status was never persisted (and Feed swallowed the error), so the
video stayed in the unwatched feed even after a refresh.

- set_video_state: atomic INSERT ... ON CONFLICT (uq_user_video) DO UPDATE for
  watched/hidden, immune to a concurrent delete; tolerate StaleDataError on the
  'new' branch and in the progress endpoint.
- PlayerModal: skip the redundant near-end progress checkpoint when we just
  auto-marked watched, removing the self-inflicted race.
- Feed: drop the optimistic override if the server rejects the change, so a
  failed request can't leave a card phantom-hidden.
2026-06-17 13:38:47 +02:00
npeter83
79249104d4 chore(release): v0.6.0 — admin Background Scheduler dashboard 2026-06-16 17:46:15 +02:00
npeter83
25dca48964 Merge feature/scheduler-controls: job tooltips, status legend, editable intervals 2026-06-16 15:58:30 +02:00
npeter83
2a02fb353e feat(scheduler): per-job help tooltips, status legend, inline interval edit
Each job shows a tooltip (what it does + what happens if it stops), a status
dot legend + per-dot tooltips clarify the colours, and the interval is inline-
editable (pencil -> number -> save) wired to the new PATCH endpoint. HU/EN/DE.
2026-06-16 15:58:23 +02:00
npeter83
84aebe16c7 feat(scheduler): admin-editable job intervals (DB-backed, live reschedule)
Add scheduler_settings (per-job interval override, migration 0015) and a
PATCH /api/admin/scheduler/jobs/{id} that persists the override and live-
reschedules the running APScheduler job. Intervals load from the DB (env
defaults as fallback); the snapshot reports the live trigger interval.
2026-06-16 15:58:23 +02:00
npeter83
b71d07c098 chore(dev): self-contained local dev stack (own Postgres + scheduler)
localdev no longer points at the shared database; it now runs its own
local Postgres and its own scheduler, fully decoupled. Removes the shared-DB
migration coupling (no more redeploys after migrations) and lets the
background scheduler actually run during local dev.
2026-06-16 15:21:43 +02:00
npeter83
c011972efb Merge feature/scheduler-dashboard: live admin scheduler dashboard + reusable poll hook 2026-06-16 14:38:58 +02:00
npeter83
73b06b1fb6 feat(scheduler): live admin Scheduler dashboard + reusable poll hook (HU/EN/DE)
New admin Scheduler page (left-nav entry) with a live, self-refreshing view of
job activity, queued work and quota. Polling is factored into a reusable
useLiveQuery hook (pauses when the tab is unfocused) that the notification bell
and future yt-dlp job queue will reuse instead of re-implementing.
2026-06-16 14:38:51 +02:00
npeter83
a339a73bd6 feat(scheduler): admin dashboard endpoint + per-job activity tracking
Record each scheduler job's run activity in-process (running / last result /
error / timestamps) and expose a snapshot. New admin-only GET
/api/admin/scheduler returns the per-job activity (with APScheduler next-run
times), the DB-derived work still queued (channels/videos pending, deep ETA,
live-refresh backlog) and the shared quota picture.
2026-06-16 14:38:51 +02:00
npeter83
e6c53b3f96 chore(release): v0.5.1 — graceful fallback for non-embeddable videos 2026-06-16 12:02:38 +02:00
npeter83
de4d657d42 fix(player): make the unavailable overlay fully opaque
The fallback used a translucent background and left the iframe mounted, so
YouTube's own 'Video unavailable' screen bled through and overlapped our
message. Use a solid background and hide the iframe while the error shows.
2026-06-16 11:58:57 +02:00
npeter83
715eeffff8 Merge fix/player-error-handling: graceful 'Open on YouTube' fallback for non-embeddable videos 2026-06-16 10:45:57 +02:00
npeter83
c2d9e7b5a9 fix(player): friendly fallback when a video can't be embedded
The in-app IFrame player showed YouTube's bare 'Video unavailable' screen
with no way out when a video couldn't be embedded (auto-generated Topic
art-tracks disable embedding -> error 101/150; removed/private -> 100).
Catch the IFrame onError and overlay a clear message + 'Open on YouTube'
button, with a tailored note for embedding-disabled videos.
2026-06-16 10:45:57 +02:00
npeter83
a76bbb5825 chore(release): v0.5.0 — demo account, clearer live videos 2026-06-16 10:23:29 +02:00
npeter83
e12e664579 Merge improvement/demo-banner: permanent demo banner instead of toast 2026-06-16 10:12:10 +02:00
npeter83
848fdd386f improvement(demo): permanent banner instead of a re-popping toast
The shared-account warning was a requiresInteraction toast that re-fired
on every reload. Replace it with a permanent, non-dismissible banner (like
the version banner) shown above the content while in the demo account.
2026-06-16 10:12:10 +02:00
npeter83
e81a494436 Merge fix/live-reenrich: re-enrich transient live videos until they settle 2026-06-16 10:06:58 +02:00
npeter83
b114dae39b fix(sync): re-enrich live videos so ended streams gain duration
enrich_pending only touches enriched_at IS NULL, so a video first seen
while live was stamped live + duration-null and never revisited — staying
'live' with no duration forever after the broadcast ended. Add refresh_live
(run after each enrich pass) that re-fetches anything still live/upcoming,
plus just-ended was_live videos that haven't got their duration yet, until
they settle. Cheap: videos.list is 1 unit per 50 ids.
2026-06-16 10:06:58 +02:00
npeter83
ddaa95f2a6 Merge improvement/live-duration-badge: LIVE/upcoming badge instead of blank duration 2026-06-16 09:36:51 +02:00
npeter83
d42a038a3f feat(card): show LIVE/upcoming badge where duration is absent
Live streams and upcoming/premiere videos have no fixed duration, so the
playlist row and video card left a blank where the runtime usually sits.
Show a LIVE (red) or upcoming badge instead, so it's clear the missing
time is by design, not a sync gap.
2026-06-16 09:36:51 +02:00
npeter83
15811b8b43 Merge improvement/demo-ux: graceful demo handling of unavailable YouTube actions 2026-06-16 09:27:34 +02:00
npeter83
484c6364e6 fix(demo): graceful UX when YouTube actions are unavailable
The shared demo account no longer hits YouTube affordances: the onboarding
wizard never opens (or renders) for it, the empty-feed prompt nudges into
the shared library instead of the connect wizard, and the browser-facing
/auth/upgrade redirects the demo home instead of returning a raw 403 JSON.
2026-06-16 09:27:34 +02:00
npeter83
5b6d898d31 Merge feature/demo-account: shared demo account (whitelist login, gating, admin reset) 2026-06-16 09:17:53 +02:00
npeter83
6405350104 feat(demo): login trigger, feature gating + admin UI (HU/EN/DE)
Login page quietly probes /auth/demo (debounced) as a valid email is
typed/pasted and reloads into the app on a match — no visible button.
Demo sessions default to the whole library, get a one-time shared-account
warning, never see the YouTube-connect onboarding/access UI or sync
actions, and admins get a demo whitelist + reset panel in Settings.
2026-06-16 09:17:34 +02:00
npeter83
e0c63c26d4 feat(demo): admin demo whitelist CRUD + state reset
Admin endpoints to manage the demo email whitelist (DB-backed, no env)
and a manual reset that wipes the demo account's per-user state and
re-seeds a few sample playlists from the shared catalog.
2026-06-16 09:17:27 +02:00
npeter83
5936436d26 feat(demo): hidden /auth/demo login + require_human guard
Whitelisted emails enter the shared demo user via /auth/demo (lazily
created, no OAuth token/scope), rate-limited per IP and answering
uniformly so it can't be hammered as an enumeration oracle. Add a
require_human dependency that blocks the demo account from quota-spending
sync endpoints and the OAuth upgrade flow, and surface is_demo on /api/me.
2026-06-16 09:17:21 +02:00
npeter83
9cac2cd335 feat(demo): demo-account schema + reusable rate limiter
Add the shared demo-account plumbing: users.is_demo marks the single
shared demo user, demo_whitelist holds the admin-curated emails that may
enter it without Google sign-in, and a small in-process RateLimiter
(generic groundwork) for throttling the demo-login endpoint per IP.
2026-06-16 09:17:14 +02:00
npeter83
af84cc0404 chore(release): v0.4.1 — feed toolbar + clearer counts/dates
Bump VERSION to 0.4.1 and add the 0.4.1 release-notes entry (feed view/content-type chips
above the videos, key+direction sort, exact view count + upload date on cards/player).
2026-06-16 03:18:58 +02:00
npeter83
5a2928b810 feat(player): exact upload date inline in the player meta too
Mirror the card change in PlayerModal: append the precise locale-aware date after the
relative time, for both the active video and a linked (navigated) video's stats.
2026-06-16 03:09:28 +02:00
npeter83
71d7440a8b feat(card): show the exact upload date inline after the relative time
There's room on the meta line, so append the precise date (locale-aware, e.g. '12 Jan
2017') after '9 yr ago' instead of hiding it in a tooltip.
2026-06-16 03:04:50 +02:00
npeter83
48e7f8d11e feat(card): exact view count on hover (disambiguate 9B vs 98)
The abbreviated count (e.g. '9B' for ~9 billion) can read like '98' since the B glyph
resembles an 8. Add a title tooltip with the full localized count on the views label.
2026-06-16 02:57:53 +02:00
npeter83
ab439f9717 Merge improvement/feed-toolbar-show-sort: Show chips + key/direction sort 2026-06-16 02:46:26 +02:00
npeter83
4921a95d09 feat(feed): Show chips in the toolbar + key+direction sort
1) Move the Show view filter (Unwatched/In progress/All/Watched/Hidden) up into the
   toolbar chip row as its own single-select group, divided from the content-type chips;
   removed the 'show' sidebar widget (sidebar now = Upload date / Language / Topic).
2) Rework ordering like the Playlists page: a sort-key dropdown (Date, Popular, Duration,
   Name, Channel subscribers, Channel priority, Surprise me) + a single asc/desc arrow
   toggle, instead of separate directional entries. 'Most viewed' is now 'Popular'.
   Backend gains the missing directions (views_asc, title_desc, subscribers_asc,
   priority_asc); the frontend maps (key, dir) -> the backend sort string, so FeedFilters
   is unchanged. Trilingual (feed.sortKey.*, feed.dirAsc/dirDesc). Feed.tsx normalized to LF.
2026-06-16 02:46:26 +02:00
npeter83
a44bdf7fdc Merge feature/1d-feed-toolbar: content-type chips + sort above the feed 2026-06-16 02:34:13 +02:00
npeter83
d96b47ae5d feat(feed): 1D — content-type chips + sort in a toolbar above the cards
Move content type and ordering out of the filter sidebar into a feed toolbar (approved
proposal 2): a prominent content-type chip row (Normal/Shorts/Live toggle chips) with the
sort control + count + reshuffle in a quieter row beneath it. The toolbar renders even on
'no matches' so content-type can be turned back on after it filtered everything out. The
sidebar keeps Show / Upload date / Language / Topic; 'sort' and 'content' widgets removed
from the layout (normalizeLayout drops them from any saved layout). New feed.sortLabel
string (HU/EN/DE).
2026-06-16 02:34:13 +02:00
npeter83
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
npeter83
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
npeter83
2d8c47b04e Merge feature/n3-multi-session: account switch without re-auth (Epic N, N3) 2026-06-16 02:05:38 +02:00
npeter83
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
npeter83
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
npeter83
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