Commit graph

259 commits

Author SHA1 Message Date
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
53708891b3 docs(release): note multilingual UI in v0.1.0 release notes 2026-06-15 02:09:22 +02:00
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
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
npeter83
6683720ab8 Merge: header video-count clarity + idle pause hidden 2026-06-14 18:43:11 +02:00
npeter83
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
npeter83
04c971f623 feat(feed): resume progress bar, play/continue/restart, in-progress filter
Video cards show a resume progress bar for started-but-unfinished videos and a
hover overlay: Play on every card, Continue + Restart on in-progress ones. The
in-app player now resumes from (and checkpoints to) the server position instead
of localStorage, accepts an explicit startAt (Restart -> 0), and refreshes the
feed on close so the card bar reflects the session. Sidebar gains an
'In progress' show filter.
2026-06-14 18:40:12 +02:00
npeter83
3f7298cca0 feat(channels): status filter + header full-history link + stable priority
- header: per-user "N without full history" count (channels_deep_pending),
  clickable with a hint -> opens the channel manager filtered to those.
- channel manager: status filter chips (All / Needs full history / Fully synced
  / Hidden); the header link deep-links to "Needs full history".
- fix: priority up/down is now an optimistic in-place cache update (no refetch /
  re-sort), so the list no longer jumps to the top and loses your scroll position;
  the new order applies on the next page load.
2026-06-14 07:08:59 +02:00
npeter83
6d2f84f791 fix(header): show per-user sync status instead of the global catalog
The header status bar read the global /api/sync/status (videos_total +
channels_backfilling), so every user saw the whole catalog's numbers —
confusing and a small cross-user info leak (e.g. "3 syncing" for a user with 2
channels). It now uses /api/sync/my-status: the user's own available video count
and how many of their own channels are still being fetched
(channels_recent_pending). The pause control stays admin-only via an isAdmin prop.
2026-06-14 06:55:18 +02:00
npeter83
4f80262046 feat(onboarding): auto-import subscriptions after read grant + empty-feed guidance
After read access is granted the wizard now imports the user's YouTube
subscriptions automatically (with a "Building your feed…" progress state), so a
new user lands on a populated feed — channels already in the shared catalog show
up instantly, new ones backfill in the background. The empty feed now prompts
users without read access to set up via the wizard instead of a bare message.
2026-06-14 06:36:12 +02:00
npeter83
9ace042510 chore: rebrand Subfeed -> Siftlode
Rename all user-facing references (UI wordmark Sift+lode, titles, app name,
legal pages, onboarding wizard, emails, README/docs) and infra paths
(/srv/subfeed -> /srv/siftlode, image tag, deploy script, backup filenames).

Internal identifiers kept on purpose: Postgres user/db "subfeed", logger
namespace, localStorage keys, and the subfeed_pgdata volume (renaming would
orphan the migrated production data).
2026-06-14 04:40:22 +02:00
npeter83
6a71e3d943 feat(legal): public privacy policy, terms, and homepage for OAuth verification
Adds login-free /privacy and /terms pages (rendered outside the authenticated
tree via a pathname switch in main.tsx) carrying the Google API Services Limited
Use disclosure, YouTube ToS / Google Privacy links, and contact + data-deletion
info. The sign-in screen now describes the app and links to both, satisfying
Google's homepage + privacy-policy requirements for the OAuth consent screen.
2026-06-14 01:29:44 +02:00
npeter83
e0980487af feat(onboarding): first-login wizard for incremental YouTube consent
After the clean name/email sign-in, a wizard walks the user through granting
YouTube read (then optionally write) one step at a time, each with a plain
rationale and an up-front heads-up about Google's "unverified app" screen.

