Replace hardcoded `--color-primary` references on the title detail screen
with a dedicated `--color-title-accent` / `--color-title-accent-foreground`
variable pair, allowing the poster-derived palette to style title UI
elements without polluting the global primary color.
- Add `--color-title-accent` and `--color-title-accent-foreground` to
`global.css` with default warm amber values
- Update `use-title-theme.ts` to set `--color-title-accent` instead of
`--color-primary`; switch from `useFocusEffect` to `useEffect`
- Replace all `bg-primary`, `text-primary`, `border-primary` etc. in
`title/[id].tsx` and title components with `title-accent` equivalents
- Add `accentColor` prop to `StarRating` so it can receive the resolved
CSS variable value at runtime
- Pass `iconColor={titleAccent}` to all `SectionHeader` instances on the
title detail screen
- Use platform-appropriate store icon (App Store / Google Play) for the
"Where to Watch" section header
- Switch progress bar in `ContinueWatchingBanner` from `bg-status-watching`
to `bg-title-accent`
* Convert to Turborepo monorepo with shared API contract package
Restructure the repository as a monorepo in preparation for adding
future clients (mobile app, CLI). Extract the oRPC contract and Zod
schemas into `@sofa/api` (packages/api/) as a JIT internal package,
and relocate the Next.js app to `@sofa/web` (apps/web/).
- Add Turborepo with Bun workspaces for task orchestration and caching
- Extract `contract.ts` and `schemas.ts` into `@sofa/api` package
- Move all app code, configs, tests, and migrations to `apps/web/`
- Update 17 import paths from `@/lib/orpc/schemas` to `@sofa/api/schemas`
- Add `outputFileTracingRoot` and `transpilePackages` to next.config.ts
- Rewrite Dockerfile with `turbo prune --docker` for efficient builds
- Update CI workflows to use `turbo run` for lint/check-types/test
- Update CLAUDE.md with monorepo structure and commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract standalone Hono API server and split shared packages
Separate all server-side concerns from the Next.js frontend into a new
`apps/server/` Hono app and dedicated shared packages, making `@sofa/web`
a frontend-only app with no direct DB or service access.
- Add `@sofa/server` (`apps/server/`) — Hono API on port 3001 hosting
oRPC procedures, Better Auth, cron jobs, and non-RPC routes
- Add `@sofa/core` (`packages/core/`) — All 15 business logic services
moved from `apps/web/lib/services/`; tests moved to `packages/core/test/`
- Add `@sofa/db` (`packages/db/`) — DB client, schema, migrations,
constants, and logger extracted from `apps/web/lib/db/` and `lib/`
- Add `@sofa/tmdb` (`packages/tmdb/`) — TMDB client and image helpers
moved from `apps/web/lib/tmdb/`
- Add `@sofa/auth` (`packages/auth/`) — Better Auth server config moved
from `apps/web/lib/auth/`
- Move oRPC procedures, handler, router, middleware to `apps/server/src/orpc/`
- Move Hono route handlers (avatars, backups, images, lists, webhooks,
health) to `apps/server/src/routes/`; delete equivalent Next.js API routes
- Strip `apps/web` to frontend-only: no DB imports, no service imports,
all data via oRPC client calls to the API server
- Add `entrypoint.sh` to start API server, wait for health, then Next.js
- Update `next.config.ts` rewrites to proxy `/rpc/*` and `/api/*` to
`INTERNAL_API_URL` (default `http://localhost:3001`)
- Update Dockerfile and CLAUDE.md for the new structure
* Migrate web app from Next.js to Vite + TanStack Router SPA and add workspace catalog
Replace Next.js with a pure Vite SPA using TanStack Router for file-based routing,
removing all SSR complexity. The API server (Hono) now serves both API routes and
SPA static files in production, simplifying Docker to a single-process container.
Key changes:
- Vite 7 + @tanstack/react-router with file-based routing via plugin
- Route guards via beforeLoad + authClient.getSession() (replaces server-side auth)
- Route loaders with queryClient.ensureQueryData() (replaces SSR data fetching)
- Self-hosted fonts via @fontsource (replaces next/font/google)
- Tailwind v4 via @tailwindcss/vite (replaces @tailwindcss/postcss)
- Single oRPC client (removed SSR client and server-side session helper)
- Hono serves SPA static files in production (single port 3000)
- Single-process Dockerfile (removed entrypoint.sh)
- Bun workspace catalog for centralized dependency version management
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract @sofa/logger and @sofa/config shared packages
- Add `@sofa/logger` (`packages/logger/`) — standalone logger package
extracted from `@sofa/db/logger`; update all imports across server,
core, auth, db, and tmdb packages
- Add `@sofa/config` (`packages/config/`) — standalone config/constants
package extracted from `@sofa/db/constants`; exports `DATA_DIR`,
`DATABASE_URL`, `CACHE_DIR`, `AVATAR_DIR`, `BACKUP_DIR`
- Move `.env.example` from `apps/web/` to repo root; update server dev
scripts to load it via `--env-file=../../.env`
- Move image serving from `/api/images` to `/images`; add `serveStatic`
fast path in `index.ts` for cached files before falling back to the
TMDB fetch route; add `/images` proxy to Vite dev config
- Fix `Sparkline` component: replace `ResponsiveContainer` with
`ResizeObserver` to avoid SSR/hydration issues with recharts
- Replace `VITE_SERVER_URL` env var with `window.location.origin` in
the oRPC client (always same-origin in both dev and production)
* Fix asset caching, SPA 404 fallback, and DATA_DIR resolution
- Add `Cache-Control: immutable` header for hashed `/assets/*` files;
return 404 for missing asset paths instead of falling back to
`index.html` (prevents serving stale chunks after deploy)
- Wrap `query.invalidate` in an arrow function in the oRPC QueryClient
error handler to avoid illegal invocation errors
- Resolve `DATA_DIR` to an absolute path via `path.resolve()` so
relative paths work regardless of the process working directory
* Migrate @sofa/logger to pino for structured logging
- Replace custom logger implementation in `packages/logger/` with pino
+ pino-pretty; add both as workspace catalog dependencies
- Add `pino` and `pino-pretty` to the workspace catalog in `package.json`
- Fix `log.error()` calls in oRPC and OpenAPI handlers to pass the
error directly instead of wrapping it in `{ error }` to match pino's
serializer expectations
* Rename discoverProcedure/statsProcedure exports to discover/stats
* Add TanStackDevtools unified panel and VS Code workspace config
- Replace separate Router/Query devtools with unified `TanStackDevtools`
from `@tanstack/react-devtools` + `@tanstack/devtools-vite` plugin
- Wrap app in `<StrictMode>` in `main.tsx`
- Add `.vscode/settings.json` (Biome formatter, format-on-save, readonly
`routeTree.gen.ts`) and `.vscode/extensions.json` (recommended extensions)
* Move test DB helpers to @sofa/db/test-utils and add root bunfig.toml
Extract in-memory SQLite setup and fixture helpers (insertUser, insertTitle,
etc.) from packages/core/test/sqlite.ts into packages/db/src/test-utils.ts
so DB test utilities live alongside the schema they depend on. Use
import.meta.dir for CWD-independent migration path resolution.
Add root bunfig.toml so `bun test` works from the repo root in addition
to `bun run test` (turbo).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix devtools plugin order and whitespace-only TMDB token check
Move devtools() to first position in Vite plugins array per TanStack
docs, and trim TMDB_API_READ_ACCESS_TOKEN before boolean coercion so
whitespace-only values are treated as unconfigured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace server actions with API routes and migrate SWR to TanStack Query
- Add 33 new REST API routes covering titles, episodes, seasons, people,
dashboard, explore, integrations, admin, backups, and account operations
- Replace SWR with @tanstack/react-query: add QueryProvider, api-client
helper, query-client singleton, and TanStack Query hooks for titles
- Migrate all 26 server actions to fetch calls against the new API routes
in 10 client components (use-title-actions, title-card, command-palette,
hero-banner, integration-card, account/backup/registration/update-check
settings sections)
- Delete lib/actions/ directory (titles.ts, watchlist.ts, people.ts,
settings.ts) and lib/swr/fetcher.ts
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Convert pages to TanStack Query client-side data fetching
Migrate dashboard, explore, settings, and people pages from server
component data fetching to client-side TanStack Query hooks. Create
query hook files for each domain (dashboard, explore, people, admin,
integrations). Add GET routes for admin/registration and admin/update-check.
Update CLAUDE.md architecture docs to reflect TanStack Query patterns.
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Migrate API layer from REST routes to oRPC with TanStack Query
Replace ~35 hand-written REST API routes and manual TanStack Query hooks
with a contract-first oRPC setup that provides end-to-end type safety
from contract → server procedures → client → TanStack Query.
- Add oRPC foundation: contract, context, auth/admin middleware, RPCHandler,
client (browser + SSR via globalThis), TanStack Query utils
- Implement ~30 procedures across 14 files (titles, episodes, seasons,
people, dashboard, explore, search, discover, stats, status,
integrations, admin, account, watchlist)
- Migrate all client components from api()/lib/queries/ to
orpc.*.queryOptions() and client.*.method() calls
- Delete all replaced REST API routes, lib/queries/, lib/api-client.ts,
and old hooks (use-discover, use-search, use-stats, use-system-health)
- Wire up SSR optimization via instrumentation.ts
- Keep non-RPC routes: auth, images, avatars, backup restore/download,
webhooks, lists, health
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Add strict Zod output schemas to all oRPC contract procedures
Replaces the permissive z.any() output schemas with precise Zod schemas
for every procedure that returns data. This makes the contract fully
self-documenting and enables runtime output validation.
- Define ~30 output schemas in schemas.ts covering titles, people,
dashboard, explore, search, discover, stats, integrations, admin, etc.
- Wire all output schemas into contract.ts
- Fix procedure return type mismatches (nullable fields, enum narrowing)
- Align consumer types (SearchResult, IntegrationConnection) with contract
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Delete lib/types.ts — infer all types from Zod schemas
The oRPC Zod schemas are now the single source of truth for domain types.
All 17 consumer files (services, components, atoms, utils) now import
inferred types (Episode, Season, ResolvedTitle, CastMember, etc.) from
lib/orpc/schemas instead of hand-written interfaces in lib/types.ts.
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Migrate all mutations to TanStack Query useMutation
Replace raw oRPC client calls with useMutation/mutateAsync via
orpc.*.mutationOptions() for consistent mutation tracking across
all components: title-card, command-palette, account, system-health,
integrations, backups, backup-schedule, and title-actions.
https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb
* Fix bugs in settings/explore and bump dependencies
- Deduplicate backup list on create (filter by filename before prepend)
- Add isError state to backup-schedule section with fallback UI
- Add error check for avatar DELETE response before clearing state
- Strengthen backup restore file guard with `instanceof File`
- Change explore trending default type from "movie" to "all"
- Pin all oRPC and TanStack Query packages to exact versions
- Bump jotai 2.18.0→2.18.1, motion 12.35.1→12.35.2,
shadcn 4.0.0→4.0.2, @types/node 25.3.5→25.4.0,
semver 6.3.1→7.7.4
* Migrate title recommendations to client-side TanStack Query
- Convert TitleRecommendations from server component with `"use cache"`
to a client component using `orpc.titles.recommendations.queryOptions()`
with an inline skeleton loading state
- Delete recommendations-grid.tsx — merged into title-recommendations.tsx
- Remove RecommendationsSkeleton from skeletons.tsx (now inline)
- Drop Suspense wrapper in title detail page (client component handles
its own loading state)
- Include userStatuses in recommendations procedure output so the
component no longer needs a separate session/tracking lookup
- Remove `revalidate` option and all `updateTag`/`cacheTag`/`cacheLife`
calls from refreshCredits and refreshRecommendations — no longer
needed without "use cache" server components
- Remove `cacheComponents: true` from next.config.ts
* Replace Jotai title atoms with React Context + TanStack Query cache
- Add title-context.tsx with TitleContext and useTitleContext /
useTitleUserInfo hooks; useTitleUserInfo reads from
orpc.titles.userInfo cache instead of atoms
- Rewrite TitleProvider to seed the userInfo cache via
queryClient.setQueryData on mount (unconditional overwrite
prevents stale data on revisit / account switch) and provide
titleId, titleType, titleName, seasons, and watchingEp via
context
- Rewrite use-title-actions to read/write userInfo via
queryClient.getQueryData / setQueryData helpers instead of
useStore + individual atoms; optimistic updates and rollbacks
now operate on a single UserInfo object in the query cache
- Migrate title-actions, title-keyboard-shortcuts, and
title-seasons off useAtomValue / useSetAtom to the new
context hooks
- Delete per-title atoms from lib/atoms/title.ts
* Migrate avatar and backup restore routes to oRPC procedures
- Add `account.uploadAvatar` and `account.removeAvatar` procedures
(FormData file input, auth middleware) replacing `PUT`/`DELETE`
`/api/account/avatar` route
- Add `admin.backups.restore` procedure (File input, admin middleware)
replacing `POST /api/admin/backups/restore` route
- Delete both REST route files
- Migrate account-section and backup-restore-section off
`useTransition` + raw `fetch` to `useMutation` via
`orpc.*.mutationOptions()`
- Rename `lib/utils/title-theme.ts` → `lib/theme.ts` and
`getTitleThemeStyle` → `getThemeCssProperties`; update title
detail page import accordingly
* Add OpenAPI v1 REST API endpoint via @orpc/openapi
- Add `@orpc/openapi` and `@orpc/json-schema` dependencies
- Add `lib/orpc/openapi-handler.ts` with OpenAPIHandler instance
- Add `app/api/v1/[[...rest]]/route.ts` catch-all serving GET/POST/PUT/DELETE
at `/api/v1` with request-headers context
- Annotate contract procedures with OpenAPI metadata (tags, summaries,
descriptions, and successStatus codes) across all ~30 procedures
* Fix 8 bugs: memory safety, data races, and correctness issues
- Move queryClient.setQueryData from useState initializer to useEffect
to avoid mutating the query cache during React render phase
- Add SQL LIMIT to per-integration event queries instead of loading all
events and trimming in memory
- Lower backup restore upload limit from 500 MB to 100 MB and stream
upload to disk instead of buffering entirely in memory
- Reorder avatar upload to write new file before deleting old one so a
failed upload doesn't remove the current avatar
- Make optimistic rollback field-specific so a failed rating update
doesn't erase concurrent status or episode-watch changes
- Gate titles.userInfo query behind session state to prevent failing
RPC calls for anonymous users
- Derive OpenAPI session cookie name from BETTER_AUTH_URL so HTTPS
deployments use the correct __Secure- prefixed cookie
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add explicit z.void() output schemas to all 17 void procedures
Ensures the OpenAPI spec correctly generates 204 No Content responses
for mutation endpoints that don't return data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
updateTag (Next.js cache revalidation) only works inside Route Handlers
and Server Actions. It was being called from cron jobs and fire-and-forget
promises, causing intermittent errors attributed to whichever route was
rendering concurrently (e.g. /explore).
- Pass `{ revalidate: false }` from cron and fire-and-forget contexts
- Add try-catch around updateTag calls as a safety net
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Drop `authRoutes` set and both redirect branches so the proxy no
longer redirects logged-in users away from auth pages or
unauthenticated users away from protected pages
- Convert discover, stats, status, and system-health server actions to
proper API route handlers under `app/api/`; delete `lib/actions/explore.ts`,
`lib/actions/settings.ts`, and `lib/actions/setup.ts`
- Add `use-discover`, `use-stats`, and `use-system-health` SWR hooks
that call the new routes; update `command-palette`, `title-card`,
`update-toast`, and `stats-display` to consume them
- Lift auth centering wrapper from individual login/register pages into
`(auth)/layout.tsx`; switch both pages from `auth.api.getSession` to
the cached `getSession()` helper
- Relocate setup wizard from `app/(auth)/setup/` to `app/setup/` (outside
auth group) with dedicated `copy-button` and `refresh-button` client
components
- Move `not-found.tsx` and `error.tsx` to app root so they apply
globally instead of only within the pages route group
- Update the success toast message to include a count of unwatched episodes from previous seasons when marking a season as watched.
- Introduce a catch-up action in the toast for users to easily access unwatched episodes.
- Refactor the logic to gather unwatched episodes from earlier seasons for improved user feedback.
- Set `modal={false}` on all Select and DropdownMenu instances to
prevent scroll lock and backdrop interference when opened inside
scrollable or fixed-position containers
- Reduce cast carousel card gap from `gap-4` to `gap-1` and switch
actor name from `line-clamp-2` to `truncate` for a tighter layout
- Fix backup schedule description layout: move `suppressHydrationWarning`
to wrapper span, add margin to inline SelectTrigger, use `!h-auto`
to override default height
- Add `nuqs` dependency; define `dashboardSearchParams` and
`exploreSearchParams` loaders in new `search-params.ts` files
- Thread `moviePeriod`/`episodePeriod` from URL into `StatsSection`
and `StatsDisplay`, replacing local `useState` with `useQueryStates`
- Rename `getUserStats` fields `moviesThisMonth`/`episodesThisWeek` →
`movieCount`/`episodeCount` and accept period args so SSR stats
match the active URL params
- Pre-fetch genre-filtered results server-side on the explore page
when `movieGenre`/`tvGenre` params are present; pass as
`initialGenreItems` to `FilterableTitleRow` so first paint is
already populated
- Render genre chips as `<Link>` elements (with click-handler
fallback) so genre selections produce shareable, bookmarkable URLs
- Add `openapi-fetch` dependency; generate `lib/tmdb/schema.d.ts` from
TMDB's OpenAPI spec and delete `lib/tmdb/types.ts`
- Rewrite `lib/tmdb/client.ts` to use the typed fetch client against
the generated schema, exporting `TmdbMovieDetails`, `TmdbTvDetails`,
`TmdbVideo`, and `TmdbGenre` directly
- Fix null-safety across services (`credits`, `availability`,
`metadata`, `person`, `webhooks`) and call sites (explore page,
search route, explore actions) to handle optional fields produced
by the stricter generated types
- Update `metadata.ts` and `metadata.test.ts` to import shared types
from `@/lib/tmdb/client` instead of the removed `types.ts`
- Exclude `lib/tmdb/schema.d.ts` from Biome linting
- Change primary argument from TMDB size string (`w500`, `w1280`, etc.)
to category name (`posters`, `backdrops`, `stills`, `logos`,
`profiles`); optional size override remains as third argument
- Update all call sites across services, actions, pages, and route
handlers to use the new category-based API
- Update image.test.ts to match the new signature and replace
size-mapping test descriptions with category-name descriptions
- Rename `lib/types/title.ts` → `lib/types.ts` (remove nested directory)
- Update all imports across services, atoms, components, and utils
from `@/lib/types/title` to `@/lib/types`
- Rename `lib/test-preload.ts` → `test/preload.ts` and
`lib/test-utils.ts` → `test/sqlite.ts`
- Update `bunfig.toml` preload path to `./test/preload.ts`
- Update all service test imports from `@/lib/test-utils` to
`@/test/sqlite`
- Delete `components/mobile-tab-bar.tsx`; co-locate `MobileTabBar` export
in `nav-bar.tsx` so both nav components share helpers
- Extract `isLinkActive` helper used by both desktop and mobile nav;
fixes `/dashboard` active match when pathname is `/`
- Replace `layoutId`-based Framer Motion indicators with a
`useActiveIndicator` hook that measures DOM rects via `ResizeObserver`,
animating `left`/`width` directly — avoids cross-tree layout ID
conflicts and correctly handles breakpoint visibility changes
- Add `aria-current="page"`, `aria-label="Primary"`, and
`focus-visible` ring styles to nav links and tab bar items
- Update layout import to pull both exports from `@/components/nav-bar`
- Thread `userRole` from session through `AuthenticatedShell` → `NavBar`
and show a styled "Admin" badge next to the user name when role is admin
- Replace `text-sm` / `text-xs/relaxed` with explicit `text-[13px]` in
NavBar search buttons and command palette input, empty, group, and
item classes for consistent sizing
- Remove `{userImage && ...}` guards around AvatarImage; pass `src`
directly so AvatarFallback renders naturally when src is undefined
- In AccountSection, clear `src` during pending state instead of
conditionally rendering the image element; guard destructive overlay
color on `!isPending` so it doesn't flash while uploading
- Add `revalidate` option to `refreshCredits` and `refreshRecommendations`;
pass `{ revalidate: false }` from `ensureEnriched` to avoid calling
`updateTag` outside Server Actions/Route Handlers
- Add `hideScrollbar` prop to all horizontal ScrollArea instances
(continue-watching, title rows, filterable row, cast carousel)
- Replace text-only type badge in HeroBanner with icon + label using
IconMovie / IconDeviceTv
- Fix StatusButton destructive hover overrides with `!important` so
they correctly supersede the status-specific background/text/ring
- Add `updateNameAction` server action that calls `auth.api.updateUser`
with validation (non-empty, max 100 chars)
- Replace static name display in AccountSection with an inline edit
mode: click pencil icon to activate, Enter/blur to save, Escape to
cancel
- Use auto-sizing ghost input (invisible span grid trick) so the field
matches text width
- Animate between display and edit states with AnimatePresence fade;
show Spinner while transition is pending
- 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 carousel.tsx and replace all Carousel/CarouselContent/CarouselItem
usage across title rows, cast carousel, continue watching, and explore
sections with ScrollArea + scrollFade horizontal scroll
- Update ScrollArea to accept a `scrollFade` prop that renders a
gradient edge overlay instead of the old per-site absolute div hack
- Remove "use client" directive from title-row and cast-carousel now
that they have no client-side hooks
- Delete unused resizable.tsx; inline TypeBadge icon into TitleHero
and delete the standalone type-badge.tsx file
- Add TitleTheme component for per-title accent color CSS injection
- Swap IconSparkles → IconThumbUp in recommendations section and grid
- Simplify ambient glow to a single size (remove mobile breakpoint override)
- 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
- Add `resolveTitle` and `resolvePerson` server actions that import
from TMDB and return the internal DB id
- Remove `tmdb-{id}-{type}` URL pattern and server-side redirect logic
from TitleDetailPage and PersonDetailPage
- Rename `importTitle` → `getOrFetchTitleByTmdbId`; add `getOrFetchTitle`
combining fetch + children lookup
- Update HeroBanner, TitleCard, and CommandPalette to call resolve
actions client-side before pushing to router
- Batch availability offer inserts into a single transaction
- Stack title-hero poster/metadata vertically on mobile (flex-col md:flex-row),
switch backdrop tall breakpoint from sm to md
- Extract bio expand/collapse logic into reusable ExpandableText component;
use it in PersonHero and TitleHero
- Fix ContinueWatchingCard image to use fill + sizes instead of fixed
width/height to avoid layout shift
- Add fade-out gradient on genre chip scrollbar edge on mobile
- Show scaled-down ambient glow on mobile instead of hiding it entirely
- Humanize knownForDepartment labels (Acting→Actor, Directing→Director, etc.)
- Change cast member name from truncate to line-clamp-2 for wrapping
- Hide tooltip arrow via [&>:last-child]:hidden instead of color-matching it
- Apply safe-area-inset padding to HeroBanner content on notched devices
- Swap Jotai `movieStatsAtom`/`episodeStatsAtom` atoms for `useState` +
`useEffect` calling `getStatsAction` directly on period change
- Remove dependency on `lib/atoms/stats` module
- Add touch-friendly tap target padding to `inlineTriggerClass` via
negative margin compensation on mobile
- 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`
- useTimeAgo: replace per-instance intervals with shared useSyncExternalStore ticker
- useTiltEffect: gate to fine-pointer devices, skip motion graphs on touch
- Carousel: bake WheelGesturesPlugin into primitive, remove from all consumers
- TitleSeasons: use memoized Set + precomputed progress map instead of Array.includes
- useSearch: stabilize results identity with useMemo
- CommandPalette: hoist shortcut grouping to module scope, stabilize effect deps
- StarRating: hoist static spring transition to module constant
- Settings toggles: use useOptimistic + useTransition for auto-rollback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add covering indexes for recommendation rank ordering, title staleness scans,
and type+status filtering. Replace N+1 per-row staleness checks in cron jobs
with set-based batch queries. Wrap availability refresh in a transaction for
atomicity. Batch episode stills query and use existence check instead of
loading all rows for count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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
bun's mock.module persists across test files in the same process,
causing image.test.ts to receive the stub instead of the real
tmdbImageUrl. Since tmdbImageUrl has no heavy runtime dependencies,
service tests work fine with the real implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 211 tests across 15 files covering services (tracking, discovery,
metadata, backup, credits, webhooks, settings, update-check, colors,
person), utilities (config, cron, providers, title-theme), and TMDB
image URL helpers
- `lib/test-preload.ts` + `bunfig.toml` wire up a global in-memory
SQLite DB with migrations for all DB-backed tests
- `lib/test-utils.ts` provides `clearAllTables()` and seed helpers
(insertUser, insertTitle, insertTvShow, insertMovieWatch, etc.)
- Export `getBackupSource`, `isKnownBackup`, `isValidBackupFilename`,
`buildBackupCron`, `performUpdateCheck` internals for direct testing
- GitHub Actions workflow runs `bun test --coverage` on push/PR to main
- Backup and update check jobs show as "disabled" (not "succeeded") when
their respective settings are off
- Disabled jobs excluded from "x of y jobs healthy" count, sorted to
bottom of list, with schedule/next-run hidden and trigger button disabled
- Active jobs sorted by next run time
- "Never run" uses amber dot to differentiate from gray "disabled" dot
- Extract RefreshButton component and add it to all three health cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the static package.json import in lib/version.ts and instead
pass APP_VERSION as a Docker build arg extracted from package.json by
the CI workflow. Both the builder and runner stages receive APP_VERSION
and GIT_COMMIT_SHA as environment variables, making runtime version
info available via process.env without bundling package.json into the
standalone output.
* Fix mobile pinch zoom and content overlapping header/footer on iOS Safari
Add viewport export with maximumScale=1 and userScalable=false to prevent
pinch-to-zoom. Add viewportFit=cover for proper safe area support.
Fix content rendering above the sticky header and below the fixed mobile
tab bar on iOS Safari by adding relative z-0 to the content wrapper,
creating a stacking context that keeps page content below both navigation
elements.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Add safe area inset support for iOS 26 and fix film grain z-index
- Add safe-area-inset-top padding to NavBar for notch/Dynamic Island
- Add safe-area-inset-left/right to NavBar, main content, and
MobileTabBar for landscape mode
- Account for safe-area-inset-bottom in content wrapper padding so
content isn't hidden behind MobileTabBar + home indicator
- Lower film grain overlay z-index from 9999 to 1 to avoid triggering
iOS 26 Safari's fixed-element tab tinting behavior
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Hide ambient glow on mobile where it overwhelms the viewport
The 800x600px blurred glow blob dominates narrow mobile screens and
clashes with title backdrop images. Hidden below sm breakpoint.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Fix asymmetric safe-area insets using separate pl/pr padding
The previous px-[max(...,env(safe-area-inset-left))] shorthand applied
only the left inset to both sides. Split into separate pl/pr with the
correct env() variable for each side.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replace mixed badge styles with a single icon type badge (with tooltip),
CSS-based dot separators, collapsed genres with hover popover, status
icons, and subtle TMDB link. Remove vote count for less noise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the `userStatus` guard on the "Mark All" button so unauthenticated
or unwatchlisted users can also access it. Drop the mobile percentage
label (redundant with the watch count) and make the watched/total count
always visible instead of hidden on small screens.
Introduces a local provider registry (lib/providers.ts) mapping TMDB
provider IDs to search URL templates for ~20 streaming services. Provider
badges on title pages are now clickable links that open the service's
search page with the title pre-filled. Categories with more than 4
providers show a "+N" overflow badge with a hover popover listing the rest.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build a provider registry mapping TMDB provider IDs to search URL
templates. `generateProviderUrl()` resolves a URL for a given provider
and title name using URL-encoded search queries. `readAvailability()`
now accepts the title name and attaches `watchUrl` to each offer.
Provider badges with a resolved URL render as `<a>` links opening in a
new tab; tooltip copy switches from the provider name to "Watch on
{name}". Badges without a known URL remain non-interactive as before.
When a show had status "watchlist", logging episode watches did not
transition it to "in_progress", so it never appeared in the Continue
Watching feed which filters for in_progress titles only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
The proxy treated /setup as a protected route, redirecting unauthenticated
users to /login. This made first-run TMDB onboarding unreachable since
no users exist yet. Add /setup to public auth routes.
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>
Base UI Select.Value renders raw values by default — add children render
functions to map values to human-readable labels (e.g. "this_month" →
"This Month", "0" → "unlimited"). Switch inline select underlines from
border-bottom to text-decoration for proper baseline alignment. Add TMDB
attribution with logo and disclaimer to settings footer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app/not-found.tsx: root 404 with cinematic atmosphere
- app/global-error.tsx: self-contained global error boundary
- app/(pages)/error.tsx: authenticated error boundary with retry
- app/(pages)/not-found.tsx: authenticated 404 with navigation
- Upgrade existing title and person not-found pages for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add global MotionConfig reducedMotion="user" in root layout
- Add color-scheme: dark, theme-color meta, skip-to-main-content link
- Add aria-hidden={true} to ~60+ decorative icons across 33 files
- Add aria-label to all icon-only buttons (logout, star rating, play, delete, etc.)
- Replace transition-all with specific properties (11 files)
- Add motion-safe: prefix for CSS hover transforms and animate-pulse
- Add prefers-reduced-motion: reduce override in globals.css
- Add useReducedMotion guard to status-dot pulsing animation
- Fix focus-visible styles on auth form inputs (focus → focus-visible, stronger ring)
- Add text-balance to all headings (13 files)
- Replace "..." with "…" (U+2026) in all loading/placeholder text
- Add autocomplete, spellCheck, role="alert" to auth form
- Add aria-label to Switch controls and filmography select
- Fix heading hierarchy (h3 → h2 where h2 was skipped)
- Add break-words to title overview, overscroll-contain to drawer
- Add confirmation dialog for destructive library removal
- Add group-focus-within:opacity-100 for keyboard-accessible backup actions
- Add role="radio" + aria-checked to star rating buttons
- Add aria-label to carousel region, role="img" to TMDB logo SVG
- Add text-foreground to native select for dark mode consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Context-based provider with useProgress() hook that auto-detects
link clicks and popstate, with manual start()/done()/set() for
router.push() calls. Uses useEffectEvent for clean effect deps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add composite indexes on userEpisodeWatches and userMovieWatches for hot queries
- Batch episode tracking: wrap season/batch watches in single transaction (~8 queries vs 8*N)
- Fix N+1 patterns in credits, recommendations, and filmography with batch prefetch+insert
- Stream TV season hydration via Suspense instead of blocking page render
- Optimize webhook logs (per-connection LIMIT 10) and system health queries
- Merge genre filter waterfalls into single Promise.all fetch
- Migrate deprecated Jotai loadable() to unwrap()
- Replace isolated createStore()+Provider with useHydrateAtoms on root store
- Remove unnecessary atomWithStorage SSR guards (handled by Jotai internally)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Coalesces concurrent imports via an in-flight promise map, and catches
SQLITE_CONSTRAINT_UNIQUE as a safety net to return the existing row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Motion's initial="hidden" rendered opacity:0 in server HTML, making content
invisible until JS hydrated — causing a multi-second blank gap after skeleton
disappearance. Replaced with CSS @keyframes stagger animation that plays
immediately from SSR. Also made color extraction fire-and-forget, merged
duplicate stats queries, and removed redundant dashboard loading.tsx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spring-animated 3D tilt, parallax image shift, and glare reflection
follow the cursor using motion values for zero-rerender performance.
Respects prefers-reduced-motion. Carousel overflow adjusted to prevent
clipping of the tilt effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename the person detail route from `app/(pages)/person/[id]` to
`app/(pages)/people/[id]` and update all internal links accordingly.
Strip crew members from the cast carousel so only actors are shown;
remove the `crew` prop from `CastCarousel` and `TitleCast` entirely.
Move the trailer trigger from the metadata row into a centered overlay
on the backdrop image using a new `variant="backdrop"` prop on
`TrailerDialog`, replacing the previous inline button approach.
Swap several icons for better semantic matches: `IconBooks` for the
library section, `IconCheck` for the "Mark Watched" button,
`IconSparkles` on the Recommendations heading, and `IconDeviceTvOld`
on the Seasons heading.
Replace a nested `motion.p` with a plain `<p>` using CSS
`transition-opacity` in `StatsDisplay`. Remove the `animate-gentle-float`
keyframe and the `feed-scroll` utility from `globals.css`, replacing the
latter with the existing `no-scrollbar` class.
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>