Commit Graph
335 Commits
Author SHA1 Message Date
peter 03e0c68c7c feat(card): tooltip the full video title only when it's clamped
The card title is line-clamped to 2 lines; measure overflow (ResizeObserver, re-checks on
responsive resize) and set the native title attribute with the full text only when it's actually
truncated, so hovering reveals the rest without a redundant tooltip on short titles.
2026-06-30 23:53:44 +02:00
peter 4cd7d4403e feat(channels): search box + clickable tag chips; fix(feed): contextual Your-tags
- Channels manager: prominent channel-name search box and the 'Your tags' chips are now
  clickable to filter the table by tag (replacing the hidden per-column DataTable popovers).
  Both filter client-side over the status-filtered list; a focus-channel intent seeds the
  search, the reset intent clears both.
- Feed 'Your tags' sidebar: count user tags in the facet endpoint (the 'other' category) and
  make the widget contextual like language/topic — counts reflect the current filter and
  zero-count chips hide (e.g. a source=search view with no tagged channels shows 'no matching
  tags' instead of the full static list). EN/HU/DE searchPlaceholder.
2026-06-30 22:30:35 +02:00
peter 56dc1d8ed8 fix(ui): make the back-to-top button resist ad-block cosmetic filters
Ad-block annoyance lists (e.g. Brave Shields) hide floating corner buttons via attribute
selectors like [aria-label="Back to top"] / [title=...]. Drop both attributes and provide the
accessible name with a visually-hidden child span instead, which those selectors can't match.
2026-06-30 05:18:07 +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 607bef1400 polish(feed): one-row toolbar — count after Source, sort right-aligned
Collapse the feed toolbar's two rows into one: the video count sits right after the Source
dropdown, and the sort controls move onto the same row, pushed right with ml-auto (wraps
gracefully on narrow widths). Applies everywhere the Feed renders (main feed + channel page).
2026-06-30 04:56:08 +02:00
peter 421ba09a19 polish(channel): inset, rounded banner card (option C)
Instead of a full-bleed banner, inset it with a margin and rounded corners so it reads as a
contained card; the avatar overlaps its bottom-left. Airier and more designed than edge-to-edge.
2026-06-30 04:48:46 +02:00
peter a1aaa8bff3 fix(channel): let the nav rail navigate away from a channel page
The channel page overlays the content column via channelView, which setPage didn't clear — so
clicking a rail item did nothing (and the next===page early-return blocked 'Feed' when a channel
was opened from the feed). setPage now clears channelView and proceeds even when the underlying
page is unchanged.
2026-06-30 04:47:54 +02:00
peter 859c354150 polish(channel): compact header — stats inline, slimmer banner
- Move the channel stats (subscribers / videos / views / joined) onto one meta line beside the
  handle under the name, dropping the separate stats row.
- Cap the banner at 150px tall so it no longer dominates the page; the wide YouTube-style crop
  is kept. Net: the video grid starts much higher.
2026-06-30 04:43:37 +02:00
peter fb74a31dd3 fix(channel): drop channel-constant sort options on the channel page
Subscribers and Channel-priority sorts are meaningless when the feed is scoped to one channel
(both are constant across its videos), so hide them there via a channelScoped flag on Feed.
The main feed keeps all sorts.
2026-06-30 04:37:00 +02:00
peter 7608e897d8 fix(channel): render the banner wide like YouTube
The stored bannerExternalUrl is the full 16:9 banner template at a low default size (512x288),
so object-contain showed the whole padded image tiny and centered. Now request a crisp wide
version (=w1707) and object-cover the desktop safe-area band (centre 2560x423, ~6:1) at full
width — matching YouTube's banner crop.
2026-06-30 04:32:43 +02:00
peter af2d80ed1f fix(channel): persist across reload, keep banner aspect, fix avatar z-order
- F5 on a channel page kept bouncing to the feed — the App init dropped history.state._chan
  like the (intentionally non-replayed) YouTube search. Now channelView restores from _chan on
  load and the stamp preserves it, so a reload stays on the channel page.
- The banner was stretched/cropped (bg-cover). Render it as an <img object-contain> with a
  capped height, letterboxed on the surface colour, so its real aspect ratio is kept.
- The round avatar's top was clipped by the banner: the banner's position:relative container
  painted above the static identity row. Give that row relative z-10 so the overlapping avatar
  sits on top.
2026-06-30 04:24:52 +02:00
peter 6209016227 fix(channel): show the Exploring badge on first visit
Refetch the channel detail after the auto-explore ingest so `explored` flips true and the
"Exploring" badge appears immediately — the GET that drove the first render predated the
explore that just created the ExploredChannel row.
2026-06-30 04:17:49 +02:00
peter 9810e43cf7 fix(player): open the channel page from the player without it bouncing back
Two bugs made clicking the channel name in the video modal just close the player:
- the second PlayerModal mount (main feed path) was missing onOpenChannel, so the handler
  hit its no-op early return;
- opening the channel synchronously pushed the _chan history entry, which the player's own
  useBackToClose teardown (history.back on unmount) then immediately popped. Now the open is
  deferred to a one-shot popstate listener that fires AFTER that teardown, so the channel
  entry lands at the feed level. Verified: player → channel name → channel page; Back → feed
  (player does not reappear).
2026-06-30 04:14:14 +02:00
peter 3db4aef653 feat(player): channel name opens the in-app channel page
The PlayerModal channel name now opens our channel page (closing the player first); a
small external-link icon beside it keeps the open-on-YouTube behaviour. Threaded
onOpenChannel from Feed into both PlayerModal mounts.
2026-06-30 03:12:44 +02:00
peter c8123dd9ae feat(channels): dedicated channel page + ephemeral explore UI
Frontend for the channel-explore feature:
- ChannelPage: banner/avatar/stats header, Subscribe/unsubscribe, an "exploring" badge
  while browsing an un-subscribed channel, Videos/About tabs. Reuses Feed scoped to the
  channel (scope=all + source=all so the per-user ephemeral videos show). Auto-ingests
  recent uploads on first visit (background, with a loading note) + "Load more from
  YouTube" to page deeper; skipped for demo / already-subscribed channels.
- App: openChannel/closeChannel as a Back-aware sub-view (history.state._chan, mirrors the
  YT-search _yt pattern); ChannelPage takes over the content column, nav rail stays.
- ChannelLink/cards/player: the channel name now opens our channel page (onChannelFilter →
  onOpenChannel); the in-card "only this channel" filter button is dropped (the page
  subsumes it). PlayerModal channel-name wiring follows in the next commit.
- api: channelDetail + exploreChannel; ChannelDetail/ExploreResult types.
- i18n EN/HU/DE: channel namespace, explore_cleanup scheduler labels, explore config group,
  channels_explore quota label.
2026-06-30 03:08:52 +02:00
peter 183e23055e fix(feed): reliably switch to relevance sort when a search starts
The relevance auto-select ran in a Feed effect that raced per-keystroke query
updates, so fast typing left the sort on the default. Move the switch into the
header input's onChange so it's set atomically with the query (only overriding
the default newest sort); the Feed effect now only reverts to newest when the
term is cleared. 'Back to feed' also sets relevance explicitly.
2026-06-30 01:36:12 +02:00
peter 12ee577660 feat(feed): Source filter in Mine, Relevance sort, auto search-source
Frontend for the Mine search finds + relevance search:
- The Source filter (organic / include search / search-only) now shows in Mine
  scope too, not just the Library.
- Returning from a YouTube search via 'Back to feed' switches the Source filter
  to 'search' so your just-found videos show in the feed you land on (filtered
  by the kept term).
- New 'Relevance' sort, offered while a search term is present and auto-selected
  when you start searching (reverts to newest when you clear it). EN/HU/DE.
2026-06-30 00:39:21 +02:00
peter 83a7d27338 chore(release): 0.18.0 2026-06-29 23:41:53 +02:00
peter 291da99a2c feat(player): keyboard + scroll-wheel controls in the video modal
Adds player-modal shortcuts and keeps keyboard focus on the modal (not the
cross-origin player iframe) on open and on player-ready, so they work until you
click into YouTube's native controls:
- F: toggle fullscreen (the player stage element, so the volume flash stays
  visible; a :fullscreen CSS rule drops the 16:9 letterbox). Esc in fullscreen
  exits fullscreen only, without also closing the modal.
- Space: play/pause (ignored while a button/input is focused).
- Scroll wheel over the video: volume up/down with an auto-fading volume-bar
  overlay. A transparent interaction layer over the iframe catches the wheel
  (the cross-origin iframe otherwise swallows it) and maps click to play/pause;
  it leaves the bottom strip uncovered so YouTube's native control bar (seek /
  settings / captions / fullscreen) stays usable, and is hidden on an embed
  error so the 'Open on YouTube' CTA stays clickable.
