Commit graph

405 commits

Author SHA1 Message Date
npeter83
29d306441a fix(plex): player + card polish from UAT
- Player: control tooltips now open ABOVE the button (no downward viewport clip);
  keyboard use also reveals the auto-hidden controls (wake() on keydown); added a
  Download button that downloads the ORIGINAL physical file (no re-encode/repackage,
  Content-Disposition attachment via GET /stream/{rk}/file?download=1).
- Plex cards: hover affordance shows what a click does (Play / Resume / Open show)
  + a quick watched/unwatched toggle (stopPropagation, no playback) — mirrors the
  YT-feed card quick-actions. New plex i18n keys en/hu/de.
- Verified in a real browser: card hover Play overlay + watched toggle (marks watched
  without opening), player download button present, tooltip-above, keyboard reveals
  controls, correct durations (Enola Holmes 3 = 1:48:04).
- NOTE: 'The Three Diablos' showing 13:05 was NOT a bug — it is a 13-min short (file
  ffprobe = 785s); the 1h42m film is 'The Last Wish'.
2026-07-05 05:26:16 +02:00
npeter83
86b86cb260 feat(plex): P2 player — rich full-page HLS/direct player + watch-state
- backend: GET /api/plex/item/{rk} detail (metadata, cast + intro/credit markers
  from Plex, episode prev/next, per-user resume+status); POST /item/{rk}/progress
  (resume checkpoint, near-end→watched) + /state (new|watched|hidden).
- frontend PlexPlayer.tsx (lazy, pulls hls.js): full-page player over the Plex
  module. Direct files stream raw <video>; remux via hls.js on the seek-restart
  session — custom controls map the ABSOLUTE timeline over the session offset, and a
  seek beyond the loaded region restarts the session at the target. Play/pause/resume
  /restart, ±10s seek, prev/next episode (Shift+←/→), volume, windowed/fullscreen,
  full keyboard, Skip intro/Skip credits from markers (shaded on the seekbar),
  auto-advance + watched-on-end, resume from saved position, 10s progress checkpoints.
- PlexBrowse: card/episode click opens the player as a history subview (browser Back
  returns to the grid/show). api client (plexItem/plexSession/plexProgress/plexSetState)
  + types; plex.player.* i18n en/hu/de. hls.js@^1.6.16.
- Verified over HTTP: detail w/ markers+cast+episode-nav, progress persist. Video
  playback itself is pending browser UAT. Subtitle/audio track SELECTION deferred.
2026-07-05 04:28:00 +02:00
npeter83
219a935121 feat(plex): P1.5 — Plex as a nav module + integrated search + filter sidebar
Per user feedback: promote Plex from a hidden feed-Source option to a first-class
left-nav module, with its own filter sidebar and the shared top search box.

- Plex is now a nav-rail module (page='plex', gated on me.plex_enabled) — its own
  page → correct browser Back/Forward; removed the Feed Source-dropdown 'plex' hack.
- PlexSidebar.tsx: left filter column (library scope, watch-state for movies:
  all/unwatched/in_progress/watched, sort) — per-account persisted (App state).
- Header search box now also serves the Plex page (integrated search); the live
  YouTube-search escalation stays feed-only.
- PlexBrowse.tsx reworked: infinite scroll (IntersectionObserver, no 'Load more'),
  content-visibility for smoother long-list scroll, show drill-down rides history
  (useHistorySubview) so Back returns to the grid; dropped the stray 'YouTube feed'
  button on the show page.
- backend /browse gains a per-user watch-state filter (show=, movies only).
- plex i18n (navLabel + filter.*) en/hu/de.

Note: episode-title 'Episode N' on some shows (e.g. Westworld) is Plex-side
(unmatched show) — we mirror what Plex has; Match/Refresh in Plex + re-sync fixes it.
2026-07-05 03:29:20 +02:00
npeter83
62636319b1 feat(plex): P1 frontend — Plex feed source, browse/search, show drill-down
- PlexBrowse.tsx: self-contained Plex mode (library scope tabs, own search +
  sort, poster-card grid with watch state + playability tint, paged; show →
  seasons/episodes drill-down). Playback announces (P2 wires the real player).
- Feed source dropdown gains a 'Plex' option (gated on me.plex_enabled); App
  swaps <Feed> for <PlexBrowse> when librarySource==='plex' so the video-feed
  hooks don't run in Plex mode. me exposes plex_enabled.
