Commit graph

667 commits

Author SHA1 Message Date
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
79e7694b24 feat(feed): /api/facets endpoint for contextual tag counts
Add a facets endpoint that returns per-tag channel counts for the current filter
context (scope, channel, date, content type, search, watch state, and the other
category's tags). Each category is counted with its own selections ignored —
standard drill-down faceting — by a new exclude_tag_category param threaded into
_filtered_query, so selecting one topic doesn't zero out the other topics. Count
is distinct channels with a matching video, keeping the channel-count chip
semantics. Reuses the feed's filter query so both stay in lockstep.
2026-06-15 12:05:53 +02:00
npeter83
6a11dd1158 Merge improvement/deep-queue-chip-clarity: clearer deep-backfill status
- improvement(channels): distinct "full history coming" label when another user
  queued a channel's full history (vs your own "full history queued")
- improvement(sync): header shows an active "fetching history" state during deep
  backfill instead of a misleading "all synced"
2026-06-15 04:31:56 +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
5db515f644 Merge feature/s2-shared-library: shared-library feed scope
S2 shared-library feed:
- feat(feed): scope=all query path to browse the whole shared catalog
- feat(feed): Mine/Library header toggle + read-scope-less browse path
2026-06-15 04:14:48 +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
e91ded61bb feat(feed): add scope=all to browse the whole shared catalog
The feed query was always scoped to the user's own non-hidden subscriptions
via an INNER JOIN on subscriptions. Add a scope param: scope=my (default)
keeps that behaviour; scope=all LEFT-joins the subscription instead, so every
video in the shared catalog shows while per-channel priority still resolves
for channels the user is subscribed to. Per-user watch state stays private via
the VideoState outer join in both modes. The priority sort is made null-safe
(coalesce to 0) since unsubscribed channels have no subscription row in all-mode.
2026-06-15 04:06:14 +02:00
npeter83
44ef725a2e Merge feature/s1-filter-quick-wins: reshuffle button + channel counts on chips
S1 filter quick wins:
- feat(filters): channel-count badge on topic/language chips
- feat(feed): reshuffle button for the shuffle ("surprise me") sort
2026-06-15 03:10:40 +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
6c8b9d6986 fix(version): VERSION-file as source of truth 2026-06-15 02:22:27 +02:00
npeter83
07f04b8f90 fix(version): read app version from the committed VERSION file
Make APP_VERSION come from the VERSION file (Dockerfile reads it for the SPA build
and ships it for the backend to read at runtime) instead of depending solely on a
deploy-time build-arg. This keeps the version correct even for a plain
`docker compose build` and removes the first-deploy bootstrap gap. git_sha/build_date
stay best-effort build-args.
2026-06-15 02:22:27 +02:00
npeter83
e98e2cbdd5 Release v0.1.0: watch progress, status bar, version/release-notes, i18n (HU/EN/DE) 2026-06-15 02:09:22 +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
844fed7d2f Merge: release notes, About dialog, and version stamping 2026-06-15 00:07:13 +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
a93ab30fb2 feat(version): /api/version + build-time version/commit stamping
Add a VERSION file (0.1.0) and inject APP_VERSION/GIT_SHA/BUILD_DATE as Docker
build-args (both stages; Vite inlines them into the SPA). New public GET
/api/version returns app_version, git_sha, build_date and the Alembic head as the
database revision. deploy.sh and the localdev build pass the args.
2026-06-15 00:06:57 +02:00
npeter83
5dd3109286 Merge: repo hygiene for public readiness 2026-06-14 23:20:02 +02:00
npeter83
167ebc74d8 chore(repo): remove internal deployment plan, sanitize infra references
Drop docs/deployment-plan.md (internal planning doc, kept out of the repo) and
replace the personal lab DB IP and host names in the env/compose/deploy docs with
generic placeholders, so the repo is safe to make public later.
2026-06-14 23:20:02 +02:00
npeter83
21b9e2710e Merge: auto-complete full history when stored >= video_count 2026-06-14 19:01:04 +02:00
npeter83
75d122de75 fix(backfill): auto-complete full history when stored >= video_count
A channel whose stored uploads already meet or exceed YouTube's advertised
video_count holds its whole history, but backfill_done could stay false forever
when the deep cursor never reached the end (e.g. a small channel that was never
deep-requested, so the demand-driven deep job never ran). Such channels nagged
as 'needs full history' despite having every video. Add reconcile_full_history()
(idempotent, no quota) and run it at the end of each recent/deep backfill cycle
so backfill_done self-heals.
2026-06-14 19:01:04 +02:00
npeter83
6683720ab8 Merge: header video-count clarity + idle pause hidden 2026-06-14 18:43:11 +02:00
npeter83
88e3b466b0 Merge: watch progress (resume bar, continue/restart, in-progress filter) 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
686c40cbb9 feat(progress): track per-user resume position server-side
Add position_seconds (+progress_updated_at) to video_states so watch progress
survives across devices and can drive a feed filter. New POST
/api/videos/{id}/progress checkpoints the player position (clearing trivially
-early and near-finished positions). Feed serialize exposes position_seconds and
a show=in_progress filter lists started-but-unfinished videos. Un-marking
'watched' now keeps a stored position instead of deleting the row.
2026-06-14 18:40:05 +02:00
npeter83
a16e613fe9 Release: onboarding auto-import, per-user header, channel status filter, stable priority, Phase B security fixes 2026-06-14 07:14:54 +02:00
npeter83
bc87b3abdc Merge: channel status filter, header full-history link, stable priority 2026-06-14 07:08:59 +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
00d167506d Merge: per-user header sync status 2026-06-14 06:55:18 +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
930381f2d4 Merge: onboarding auto-import + empty-feed guidance 2026-06-14 06:36:12 +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
027b8492ee Merge: Phase B security fixes 2026-06-14 05:59:34 +02:00
npeter83
1eeaad61d9 fix(security): patch cryptography CVEs, upgrade pip at build, harden /auth/upgrade
- requirements: cryptography >=46.0.7 (was pinned <46, which excluded the fix for
  the CVEs pip-audit flagged in our Fernet/crypto library). pip-audit now clean.
- Dockerfile: upgrade pip before installing deps (patches installer-level CVEs).
- auth: /auth/upgrade now defaults to the least-privileged read scope; only an
  explicit access=write requests the write scope.
2026-06-14 05:59:34 +02:00
npeter83
842c6d800e Merge: rebrand to Siftlode 2026-06-14 04:40:22 +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
53307ea206 Merge onboarding wizard, scope split, hardening, legal pages, and deploy setup 2026-06-14 01:39:43 +02:00
npeter83
a322f87caf chore(deploy): hardened prod compose, rollout script, and CI for the server
docker-compose.prod.yml targets the server: Postgres is never published, the
app binds to 127.0.0.1 (Caddy proxies it), and every service has a memory/CPU cap
plus no-new-privileges / cap_drop / read-only rootfs. deploy/deploy.sh rolls out
main with a host-side build (migrations run via the entrypoint). CI type-checks
and builds the frontend and byte-compiles the backend on every push to main.
2026-06-14 01:39:43 +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
4765db89de feat(auth): split base sign-in from YouTube scopes for incremental onboarding
Base login now requests only openid/email/profile (non-sensitive), so a new user
gets a clean Google consent with no "unverified app" warning and no 7-day refresh
token expiry. YouTube read (youtube.readonly) and write (youtube) are granted later
by the onboarding wizard via a parameterized /auth/upgrade?access=read|write.

Security fixes folded in from the baseline audit:
- config: refuse to boot in production (https OAUTH_REDIRECT_URL) with the
  placeholder/short SECRET_KEY or a missing TOKEN_ENCRYPTION_KEY, closing a
  session-forgery / admin-impersonation hole.
- main: mark the session cookie Secure when served over HTTPS.
- me: expose can_read; sync/subscriptions returns a friendly 403 (not a 500)
  until YouTube read access is granted.
2026-06-13 23:56:34 +02:00
npeter83
9163a5f68e Merge: smoother long-feed scrolling 2026-06-12 18:17:03 +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
203624518d Merge: stop stale toasts resurrecting on reload 2026-06-12 18:07:31 +02:00