- Add `lib/constants.ts` exporting `DATA_DIR`, `DATABASE_URL`, `CACHE_DIR`,
`BACKUP_DIR`, `AVATAR_DIR`, `TMDB_API_BASE_URL`, and `TMDB_IMAGE_BASE_URL`
- Replace inline `process.env` derivations in `db/client`, `backup`,
`image-cache`, `system-health`, `tmdb/client`, `actions/settings`,
and `api/avatars` with imports from the new module
- Add `uploadAvatarAction` and `removeAvatarAction` server actions;
store files under `DATA_DIR/avatars/{userId}.{ext}` via Bun.write
- Add `GET /api/avatars/[userId]` route that scans the avatar dir with
Bun.Glob and serves with immutable Cache-Control (cache-busted via
query param)
- Refactor AccountSection with hover overlay, AnimatePresence fade,
and file input; click avatar to upload (no image) or remove (has image)
- Replace NavBar user initial badge with Avatar + DropdownMenu showing
name, email, settings link, and sign-out
- Thread `userEmail` and `userImage` props through AuthenticatedShell
→ NavBar and SettingsPage → AccountSection
- Delete filterable-row, backup-schedule, integrations, and system-health
atom files; replace with useState/useTransition + server action calls
- Replace /api/explore/discover route with discoverByGenre server action;
refactor FilterableTitleRow to call it directly via useTransition
- Wrap PagesLayout and AuthLayout children in Suspense to fix dynamic
rendering errors during PPR static generation; remove StoreProvider
- Sequence ExplorePage session fetch before TMDB calls to prevent
build-time requests during static generation
- Drop unnecessary "use client" directives from dashboard and person
components that have no client-side hooks or browser API usage
- Improve Dockerfile: add bun install layer cache mount, copy
.next/cache from builder, reorder ENV declarations
- Delete 10 API routes (stats, system-health, update-check, jobs/trigger,
backup/restore, person, titles import/resolve, registration/status) and
move logic into server actions in lib/actions/settings.ts and
lib/actions/watchlist.ts
- Refactor SystemHealthCards to accept initialData prop and hydrate Jotai
atoms via useHydrateAtoms; replace useSystemHealth SWR hook with a
lightweight useSystemHealthRefresh that calls getSystemHealthAction
- Convert BackupRestoreSection to use restoreBackupAction + useTransition
instead of a raw fetch call
- Split SystemStatusCard, BackgroundJobsCard, and StorageCard into
standalone components reading from systemHealthDataAtom
- Make SetupPage async with connection() to opt into dynamic rendering
- Overhaul `lib/auth/server.ts` and `lib/auth/session.ts`; update all API
routes and server actions to use the revised session pattern
- Refactor server actions (settings, titles, watchlist, setup) for
consistency with new auth layer
- Extract `SetupForm` into its own client component with `useActionState`,
animated steps, and copyable env snippets
- Move landing page redirect logic into `app/page.tsx`; slim down
`LandingPage` component
- Add `proxy.ts` for local dev proxying
- Minor cleanup to `NavBar`, `MobileTabBar`, and `TitleCard`
- New `lists` service fetches Sonarr/Radarr library via their REST APIs
and auto-imports matching titles from TMDB into the user's watchlist
- `app/api/lists/[token]/route.ts` webhook endpoint triggers a list sync
- Unified `IntegrationCard` component replaces `WebhookCard`, handling
both webhook-style (Plex/Jellyfin/Emby) and list-style (Sonarr/Radarr)
integrations with per-type config forms
- Schema migration adds `sonarr` and `radarr` to the integration type
enum and a `listConnections` table for list-based integrations
- 212 tests added for the lists service covering import, deduplication,
and error handling
Cast, availability, recommendations, colors, and trailers were missing
on first load because importTitle fire-and-forgot enrichment tasks.
getTitleWithChildren now calls ensureEnriched() to backfill any missing
data before rendering — works for new imports, old titles, and shells.
Also removes the awaitEnrichment option from importTitle (redundant now)
and optimizes DB queries: reuses already-read data for existence checks,
caches fetchSeasonsFromDb result, and skips re-reads when no backfill ran.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store TMDB genres in normalized tables (genres + titleGenres) and content
ratings as a column on titles. Both are fetched during import/refresh using
append_to_response for content ratings (no extra API calls). Displayed in
the title hero between type badge and year. Also fixes pre-existing type
error for profile_path on TmdbSearchResult.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Person results from multi-search were using poster_path instead of
profile_path, causing missing profile images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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.
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>
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>
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>
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>
- 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>
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>
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>
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>
Replace client-side data fetching with server-side queries and server
actions, eliminating 3 API route files. Extract page into granular
client components (account, integrations, server, webhook card) with
optimistic updates. Rename sections: Media Servers → Integrations,
Administration → Server. Add admin badge to account section.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all title page mutations (status, rating, movie watch, episode
watch/unwatch, season watch/unwatch) from API route handlers to server
actions called directly from TitleInteractionProvider. Same optimistic
update pattern, no behavioral change — just eliminates the fetch()
round-trip through 5 API routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fully client-side dashboard and title detail pages with server
component orchestrators that fetch data directly via service functions,
eliminating extra network round-trips through API routes. Each section
streams independently through its own Suspense boundary.
- Extract getUserStats(), getTitleWithChildren(), getRecommendationsForTitle()
into service layer; update getNewAvailableFeed() to include tmdbId/voteAverage
- Split dashboard into server sections (stats, continue watching, library,
recommendations) with client children for animations
- Split title page into server hero + client interaction provider with shared
context for optimistic mutations across actions and seasons
- Add generateMetadata with OG tags, server-side TMDB ID resolution via
redirect(), loading.tsx and not-found.tsx for both pages
- Add per-section skeleton components, fix ContinueWatchingSkeleton dimensions
- Remove 6 unused API routes (feed/*, titles/[id] GET, titles/[id]/recommendations)
- Remove components/stats-summary.tsx, add lib/types/title.ts for shared types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Navigate instantly to /titles/tmdb-{id}-{type} and show a skeleton
while the new /api/titles/resolve endpoint handles the full import
(TMDB fetch, availability, recommendations, colors). Existing titles
resolve in ~5ms; the URL is replaced with the UUID once resolved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce a local disk cache for TMDB images served through a proxy API
route (/api/images/[...path]), eliminating direct client-side CDN
dependencies. Images are cached by category (posters, backdrops, stills,
logos) and served with immutable cache headers.
Move all tmdbImageUrl() calls from client components to API routes and
server components so clients receive ready-to-use URLs. This removes the
need to expose TMDB_IMAGE_BASE_URL and IMAGE_CACHE_ENABLED via
next.config.ts env block. The landing page is split into a server
wrapper (app/page.tsx) and client component (components/landing-page.tsx).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract dominant colors from movie/TV poster images server-side and use
them to create per-title atmospheric effects on detail pages — tinted
backdrop gradients, ambient glow orbs, and colored poster shadows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user finishes watching on their media server, a webhook fires and
Sofa logs it as watched, triggering all existing auto-transitions. Each
user configures their connection in settings and gets a unique webhook URL.
- Add webhookConnections and webhookEventLog schema tables
- Add TMDB findByExternalId for resolving IMDB/TVDB IDs
- Add source parameter to tracking functions (plex/jellyfin)
- Add webhook processing service with payload parsers, title resolution,
and deduplication
- Add public webhook receiver route (token-based auth)
- Add authenticated settings API routes for managing connections
- Add Media Servers section to settings UI with Plex/Jellyfin cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The command palette (⌘K) already handles direct title search. Replace the
redundant /search page with a new /explore page featuring trending titles,
popular movies/TV, and genre browsing powered by TMDB discovery endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
First user to register automatically becomes admin and registration
closes. Admins can re-open registration from a new settings page.
Uses Better Auth admin plugin for role management and a new appSettings
table for the registration flag. Mobile tab bar now links to settings
instead of inline logout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without TMDB_API_KEY the app silently fails on search and import.
This adds a /setup page that guides admins through obtaining and
configuring the key, redirects unconfigured visitors from the
landing page, and returns clear error messages from the search API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Docker self-hosting support:
- Dockerfile (multi-stage Alpine build with tini init)
- docker-compose.yml with named volume for SQLite persistence
- /api/health endpoint for container health checks
- Auto-migration on startup via drizzle-orm/libsql/migrator
- Graceful shutdown (SIGTERM stops scheduler, closes DB)
- Next.js standalone output mode for minimal image size
Database driver migration (better-sqlite3 → @libsql/client):
- Eliminates native C++ compilation, enabling Alpine Docker images
- All DB queries converted from sync to async across services and routes
- DATABASE_URL now uses libsql file: prefix format
- drizzle.config.ts dialect changed to turso for libsql support
- Initial migration files generated in drizzle/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Toggle episode watched state, bulk unwatch seasons via new DELETE endpoints,
and replace outer <button> with <div role="button"> in season accordion to
avoid invalid nested button HTML that caused hydration errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TV episodes were missing when: (1) a title existed as a recommendation
"shell" with no episode data, (2) a prior refreshTvChildren() call
failed partway through, or (3) the title detail page was loaded before
episodes finished importing. importTitle() now re-fetches episodes for
existing TV titles with zero seasons, the GET endpoint hydrates shell
titles on access, and refreshTvChildren() handles per-season errors
gracefully instead of aborting entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add premium "Late Night Screening Room" experience with warm indigo/amber
palette, framer-motion spring animations, Cmd+K command palette with TMDB
search, keyboard shortcuts (G H, G S, ?, W, M, 1-5), sonner toasts with
optimistic updates, skeleton loading states, stats dashboard, search
autocomplete with filter tabs, cinematic backdrop with film grain, season
progress bars, and staggered card reveal animations throughout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add all 10 milestones: Drizzle ORM + SQLite database with WAL mode,
Better Auth email/password authentication, TMDB API integration for
search and metadata import, TV season/episode caching, user tracking
(watchlist/status/watches/ratings with auto-transitions), discovery
feeds (continue watching, library, recommendations), US streaming
availability via TMDB providers, background job scheduler with
instrumentation hook, and dark cinema-themed frontend with DM Serif
Display + DM Sans typography and amber accent design system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>