- api client (plexLibraries/plexBrowse/plexShow/plexImageUrl) + types; librarySource
  union + share-URL restore gain 'plex'; new plex i18n namespace en/hu/de.
- Verified over HTTP (authed): libraries, browse+FTS, image proxy 200 image/jpeg.
2026-07-05 02:32:00 +02:00
npeter83
c764d3f1ad i18n(config): friendly labels for the download_* config fields (en/hu/de)
The Downloads config group was added to the registry without field translations,
so it showed raw keys. Adds label+hint for all 9 download_* fields, matching the
Plex group's treatment.
2026-07-05 01:42:56 +02:00
npeter83
5684d6c406 feat(plex): P0 admin Config UI — connection test + library picker
- ConfigPanel 'plex' group auto-renders; adds a Test-connection button + a
  library-picker (checked sections write plex_libraries, applied on Save)
- api.testPlex + PlexTestResult/PlexSection types
- i18n config namespace: plex group + 7 fields + test strings (en/hu/de);
  also fills the previously-missing 'downloads' group label
2026-07-05 01:35:08 +02:00
npeter83
401fe90256 docs(release): note player auto-advance + loop in v0.22.3 2026-07-05 00:21:15 +02:00
npeter83
d02c338465 feat(player): persistent auto-advance + loop playback modes
Two saved-to-account playback settings on the player's prev/next bar (any queued
player — feed or playlist): Auto-advance (Off/Next/Prev/Random — what plays when a
video ends) and Loop (Off / One = repeat the current video / All = wrap the list
at its ends; a single-item list repeats). Stored in users.preferences
(playerAutoAdvance/playerLoop), read from the cached me + written via savePrefs so
they apply everywhere and survive reloads.

