Commit graph

11 commits

Author SHA1 Message Date
npeter83
d610647d13 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
49ab652692 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
bf54b4b5a5 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
f6c5488566 chore: structured timestamped logging across the backend
- uvicorn --log-config (log_config.json): timestamped formatters for uvicorn
  access/error and the app's own "subfeed" loggers (ms precision)
- Log key activity: startup/shutdown, login/denied, token refresh, YouTube API
  errors, subscription imports, sync pause/resume
- Background sync jobs no longer swallow errors silently — failures in RSS poll,
  backfill, enrichment, autotag and resync are logged with tracebacks
2026-06-11 04:26:18 +02:00
npeter83
ecaf516429 chore: surface scheduler logs in container output (INFO for subfeed loggers) 2026-06-11 04:19:54 +02:00
npeter83
e56502789f feat: M4 (part 2) — React reader UI with theming
- Vite + React + TS + Tailwind SPA served by FastAPI (multi-stage Docker build)
- Four color schemes (Midnight default, Forest, Slate, YouTube) x dark/light,
  adjustable text size; persisted to user preferences and localStorage
- Header search, grid/list toggle, theme menu; sidebar filters (show state,
  sort, include Shorts/live, language + topic tag chips with any/all matching)
- Infinite-scroll feed of video cards; click opens youtube.com in a new tab;
  per-video watched / saved / hidden actions with optimistic updates
- SPA fallback routing; login screen for unauthenticated users
2026-06-11 02:19:47 +02:00
npeter83
9a377b7e7e feat: M4 (part 1) — feed API, watch state, user preferences
- GET /api/feed: filter by tags (and/or), channel, duration, age, search; sort
  newest/oldest/views/duration/shuffle; default hides Shorts, live/upcoming and
  watched (was_live VODs stay visible); offset paging with has_more
- POST /api/videos/{id}/state for watched/saved/hidden/new
- User.preferences JSON + GET /api/me and PUT /api/me/preferences
- Migration 0004 (preferences column + video_states table)
2026-06-11 02:11:02 +02:00
npeter83
68dad91e8a feat: M3 — automatic channel tagging (language + topic system tags)
- Tag and ChannelTag models + migration 0003 (partial unique indexes split
  system vs per-user tag names)
- Offline language detection (py3langid) constrained to a curated language set,
  with the channel's declared default language as a strong prior
- Topic tags mapped from YouTube topicDetails + dominant video category; the
  generic "Lifestyle" catch-all is intentionally dropped
- System (auto) tags are regenerated idempotently and never touch user tags;
  orphaned system tags are cleaned up
- GET /api/tags and admin POST /api/tags/recompute; scheduled autotag job
2026-06-11 01:57:19 +02:00
npeter83
cff1d23071 feat: M2 (part 2) — RSS poller, backfill, enrichment, scheduler
- Free per-channel RSS reader for quota-less fresh-video detection
- Recent-first backfill (configurable: 100 videos / 1 year) plus resumable deep
  backfill from the uploads playlist
- Enrichment via videos.list: duration, view/like counts, category, topics,
  language, Shorts heuristic and livestream/premiere classification
- Reusable sync runners + APScheduler jobs (rss / enrich / backfill), all
  quota-aware with a reserve so backfill never starves fresh enrichment
- Manual triggers: POST /api/sync/{rss,backfill,enrich}
- Exact insert counting via RETURNING with in-batch de-duplication
2026-06-11 01:36:41 +02:00
npeter83
24b6e0026d feat: M2 (part 1) — subscription import, YouTube client, quota guard
- Channel/Subscription/Video/ApiQuotaUsage models + migration 0002
- Synchronous YouTube Data API client with OAuth token refresh and per-call
  quota accounting (API key preferred for public reads when configured)
- Subscription import: upsert channels + subscription links, prune unsubscribed,
  fetch channel details (uploads playlist, topics, stats, handle, country)
- Central quota guard tracking units per US-Pacific day against a daily budget
- POST /api/sync/subscriptions and GET /api/sync/status endpoints
2026-06-11 01:22:07 +02:00
npeter83
3a5209e96c feat: M1 foundation — compose stack, FastAPI, Google OAuth, encrypted tokens
- docker-compose with Postgres 16 + slim Python API image
- FastAPI app with session middleware, health endpoint, static login page
- Google OAuth (Authlib) with email invite-list whitelist; admin role support
- User + OAuthToken models; refresh tokens encrypted at rest (Fernet)
- Alembic migrations, run automatically on container startup
- Postgres backup/restore scripts for portability between machines
2026-06-11 01:01:37 +02:00