i18n shortcutsHint EN/HU/DE.
2026-06-29 23:37:18 +02:00
peter 1eb580f6fa fix(search): make the YouTube-search view its own history entry
The live-search results view had no browser-history entry of its own, so a
player opened over the results sat directly on the feed page entry. Pressing
Back (e.g. the mouse back button over the player) could pop past both the
player and the search in one step, bouncing from the search results to the
normal feed instead of just closing the player.

The search is now a feed sub-view that owns a history entry (history.state._yt):
entering a search pushes it, the popstate handler derives ytSearch from it, and
"Back to feed" pops it. Back now steps player -> search -> feed: the first Back
closes only the player (results stay), the second returns to the normal feed.
A reload drops any stale _yt so the first Back can't resurrect a gone search.
2026-06-29 23:03:25 +02:00
peter dca339bd3d feat(search): drop quota wording when the live search source is scrape
The search response now reports its source; in scrape mode (zero quota) the
results banner and Load-more button drop the 'uses quota' wording. Adds the
search_source toggle's labels/hints and updates the per-user-limit hint to note
the cost only applies to the api source. EN/HU/DE.
2026-06-29 22:30:03 +02:00
peter ae7e077852 fix(search): refresh feed after returning from YouTube search
A live search ingests new catalog videos, but the normal feed query was disabled during
search mode and kept its pre-search cache — so 'Back to feed' showed the stale (often empty)
result until a manual reload. Drop the feed / feed-count / facets caches when leaving search
mode so the feed re-fetches fresh and immediately reflects the just-ingested videos.
2026-06-29 02:40:32 +02:00
peter 6ea0972899 fix(search): include Library source filter in shareable URL
The new library_source provenance filter wasn't serialised into the Share-view URL, so a
copied link lost the 'search results only' (or 'include search') selection. Add it as the
'source' param (emitted only in 'all' scope, omitted for the default 'organic').
2026-06-29 02:30:37 +02:00
peter ad921f0233 fix(feed): stop search-box flicker (keepPreviousData + debounced query)
Typing in the search box changed the feed/count/facets query keys on every keystroke, so
each query dropped to its loading state and blanked the content — the whole feed area
flickered. Debounce the search term feeding the queries (the input still updates instantly)
so they only re-run after a pause, and keep previous results on screen during a refetch via
placeholderData: keepPreviousData, so the feed and tag counts update in place without blanking.
2026-06-29 02:19:18 +02:00
peter 18cf277061 feat(search): Library provenance filter (organic / all / search-only)
Replace the binary 'show search-discovered' toggle with a 3-way Source selector in the
Library toolbar, so users can also see ONLY search-discovered videos — not just hide or
mix them. Backend: feed param library_source = organic (default, hides via_search) | all
(both) | search (only via_search), applied in scope=all. Strings in HU/EN/DE.
2026-06-29 02:11:53 +02:00
peter fb57abdf6a feat(search): live YouTube search UI
Surface live YouTube search in the existing feed, triggered explicitly so the
expensive API call is never per-keystroke.

