Commit Graph
11 Commits
Author SHA1 Message Date
peter fca3fa13ef E5 S1: shared Carousel primitive + Plex card-art serializer
Wave 5a building blocks under the Seerr-inspired Plex enrichment (E5).

Carousel (frontend/src/components/Carousel.tsx): the one horizontal-rail
component, replacing the hand-rolled `flex overflow-x-auto` strips and the
home of the future Plex-Discover rows. Edge-fade mask, hover/focus scroll
buttons that appear only on the clipped side (useHScroll — the X-axis twin
of useScrollFade, same watch-the-children discipline), native scrollbar
hidden behind .no-scrollbar, reduced-motion-aware paging. First consumers:
CastStrip + RelatedStrip now scroll inside it (dedup + free affordances).
New i18n: common.seeAll / scrollLeft / scrollRight (en+hu).

Card-art serializer (backend/app/plex/cards.py): movie_card + show_card now
emit `art` (the wide backdrop, /api/plex/image/{rk}?variant=art) so rails and
the crossfading hero get a landscape image with no per-item fetch — gated on
art_key so an art-less item stays null and falls back to the poster. FE type
PlexCard.art?.

Tests: Carousel.dom.test (children/header/see-all, clipped-edge buttons +
paging), test_plex_cards (art present/absent by art_key). Added a
ResizeObserver stub to PlexPersonView.dom.test (it renders CastStrip, now a
Carousel consumer).

Gate: tsc x3 / eslint / prettier / knip / vitest 324 green; ruff green;
backend test lane (docker-compose.test.yml) green on the plex card tests.
2026-08-20 08:50:47 +02:00
peter a393fd2fb1 fix(ui): R3 S1 review fixes — don't blank data on transient refetch errors
- The honest-state branch now guards on `isError && !data`, so a background/window-focus refetch
  failure keeps already-visible content instead of replacing the list with a full error screen
  (regression the bare `isError` introduced). Applied to AuditLog, Channels, ChannelDiscovery,
  PlexBrowse, PlexPlaylistView, Messages(people), Feed — matching the guard already used in
  Messages(conversations)/NotificationsPanel/Scheduler.
- Drop the unused DataTable loading/error/onRetry props (every table page uses the outer
  StateMessage branch, not the props) — reverts DataTable to its simple empty block.
- StateMessage announces errors with role=alert/aria-live=assertive (was polite for all tones).
- Remove the now-unused common.empty string (en+hu), left over from the removed QueryState render-prop.
2026-07-22 04:14:46 +02:00
peter d9a08892a5 feat(ui): R3 S1 — honest error states across the pages
The error state didn't exist as a concept: a failed main query rendered as an "empty" list
("No channels" while the request actually died), so a backend restart read as "everything is
empty". Add the convention that an empty state may NEVER show while a query is erroring.

- New components/QueryState.tsx: shared StateMessage (loading / error+Retry, house style, with
  role=status/aria-live — the app had zero live regions).
- DataTable gains loading/error/onRetry props (suppresses empty-text on error).
- isError branch (before the empty branch) added across the false-empty pages: Feed (retry),
  Channels, ChannelDiscovery, AuditLog, NotificationsPanel, Messages (conversations + people),
  Scheduler, Stats, PlexBrowse grid, PlexPlaylistView. ChannelPage's main content is Feed, now
  covered. common.loadError/empty strings (en+hu).
2026-07-22 03:58:51 +02:00
peter 83e673718f feat(playlists): hybrid table over the drag list, and guard its destructive paths
The column sort and the filters are a VIEW; the playlist's stored order only
changes when the user presses "Save this order", which still goes through the
undoable path. Picking a sort used to write straight to the server, so merely
looking at the list by title rewrote the playlist and marked a YouTube-linked
mirror dirty.

The table:
- playlistColumns.tsx holds the column defs; the body renders them as table rows
  and fills the grip cell itself, since the handle needs the row's sortable
  listeners that a render closure can't reach.
- The # column shows the STORED position, so a view-sort never renumbers the
  playlist under the reader.
- Saving is withheld while a filter is active (it would drop the hidden items),
  and drag is disabled under any view, with the reason spelled out rather than
  silently doing nothing.
- applyView/sameOrder live in lib/playlistView.ts with tests: the sort and the
  channel grouping compose, and getting that wrong produces a plausible order
  that the save button would then persist.
- dnd-kit KeyboardSensor: the grip was focusable and called itself "reorder",
  but only a pointer could move it.

The guards:
- Delete is ONE dialog with every outcome. It was two chained confirms where the
  second one's Cancel/Escape/backdrop meant "delete here only" — dismissing the
  dialog deleted the playlist. ConfirmProvider gained useChoice() for it.
- ConfirmProvider focuses Cancel when an offered action is destructive; the
  confirm button took autoFocus unconditionally, so a stray Enter on an open
  dialog ran "Delete user" or "Clear audit log".
- Removing an item asks first, and rebases the undo history instead of throwing
  it away: useUndoable.rebase maps every snapshot, so the reorder history stays
  valid for the remaining items.
- A failed detail query showed "Loading…" forever; it now says so and offers a
  retry. Rename and reorder failures are surfaced instead of swallowed (reorder
  was a bare .then(), which also produced an unhandled rejection).
- Local reorder/remove no longer require YouTube write scope.
- playlistName() replaces the watch-later localizer copied into three files.
2026-07-20 23:30:49 +02:00
peter 53bc7c62fa feat(welcome): link to the public source repository from the footer
Siftlode is open source; add a discreet 'Source code' link (trilingual) in the
landing-page footer next to Privacy/Terms, pointing at the public repo.
2026-07-01 14:27:42 +02:00
peter 06dda5338f feat(ui): floating back-to-top button on long scrollable pages
A reusable BackToTop fades in once the active page's <main> is scrolled past ~600px and
smooth-scrolls it back to the top. Rendered once at the App root, portaled to <body> for
viewport-fixed placement (bottom-right, below the chat dock), and re-binds to the live <main> on
navigation (page or channel change) so it works on every scrollable page incl. the channel page.
EN/HU/DE.
2026-06-30 05:02:42 +02:00
peter 63bb40afa3 fix(inbox): access-requests nudge points to Users + a jump link, no pile-up
The pending-access-requests notification still said "review in Settings -> Account",
but that moved to the new Users page in 5a. Update the text (EN/HU/DE), and give the
notification a durable inbox link ("Review" -> Users page) via a new access-requests
meta kind (persists across reload, unlike a live action callback). Also stop it
piling up: persisted notices reload as dismissed history, so the old dedupe never
matched and a fresh copy was added every load — now removeByMetaKind drops any prior
access-requests notice before re-issuing, keeping exactly one current nudge.
2026-06-19 16:10:08 +02:00
peter 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
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 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 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