Commit graph

511 commits

Author SHA1 Message Date
npeter83
4a1a025353 feat(settings): explicit Save/Discard for preferences with dirty tracking
Settings-page prefs (theme/scheme/dark-mode/list-view/perf/hints/font + the
notification settings) were each auto-saved to the server on every toggle via
fire-and-forget savePrefs().catch(() => {}) — silent on failure, and no
positive confirmation on success, so the user had zero feedback either way.

Make them a draft instead: changes apply locally for instant preview but
persist only on an explicit Save (or revert on Discard). App owns the live
draft + the last-saved baseline, computes dirty, and exposes a controller to
the panel. The panel shows a Save/Discard bar with 'Saving…' → 'Settings
saved' (auto-clearing) / 'Couldn't save' feedback. Leaving the page with
unsaved changes prompts a confirm (in-app nav + browser Back), and a
beforeunload guards reload/close. savePrefs is now idempotent so the Save
survives a transient gateway blip; failures surface via the connection-lost
status. Language & sidebar layout stay instant (edited outside this page).

New i18n keys settings.save.* / settings.unsaved.* in EN/HU/DE.
2026-06-18 23:59:40 +02:00
npeter83
d61e844f6b Merge bug/optimistic-state-notify-on-success: gate state-change notices on server confirmation 2026-06-18 23:27:59 +02:00
npeter83
9f9d2544ef fix(web): only announce a state change after the server confirms it
The 'Marked watched' / 'Hidden' notices (and resolving a stale notice on
unwatch/unhide) fired synchronously, before the api.setState call resolved.
When the write failed — e.g. the API was unreachable — the optimistic card
override was rolled back in .catch, but the notice had already been emitted
and lingered, falsely claiming a change that never persisted.

Move all three branches into the .then() so the notice is produced only once
the server has actually applied the change; on failure the card reverts and
nothing is announced (the connection-lost status already explains why).
2026-06-18 23:27:59 +02:00
npeter83
0ab6742b44 Merge improvement/connection-lost-self-resolving: honour the 'clears once it's back' promise 2026-06-18 23:17:33 +02:00
npeter83
1278d079e2 fix(web): make the 'connection lost' notice self-resolving
The notice promised 'this will clear once it's back', but nothing ever
removed it: the toast auto-hid on a 15s timer (not on recovery) and the
v0.9.0 unified inbox kept every notify() in persistent history, so the
notice lingered there forever.