- Header: the search box still filters the local catalog as you type; Enter or a
  YouTube button escalates the term to a live search (hidden for the demo account).
- Feed: a dedicated infinite query renders results in the same VirtualFeed cards +
  in-app player, under a banner with a back button and a quota note. No auto-paginate
  (each page spends 100 units) — an explicit 'Load more (uses quota)' button instead;
  quota/limit errors (incl. 429) shown inline. The empty local feed offers a
  'Search YouTube for <q>' CTA.
- Library: a 'Search-discovered' toggle reveals search-ingested videos (hidden by
  default); sent as exclude_search_discovered.
- Admin: search_daily_limit_per_user config field; new videos_search quota label.
- All new strings translated in HU/EN/DE.
2026-06-29 02:01:44 +02:00
peter cd12374ceb chore(release): 0.17.0 — maintenance epic + messaging account-switch fix 2026-06-29 01:12:22 +02:00
peter 47860337a1 fix(messages): don't inherit a stale chat thread across account switch
Switching accounts does location.reload(), which preserves history.state — so the new
account re-mounted the Messages module from the previous account's _sub sub-view, whose
partnerId belongs to the OLD identity (often the new account's own id → an empty
self-thread). Symptom: after switching to the recipient you land on a blank thread and
the just-received message never shows (F5 keeps history.state); only going back to the
list and reopening the real conversation works.

