mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
* feat: add filtering and sorting across library, explore, and upcoming Add comprehensive filtering and sorting to all list views on both web and mobile. Library (new dedicated page): - New /library route (web) with URL search param persistence - New Library tab (5th tab, mobile) with FlashList grid - Filters: status (multi-select), type, genre (library-only), user rating range, release year (decade presets + custom), content rating, available to stream - Sort by: title, date added, release date, popularity, user rating, TMDB rating - Text search within collection - Filter popover (web) / bottom sheet with Apply (mobile) - Remove old dashboard.library endpoint in favor of library.list Explore/Discover: - New Discover section on Explore page with inline filter controls - TMDB filters: genre (now optional), year range, min rating, sort order, original language, streaming provider - Watch provider list endpoint (WATCH_REGION env var, default "US") - Default to popular content when no filters selected Upcoming: - Type filter (All/Movies/TV Shows) and status filter (All/Watching/Watchlist) - Toggle chips on both web (URL params) and mobile - Backend: mediaType and statusFilter params on upcoming endpoint Navigation: - Library added to web nav bar (desktop + mobile tab bar) - Library added as 5th tab on mobile (between Home and Explore) - Dashboard library section simplified to compact preview with "See all" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add user streaming platform subscriptions and redesign filter UI Replace the denormalized availabilityOffers table with a normalized platforms + titleAvailability + userPlatforms schema. Users can now declare which streaming services they subscribe to, enabling personalized "On your services" availability display and filtered library/explore results. Backend: new platforms table seeded from TMDB provider data on startup, new API endpoints (platforms.list, account.platforms, account.updatePlatforms), title detail annotates availability with isUserSubscribed flag, library "on my services" filter checks user's subscribed platforms. Web UI: post-registration onboarding page for platform selection, streaming services settings section, title detail split into "On your services" / "Also available on", explore dropdown uses platforms table with active filter highlighting. Library filters redesigned from cluttered popover to clean collapsible inline strip with consolidated dropdowns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address race conditions, stale state, and edge cases in platforms feature - Fix debounced autosave race in streaming services settings: use ref for latest selection + counter guard so only the most recent save updates UI. Clean up timers on unmount. - Fix explore provider dropdown desyncing from query filter when switching Movie/TV type: reset selectedPlatformId alongside providerId. - Fix platformIdsExist rejecting duplicate valid IDs by deduplicating input before comparing against DB results. - Fix stale platform metadata: always upsert name/logo on TMDB refresh instead of skipping when platform already exists. - Fix invisible ratingMax filter: clear ratingMax when user changes ratingMin since the UI only exposes a single rating dropdown now. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear stale saved-state timer and preserve falsy filter values - Clear previous savedTimerRef timeout before scheduling a new one in streaming-services-section, preventing an older timer from hiding the "Saved" indicator too early on rapid successive saves - Replace `value || undefined` with explicit empty-value check in library filter handler so numeric 0 (e.g. ratingMin=0) is not incorrectly dropped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add TanStack Form and migrate auth + change password forms Set up TanStack Form v1 composition layer with createFormHook, pre-bound field components (TextField, TextareaField, CheckboxField, SwitchField, SubmitButton), and migrate the two traditional submit-based forms: - AuthForm: replace 4 useState calls with useAppForm, use form.Field for each input while preserving motion animation layout - ChangePasswordDialog: replace 6 useState calls with useAppForm + Zod schema validation (cross-field password match, min length), per-field error display, form.reset() on dialog close Also adds @tanstack/react-form to the monorepo catalog and updates both web and native apps to reference it via catalog:. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# CLAUDE.md
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Root commands (via Turborepo)
|
|
bun run dev # Start API, Vite dev, and Expo dev servers
|
|
bun run dev:docs # Start the fumadocs/Next.js dev server
|
|
bun run build # Production build (both apps)
|
|
bun run lint # Oxlint lint check
|
|
bun run format # Oxfmt format (auto-fix)
|
|
bun run check-types # TypeScript type check
|
|
bun run test # Run tests
|
|
bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes)
|
|
bun run i18n:extract # Run LingUI's string extraction
|
|
bun run i18n:claude # Prompt Claude Code to fill in untranslated strings
|
|
|
|
# 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)
|
|
```
|
|
|
|
**Use Bun, not Node.js** — `bun <file>`, `bun install`, `bun run <script>`, `bunx <pkg>`. Bun auto-loads `.env`. **Never run `bun test` directly** — always use `bun run test` which runs Vitest via Turborepo.
|
|
|
|
## CRITICAL: Pre-commit checks
|
|
|
|
Before declaring a task is complete, all four commands MUST pass with zero errors or warnings:
|
|
|
|
```bash
|
|
bun run lint
|
|
bun run format:check
|
|
bun run check-types
|
|
bun run test
|
|
```
|
|
|
|
## Architecture
|
|
|
|
**Sofa** is a self-hosted movie & TV tracking app built as a Turborepo monorepo.
|
|
|
|
```
|
|
couch-potato/
|
|
├── apps/
|
|
│ ├── native/ # @sofa/native — Expo 55 React Native app (Expo Router, UniWind)
|
|
│ ├── public-api/ # @sofa/public-api — Hono microservice on Vercel (version check, telemetry)
|
|
│ ├── server/ # @sofa/server — Hono API server (oRPC, auth, cron, webhooks)
|
|
│ └── web/ # @sofa/web — Vite SPA (TanStack Router, TanStack Query)
|
|
├── docs/ # sofa-docs — Fumadocs (Next.js) documentation site + OpenAPI reference
|
|
├── packages/
|
|
│ ├── api/ # @sofa/api — oRPC contract + Zod schemas (shared, JIT)
|
|
│ ├── auth/ # @sofa/auth — Better Auth server config (JIT)
|
|
│ ├── config/ # @sofa/config — Path constants + TMDB URLs from env (JIT)
|
|
│ ├── core/ # @sofa/core — Business logic services (JIT, DB-agnostic)
|
|
│ ├── db/ # @sofa/db — Drizzle schema, client, queries, migrations (JIT)
|
|
│ ├── logger/ # @sofa/logger — Pino-based structured logging (JIT)
|
|
│ ├── test/ # @sofa/test — Vitest setup + in-memory DB test helpers
|
|
│ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper (JIT)
|
|
├── .oxlintrc.json
|
|
├── .oxfmtrc.json
|
|
├── Dockerfile
|
|
├── package.json
|
|
└── turbo.json
|
|
```
|
|
|
|
All shared packages are JIT (raw TypeScript exports, no build step).
|
|
|
|
### Apps
|
|
|
|
- **`@sofa/server`** — Hono API server. Hosts oRPC procedures, Better Auth, webhook/image/backup routes, and cron jobs. Runs DB migrations on startup. Dev: port 3001. Prod: serves SPA static files too, port 3000.
|
|
- **`@sofa/web`** — Vite SPA with TanStack Router (file-based routing). No SSR, no DB. All data via oRPC. Vite dev server proxies `/api/*` and `/rpc/*` to the API server.
|
|
- **`@sofa/native`** — Expo Router app with 4-tab layout (Home, Explore, Search, Settings). UniWind for styling, `@better-auth/expo` with SecureStore for auth, oRPC client for API calls. Dark-only cinema theme matching web.
|
|
- **`@sofa/public-api`** — Minimal Hono microservice deployed on Vercel. Endpoints: `GET /v1/version` (latest release from GitHub), `POST /v1/telemetry` (forwards instance stats to PostHog), and OAuth device-code proxy for Trakt/Simkl imports (`/v1/import/:provider/device-code`, `/v1/import/:provider/poll`). Dev: port 3002.
|
|
- **`sofa-docs`** — Fumadocs site (Next.js) with landing page, Markdown docs, and auto-generated OpenAPI API reference. Content lives in `docs/content/docs/`. API docs are generated from `docs/openapi.json` via `fumadocs-openapi`.
|
|
|
|
### Stack
|
|
|
|
- **Frontend**: Vite 7 SPA, React 19, TanStack Router (file-based), Tailwind CSS v4, shadcn, Jotai
|
|
- **Mobile**: Expo 55, Expo Router, UniWind (Tailwind for RN), `@tabler/icons-react-native`
|
|
- **API**: Hono on Bun, oRPC (contract-first) with `@orpc/tanstack-query`
|
|
- **Database**: SQLite via `bun:sqlite` + Drizzle ORM (WAL mode, sync queries, auto-migrations)
|
|
- **Auth**: Better Auth with Drizzle adapter — email/password + optional OIDC/SSO
|
|
- **Monorepo**: Turborepo with Bun workspaces
|
|
- **Docs**: Fumadocs (Next.js), fumadocs-openapi for API reference
|
|
- **Testing**: Vitest (unit tests on Node.js, browser component tests via Playwright)
|
|
- **Linting**: Oxlint + Oxfmt (2-space indent, organized imports, Tailwind class sorting)
|
|
- **External API**: TMDB (The Movie Database)
|
|
- **Translation**: LingUI + Crowdin
|
|
|
|
### Package imports
|
|
|
|
Path aliases: `@/*` maps to `src/` in both `apps/web/` and `apps/native/`.
|
|
|
|
Cross-package imports:
|
|
|
|
- `@sofa/api/contract`, `@sofa/api/schemas` — Contract and Zod types
|
|
- `@sofa/db/queries/*` — Query layer (e.g., `@sofa/db/queries/tracking`, `@sofa/db/queries/metadata`)
|
|
- `@sofa/db/client`, `@sofa/db/schema`, `@sofa/db/migrate`
|
|
- `@sofa/test/setup`, `@sofa/test/db` — Test setup (Vitest) and DB helpers (in-memory SQLite, seed functions)
|
|
- `@sofa/tmdb/client`, `@sofa/tmdb/image`
|
|
- `@sofa/core/metadata`, `@sofa/core/tracking`, `@sofa/core/imports`, etc.
|
|
- `@sofa/auth/server`, `@sofa/auth/config`
|
|
- `@sofa/config` — Path constants (`DATA_DIR`, `DATABASE_URL`, `CACHE_DIR`, `BACKUP_DIR`, `AVATAR_DIR`) and TMDB URLs
|
|
- `@sofa/logger` — `createLogger(name)` for structured logging
|
|
|
|
### Key patterns
|
|
|
|
**Layered architecture** — Strict separation: `apps/server/ → @sofa/core/* → @sofa/db/queries/* → @sofa/db/client`. Server procedures/routes call core services for all business logic. Core services call query functions for all DB access. Core must **never** import `@sofa/db/client` directly. Type-only imports from `@sofa/db/schema` are allowed in core (for `$inferInsert`/`$inferSelect`), but runtime imports are not.
|
|
|
|
**Query layer** — All Drizzle ORM queries live in `packages/db/src/queries/`, organized by domain (tracking, metadata, discovery, etc.). Plain exported functions, transactions hidden as implementation details. Wildcard export: `"./queries/*": "./src/queries/*.ts"`.
|
|
|
|
**oRPC queries** use `orpc.*.queryOptions()` with TanStack Query. **Mutations** use `client.*()` directly. **Route loaders** prefetch via `queryClient.ensureQueryData()`.
|
|
|
|
**Auth guards** — Web routes use `beforeLoad` + `authClient.getSession()`. API procedures use auth middleware that calls `auth.api.getSession()`.
|
|
|
|
**TMDB images** — Only paths stored in DB. The API server resolves full URLs via `tmdbImageUrl()`. When `IMAGE_CACHE_ENABLED` (default), images are downloaded to disk and served via `/images/:category/:filename`.
|
|
|
|
**Database IDs** — All app tables use UUIDv7 text PKs via `Bun.randomUUIDv7()`.
|
|
|
|
**i18n (LingUI)** — All user-facing strings are wrapped with LingUI macros. The `@sofa/i18n` shared package holds the i18n singleton, locale metadata, and `Intl`-based format utilities. Config lives at the repo root (`lingui.config.ts`). PO catalogs and compiled TS files live in `packages/i18n/src/po/`. Crowdin syncs via `crowdin.yml` with `languages_mapping` to map regional codes (es-ES→es, pt-PT→pt).
|
|
|
|
- **JSX text** → `<Trans>` from `@lingui/react/macro`
|
|
- **Strings in hooks/components** → `const { t } = useLingui()` from `@lingui/react/macro`, then `` t`string` ``
|
|
- **Strings outside React** (plain modules) → `import { msg } from "@lingui/core/macro"` + `import { i18n } from "@sofa/i18n"`, then ``i18n._(msg`string`)``
|
|
- **Pluralization** → `import { plural } from "@lingui/core/macro"`. Use standalone when it's the whole string: `plural(count, { one: "# item", other: "# items" })`. Nest inside `t` only when there's surrounding text: `` t`You have ${plural(count, { one: "# item", other: "# items" })} in your cart` ``
|
|
- **DO NOT use** `t(i18n)` — it's deprecated in v5 and removed in v6. Use `i18n._(msg`...`)` instead.
|
|
- **Date/number formatting** → use `formatDate`, `formatRelativeTime`, `formatNumber`, `formatBytes` from `@sofa/i18n/format` (Intl-based, locale-aware). Never use `date-fns` in app code.
|
|
- **Native Intl polyfills** → `@formatjs/intl-*` polyfills loaded in `apps/native/src/lib/intl-polyfills.ts` (strict dependency order, with locale data for all 6 languages).
|
|
- **Error messages** → Server throws `ORPCError` with `data: { code: AppErrorCode.XXX }`. Clients map codes to localized strings via per-app `error-messages.ts`. Never display `error.message` to users.
|
|
- After adding/changing strings, run `bun run i18n:extract`. The Vite and Metro plugins compile `.po` catalogs on the fly — no manual compile step needed.
|
|
|
|
### Environment variables
|
|
|
|
Required: `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
|
|
|
|
Optional:
|
|
|
|
- `DATA_DIR` — Root for DB + cache (default `./data`). `DATABASE_URL` and `CACHE_DIR` derived from it but overridable.
|
|
- `TMDB_API_BASE_URL`, `TMDB_IMAGE_BASE_URL` — Override TMDB endpoints.
|
|
- `PUBLIC_API_URL` — Base URL for centralized public API (default: `https://public-api.sofa.watch`). Used for update checks and OAuth import proxy.
|
|
- `IMAGE_CACHE_ENABLED` — Default `true`. Set `false` for direct TMDB CDN URLs.
|
|
- `LOG_LEVEL` — `error`/`warn`/`info`/`debug` (default: `info`).
|
|
- OIDC: `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER_URL`, `OIDC_PROVIDER_NAME`, `OIDC_AUTO_REGISTER`, `DISABLE_PASSWORD_LOGIN`.
|
|
|
|
### Testing
|
|
|
|
**Vitest** (not Bun's built-in test runner). Always run tests via `bun run test` (never `bun test`).
|
|
|
|
- **`@sofa/test`** — Shared test package. `@sofa/test/setup` is the Vitest setup file (mocks `@sofa/db/client` with in-memory SQLite, polyfills `Bun.randomUUIDv7`). `@sofa/test/db` exports the test DB instance, seed helpers (`insertUser`, `insertTitle`, etc.), and `clearAllTables`.
|
|
- **`packages/core/test/`** — Unit tests for business logic. Config: `packages/core/vitest.config.ts`.
|
|
- **`apps/web/`** — Browser component tests via `@vitest/browser` + Playwright. Config: `apps/web/vitest.config.ts`.
|
|
- To add tests to a new package: create a `vitest.config.ts` with `setupFiles: ["@sofa/test/setup"]`, add `"test": "vitest run"` to scripts, and add the project path to the root `vitest.config.ts` `projects` array. Do **not** add `@sofa/test` to the package's `devDependencies` — it resolves via hoisted `node_modules`, and adding it would cause `turbo prune` to pull `better-sqlite3` into Docker builds.
|
|
|
|
### Docker
|
|
|
|
Single container, single process. Hono serves API + SPA on port 3000. API routes (`/rpc/*`, `/api/*`) mounted first; unmatched routes fall back to `index.html`.
|
|
|
|
## Browser Automation
|
|
|
|
Use `agent-browser` for web automation (`agent-browser --help` for all commands). Auth credentials: `demo@sofa.watch` / `password`.
|