Integrate TMDB credits data into the app: cast carousels on title detail
pages, person detail pages with biography and filmography, and person
search results in the command palette. Includes new persons/titleCast
DB tables, profile image caching, and a nightly credits refresh cron job.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetch trailer video keys from TMDB videos API during title import/refresh,
store in DB, and display a "Trailer" button on title pages that opens a
media-chrome Sutro-themed YouTube player in a fullwidth dialog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add try/catch around `request.json()` in all POST routes to return a
400 instead of crashing on malformed bodies. Validate `type` as a
strict `"movie" | "tv"` enum in search, discover, import, and resolve
routes. Validate `tmdbId` as a positive integer. Add a
`SORT_BY_PATTERN` regex and page-range check (1–500) to the discover
route. Wrap all outbound TMDB calls in try/catch and return 502 on
failure so clients get a structured error rather than an unhandled
rejection.
Fix optimistic-update rollbacks in `use-title-actions`: capture
`prevStatus` and `prevWatches` before each mutation and restore both
atoms in the catch block for catchUp, handleMarkSeason,
handleUnmarkSeason, and single-episode toggle.
Fix a bug in `getContinueWatchingFeed` where the watchDateMap could
hold a stale date for episodes watched more than once; the map now
keeps the most-recent `watchedAt` per episode.
Serialize all backup operations through a promise-based queue
(`withBackupLock`) to prevent race conditions under concurrent
requests. Extract internal helpers so `pruneBackups` and
`deleteBackup` can participate in the same lock.
Strengthen `restoreFromBackup`: write the temp file alongside the
live database (`dbDir`), validate it with a full integrity check,
foreign key check, and a required-tables list before touching the
live DB, then use a synchronous `renameSync` for an atomic swap.
Clear WAL/SHM files synchronously in the same tick to avoid
interleaving. Run migrations after restore so older backups are
brought up-to-date automatically.
Add milliseconds to all backup timestamps (`HHmmssSSS`) to avoid
filename collisions when backups are created in rapid succession.
Update filename regexes to accept both the old and new formats.
Add createLogger to tmdb client, image-cache, availability, and
webhooks services. Replace all silent .catch(() => {}) in metadata.ts
with debug-level error logging. Add progress and success logs in cron
job bodies, colors extraction, and webhook processing. Fix pre-existing
unused parameter lint warning in title-card.tsx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the "On Watchlist" text badge overlay from title card posters.
Instead, indicate library status via a primary-colored card ring and a
small color-coded dot next to the title text with tooltip on hover.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual fetch/useState/useEffect patterns with SWR hooks in
system health section and command palette search, gaining automatic
caching, request deduplication, and stale-while-revalidate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace nested loop queries with batch fetches using inArray() across
discovery, tracking, metadata, settings, and system-health services.
The biggest win is getContinueWatchingFeed() going from ~375+ queries
to 5. Add a cached getSession() wrapper via React.cache() to deduplicate
auth lookups shared between layouts and pages. Parallelize independent
async operations in importTitle(), cacheImagesJob(), and the explore page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rebuild the background jobs card as a sortable table showing each job's
schedule, last run time (live-updating via a new useTimeAgo hook), last
duration, and a manual trigger button backed by a new POST
/api/admin/jobs/trigger route. Extract StatusDot into a shared
component. Add cronToHuman() to display schedule patterns as readable
strings (e.g. "Every 6h", "Daily at 03:00"). Replace static
formatDistanceToNow calls throughout the health section with a
LiveTimeAgo component that refreshes every 30 seconds. Also swap a
handful of section icons for better visual matches across settings cards.
Checks current version against latest GitHub release every 6 hours via
cron job, caches result in appSettings, and surfaces updates through a
toast notification (once per browser session) and an animated badge in
the settings footer. Includes an admin toggle to enable/disable checks
(enabled by default). Also renames ServerSection to RegistrationSection
now that update checks are in their own card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows a subtle progress bar at the bottom of title cards indicating
watched/total episodes, with a tooltip for exact counts. Uses a single
efficient SQL query with JOINs to batch-fetch progress for all visible
titles on the Explore page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title cards now show the user's existing watch status (watchlist,
watching, completed) with a color-coded badge on the poster and a
status-aware quick-add button that prevents re-adding tracked titles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace ad-hoc useState/useEffect fetch patterns with Jotai atoms and
loadables in StatsDisplay, FilterableTitleRow, BackupScheduleSection,
IntegrationsSection, and CommandPalette. Each component now gets a
scoped Jotai Provider with a pre-initialized store so server-rendered
initial values hydrate correctly. Async data fetching moves into
atom-level loadables, eliminating manual loading flags and cancellation
logic throughout.
Add Emby as a third media server integration alongside Plex and Jellyfin.
Emby webhooks use a similar payload format (JSON with nested Item object
and ProviderIds). Also removes the mediaServerUsername field from all
webhook connections since it was never used for authentication — the
token-in-URL is the sole auth mechanism. This simplifies the connection
UX to a single "Connect" button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the standalone GenreBrowser section with FilterableTitleRow components
that add genre chips directly beneath each row heading. Selecting a genre
replaces the carousel content inline; deselecting restores popular titles.
Also fix TitleCardSkeleton spacing to match the real card layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all server actions from scattered page-level files into shared lib/actions/
directory (settings.ts, titles.ts, watchlist.ts) so they can be reused across
the app. Add a hover-triggered plus button on explore page title cards that lets
users add titles to their watchlist without navigating away.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add admin-only system health API and UI showing database stats, TMDB
connection status, background job history, storage usage, and environment
variables (with redacted secrets). Split monolithic backup card into
three focused cards (backups, schedule, restore) with dedicated section
headers for Server, Security, and Backups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "unlimited" (value 0) to the keeping-last dropdown
- Skip pruning when retention is set to unlimited
- Show toast confirmation when changing retention
- Update description to read "Keeping unlimited backups" or "Keeping last N backups"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Show "Next backup in X" in scheduled backups description
- Add download button to manual backup success toast
- Remove redundant schedule description from header
- Reorder day-of-week picker above time picker for weekly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move /setup out of protected (pages) route group so it's accessible
without auth (fixes fresh install redirect loop)
- Redirect /login → /register when zero users exist
- Add "Get Started" button on landing page for fresh installs
- Hide register button/link when registration is closed
- Add auth redirects: logged-in users on /login or /register → /dashboard
- Convert register page to server component with server-side checks
- Fix animation snap on auth form buttons (transition-all → scoped)
- Update proxy middleware with /login and /register routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make backup frequency (6h/12h/1d/7d), time, and day-of-week
configurable via settings, stored in appSettings key-value table
- Add rescheduleBackup() to dynamically update cron job at runtime
- Differentiate manual (sofa-manual-*) vs scheduled (sofa-scheduled-*)
backup filenames with source icons and tooltips in the backup list
- Pruning now only targets scheduled backups, leaving manual ones intact
- Merge backup-actions.ts into actions.ts, use shadcn DropdownMenu for
all selectors (consistent with dashboard stats cards)
- Fix hydration mismatch on relative backup timestamps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created a new `cronRuns` table to track job execution history, including job name, status, timestamps, and error messages.
- Updated job scheduling logic to log job status in the `cronRuns` table, capturing success and error states.
- Enhanced error handling to log detailed error messages in the database.
This commit establishes a foundation for monitoring scheduled jobs and their outcomes.
Background sparkline area charts on Movies and Episodes cards show watch
activity distribution over the selected period. Periods now use rolling
windows (past 24h/7d/30d/365d) instead of calendar boundaries. Bumped
stats grid breakpoint to lg. Upgraded recharts 2.15.4 → 3.7.0 and fixed
chart.tsx types for v3 (TooltipContentProps, LegendPayload).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Movies and Episodes cards now have an interactive dropdown to switch
between Today, This Week, This Month, and This Year. Extracts reusable
getWatchCount helper in discovery service and adds /api/stats route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace progress bar with watch/unwatch buttons on desktop hover.
Move episode counter to the right side next to progress bar. Rename
"Mark all"/"Unmark all" to "Watch all"/"Unwatch all".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clean up settings section design: consolidate backup cards, add
hover-reveal actions with tooltips, improve icon sizing across
components, and replace custom timeAgo helper with date-fns
formatDistanceToNow in webhook card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use date-fns format() instead of toLocaleDateString() for deterministic
server/client date rendering
- Set nativeButton={false} on download link Button to suppress base-ui warning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backup service using VACUUM INTO for WAL-safe atomic snapshots
- Server-side storage in DATA_DIR/backups with download/upload API routes
- Restore with integrity validation and automatic pre-restore safety backup
- Scheduled daily backups via cron with configurable retention
- Admin-only settings UI consolidated under single Server section
- Clean up account section sign-out button, fix switch sub-pixel rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wraps the hero banner h2 in a Link with a subtle primary color
hover transition, matching the app's warm-accent interaction pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title rows (TitleRow, ContinueWatchingList) now use the shadcn Carousel
component backed by Embla with embla-carousel-wheel-gestures, enabling
mouse wheel horizontal scrolling and drag-to-scroll on desktop while
preserving touch swipe, stagger animations, and existing layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-locate `StarRating` and `StatusButton` under the title detail
`_components/` directory since they're only used there. Delete the
unused `SearchAutocomplete` and `SearchBar` components from the shared
`components/` root.
Replaces the 335-line React Context provider with a scoped Jotai store,
giving consumers granular subscriptions and stable handler callbacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
drizzle-orm/bun-sqlite is fully synchronous — all queries return values
directly, not promises. Remove await from all db calls, drop async from
functions that no longer need it, simplify Promise.all patterns that
wrapped sync operations, and fix setSetting() which was missing .run()
(previously masked by await triggering execution via thenable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap the hand-rolled Scheduler class (setInterval-based) for croner,
a battle-tested cron library with overlap protection and proper cron
expressions. Consolidate lib/jobs/ into a single lib/cron.ts and
remove the unused admin jobs API route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Override CSS custom properties (--primary, --ring, --primary-foreground,
--status-watching) on the title detail page wrapper using the vibrant
color from the poster's node-vibrant palette. All Tailwind classes
referencing these variables automatically pick up the new color.
Also await lazy color extraction in getTitleWithChildren() so palettes
are available on first visit to never-before-opened titles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The distinction between "Watchlist" and "Watching" added no value — the
progress bar already shows whether you've started. Now there are just two
visible states: Watching (amber) and Completed (green).
Adding a show via "+ Watchlist" now sets in_progress directly, and both
the old watchlist and in_progress DB values render as "Watching" for
backward compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
overflow-x-hidden on the pages wrapper div created a new scroll
context that prevented position: sticky from working on the header.
Moving it to <body> preserves the horizontal overflow clipping
without breaking sticky positioning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The search modal was inaccessible on mobile since the trigger was
desktop-only. Replace the settings and sign-out buttons (already
available in the mobile tab bar) with a full-width search bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When marking an episode as watched, if earlier episodes are unwatched,
the success toast now includes a "Catch up" action button that offers
to mark all previous episodes as watched in one click.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Status (watchlist/watching/completed) for TV shows is now automatically
determined by episode progress rather than requiring manual selection from
a dropdown. The only manual action is adding/removing from watchlist —
watching and completed states flow from episode data.
- Decouple markAllEpisodesWatched from setTitleStatus and export it
- Replace status dropdown with toggle button + read-only status badge
- Add "Mark All Watched" button with confirmation dialog for TV shows
- Fix optimistic transitions (watchlist → watching on first episode)
- Simplify W keyboard shortcut to toggle watchlist on/off
- Add optimistic completion check when marking entire seasons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace @libsql/client with bun:sqlite for zero-dependency SQLite access,
swap drizzle-orm/libsql adapter for drizzle-orm/bun-sqlite, and rewrite
Dockerfile to use oven/bun:1-alpine. The raw Database instance is no longer
exported via Proxy (native C++ methods lose `this` binding through
Reflect.get); instead, a closeDatabase() helper handles graceful shutdown
and the health check uses Drizzle's db.run() directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace separate DATABASE_URL and IMAGE_CACHE_DIR env vars with a
single DATA_DIR root (default: ./data, Docker: /data). DATABASE_URL
and CACHE_DIR are derived from it but can still be overridden
individually.
Also:
- Pin pnpm to @10 for reproducible Docker builds
- Add --link to COPY instructions for better BuildKit cache reuse
- Add syntax directive for BuildKit features
- Simplify Dockerfile mkdir to just /data (ensureImageDirs handles subdirs)
- Remove redundant DATABASE_URL from docker-compose.yml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support self-hosted OIDC providers (Authentik, Authelia, Keycloak, etc.)
configured entirely via environment variables. Uses Better Auth's
hooks.before to gate email/password sign-up at the endpoint level, and
disables emailAndPassword entirely when DISABLE_PASSWORD_LOGIN is set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>