Fix: (1) strip _sub/_ov from history.state before the switch reload so the new account
starts at the module root; (2) guard Messages so a thread view with partnerId === meId
falls back to the conversation list (can't message yourself).
2026-06-29 00:48:18 +02:00
peter 4736641f6d refactor(player): extract description link-rendering into lib/descriptionLinks
Moves the ~140 lines of pure URL/timestamp/hashtag parsing + renderDescription out
of the 659-line PlayerModal into a standalone, testable module. No behavior change.
2026-06-29 00:24:49 +02:00
peter 3f3d64a977 refactor(i18n,format): translate ETA strings; consolidate time/duration formatters
- formatEta was hardcoded English (violated the trilingual rule) -> time.eta.* keys
  in EN/HU/DE; Scheduler's countdown 'now' likewise.
- relativeFromMs added to format.ts; NotificationsPanel drops its duplicate
  relativeTime/relativeFromMs and the now-orphaned notifications.time.* keys.
- Channels' fmtTotalDuration moved to format.ts as formatTotalHours.
2026-06-29 00:22:21 +02:00
peter a6c2357930 refactor(ui): useDismiss hook for outside-click/Escape close
lib/useDismiss.ts replaces the identical mousedown-outside + Escape effect that
DataTable (filter popover), Channels (tag picker) and AddToPlaylist each hand-rolled.
Positioning stays with each caller (it genuinely varies); AddToPlaylist keeps its
own resize/scroll reposition effect.
2026-06-29 00:11:44 +02:00
peter e04ba3fb7c refactor(ui): shared DraftSaveBar (Settings + Config save bars)
One Save/Discard bar with an inline (Settings card) and floating (Config page)
variant, replacing the two near-identical hand-rolled bars + their state machine.
2026-06-29 00:07:37 +02:00
peter aae123faf6 refactor(ui): shared form primitives (Switch, Section, SettingRow, HintLabel)
components/ui/form.tsx replaces the per-panel copies:
- Switch — was duplicated in SettingsPanel (Switch) + ConfigPanel (Toggle) +
  Sidebar (inline labeled toggle).
- Section (with a card variant) — was in SettingsPanel + Stats (plain) and
  AdminUsers (glass card).
- SettingRow + HintLabel — the label+hint+control row was identical in SettingsPanel + Stats.
No visual change intended (Sidebar's toggle gains the standard knob shadow).
2026-06-29 00:04:45 +02:00
peter 8456b2245d refactor(frontend): adopt store/storage helpers; centralize persistence
- errorDialog + hints now use createStore (drop their bespoke listener arrays).
- theme/sidebarLayout/notifications/App filters/Playlists plSort use readMerged/
  readJSON/writeJSON instead of inline try/JSON.parse/catch.
- Stats, SettingsPanel and App's channel filter/view tabs use usePersistedState
  (the 3 sites that reinvented usePersistedTab inline).
- Every siftlode.* key now sourced from the LS registry (no scattered literals).
2026-06-26 03:30:19 +02:00
peter 7ae3802f38 feat(frontend): add createStore factory + central localStorage registry
- lib/store.ts: createStore<T> — one observable-value primitive (get/set/subscribe/use)
  replacing the hand-rolled listeners/subscribe/emit triad several modules each had.
- lib/storage.ts: LS key registry (every siftlode.* key in one place), readMerged/
  readJSON/writeJSON (the try/JSON.parse/merge/catch done once), and usePersistedState
  (the reactive persisted-string hook, generalizing usePersistedTab).
2026-06-26 03:30:19 +02:00
peter b0eef60878 chore(release): 0.16.2 — access-requests UX + back-navigation fixes 2026-06-26 01:42:12 +02:00
peter 795569530b fix(nav): Back steps through sub-views and closes modals before leaving a page
In-app history only tracked the top-level page, so Back from a module sub-view
(e.g. a Messages thread) or with a modal open jumped straight to the previous
module. Add two history primitives: useHistorySubview (a module's sub-view rides
in history.state, so Back returns to its root first) and useBackToClose (a
mounted overlay occupies one history entry; Back closes the topmost, nesting-safe
so a button-close doesn't trip the modals underneath). Apply to the Messages page
views and to PlayerModal + the shared Modal. setPage now pushes a clean entry so
each page starts at its root.
2026-06-26 01:37:43 +02:00
peter 661a7bcc31 fix(notifications): access-requests nudge auto-clears on approval
The admin access-requests notice was issued from the on-user-load effect (keyed
on user id), so it only re-resolved on a fresh load — it lingered until F5 after
an approval dropped the pending count to 0. Move it to its own effect keyed on
the pending count, so approving/denying (which refetches /api/me) clears or
re-counts the notice live.
2026-06-26 01:19:01 +02:00
peter 88e2d3721f fix(url): strip leftover query string once signed in
The pre-login ?access=requested (and ?verify/?reset) lingered in the address bar
after login. Strip the query string on auth (the logged-in app reads nothing from
the URL); Welcome still reads its params while logged out. stripUrlParams now
preserves history.state so it's safe to call after the in-app page stamp.
2026-06-26 01:13:26 +02:00
peter 7f4469beea fix(notifications): Review link opens the Access requests tab, not the last-used one
The access-requests notice's Review link navigated to the Users page but the
persisted tab (often Demo) loaded instead. Pre-select the access tab before
navigating (focusAccessRequestsTab writes the persisted-tab key, which the page
reads on its fresh mount).
2026-06-26 01:05:14 +02:00
peter 1dd251440e chore(release): 0.16.1 — notification context + YouTube API egress proxy 2026-06-26 00:36:46 +02:00
peter e4b24b1b7d feat(youtube): optional egress proxy for YouTube API calls
New youtube_api_proxy setting (env fallback + admin Config UI, env YOUTUBE_API_PROXY):
when set, the YouTubeClient routes all its httpx traffic through that HTTP(S) proxy.
Lets a dynamic-IP host send API calls through a fixed-IP host (e.g. the server over
private tunnel) so an IP-restricted API key keeps working. Empty = direct.
2026-06-26 00:32:17 +02:00
peter 104c2c9a35 fix(notifications): name the entity in action toasts
Eight confirmations stated a bare fact without saying what they acted on. Pass
the entity through the mutation variables and interpolate it: channel
unsubscribe/reset name the channel, whitelist/demo-whitelist adds name the
email, tag delete names the tag, and playlist revert/push name the playlist.
EN/HU/DE.
2026-06-26 00:32:17 +02:00
peter e75f4176ea chore(release): 0.16.0 — end-to-end encrypted real-time messaging 2026-06-26 00:01:54 +02:00
peter a8481f7716 fix(messages): make the dock new-message flash actually visible
The ring-based flash relied on ring-accent and a CSS transition that never
restarted on a repeat message, so it was effectively invisible. Replace it with
a keyed inset-glow overlay (animation replays on every flash counter bump, and
inset shadow isn't clipped by the window's overflow-hidden) — flashes whether
the window is expanded, freshly auto-opened, or minimised.
2026-06-26 00:00:21 +02:00
peter 3b380957d4 fix(messages): gate on server key state, not just local unlock
The Messages UI trusted only the per-device IndexedDB key, so if the server's
key record was gone (deleted, DB-restored, admin-reset) while a stale private
key lingered in the browser, the app looked 'unlocked' but no one could be
messaged and no setup was offered. Add useKeyState (server 'configured' AND
local unlock): show setup when the server has no key (setup overwrites the stale
local key), unlock when it has one this device hasn't opened, ready otherwise.
ChatThread is now self-contained. Also fix the header title on the Messages and
Notifications pages (was falling through to 'Channel manager').
2026-06-25 22:58:29 +02:00
peter 44e9e33354 feat(messages): auto-open/flash dock on incoming + persist dock across reload
The live-message push now carries both parties, so an incoming message opens
that conversation's dock window if it's closed, or flashes it once if it's
already open (expanded or minimised, without disturbing the minimised state) —
only for messages from someone else, never your own echo.

Dock state (open windows + minimised state) is persisted per user, so a reload
restores exactly what was open, minimised, or closed.
2026-06-25 22:46:54 +02:00
peter 91522d9689 feat(messages): standalone Messages module + floating chat dock
Move messaging out of the notification center into its own left-nav module
(own page, own unread badge), so a reload returns to it and notifications stay
separate. Add a Messenger-style floating dock (bottom-right): pop a conversation
out from the page, keep chatting across navigation, minimise (rollup) or close.

- Messages is now a routed page; NotificationsPanel reverts to inbox-only and the
  nav badge no longer mixes in messages.
- Extract shared KeyGate + ChatThread (used by both the page and dock windows);
  e2ee exposes a shared unlock subscription so page and dock agree.
- ChatDock (always mounted for human users) owns the app-wide live-message
  subscription and per-device key restore. EN/HU/DE strings.
2026-06-25 22:34:24 +02:00
peter 5807faf790 feat(messages): E2EE real-time messaging UI + system welcome
A Messages tab in the notification center (hidden for demo): set up secure
messaging with a passphrase (key generated + wrapped in-browser via WebCrypto,
stored non-extractable per device), unlock on other devices, then chat with
end-to-end encrypted, live-delivered messages. The server-authored Siftlode
welcome is readable before any key setup.

- lib/e2ee.ts: ECDH P-256 + HKDF + AES-GCM, PBKDF2-wrapped key, IndexedDB.
- lib/messagesSocket.ts: WebSocket client with backoff reconnect.
- Messages.tsx: key gate, conversation list with decrypted previews, directory,
  thread with client-side decrypt + encrypt-on-send. Unread feeds the nav badge.
  EN/HU/DE strings.
2026-06-25 22:05:35 +02:00
peter bcba373431 chore(release): 0.15.0 — feed virtualization + keyset pagination 2026-06-25 20:04:25 +02:00