The visible step is derived from the granted scopes (can_read/can_write) so the
flow resumes correctly across the full-page consent redirect; it's dismissible
and reopenable from Settings -> Account, which now lists read and write as
separate, individually-grantable access rows.
2026-06-14 01:11:29 +02:00
npeter83
a8e98d03ae perf(feed): smoother scrolling for long feeds
Three low-risk wins for large filtered feeds:
- content-visibility:auto on cards (.cv-card/.cv-row) so the browser skips
  layout/paint for off-screen cards; contain-intrinsic-size keeps the scrollbar
  stable and is remembered per card after first render.
- memo(VideoCard) + stable onState/onChannelFilter callbacks (onState reads the
  loaded list via a ref) so appending a page only renders the ~60 new cards
  instead of reconciling every card already on screen.
- Prefetch the next page earlier (sentinel rootMargin 800px → 1500px) so the
  'Loading more…' flash is far less likely during fast scrolling.
2026-06-12 18:17:03 +02:00
npeter83
a64a8830eb fix(notifications): don't resurrect stale toasts on reload
Persisted notifications were restored with their saved 'dismissed' state, so any
that were still active when the page closed came back as active toasts — but
their auto-dismiss timers aren't re-armed on load, leaving them stuck on screen
forever (a burst of errors could pile up dozens). Mark restored entries as
dismissed on load: they stay in the bell history but no longer reappear as live
toasts. The toast surface is session-transient; history persists.
2026-06-12 18:07:31 +02:00
npeter83
a640b181ee fix(ui): robust avatars — no-referrer + graceful fallback
Channel/account avatars come from Google's image CDNs (yt3.ggpht.com,
lh3.googleusercontent.com). On a feed page dozens load at once; the CDN
rate-limits the referrer-bearing burst (429), so a random subset rendered the
browser's broken-image icon (the URLs themselves are valid — verified 200).

Add a shared <Avatar> that sets referrerPolicy="no-referrer" (which the CDNs
serve without throttling) and falls back to a neutral initial placeholder on
error instead of the broken-image icon. Use it for video-card, player, channel
manager, header and settings avatars.
2026-06-12 18:01:43 +02:00
npeter83
db0ffc3717 fix(feed): clear optimistic overrides on refetch
The feed kept per-video optimistic status overrides until filters changed, so a
status reverted elsewhere (e.g. Unwatch from the notification center) was masked
by the stale override and stayed filtered out of the current view. Also clear
overrides when fresh feed data arrives — the server is then authoritative.
2026-06-12 17:39:28 +02:00
npeter83
d5dab5377f feat(notifications): notify on 'watched' with an Unwatch action
Mirror the hide flow for watched: marking a video watched (card, modal toggle,
or auto-watch) raises a toast and a bell-history entry with an Unwatch action —
no Find-in-feed, just revert. NotifMeta becomes a discriminated union
(video-hidden | video-watched); unhide/unwatch share one revert-to-new helper.
2026-06-12 17:39:28 +02:00
npeter83
1bdde051c1 feat(player): linkify descriptions and play YouTube links inline
- Narrow the title hover target to the actual text, not the whole row.
- Linkify descriptions: timestamps (mm:ss / hh:mm:ss) seek the player; emails
  become mailto:; hashtags link to YouTube's hashtag feed; other URLs open in a
  new tab. Blank lines are stripped so the popover isn't mostly whitespace.
- YouTube links play in the inline player: a link to the current video seeks
  (honoring t=), a link to another video navigates the player to it, with a Back
  button to the original. While on a linked video the title/author come from the
  player and its views/date/duration + a clickable channel come from the detail
  endpoint, which falls back to the YouTube API (videos.list, attributed to the
  user) for videos not in our DB.
2026-06-12 17:39:20 +02:00
npeter83
195044042f feat(player): polish the description popover (opaque, portaled, opens upward)
The popover bled through (it used the translucent glass surface), was clipped by
the modal card's overflow, and ran off the bottom of the viewport. Make it an
opaque surface, render it through a portal to <body> with fixed positioning so
nothing clips it, and anchor it above the title so it grows upward where there's
room. Add a small hover grace so the pointer can travel title → popover.
2026-06-12 17:39:08 +02:00
npeter83
fcb77ac2e1 feat(player): watched controls, compact layout, description popover
- Watched: an explicit toggle in the modal (Mark watched / Watched→unmark) plus
  auto-mark when playback reaches the end (within 10s, or on the ended event).
