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>
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>
Co-locate PlexIcon/JellyfinIcon under settings/_components/icons.tsx.
Update Plex icon to CoreUI Brands SVG with corrected viewBox. Add
animated height transitions to collapsible panels, fix open-state
padding, and turn the Plex Pass notice into a linked external reference
with an inline icon.
Optimistically update episodeWatches client state when setting status
to "completed" on a TV title, so episodes render as watched immediately
without requiring a page refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove react-hotkeys-hook, KeyboardProvider context, useRegisterShortcut
wrapper, and KeyboardHelpDialog component. Components now call TanStack
useHotkey/useHotkeySequence directly. Shared state uses jotai atoms in
lib/atoms/, shortcut metadata lives as a static const in lib/constants/.
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>
Wrap Plex and Jellyfin webhook cards in collapsible sections with chevron
indicators. Fix input/button height mismatch by using default button sizes
(h-7) instead of lg (h-8). Add Plex Pass requirement info box and move
the enable/disable switch into the collapsible body.
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>
Replace client-side session checks (useSession + router.replace) with
a two-layer server-side approach: Next.js middleware performs a fast
cookie presence check for all protected routes, and the (pages) layout
does a full server-side session validation before rendering.
Move login/register into a dedicated (auth) route group with its own
minimal layout so they sit outside the authenticated shell. Lift the
Toaster to the root layout so it remains available across all route
groups.
Replace the cool indigo hue (270) across all CSS custom properties with
a warm charcoal base (hue 55), giving the dark cinema palette a subtler
amber-tinted warmth. Adjust lightness and chroma values on borders,
inputs, and foreground tokens for improved contrast against the new
base.
Remove breadcrumb navigation, use full-viewport backdrop technique
matching explore page, and fix mobile hero layout: poster and title
info now sit side-by-side at all sizes with a smaller poster and
scaled-down typography on mobile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use full-bleed margin technique to break out of max-w-6xl container,
add explicit w-full to prevent aspect-ratio from shrinking the image
container when max-height kicks in, and add overflow-x-hidden to the
page wrapper to prevent scrollbar from 100vw units.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap the "Sofa" wordmark for an SVG sofa icon across the nav bar,
auth forms, and landing page. Add SVG favicon replacing the old .ico.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 4 broken poster paths (The Dark Knight, Interstellar, Breaking
Bad, The Office) and correct the Forrest Gump path which was incorrectly
using Interstellar's poster.
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>
Renames the env var for clarity since it holds a read access token, not an API
key. Adds optional TMDB_API_BASE_URL and TMDB_IMAGE_BASE_URL env vars for
advanced users. Centralizes image URL construction into a shared tmdbImageUrl()
helper, replacing hardcoded URLs across all components.
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>
- Continue Watching card uses viewport-relative width on mobile (calc(100vw-3rem))
with edge-to-edge scroll container using negative margin trick
- Nav bar hides user name on mobile (bottom tab bar shows it instead)
- Season rows show percentage text on mobile where progress bar is hidden
- Season 5 correctly shows "94%" instead of requiring the hidden progress bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 12 well-known movie/TV posters arranged in tilted columns behind hero
- Low opacity (12%) with radial gradient fade keeping center text clear
- Spring entrance animations with staggered delays
- Posters hidden on mobile (sm:block) for clean experience on small screens
- Added next/image for optimized poster loading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>