The feed's player queue is now the live filtered feed with the watch-state filter
NOT applied — marking the current video watched keeps it in the sequence (no
reindex/reload mid-play), while a hidden video still drops out. Removes the old
frozen-queue workaround and the boolean autoAdvance prop. i18n en/hu/de.
2026-07-05 00:20:50 +02:00
npeter83
597cec6517 docs(release): note the saved-view persistence fix in v0.22.3 2026-07-04 23:42:40 +02:00
npeter83
25e04d3072 fix(views): keep the applied saved view (or filters) across a reload
loadAccountFilters preferred the starred default view's mirror over the stored
filters, so a reload always snapped back to the default even after you'd picked
another view. Now your last-applied filters (setFilters persists them on every
change) win; the default view only seeds a fresh account that has never stored
filters. Re-apply the default from the sidebar to return to it.
2026-07-04 23:42:13 +02:00
npeter83
6c81419057 feat(player): full-height glassy nav strips flanking the modal
Replace the small centred prev/next arrow buttons with faint translucent glass
strips that span the modal's full height, hugging each side of the card (hidden
on narrow screens where there's no room). Same stepping behaviour + Shift+arrow
shortcut; just a larger, easier target that reads as part of the dialog.
2026-07-04 23:42:13 +02:00
npeter83
c81c4a351b feat(player): show the Shift+arrow shortcut in prev/next tooltips 2026-07-04 23:20:02 +02:00
npeter83
7ea91ae42c docs(release): note player stepping + native controls in v0.22.3 2026-07-04 22:26:22 +02:00
npeter83
53eba7a57d feat(player): step through the feed, reachable native controls, hover-intent
Three in-app player refinements:
- Prev/next stepping: the modal now takes the feed's loaded order as a queue, with
  faint arrow zones flanking the card and Shift+Left/Right shortcuts (plain arrows
  seek ±5s). The queue is frozen at open so marking the current video watched can't
  drop it and reload a different one; auto-advance-on-end stays off for the feed
  (it isn't a playlist) — playlists keep theirs.
- Reachable native YouTube controls: the interaction overlay (wheel volume / click
  pause / keyboard focus) now covers only the centre band (top-[12%] bottom-[22%]),
  leaving the top-right cluster (volume/CC/settings) and the bottom bar (seek /
  More videos / fullscreen) clickable. Verified live: settings menu + fullscreen work.
- Hover-intent description: the title popover now waits ~400ms so a quick pass no
  longer flashes it.
2026-07-04 22:25:37 +02:00
npeter83
5d56fbed99 chore(release): v0.22.3 — source URL reference + adaptive watch player 2026-07-04 21:26:08 +02:00
npeter83
2beec6bb18 fix(watch): shrink-wrap the player to the video aspect
A vertical/short clip rendered in a fixed full-width box with large black side
bars. The player container now uses w-fit + mx-auto and the video max-h/max-w, so
a portrait clip becomes a narrow centered player and a landscape clip fills the
width — no letterboxing either way.
2026-07-04 21:25:37 +02:00
npeter83
42d465d760 feat(downloads): store + show a canonical "downloaded from" source URL
Every download now records the clean source page URL and shows it on the
Downloads page (open in a new tab, or copy to clipboard). The worker stores
yt-dlp's canonical webpage_url on the asset (migration 0043 adds
media_assets.source_webpage_url); the serializer prefers it and falls back to a
URL derived from source_kind+source_ref, so YouTube, external YouTube links and
external URLs (e.g. Facebook reels) all get a correct reference, and queued/older
rows work before the worker fills it. Edit clips return null (a clip's source is
the user's own earlier download, not a web page). i18n en/hu/de.
2026-07-04 21:25:37 +02:00
npeter83
1c5a58b4de docs(release): note code-splitting in v0.22.2 2026-07-04 19:44:19 +02:00
npeter83
2344902a7f perf: route- and modal-level code splitting (React.lazy)
The whole app shipped in one bundle, so the logged-out landing and every page
pulled all module code. Split into lazy chunks:
- main.tsx: lazy App + the public leaves (WatchPage, Privacy, Terms), so a
  public /watch share link never downloads the authenticated app.
- App.tsx: each module page (Feed, Channels, Playlists, Stats, Scheduler,
  Config, Users, Settings, Notifications, Messages, Downloads, ChannelPage) and
  the About/ReleaseNotes/Onboarding modals load on demand behind <Suspense>.
- Heavy modals lazy in their parents: PlayerModal (Feed, Playlists),
  DownloadDialog (DownloadButton — kept out of the feed chunk), and the
  VideoEditor/ShareDialog/ProfileEditor (DownloadCenter).
- Extracted focusAccessRequestsTab + the tab constants to lib/adminUsersTab so
  callers can pre-select the admin tab without statically importing the now
  lazy-loaded AdminUsers page (which would defeat the split).

Build now emits ~25 chunks. Landing no longer downloads any module code
(~350 KB deferred); dev landing Perf 93->95. Verified in a real browser: every
page + the video editor load with no console errors.
2026-07-04 19:43:50 +02:00
npeter83
e612e09c4e docs(release): note a11y improvements in v0.22.2 2026-07-04 19:13:35 +02:00
npeter83
35c3d0f2cb perf(feed): intrinsic dimensions + async decode on video thumbnails
Give the feed card thumbnail width/height (16:9) so the browser reserves the
aspect box before load (CLS), and decoding=async so image decode never blocks
the main thread. loading=lazy was already set.
2026-07-04 19:13:11 +02:00
npeter83
92eed1ec9b fix(a11y): accessible names for switches, swatches, selects and inputs
Lighthouse a11y across the authenticated module pages flagged: the clickable
Siftlode logo had aria-label='Feed' (accessible name didn't include its visible
text); the shared Switch, theme swatches, sort selects and several number/range
inputs had no accessible name/label. Give Switch role='switch'+aria-checked and
an optional label (passed at every call site), aria-label the swatches, selects
(feed/playlists sort) and the settings/config/scheduler inputs, and drop the
mismatched logo aria-label so its visible text is the name. All 11 module pages
now score 100 accessibility (settings 86->100, playlists 89->100, others 92-95->100).
2026-07-04 19:13:11 +02:00
npeter83
3911ab48f3 chore(release): v0.22.2 — landing perf + SEO polish 2026-07-04 18:19:07 +02:00
npeter83
603cc9854c fix(auth): /api/me answers 200 when logged out (no console error)
The logged-out landing probed /api/me, which 401'd, and the browser logs every
non-2xx fetch as a console error regardless of how the app handles it. The
bootstrap probe now returns 200 with {authenticated:false} via a new
optional_current_user dependency; api.me() maps that to null. The render gate
treats no-data as 'signed out' -> the landing, and a thrown error as a real
network/5xx failure. Other protected endpoints still 401, so mid-session expiry
is still caught by the global handler. Lighthouse (dev): Best Practices 96->100.
2026-07-04 18:17:24 +02:00
npeter83
2a8d5c0a1e feat(seo): meta description, Open Graph tags and robots.txt
The public landing had no meta description and no robots.txt (the SPA fallback
returned index.html for /robots.txt, so crawlers saw an invalid file). Add a
description + basic OG tags to index.html and a real robots.txt that allows the
landing but disallows the API/auth/watch surfaces. Lighthouse (dev): SEO 83->100.
2026-07-04 18:17:24 +02:00
npeter83
a07fd82ad6 perf(landing): WebP welcome screenshots + long-cache static assets
The 3 landing screenshots were 2.3 MB of PNG (feed.png alone 1.6 MB, decoded
at 2400x1350 for a ~400px slot) — the biggest LCP/transfer cost on the public
page. Re-encoded to WebP capped at 1600px (~284 KB total, feed 91% smaller).
Also set Cache-Control: content-hashed /assets/* are immutable for a year,
other SPA-root static files (welcome images, favicon, robots) get a 7-day TTL;
index.html stays no-cache. Register image/webp+avif mimetypes so FileResponse
serves the right Content-Type. Lighthouse (dev): Perf 80->93, LCP 1.8s->1.2s.
2026-07-04 18:17:11 +02:00
npeter83
95984a7ce4 chore(release): v0.22.1 — iOS/Safari-compatible downloads 2026-07-04 17:30:02 +02:00
npeter83
abedca5b8c chore(release): v0.22.0 — Download Center worker/sidecar in all composes + docs
Prep the Download Center epic (Phase 1 + editor + share) for prod/self-host:
- Dockerfile: create /downloads owned by appuser so a named-volume mount is writable (prod Linux).
- docker-compose.{home,selfhost,yml}: add the 'worker' (yt-dlp/ffmpeg job loop) + 'bgutil-pot'
  (PO-token) services + a downloads mount (DOWNLOAD_ROOT, WORKER_ENABLED). Media defaults to a
  named volume; DOWNLOAD_HOST_PATH points it at a host dir (e.g. a Plex-readable folder).
- README / docs/self-hosting.md / .env.example / install.{sh,ps1}: document the Download Center,
  the two extra containers, and DOWNLOAD_HOST_PATH.
- VERSION 0.22.0 + releaseNotes entry.
2026-07-04 06:31:31 +02:00
npeter83
7ab76ccafb feat(ui): favicon, dynamic tab title, clickable logo, styled module headers
Session-close cosmetics:
1. Favicon — an 'S' monogram (indigo→violet rounded square, path-drawn so it's font-independent)
   at public/favicon.svg, linked in index.html (tab had none before).
2. Dynamic document.title — reflects the current module ('Downloads · Siftlode'); Feed = brand
   only; an open channel page shows the channel name. Shared pageTitleKey() in lib/pageMeta.ts
   keeps the tab title and the top-bar header in lockstep.
3. The 'Siftlode' logo already navigated to Feed but read as static text — added a hover
   background + cursor affordance so it's clearly clickable.
4. Module header — new shared PageTitle component (used by every non-feed module via Header) with
   a tracked small-caps 'eyebrow' + leading accent dot (user-picked style B), styled in one place.
Verified in a real browser: favicon served (image/svg+xml), title updates per module, logo→feed,
header restyled; no console errors.
2026-07-04 06:17:40 +02:00
npeter83
4493501d10 fix(downloads): live-refresh the storage bar so a finished edit/download shows immediately
The 'Your storage' footprint used a one-shot query, refetched only at enqueue time — when the job
is still queued (0 bytes) and the file lands asynchronously via the worker. So the footprint stayed
stale (0 B) until a manual reload. Switch the UsageBar to useLiveQuery(2s), like the library list,
so the footprint updates in lockstep the moment a clip/download completes (and after a delete).
2026-07-04 05:30:43 +02:00
npeter83
1e3fad9a8f feat(downloads): edit + remove actions on shared-with-me items (no re-share)
A shared-with-me item only had a download button. Add two actions (Share is intentionally NOT
offered — no chain re-sharing of someone else's file):
- Edit: the editor now accepts an accessible (owned OR shared) source, so editing a shared video
  produces the editor's OWN clip in their library (counts against their quota, fully theirs
  including share); the source file is only read. Route uses _accessible_job.
- Remove from my list: DELETE /api/downloads/shared/{job_id} deletes only the recipient's share
  grant — the owner's job and physical file are untouched (per-user dismissal).
i18n en/hu/de. Verified in a real browser (edit a shared 70-min video → own 2:31 clip; remove
confirm shows 'won't delete the owner's file').
2026-07-04 05:21:50 +02:00
npeter83
b7f365dd5a feat(downloads): let admins set a quota for any user, not only downloaders
The admin System tab only listed users with an existing footprint, so a quota could be set only
after someone downloaded something. Add a 'Set a user's quota…' picker (over /api/admin/users,
excl. demo) beside the Per-user footprint heading that opens the quota editor for ANY user — the
quota GET/PUT endpoints already resolve defaults + create a row on demand, so no backend change.
Also show an empty-state line when no one has a footprint yet. i18n en/hu/de.
2026-07-04 05:07:23 +02:00
npeter83
1e557db12e fix(scheduler): give the download_gc job a proper name + tooltip
The download-center GC job showed its raw id 'download_gc' with a raw-key tooltip, unlike every
other job. Add its label ('Download cleanup') + description to scheduler i18n (en/hu/de), and make
the job tooltip fall back to empty for any job lacking a description (so a future/edge job never
shows a raw 'scheduler.jobDesc.*' key — also fixes demo_reset).
2026-07-04 04:58:51 +02:00
npeter83
80e2df5450 fix(tooltip): keep hint captions inside the viewport near screen edges
The tooltip clamped its centre to a fixed 92px margin, but the caption is up to 240px wide
(120px half), so a left-edge anchor (e.g. the nav-rail counts icon at x~40) pushed the centred box
~28px off the left of the viewport. Clamp with the caption's max half-width so it can't overflow on
first paint, then a useLayoutEffect re-centres on the anchor using its actual width. Verified: the
nav-rail counts tooltip now sits fully on-screen.
2026-07-04 04:45:51 +02:00
npeter83
214dac5862 fix(sync): keep the pause control inline in the nav rail, not on an orphan line
In the left-nav rail SyncStatus, the pause button rendered as its own bottom block, so with only
deep-history pending (idle) it dropped to a lonely line under 'N without full history'. Now it sits
right-aligned on the primary status row — the sync-state line when there is one, otherwise the
'N without full history' row — via flex justify-between. No change to the all-synced state.
2026-07-04 04:41:13 +02:00
npeter83
391b8fda33 feat(downloads): share UI — user picker + public watch links + watch page
Rework the share dialog into two clear modes and add the public /watch player page:
- ShareDialog: (A) 'Share with a user' — autocomplete picker over registered users (was a blind
  email box that 404'd on non-users); (B) 'Share a link' — create/list/copy/revoke public links
  with allow-download toggle, optional expiry (1/7/30d), optional password; per-link view count.
- WatchPage: standalone login-free player at /watch/<token> (routed in main.tsx like /privacy),
  self-contained mini-i18n (en/hu/de by browser language); password gate → unlock → play; shows a
  Download button only when the link allows it.
- api: ShareLink/ShareRecipient types + link CRUD + recipients; share i18n (en/hu/de).
Verified end-to-end in a real browser: user picker, link create, public playback, stream-only vs
downloadable, password gate + unlock, no console errors.
2026-07-04 04:32:31 +02:00
npeter83
12e610659e fix(downloads): clamp editor hover-scrub popover inside the track
The hover-scrub thumbnail was centered on the cursor with a fixed -translate-x-1/2, so near the
filmstrip's right (or left) edge it overflowed the modal and triggered a horizontal scrollbar. Now
its left edge is clamped to [0, trackW - popoverW] so it shifts inward at the edges and stays fully
within the viewport. Verified at both edges in a real browser.
2026-07-04 03:41:18 +02:00
npeter83
2dccae0e54 feat(downloads): editor v2 — cut-list, join, aspect filmstrip + hover scrub
Rework VideoEditor around a segment cut-list: N draggable cut markers → segments, per-segment
keep/drop (eye toggle + dropped hatch), output = Separate files (one job per kept segment) or
Join into one (segments cut-list → one concatenated file). Per-segment numeric Start/End inputs.
Filmstrip fixed: aspect-correct tiles (no vertical squish) + YouTube-style hover-scrub thumbnail
from the sprite. EditSpec.segments type; editor v2 i18n (en/hu/de). Verified end-to-end in a real
browser (3-segment split, drop middle, join → 0:12 clip from a 0:19 source).
2026-07-04 03:26:33 +02:00
npeter83
04c461837f feat(downloads): editor UI — trim/split/crop clips (phase 2)
VideoEditor modal on a finished Library download: HTML5 <video> scrubber, filmstrip timeline
(lazy server storyboard sprite) with draggable in/out handles, draggable/resizable crop overlay,
per-edit Precise (re-encode) vs Fast (stream-copy) cut toggle, split-into-N fan-out (N trim jobs),
optional clip name. Edited clips show a 'Clip' badge; 'editing' phase gets a % bar. New api
enqueueEdit/downloadStoryboard/storyboardImageUrl + EditSpec type; editor i18n (en/hu/de).
2026-07-04 01:10:42 +02:00
npeter83
fc69656d12 feat(downloads): granular post-processing phase labels
ffmpeg post-steps have no byte-progress, so instead of a silent 'Processing' the row now names
the current step (Merging / Extracting audio / Embedding thumbnail / Removing sponsors / Writing
metadata) with an indeterminate pulse. Byte-progress phases (video/audio) keep the % bar.
i18n en/hu/de.
2026-07-03 04:01:53 +02:00
npeter83
b200f61642 feat(downloads): clear multi-phase download progress + transient-error retries
The progress bar looked broken (95% -> 5% -> recount) because yt-dlp downloads the video and
audio streams separately (each 0->100%) then merges — with no indication of which step is
running. Now:
- worker labels the phase from the stream codecs (video / audio) and via a postprocessor hook
  (merging / processing), so the user sees what's happening
- DownloadCenter shows the phase as the status and renders an indeterminate pulse (no bogus %)
  during merge/processing; phase i18n en/hu/de
- yt-dlp retries (3) + fragment_retries (5) so transient network blips self-heal instead of
  failing the job (seen once as a DNS 'No address associated with hostname' error)

Verified: phase transitions queued -> video -> processing -> done.
2026-07-03 02:42:36 +02:00
npeter83
0045d41a74 fix(downloads): file download honours the per-tab account (?account=)
A plain <a> file download can't send the X-Siftlode-Account header, so current_user resolved
it to the session-default account — 404 'Unknown download' when the tab acts as a non-default
wallet account that owns the file. resolved_user_id now also honours a ?account= query param
(the same wallet-gated selection the WebSocket already uses), and downloadFileUrl appends the
active account id. Verified: default account -> 404, ?account=<owner> -> 206 with the right
Content-Disposition.
2026-07-03 02:08:10 +02:00
npeter83
7d1ed24fea feat(downloads): M5 — Download Center frontend
- api.ts: download types + methods (profiles, enqueue, list/manage, file url, share,
  usage, index, admin storage/quota)
- format.ts: formatBytes / formatSpeed
- i18n: en/hu/de downloads.json (trilingual)
- Downloads nav module (Download icon, active-count badge, hidden for demo) placed after
  Messages; urlState page + App render + Header title
- DownloadButton: self-contained per-card/player affordance (demo-hidden, reflects the
  per-user download index: downloaded / in-queue), opens the preset dialog
- DownloadDialog: preset picker + optional display name -> enqueue + 'View' toast
- ProfileEditor: manage built-in + custom formats (full spec form incl. SponsorBlock)
- DownloadCenter: Queue / Library / Shared / (admin) System tabs, live progress via
  useLiveQuery, usage bar, pause/resume/cancel/delete, save-to-device, rename, share,
  admin storage dashboard + per-user quota editor
- wired DownloadButton into VideoCard + PlayerModal
- lib/nav.ts: decoupled navigator so the download toast can jump to the page

tsc + vite build clean. Verified in a headless authed browser: page + tabs render, Library
shows a real download with usage bar + actions, no console errors.
2026-07-03 01:52:58 +02:00
npeter83
9ca90f515c chore(release): 0.21.0 2026-07-02 02:19:25 +02:00
npeter83
df09b49c0c improvement(welcome): prominent open-source callout in the page body
Add an accent-tinted 'Free and open source' band between the app preview and the feature grid
— a short line about reading the code / self-hosting / filing an issue, linking to the public
repo. The compact footer link stays. Trilingual.
2026-07-02 02:14:27 +02:00
npeter83
d560825685 fix(state): scope all per-account localStorage by account id
Multi-account-in-one-browser (esp. with per-tab accounts) leaked one account's client state
into another via shared localStorage keys. Scope every account-specific key by the tab's active
account (accountKey/readAccount/writeAccount/useAccountPersistedState helpers), so nothing bleeds
across accounts or tabs:

- Real leaks: selected playlist, client notification history + settings, onboarding-dismissed.
- UI position: feed page, channel-manager filter/view + tables, playlist sort, Settings/Stats/
  Users/Config tabs.
- Previously DB-adopted caches (theme, hints, performance mode, sidebar layout, nav/filter
  collapse) — now the cache is per-account too, so there's no flash of the other account's value
  on login.

Kept intentionally global: siftlode.lang (needed pre-login on the Welcome page; the DB pref still
scopes it per-account after sign-in) and siftlode.seenVersion (a per-browser 'new version' banner).
E2EE private keys (IndexedDB) and the chat-dock key were already per-user.
2026-07-02 01:45:16 +02:00
npeter83
59de0ffdfd fix(feed): scope saved filters + default view per account
Feed filters and the default-saved-view mirror lived under shared localStorage keys, so one
account's view (incl. its starred default) leaked into another account signed into the same
browser — visible with per-tab accounts as a fresh account showing the previous account's
'N active' filters instead of a clean default.

Key both by the tab's active account (siftlode.filters.<id> / siftlode.defaultViewFilters.<id>);
load a tab's filters from its own account on login/switch/add, falling back to defaults when the
account isn't known yet. A share link's filters still win and are persisted to the account. The
old shared keys are simply no longer read (a one-time reset to defaults on the first load after
this change; migrating them would risk re-leaking across accounts).
2026-07-02 00:37:23 +02:00
npeter83
f32c3f08dc fix(auth): pin each tab to its account so cross-tab changes don't leak in
A tab that never explicitly picked an account rode the session's default, so when ANOTHER
tab changed that default (adding or switching an account), this tab silently swapped identity
on its next refetch — the account chip updated while the feed/saved-views lagged until reload.

- Pin every tab to whatever account it first loaded as (write the sessionStorage override on
  load when none is set), so its requests always carry its own account header and cross-tab
  default changes can't reach it.
- 'Add account' now clears this tab's pin before the Google redirect, so on return the tab
  adopts the freshly-added account (and pins that) — the current tab switches to it while other
  tabs keep their own, matching the Gmail add-account model.
2026-07-02 00:16:51 +02:00
npeter83
5cd807ec51 feat(auth): per-tab account — different account per browser tab
Two tabs in one browser can now run two different signed-in accounts at once.

- The signed session cookie stays the browser's account WALLET (account_ids). Which account a
  given tab acts as is a per-tab choice held in sessionStorage and sent per-request via the
  X-Siftlode-Account header; current_user honours it only for an account already in the wallet,
  without mutating the cookie's default account. Switching accounts sets the header + reloads
  THIS tab only, instead of the old cookie-wide switch that changed every tab.
- WebSocket can't send headers, so the per-tab account rides in the ?account= query param
  (validated against the wallet).
- Logout is per-tab aware: it signs the requesting tab's active account out of the wallet
  (promoting a new default only if the removed one was the default), and the tab drops its
  override. A stale per-tab header account 401s just that tab instead of clearing the session.
- Serve index.html with Cache-Control: no-cache so a deploy's new hashed bundle is picked up
  immediately instead of the browser running a heuristically-cached stale index.html.
2026-07-01 23:43:35 +02:00
npeter83
c0487101fe polish(layout): tidy filter header + sync status at higher browser zoom
- Filter header: show the active-filter count as a compact number pill instead of
  '{n} active' text, and keep the action buttons on one line (shrink-0 + nowrap) so
  nothing truncates or wraps in a narrow / zoomed-in sidebar (e.g. 125% browser zoom).
- Sync status: anchor the 'all synced' state with a small check icon so it no longer
  reads as orphaned text at the top of the nav rail.
2026-07-01 23:19:06 +02:00