- Compact layout: drop the header bar and the redundant 'Open on YouTube' button
  (the embed's own YouTube logo already jumps out); Close moves to the title row,
  channel + meta share one line — fits without a scrollbar at higher zoom.
- Card actions reflect status: watched shows a double-check, saved a filled
  bookmark, with matching tooltips.
- Description: new GET /api/videos/{id} exposes the already-stored description,
  shown in a popover when hovering the modal title (fetched lazily).
2026-06-12 17:39:01 +02:00
npeter83
99dfa7691c feat(player): in-app modal YouTube player with resume
Left-clicking a feed card (Ctrl/Cmd/middle still open youtube.com in a new tab)
opens a modal that plays the video in-app via the YouTube IFrame Player API
instead of leaving the app. Using the JS API (not a bare embed) lets us read the
playback position: it's checkpointed per-video in localStorage and on close, and
restored via the 'start' param when the video is reopened. The modal closes via
a header button, the backdrop, or ESC (ESC only while focus is on our page — a
cross-origin iframe owns its own key events).
2026-06-12 17:38:45 +02:00
npeter83
d0b8ef796a fix(sidebar): resolve channel filter name from channel list after refresh
The channel filter chip stored only the channel id in the URL, so after a
page refresh the human name was lost and it fell back to "This channel".
Resolve the title from the cached channels list keyed by id, and show a
"Loading…" label instead of the misleading fallback while it resolves.
2026-06-12 14:08:49 +02:00
npeter83
f255728f75 feat(stats): per-user API quota attribution + admin usage page
Track who burned how much YouTube API quota. A QuotaEvent audit log (migration
0009) records every spend with the triggering user (NULL = background/system) and
an action label, set via a request/job-scoped contextvar (quota.attribute) so no
call signatures change. User-initiated work (sync subscriptions, unsubscribe,
opt-in recent backfill, manual enrich) attributes to the user; scheduler work to
System, split by action.

- backend: QuotaEvent model + migration 0009; quota.attribute() contextvar;
  record_usage logs events; entry points wrapped (routes/sync, routes/channels,
  scheduler); GET /api/quota/my-usage + GET /api/quota/admin
- frontend: admin-only Stats page (header nav, page=stats) with daily bars +
  per-user breakdown by action and range picker; 'Your API usage' in Settings ->
  Sync for every user

Verified: attribution + endpoints compute correctly; events are per-user vs System.
2026-06-12 02:47:55 +02:00
npeter83
de0decee59 fix(channels): reflect shared deep-queue state in the full-history chip
deep_requested is per-subscription, but deep backfill is channel-wide and shared:
once any subscriber requests full history, the whole back-catalog arrives for
everyone. The chip only looked at the current user's flag, so a second subscriber
saw a misleading 'get full history' on a channel already queued by someone else.
Add a channel-level deep_in_queue to /api/channels and show an informational
'full history queued' badge (vs the owner's cancelable button) in that case.
2026-06-12 02:29:10 +02:00
npeter83
2add173760 feat(m5c): onboarding — DB invites, request-access, admin approval, email
Move the access whitelist from the ALLOWED_EMAILS env var into a DB Invite table
(env kept as bootstrap fallback), and add a self-service request + admin approval
flow with fail-soft email.

- models: Invite(email, status pending|approved|denied, requested_at, decided_*)
- migration 0008: invites table; seed env ALLOWED_EMAILS u ADMIN_EMAILS as approved
- auth: is_allowed() (DB-first, env fallback); a denied Google login records a pending
  request and bounces to /?access=requested instead of a raw 403; public POST
  /auth/request-access; upsert is idempotent so repeats don't re-spam admins
- routes/admin.py (admin-only): list/approve/deny invites + manual add
- email.py: smtplib + Gmail App Password, fail-soft (skips if SMTP unset)
- /api/me exposes pending_invites; config + .env.example gain SMTP_*
- UI: Login 'Request access' form + access=requested/denied handling; Settings ->
  Access requests (approve/deny + add); admin nudge toast on pending requests

Verified locally: request-access creates a pending invite and emails the admin;
seed approved npeter83; guinea-pig yt.trash2023 denied until approved.
2026-06-12 01:43:07 +02:00
npeter83
15007250db feat(channels): kick recent backfill immediately on 'full history' opt-in
When a user requests full history for a channel whose recent uploads aren't
fetched yet, run a one-channel recent backfill synchronously in the request so
the feed populates at once instead of waiting for the scheduler. Deep paging
still follows on the scheduler (recent-then-deep). The deep-toggle mutation now
also refreshes my-status and the feed.
2026-06-12 00:00:50 +02:00
npeter83
43c05ea14b feat(m5b): optional YouTube write scope via incremental OAuth
Default login now requests read-only (youtube.readonly); write (unsubscribe,
later playlist export) is an explicit opt-in.

- auth.py: split READ_SCOPES / WRITE_SCOPES; new GET /auth/upgrade (incremental
  consent, prompt=consent); has_write_scope() helper
- /api/me exposes can_write
- youtube/client.py: delete_subscription (50 units, OAuth-only)
- DELETE /api/channels/{id}/subscription, gated on write scope (403 otherwise)
- UI: Settings - Account 'Playlist editing & YouTube export' enable button;
  per-channel 'Unsubscribe on YouTube' (with confirm) shown only when can_write

Browser-facing; develop/test locally until the public HTTPS login lands. Needs a
one-time Console step: add youtube.readonly to the OAuth consent screen scopes.
2026-06-11 23:27:11 +02:00
npeter83
beb961c021 feat(m5d): demand-driven deep backfill + per-user ETA
Per-user opt-in to full-history (deep) backfill so a new user's unique
channels no longer trigger a big shared-quota burst.

- migration 0007: Subscription.deep_requested (default false); seed admins'
  existing subscriptions to preserve today's global-backfill behaviour
- run_deep_backfill is now demand-driven: only channels at least one user has
  requested are deep-backfilled; recent backfill stays unconditional (cheap)
- estimate_deep_backfill ETA helper (quota-bound) surfaced in /api/sync/my-status
- POST /api/sync/deep-all to opt all my channels in; PATCH channels accepts
  deep_requested
- UI: per-channel Full history toggle, Backfill everything action, deep
  progress + ETA in Channels header and Settings - Sync
2026-06-11 23:07:09 +02:00
npeter83
0a6befd578 style(ui): glass opacity 82% -> 78% 2026-06-11 22:49:04 +02:00
npeter83
94a077d699 fix(ui): restore glass translucency (82%) — opaque value killed the effect
backdrop-filter blur does work here; the previous 94% opacity made .glass nearly
solid so the blurred backdrop never showed. Dial back to 82% with a strong blur so
overlays read as frosted glass again, while staying less see-through than the old
72% that made the account menu look too transparent.
2026-06-11 22:45:14 +02:00
npeter83
f429889250 fix(ui): opacity-based frosted glass; drop full-page settings blur
Evidence (sharp video behind overlays) shows backdrop-filter is a no-op here, most
likely because browser hardware acceleration is off — so the glass relied on a blur
that never rendered. Lean on opacity instead (94% surface) so overlay menus/panels
read as frosted glass regardless of GPU; keep the blur as a bonus when available.
Revert the settings backdrop to a plain dim scrim (no whole-page blur) and make the
header a solid bar (no backdrop-filter ancestor).
2026-06-11 22:39:52 +02:00
npeter83
4a35e8b42a fix(ui): stronger frosted glass + frosted settings backdrop; declutter header
- Increase .glass blur (32px) so overlay menus (account) read as proper frosted glass.
- Blur the settings modal backdrop (overlay backdrop-blur) so the panel reliably
  frosts the content behind it instead of showing a sharp video through.
- Remove the now-redundant header buttons (dark/light, color scheme, grid/list) —
  they live in Settings → Appearance. Header keeps search, sync, bell, account.
2026-06-11 22:16:07 +02:00
npeter83
b7d448b403 perf(ui): drop backdrop-filter from bulk glass cards
Video cards / channel rows / sidebar widgets render in bulk over a solid background
where backdrop blur adds little but is GPU-expensive and triggers forced reflow
(browser perf 'Violation' logs). Keep translucency + depth; reserve blur for the few
.glass overlay surfaces (menus, panels, toasts, tooltips).
2026-06-11 22:04:01 +02:00
npeter83
3f4d5ed9d7 fix(dev): vite proxy to 127.0.0.1 + throttle error notifications
The dev proxy targeted 'localhost', which Node can resolve to IPv6 ::1 that the
Docker publish doesn't answer after a container recreate — every /api call failed,
spamming 'Network error'. Pin the proxy to 127.0.0.1. Also collapse bursts of
connection/5xx failures into one notification per 30s so a brief restart no longer
floods the notification center.
2026-06-11 22:00:57 +02:00
npeter83
bc9b9a854c fix(ui): portal tooltips, opaque glass, settings rail+stable height, notif test, channel help
- Tooltip: render in a portal with fixed positioning + edge-flip so hints are never
  clipped by overflow/stacking ancestors (fixes mispositioned/hidden bubbles app-wide).
- Glass: raise opacity so overlay menus/panels stay readable over content.
- SettingsPanel: vertical tab rail (no wrapping/jumping), content grid-stacked so the
  panel sizes to the tallest tab (stable height) and floats to its content height.
- Notifications: the test toast is now a normal auto-dismissing toast (with countdown
  bar) that also plays the sound via a new force-sound flag.
- Channel manager: explain priority/tags/hide and what 'Sync subscriptions' does;
  add a 'Channel priority' feed sort so priority is actually meaningful.
2026-06-11 21:30:25 +02:00
npeter83
002579e5e5 feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
  performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
  sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
  horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
  notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
  hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
npeter83
a8822d3935 feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
npeter83
a419ac2943 fix(ui): notification UX — outside-click close, toast countdown, hide entry actions
- NotificationCenter closes on outside click (document listener, not an overlay
  that the blurred header trapped) and no longer needs a second bell click.
- Toasts show a level-colored countdown bar and auto-dismiss faster (6s default).
- Hidden-video notifications carry structured meta so the center offers an in-app
  'Find in feed' (jump to that channel's hidden videos) and a one-click 'Unhide',
  working even after a reload when the live Undo callback is gone.
2026-06-11 19:46:58 +02:00
npeter83
a105e5c184 feat(ui): notification center with leveled toasts and error capture
- Replace the toast store with a notification store: levels (info/success/
  warning/error/fatal), requiresInteraction, and a persisted history.
- Move toasts to the top-right, styled per level, with manual dismiss.
- Add a bell in the header opening a Notification Center (history, unread badge,
  'needs attention' vs info, clear all).
- Capture network failures and 5xx responses (api layer) and render crashes
  (ErrorBoundary) as notifications.
- Sound + server-sourced events + per-account settings remain for 6b.
2026-06-11 19:26:34 +02:00
npeter83
ae0cd89e20 feat(ui): customizable sidebar — collapsible, reorderable, toggleable widgets
Each filter group is now a card with a collapse chevron. An Edit mode (pencil)
reveals drag handles (@dnd-kit) to reorder and eye toggles to show/hide widgets,
plus Reset to defaults. Layout (order/collapsed/hidden) persists to localStorage
and the server preferences blob, adopted on login.
2026-06-11 19:21:48 +02:00