Model it as a single live status instead: one sticky, transient (never
persisted) notice while the server is unreachable, removed the moment any
request reaches the server again. Adds a 'transient' notification flag
(sticky toast + excluded from history so a reload can't orphan it) and
replaces the throttled-toast helper with a connectivity lost/restored pair.
i18n the strings (errors.offline.*) in EN/HU/DE — they were hardcoded English.
2026-06-18 23:17:32 +02:00
npeter83
4f7aac7b92 Merge bug/proxy-keepalive-502-retry: stop player POSTs surfacing proxy keepalive 502s 2026-06-18 15:13:09 +02:00
npeter83
6bed9a1427 fix(web): retry idempotent calls on gateway 502/503/504, soften the rest
Retry GET (and opt-in idempotent writes: setState/clearState/saveProgress)
once on a fresh connection after a network error or 502/503/504, masking a
transient proxy<->upstream keepalive reset. If it still fails, treat
502/503/504 as the soft self-clearing 'Connection lost' toast instead of the
blocking error modal — a gateway that can't reach the app means 'restarting',
not a request the app refused. Genuine 500s keep the modal.
2026-06-18 15:13:01 +02:00
npeter83
02a6e5ff94 fix(api): raise uvicorn keepalive above the proxy reuse window
uvicorn's default --timeout-keep-alive is 5s, shorter than Caddy's pooled
keepalive idle window, so the proxy reused connections uvicorn had just
closed -> broken pipe / connection reset -> 502. The player's progress/state
POSTs fire every 5s (resonant with the 5s default) and, being non-idempotent,
could not be auto-retried by the proxy, so those 502s reached the user.
Set --timeout-keep-alive 75 so the upstream outlives the proxy's reuse.
2026-06-18 15:13:01 +02:00
npeter83
d5e4f5353e docs(release): reflect unified notification center in v0.9.0 notes 2026-06-18 04:37:51 +02:00
npeter83
310e9b63b6 Merge feature/followups-batch-and-bell-unify: maintenance batch knob + unified notification inbox 2026-06-18 04:37:27 +02:00
npeter83
d3c6ce659c feat(notifications): unify the two rail indicators into one inbox
Fold the client-side transient bell into the inbox page so there's a single
notification indicator. The inbox now has two groups — "System" (durable,
server-backed) and "Activity" (client-side events with their Unhide/Unwatch/
Find-in-feed actions) — and the nav badge sums both unread counts. The separate
rail bell (NotificationCenter) is removed.

Activity items get a per-item clear (X) alongside the global Clear all. Unhiding
or unwatching a video from anywhere — a card, the toast's Undo, or the inbox —
now quietly resolves the original "Hidden/Watched X" entry (no duplicate "Unhidden
X" toast, no stale entry with a dead action), via a new resolveVideo store helper.
2026-06-18 04:37:20 +02:00
npeter83
dd83718304 feat(scheduler): admin-tunable maintenance re-validation batch size
Expose the maintenance job's rolling re-validation batch (videos re-checked per
run) as an admin control on the Scheduler dashboard, stored in app_state
(migration 0018; NULL = the env/config default). The job reads the effective
value each run via a state helper, clamped to a sane range. Trilingual.
2026-06-18 04:37:08 +02:00
npeter83
264a3c74af docs(release): note admin scheduler run-now + live progress under v0.9.0 2026-06-18 04:02:11 +02:00
npeter83
89be480706 Merge feature/scheduler-run-now: run-now/run-all triggers, live progress, completion notices, nav badge fix 2026-06-18 04:01:38 +02:00
npeter83
00d07b9153 fix(nav): numeric inbox badge with active-state contrast
Show the unread count as a number on the collapsed nav rail (not just a dot), and
invert the badge colours on the active row so it isn't an accent-on-accent red blob;
centre the number on the circle.
2026-06-18 04:01:19 +02:00
npeter83
ed4194a8d3 feat(scheduler): admin run-now/run-all triggers + live progress + completion notices
Add per-job "Run now" buttons and a "Start all now" button to the admin Scheduler
dashboard (admin-gated endpoints). Triggers run the job in a background thread
independent of its interval, refusing a concurrent run (409). While running, the
long jobs (maintenance, enrich, backfill, shorts) report live progress through a
decoupled contextvar sink, shown as a progress bar on the job row via the existing
4s poll. A manually-triggered run posts a completion notification to the triggering
admin's inbox (scheduled runs stay silent to avoid spam); the inbox renders the
"scheduler" type trilingually from type+data. While here, give the maintenance job
its missing dashboard label/description in all three languages.
2026-06-18 04:01:10 +02:00
npeter83
3a0789ebe7 chore(release): v0.9.0 — notification inbox + automatic video maintenance
Bump VERSION to 0.9.0 and add the user-facing release notes for the durable
notification inbox (P1) and the maintenance/validation job that retires videos
that can no longer be played.
2026-06-18 03:22:22 +02:00
npeter83
46fc56371f Merge feature/maintenance-and-inbox: validation job + notification inbox (P1) 2026-06-18 03:20:35 +02:00
npeter83
c4aecf9f77 feat(maintenance): scheduled job to retire unplayable videos
Add a daily maintenance/validation job that detects videos which can't be played
anywhere and retires them safely. Two phases: re-check already-flagged videos
(recover if available again, else hard-delete once the grace period elapses,
cascading to states/playlist items), and a rolling re-validation of the
least-recently-checked currently-available videos that flags newly-unplayable
ones (hidden from the feed immediately via unavailable_since).

Detection is ~free: a video missing from the videos.list response is
deleted-or-private; an `upcoming` premiere >2 days past its scheduled start that
never went live is abandoned. A still-live broadcast is kept (legit 24/7 stream).
Enrichment now also fetches part=status to populate the status columns. Grace is
7 days for removed videos, none for abandoned. Before deleting, affected users
get one batched notification (never per-video). Interval is admin-tunable via the
Scheduler dashboard; batch size and grace are config. Quota-attributed to the
system and bounded by the same backfill reserve as the other jobs.
2026-06-18 03:20:28 +02:00
npeter83
b9a3a9012d feat(notifications): durable per-user inbox (P1) + maintenance schema
Add a server-backed notification center that coexists with the client-side
transient bell: a per-user `notifications` table (type/title/body/data JSON/
read/dismissed), a `/api/me/notifications` CRUD API (list, unread_count, read,
read_all, dismiss, clear), and a left-nav inbox module with a live unread badge
polled via useLiveQuery. Known types render trilingual text from type+data
(English stored text is the fallback); read rows are trimmed past a soft cap.

Also adds the schema the maintenance job builds on: videos.list?part=status
columns (embeddable/privacy_status/upload_status) and the validation lifecycle
columns (last_checked_at, unavailable_since, unavailable_reason).

Page-id validation is centralized in one PAGES source of truth (isPage) so the
new page survives reload without a second allowlist to keep in sync.
2026-06-18 03:20:17 +02:00
npeter83
a11a8db278 chore(release): v0.8.0 — channel columns, tag workflow, video reset, error dialog 2026-06-18 01:18:41 +02:00
npeter83
e85d30a3f3 Merge improvement/tags-ux: tag UX overhaul, user-tags feed widget, video reset, global error dialog 2026-06-18 01:17:38 +02:00
npeter83
1e530d23a6 feat(tags): tag UX overhaul + per-card video reset
- Channel-row tags show only what's attached; a '+' opens a per-channel picker. Clicking a tag
  filters the feed by it; a 'Your tags' sidebar widget makes that filter visible/clearable.
- Tag manager dialog (add/rename/delete; delete only confirms when the tag is in use), reachable
  from the Channel manager and the feed sidebar; hovering a tag's count lists its channels, each a
  link that focuses it in the Channel manager (DataTable gains an external filter input).
- Video cards get a reset action that clears all watch state (incl. an in-progress position).
- api.req() now raises the error dialog on 5xx and 400/409/422 with the server's reason.
2026-06-18 01:17:31 +02:00
npeter83
b55a944e7c fix(ux): modal error dialog for server-refused actions + ESC closes only the topmost
- Duplicate-tag create/rename hit the uq_tags_user_name constraint and 500'd; now caught and
  returned as a clear 409 ('You already have a tag named …').
- New global error dialog (errorDialog store + ErrorDialog modal) for definitive server refusals;
  the api layer wiring + mount land with the rest of the round.
- Modal now keeps a stack so ESC dismisses only the top modal — an error over the tag editor
  closes the error and leaves the editor open.
2026-06-18 01:17:31 +02:00
npeter83
edd8c85132 Merge improvement/channel-aggregates: aggregate columns, attached-only tags, auto-width DataTable 2026-06-17 23:24:31 +02:00
npeter83
0aa16543d9 feat(channels): aggregate columns + smarter tag display
- Add Last upload, total Length and a Normal/Shorts/Live breakdown, computed on read in the
  existing grouped query (~35ms over the full catalog — measured, so no denormalization).
- Subscribers shown compact (919K), since the YouTube API already rounds them.
- Tag cell now shows only the tags actually attached to a channel; a '+' opens a per-channel
  picker to attach/detach, instead of listing every user tag on every row.
2026-06-17 23:24:25 +02:00
npeter83
14da25bf19 feat(ui): content-driven column widths in DataTable
Columns auto-size to their content (table-layout: auto) via a per-column 'nowrap' flag;
headers never wrap. Replaces hand-tuned fixed widths so values like 'N / S / L' no longer
wrap, and callers give a min-width only where a column must not collapse.
2026-06-17 23:24:25 +02:00
npeter83
a7d087c6d3 chore(release): v0.7.0 — data-table channel manager, sidebar reorg, fixes 2026-06-17 19:17:35 +02:00
npeter83
a007a31bbf Merge improvement/channel-manager-1: data-table channel manager + reusable DataTable + admin reset 2026-06-17 19:16:30 +02:00
npeter83
5610dba5eb feat(channels): data-table channel manager
Replace the stacked card rows with the reusable DataTable: sortable columns, in-header
Channel/Tags filters, status chips + page controls in one row, wider layout. Tag toggle
is now optimistic (no full refetch). Sync column collapses to one 'fully synced' chip;
Actions column carries hide/unsubscribe + a backfill control (admin reset / per-user
full-history opt-in). Channel status filter persists across reloads.
2026-06-17 19:16:23 +02:00
npeter83
0210b84166 feat(channels): admin reset / re-backfill endpoint
POST /api/channels/{id}/reset-backfill (admin-only): clear the channel's backfill
markers, re-opt into deep backfill and re-run its recent pull now — a re-fetch-from-
scratch trigger regardless of current sync state. Idempotent (videos upsert by id).
2026-06-17 19:16:23 +02:00
npeter83
9872a0115c feat(ui): reusable DataTable component
Generic client-side table: per-column sort, in-header filters (text/select/multi),
built-in pagination with a user-set page size (incl. All) and an editable jump-to-page
box. Sort/filter/page/size persist to localStorage (survive F5). Falls back to a compact
card list below md. Intended for reuse across modules (channels now, playlists next).
2026-06-17 19:16:23 +02:00
npeter83
4759103f26 Merge bug/settings-tab-persist: keep the selected Settings tab on reload 2026-06-17 14:30:56 +02:00
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