mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
Migrate web app from Next.js to Vite + TanStack Router SPA (#6)
* 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>
This commit is contained in:
@@ -5,14 +5,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
bun run dev # Start Next.js dev server
|
||||
bun run build # Production build
|
||||
# Root commands (via Turborepo)
|
||||
bun run dev # Start API server + Vite dev server
|
||||
bun run build # Production build (both apps)
|
||||
bun run lint # Biome lint check
|
||||
bun run format # Biome format (auto-fix)
|
||||
bun run db:push # Push schema changes to SQLite database
|
||||
bun run db:generate # Generate Drizzle migration files
|
||||
bun run db:migrate # Run Drizzle migrations
|
||||
bun run db:studio # Open Drizzle Studio (visual DB browser)
|
||||
bun run check-types # TypeScript type check
|
||||
bun run test # Run tests
|
||||
|
||||
# Database commands (run from packages/db/)
|
||||
cd packages/db && bun run db:push # Push schema changes to SQLite database
|
||||
cd packages/db && bun run db:generate # Generate Drizzle migration files
|
||||
cd packages/db && bun run db:migrate # Run Drizzle migrations
|
||||
cd packages/db && bun run db:studio # Open Drizzle Studio (visual DB browser)
|
||||
```
|
||||
|
||||
IMPORTANT: Default to using Bun instead of Node.js:
|
||||
@@ -39,67 +44,113 @@ bun run test
|
||||
|
||||
## Architecture
|
||||
|
||||
**Sofa** is a self-hosted movie & TV tracking app (like Trakt/TVTime) built as a single Next.js 16 application with SQLite.
|
||||
**Sofa** is a self-hosted movie & TV tracking app (like Trakt/TVTime) built as a Turborepo monorepo with a standalone Hono API server, a Vite + TanStack Router SPA frontend, and shared packages.
|
||||
|
||||
### Monorepo structure
|
||||
|
||||
```
|
||||
couch-potato/
|
||||
├── apps/
|
||||
│ ├── server/ # @sofa/server — Hono API server (oRPC, auth, cron, webhooks)
|
||||
│ └── web/ # @sofa/web — Vite SPA (TanStack Router, TanStack Query)
|
||||
├── packages/
|
||||
│ ├── api/ # @sofa/api — oRPC contract + Zod schemas (shared)
|
||||
│ ├── auth/ # @sofa/auth — Better Auth server config
|
||||
│ ├── core/ # @sofa/core — Business logic services
|
||||
│ ├── db/ # @sofa/db — Database client, schema, migrations, constants, logger
|
||||
│ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper
|
||||
├── turbo.json # Turborepo task configuration
|
||||
├── biome.json # Shared Biome config
|
||||
├── Dockerfile # Multi-stage Docker build (turbo prune, single process)
|
||||
└── package.json # Root workspace config
|
||||
```
|
||||
|
||||
- **`@sofa/server`** (`apps/server/`) — Hono API server. Hosts oRPC procedures, Better Auth, webhook/image/backup routes, and cron jobs. Runs DB migrations on startup. In production, also serves the SPA static files on port 3000. In dev, runs on port 3001.
|
||||
- **`@sofa/web`** (`apps/web/`) — Vite SPA with TanStack Router (file-based routing). No SSR, no DB access, no services. All data fetched via oRPC client calls. Vite dev server proxies `/api/*` and `/rpc/*` to the API server.
|
||||
- **`@sofa/api`** (`packages/api/`) — JIT package with the oRPC contract and Zod schemas. No build step.
|
||||
- **`@sofa/db`** (`packages/db/`) — Database layer: Drizzle schema, client, migrations, constants, logger.
|
||||
- **`@sofa/tmdb`** (`packages/tmdb/`) — TMDB API client (openapi-fetch) and image URL construction.
|
||||
- **`@sofa/core`** (`packages/core/`) — All business logic services (metadata, tracking, discovery, etc.).
|
||||
- **`@sofa/auth`** (`packages/auth/`) — Better Auth server config + environment checks.
|
||||
|
||||
All shared packages are JIT (raw TypeScript exports, no build step). Consumers transpile via their own bundlers.
|
||||
|
||||
### Stack
|
||||
|
||||
- **Framework**: Next.js 16 (App Router), React 19, TypeScript
|
||||
- **Frontend**: Vite 7 SPA, React 19, TypeScript, TanStack Router (file-based routing)
|
||||
- **API Server**: Hono (standalone Bun server)
|
||||
- **Monorepo**: Turborepo with Bun workspaces
|
||||
- **Database**: SQLite via bun:sqlite + Drizzle ORM (WAL mode, singleton via `globalThis`, sync queries, auto-migrations on startup)
|
||||
- **Auth**: Better Auth with Drizzle adapter — email/password + optional OIDC/SSO via `genericOAuth` plugin
|
||||
- **Styling**: Tailwind CSS v4, shadcn components, dark cinema theme with warm primary accents
|
||||
- **Fonts**: DM Serif Display (display), DM Sans (body), Geist Mono (mono)
|
||||
- **Styling**: Tailwind CSS v4 (via `@tailwindcss/vite`), shadcn components, dark cinema theme with warm primary accents
|
||||
- **Fonts**: DM Serif Display (display), DM Sans (body), Geist Mono (mono) — self-hosted via `@fontsource`
|
||||
- **API**: oRPC (contract-first, type-safe RPC) with `@orpc/tanstack-query` for TanStack Query integration
|
||||
- **State**: Jotai (client), TanStack Query (data fetching & mutations via oRPC)
|
||||
- **Linting**: Biome (2-space indent, organized imports, React/Next.js recommended rules)
|
||||
- **Linting**: Biome (2-space indent, organized imports, React recommended rules)
|
||||
- **External API**: TMDB (The Movie Database) with Bearer token auth
|
||||
|
||||
### Path alias
|
||||
### Package imports
|
||||
|
||||
`@/*` maps to project root (`./`), e.g. `@/lib/db/client` → `./lib/db/client`.
|
||||
Within `apps/web/`, `@/*` maps to `apps/web/src/`, e.g. `@/components/nav-bar` → `apps/web/src/components/nav-bar`.
|
||||
|
||||
Cross-package imports use the package name:
|
||||
- `@sofa/api/contract`, `@sofa/api/schemas` — Contract and types
|
||||
- `@sofa/db/client`, `@sofa/db/schema`, `@sofa/db/constants`, `@sofa/db/logger` — Database layer
|
||||
- `@sofa/tmdb/client`, `@sofa/tmdb/image` — TMDB client
|
||||
- `@sofa/core/metadata`, `@sofa/core/tracking`, etc. — Business logic
|
||||
- `@sofa/auth/server`, `@sofa/auth/config` — Auth config
|
||||
|
||||
### Key directories
|
||||
|
||||
- `lib/db/schema.ts` — Single file with all Drizzle table definitions (auth tables + app tables)
|
||||
- `lib/db/migrate.ts` — Auto-migration runner (executed on startup via instrumentation)
|
||||
- `lib/services/` — Business logic layer (metadata, tracking, discovery, availability, credits, person, webhooks, backup, settings, colors, update-check, system-health)
|
||||
- `lib/tmdb/` — TMDB API client and TypeScript types
|
||||
- `lib/auth/` — Better Auth server config (`server.ts`), client hooks (`client.ts`), cached session helper (`session.ts`)
|
||||
- `lib/config.ts` — Server-side environment checks (TMDB configured, OIDC configured, etc.)
|
||||
- `lib/logger.ts` — Structured logger with `LOG_LEVEL` support
|
||||
- `lib/types/` — Shared TypeScript types (e.g. `title.ts`)
|
||||
- `lib/cron.ts` — Background job scheduler (croner, `globalThis` singleton)
|
||||
- `lib/query-client.ts` — Singleton `QueryClient` with default options (30s stale time)
|
||||
- `lib/orpc/` — oRPC API layer (contract-first, type-safe RPC):
|
||||
- `contract.ts` — Full API contract definition (~30 procedures)
|
||||
- `schemas.ts` — Shared Zod input schemas used by contract
|
||||
- `context.ts` — Context type & `os` implementer
|
||||
- `middleware.ts` — Auth (`authed`) and admin (`admin`) middleware
|
||||
- `router.ts` — Assembled router implementing the contract
|
||||
- `handler.ts` — RPCHandler instance with error logging
|
||||
- `client.ts` — Client-side oRPC client (RPCLink)
|
||||
- `client.server.ts` — Server-side oRPC client (zero HTTP overhead for SSR)
|
||||
- `tanstack.ts` — `orpc` utils for TanStack Query (`orpc.*.queryOptions()`)
|
||||
- `procedures/` — Procedure implementations by domain (titles, episodes, seasons, people, dashboard, explore, search, discover, stats, status, integrations, admin, account, watchlist)
|
||||
- `app/rpc/[[...rest]]/route.ts` — Next.js catch-all route handler for oRPC
|
||||
- `app/api/` — Non-RPC routes (auth, images, avatars, backups, webhooks, lists, health)
|
||||
- `app/(pages)/` — Authenticated pages (dashboard, explore, titles/[id], people/[id], settings)
|
||||
- `app/(auth)/` — Auth pages (login, register, setup)
|
||||
**API server** (`apps/server/src/`):
|
||||
- `index.ts` — Hono app entry point, startup, CORS, route mounting, graceful shutdown
|
||||
- `cron.ts` — Background job scheduler (croner)
|
||||
- `orpc/` — oRPC server layer (context, middleware, router, handler, procedures/)
|
||||
- `routes/` — Non-RPC Hono routes (auth, images, avatars, backups, webhooks, lists, health)
|
||||
|
||||
**Web app** (`apps/web/src/`):
|
||||
- `main.tsx` — React root: creates router + renders `<RouterProvider />`
|
||||
- `routes/` — TanStack Router file-based routes (auto-generates `routeTree.gen.ts`)
|
||||
- `__root.tsx` — Root layout (providers, head meta, global error/not-found)
|
||||
- `_app.tsx` — Authenticated layout (auth guard via `beforeLoad`, navbar, shell)
|
||||
- `_app/dashboard.tsx`, `_app/explore.tsx`, `_app/settings.tsx`, `_app/titles.$id.tsx`, `_app/people.$id.tsx`
|
||||
- `_auth.tsx` — Auth layout (centering wrapper)
|
||||
- `_auth/login.tsx`, `_auth/register.tsx`
|
||||
- `index.tsx`, `setup.tsx`
|
||||
- `components/` — App components + `components/ui/` for shadcn primitives
|
||||
- `components/query-provider.tsx` — `QueryClientProvider` wrapper used in root layout
|
||||
- `lib/orpc/client.ts` — oRPC client (always same-origin via Vite proxy or Hono static serving)
|
||||
- `lib/orpc/tanstack.ts` — `orpc` utils for TanStack Query
|
||||
- `lib/auth/client.ts` — Better Auth client hooks
|
||||
- `lib/theme.ts` — Color theme CSS properties from title palettes
|
||||
- `styles/globals.css` — Tailwind + font imports
|
||||
|
||||
**Shared packages**:
|
||||
- `packages/api/src/contract.ts` — Full oRPC API contract definition (~30 procedures)
|
||||
- `packages/api/src/schemas.ts` — Shared Zod schemas and inferred TypeScript types
|
||||
- `packages/db/src/schema.ts` — All Drizzle table definitions
|
||||
- `packages/db/drizzle/` — Migration files
|
||||
- `packages/core/src/` — All service files (metadata, tracking, discovery, availability, credits, person, webhooks, backup, settings, colors, update-check, system-health, lists, image-cache)
|
||||
- `packages/core/test/` — Service tests with in-memory SQLite
|
||||
|
||||
### Auth pattern
|
||||
|
||||
Server components and layouts use the cached session helper to avoid redundant lookups:
|
||||
**Web app** — Route `beforeLoad` guards check session via Better Auth client SDK:
|
||||
|
||||
```typescript
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
const session = await getSession();
|
||||
// In route file (e.g. _app.tsx)
|
||||
beforeLoad: async () => {
|
||||
const { data: session } = await authClient.getSession();
|
||||
if (!session) throw redirect({ to: "/login" });
|
||||
return { session };
|
||||
},
|
||||
```
|
||||
|
||||
oRPC procedures use auth middleware that calls Better Auth:
|
||||
Session is available to child routes via `Route.useRouteContext()`.
|
||||
|
||||
**API server** — oRPC procedures use auth middleware that calls Better Auth:
|
||||
|
||||
```typescript
|
||||
// lib/orpc/middleware.ts — applied to procedures
|
||||
// apps/server/src/orpc/middleware.ts
|
||||
export const authed = base.middleware(async ({ context, next }) => {
|
||||
const session = await auth.api.getSession({ headers: context.headers });
|
||||
if (!session) throw new ORPCError("UNAUTHORIZED");
|
||||
@@ -109,12 +160,18 @@ export const authed = base.middleware(async ({ context, next }) => {
|
||||
|
||||
### oRPC API layer
|
||||
|
||||
All API procedures use oRPC with a contract-first approach. The contract defines the API shape in `lib/orpc/contract.ts`, procedures implement it in `lib/orpc/procedures/`, and the client consumes it with full type safety.
|
||||
All API procedures use oRPC with a contract-first approach. The contract is defined in `packages/api/src/contract.ts`, procedures implement it in `apps/server/src/orpc/procedures/`, and the client consumes it with full type safety.
|
||||
|
||||
**Pattern — importing types from the shared package:**
|
||||
```typescript
|
||||
import type { ResolvedTitle, Season } from "@sofa/api/schemas";
|
||||
import { contract } from "@sofa/api/contract";
|
||||
```
|
||||
|
||||
**Pattern — query in component:**
|
||||
```typescript
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { orpc } from "@/lib/orpc/tanstack";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
|
||||
const { data } = useQuery(orpc.dashboard.stats.queryOptions());
|
||||
```
|
||||
@@ -126,32 +183,36 @@ import { client } from "@/lib/orpc/client";
|
||||
await client.titles.updateStatus({ id: titleId, status: "in_progress" });
|
||||
```
|
||||
|
||||
**Pattern — procedure definition:**
|
||||
**Pattern — route loader with TanStack Query prefetch:**
|
||||
```typescript
|
||||
// lib/orpc/procedures/dashboard.ts
|
||||
export const stats = os.dashboard.stats.func(async (input, context, meta) => {
|
||||
const { user } = context;
|
||||
return getUserStats(user.id);
|
||||
}, authed);
|
||||
export const Route = createFileRoute("/_app/titles/$id")({
|
||||
loader: async ({ params, context }) => {
|
||||
await context.queryClient.ensureQueryData(
|
||||
orpc.titles.detail.queryOptions({ input: { id: params.id } }),
|
||||
);
|
||||
},
|
||||
component: TitlePage,
|
||||
});
|
||||
```
|
||||
|
||||
**Page patterns:**
|
||||
- **Thin server shell** (titles, people): Server component fetches initial data via service layer, passes as `initialData` prop to client component that uses TanStack Query via `orpc` for refetching/mutations.
|
||||
- **Full client-side** (dashboard, explore, settings): Server component handles auth only; client components fetch all data via `orpc.*.queryOptions()` with skeleton loading states.
|
||||
- **Route loaders** (titles, people): `beforeLoad`/`loader` prefetch data into TanStack Query cache via `queryClient.ensureQueryData()`. Components read via `useQuery()`/`useSuspenseQuery()`.
|
||||
- **Full client-side** (dashboard, explore, settings): Components fetch all data via `orpc.*.queryOptions()` with skeleton loading states.
|
||||
- **Error/loading states**: Routes use `pendingComponent`, `errorComponent`, `notFoundComponent`.
|
||||
|
||||
### Non-RPC routes
|
||||
### Non-RPC routes (API server)
|
||||
|
||||
Only file-serving, auth, and webhook routes remain as traditional Next.js API routes:
|
||||
- `/api/auth/[...all]` — Better Auth catch-all
|
||||
- `/api/images/[...path]` — Image proxy/cache serving
|
||||
- `/api/avatars/[userId]` — Avatar file serving
|
||||
- `/api/account/avatar` — Avatar upload (FormData)
|
||||
- `/api/admin/backups/restore` — Backup restore (FormData upload)
|
||||
- `/api/backup/[filename]` — Backup file download
|
||||
- `/api/webhooks/[token]` — Plex/Jellyfin/Emby webhooks
|
||||
- `/api/lists/[token]` — External list feeds
|
||||
Hono route handlers in `apps/server/src/routes/`:
|
||||
- `/images/:category/:filename` — Image proxy/cache serving
|
||||
- `/api/auth/*` — Better Auth catch-all
|
||||
- `/api/avatars/:userId` — Avatar file serving
|
||||
- `/api/backup/:filename` — Backup file download
|
||||
- `/api/webhooks/:token` — Plex/Jellyfin/Emby webhooks
|
||||
- `/api/lists/:token` — External list feeds (Sonarr/Radarr)
|
||||
- `/api/health` — Simple health check (no auth)
|
||||
|
||||
In dev, Vite proxies these to the API server. In production, Hono serves both API and SPA — the browser only sees port 3000.
|
||||
|
||||
### Database schema
|
||||
|
||||
All app tables use UUIDv7 text primary keys generated via `Bun.randomUUIDv7()`. Better Auth tables use their own ID format. Key relationships:
|
||||
@@ -167,35 +228,31 @@ All app tables use UUIDv7 text primary keys generated via `Bun.randomUUIDv7()`.
|
||||
- `cronRuns` — Background job execution history
|
||||
- `appSettings` — Key-value store for runtime app configuration
|
||||
|
||||
### Service layer
|
||||
### Service layer (`packages/core/src/`)
|
||||
|
||||
- **metadata.ts**: `importTitle()` fetches from TMDB, inserts into DB, fire-and-forgets availability + recommendations + credits + image caching. `refreshTvChildren()` fetches all seasons/episodes with 250ms rate limiting.
|
||||
- **image-cache.ts**: Downloads and caches TMDB images to local disk. `cacheImagesForTitle()` caches poster + backdrop, `cacheEpisodeStills()` caches episode stills, `cacheProviderLogos()` caches streaming provider logos, `cacheProfilePhotos()` caches person profile images.
|
||||
- **image-cache.ts**: Downloads and caches TMDB images to local disk.
|
||||
- **tracking.ts**: Auto-transitions — logging a movie watch sets status to `completed`; logging an episode watch sets `in_progress`; all episodes watched auto-completes the series.
|
||||
- **discovery.ts**: Feed generators — continue watching (next unwatched episode per in-progress show), library titles with availability, personalized recommendations from completed/highly-rated titles.
|
||||
- **discovery.ts**: Feed generators — continue watching, library with availability, personalized recommendations.
|
||||
- **availability.ts**: Caches US streaming providers from TMDB watch/providers endpoint.
|
||||
- **credits.ts**: Fetches and caches cast/crew data from TMDB, links persons to titles via `titleCast`.
|
||||
- **credits.ts**: Fetches and caches cast/crew data from TMDB.
|
||||
- **person.ts**: Person detail and filmography lookups.
|
||||
- **webhooks.ts**: Processes incoming Plex/Jellyfin/Emby webhook events to auto-log watches.
|
||||
- **backup.ts**: Database backup/restore with configurable scheduled backups and retention pruning.
|
||||
- **settings.ts**: Runtime app settings (registration open/closed, backup config, etc.) via `appSettings` table.
|
||||
- **webhooks.ts**: Processes incoming Plex/Jellyfin/Emby webhook events.
|
||||
- **backup.ts**: Database backup/restore with configurable scheduled backups.
|
||||
- **settings.ts**: Runtime app settings via `appSettings` table.
|
||||
- **colors.ts**: Extracts dominant color palettes from title posters via node-vibrant.
|
||||
- **update-check.ts**: Checks for new Sofa releases.
|
||||
- **system-health.ts**: System health diagnostics.
|
||||
|
||||
### TMDB images
|
||||
|
||||
Only paths are stored in DB. Image URLs are resolved **server-side only** — API routes and server components call `tmdbImageUrl()` from `lib/tmdb/image.ts` before sending data to clients. Client components never import `tmdbImageUrl`; they receive ready-to-use URLs.
|
||||
Only paths are stored in DB. Image URLs are resolved by the API server — `tmdbImageUrl()` from `@sofa/tmdb/image` is called before sending data to clients. Client components receive ready-to-use URLs.
|
||||
|
||||
When `IMAGE_CACHE_ENABLED` is set (default), images are downloaded to local disk (`CACHE_DIR`, derived from `DATA_DIR/images`) and served via `app/api/images/[...path]/route.ts`. Categories: `posters` (w500), `backdrops` (w1280), `stills` (w1280), `logos` (w92), `profiles` (w185). When disabled, `tmdbImageUrl()` returns direct TMDB CDN URLs using `TMDB_IMAGE_BASE_URL` (defaults to `https://image.tmdb.org/t/p`).
|
||||
|
||||
Core files: `lib/services/image-cache.ts` (caching logic), `lib/tmdb/image.ts` (URL construction), `app/api/images/[...path]/route.ts` (proxy route).
|
||||
When `IMAGE_CACHE_ENABLED` is set (default), images are downloaded to local disk and served via `/images/:category/:filename`. When disabled, `tmdbImageUrl()` returns direct TMDB CDN URLs.
|
||||
|
||||
### Background jobs
|
||||
|
||||
Defined in `lib/cron.ts` using croner cron expressions, started via Next.js instrumentation hook (`instrumentation.ts`) when `NEXT_RUNTIME === "nodejs"`. The instrumentation hook also runs DB migrations on startup and registers graceful shutdown handlers.
|
||||
|
||||
Jobs (all use `protect: true` to prevent overlapping runs, 300ms delay between TMDB calls):
|
||||
Defined in `apps/server/src/cron.ts` using croner, started when the API server boots. Jobs (all use `protect: true` to prevent overlapping runs, 300ms delay between TMDB calls):
|
||||
- `nightlyRefreshLibrary` (`0 3 * * *`) — refreshes stale library titles (7d) and non-library titles (30d)
|
||||
- `refreshAvailability` (`0 */6 * * *`) — streaming provider data
|
||||
- `refreshRecommendations` (`0 */12 * * *`)
|
||||
@@ -207,10 +264,14 @@ Jobs (all use `protect: true` to prevent overlapping runs, 300ms delay between T
|
||||
|
||||
### Environment variables
|
||||
|
||||
See `.env.example`: `DATA_DIR` (root for DB + cache, default `./data`), `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`. `DATABASE_URL` and `CACHE_DIR` are derived from `DATA_DIR` but can be overridden individually.
|
||||
`DATA_DIR` (root for DB + cache, default `./data`), `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`. `DATABASE_URL` and `CACHE_DIR` are derived from `DATA_DIR` but can be overridden individually.
|
||||
|
||||
Optional: `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER_URL`, `OIDC_PROVIDER_NAME`, `OIDC_AUTO_REGISTER`, `DISABLE_PASSWORD_LOGIN` (OIDC/SSO support). `LOG_LEVEL` (error/warn/info/debug, default: info). `IMAGE_CACHE_ENABLED` (default: true).
|
||||
|
||||
### Docker deployment
|
||||
|
||||
Single container, single process. Hono serves both the API and the Vite-built SPA static files on port 3000. API routes (`/rpc/*`, `/api/*`) are mounted first; unmatched routes fall back to `index.html` for SPA routing.
|
||||
|
||||
## Browser Automation
|
||||
|
||||
Use `agent-browser` for web automation. Run `agent-browser --help` for all commands.
|
||||
|
||||
Reference in New Issue
Block a user