- Replace root `eslint-plugin-lingui` jsPlugin with `@e18e/eslint-plugin`; move lingui rules into per-app `.oxlintrc.json` overrides for native and web
- Add `jsx-a11y`, `react-hooks-js` (native), and expanded lingui rule sets to per-app configs
- Add `oxc/no-barrel-file` warning at threshold 0 to root config
- Fix `e18e/prefer-url-canparse`: replace `try { new URL() } catch` with `URL.canParse()` in server-url screen and server lib
- Fix `e18e/prefer-timer-args`: pass callback args directly to `setTimeout` instead of wrapping in arrow functions (use-debounce, integration-card)
- Fix `e18e/prefer-static-regex`: hoist `/\/+$/` to module-level constant in server-url screen
- Fix `react-hooks-js/refs` and `react-hooks-js/set-state-in-effect`: convert `useRef` tracking patterns to `useState` + render-time derived updates in `use-server-connection`, `expandable-text`, and settings screen
- Fix `react-hooks-js/immutability`: extract stable palette sub-values before `useMemo` deps in title detail screen
- Apply `e18e` and other rule fixes across core, tmdb, web components, and i18n packages
- New integrations.test.ts: 13 tests covering CRUD, token generation,
token lookup, regeneration, and multi-user isolation
- New cache.test.ts: 3 tests for purgeMetadataCache (shell title
deletion, fully-fetched title preservation, empty case)
- New cron.test.ts: 5 tests for cron run lifecycle (start, complete,
fail with Error and string)
- New telemetry.test.ts: 7 tests for isTelemetryEnabled toggle and
performTelemetryReport (disabled skip, enabled send, 24h interval
throttle, interval expiry, fetch failure resilience)
345 → 386 tests passing across 27 files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace bun:test with Vitest across all 22 test files
- Create @sofa/test package with shared setup and DB test helpers
- setup.ts: vi.mock for @sofa/db/client, Bun.randomUUIDv7 polyfill
- db.ts: in-memory SQLite via better-sqlite3, seed helpers
- Add per-project vitest configs (packages/core, apps/web)
- Add root vitest.config.ts with projects and v8 coverage
- Set up @vitest/browser + Playwright for web component tests
- Move validateBackupDatabase from core/backup.ts to @sofa/db/client
- Derive required backup tables from schema instead of hard-coded list
- Update CI workflow with Playwright install and Codecov upload
- Update CLAUDE.md documentation for Vitest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `ContinueWatching` (systemSmall) and `Upcoming` (systemSmall) SwiftUI widgets via `expo-widgets`, registered in `app.json` under the `group.com.jakejarvis.sofa` app group
- Add `widget-images` local Expo module with a Swift `WidgetImagesModule` that downloads and resizes remote poster images into the shared app group container for use by widgets; also copies bundled assets (sofa icon fallback)
- Add `src/lib/widgets.ts` to fetch dashboard data, cache poster images, and write a JSON timeline to the shared container via `expo-widgets`
- Add `ContinueWatching` and `Upcoming` SwiftUI widget views in `src/widgets/`
- Add `useWidgetRefresh` hook that refreshes widgets on foreground (throttled to once per minute) and on session ready
- Debounce widget refresh in `invalidateTitleQueries` so rapid mutations (e.g. marking multiple episodes watched) batch into a single refresh
- Expose `titles.upcoming` from the dashboard procedure and add `upcomingTitles` field to the dashboard schema
- Bump iOS deployment target to 16.2 (required by WidgetKit timeline APIs)
- Export `isLocaleRTL` from `@sofa/i18n` to detect right-to-left locales
- Native: call `I18nManager.allowRTL` / `forceRTL` on locale init and on locale change; reload the app immediately if the current layout direction doesn't match the persisted locale, and prompt the user to restart when switching between LTR and RTL locales
- Native: flip the `SettingsRow` chevron to `IconChevronLeft` when `I18nManager.isRTL` is true
- Native: add Arabic Intl polyfill locale data (PluralRules, NumberFormat, DateTimeFormat, RelativeTimeFormat)
- Web: enable `"rtl": true` in `components.json` and regenerate all shadcn UI components to use CSS logical properties (`ms-*`, `me-*`, `ps-*`, `pe-*`, `text-start`, `text-end`) instead of physical `ml-*`/`mr-*`/`text-left` equivalents
- Web: apply the same logical-property conversions to non-generated components (nav-bar, settings sections, title cards, hero banner, continue-watching card)
- Web: set `document.dir` based on the active locale's RTL flag in the i18n initialiser and root route
- Add `../../packages/i18n/src/po.d.ts` to native `tsconfig.json` includes so TypeScript accepts `.po` imports
- Replace horizontal scrolling stats list on native with a 2×2 grid layout; each card gets a color-coded icon, tint background, and an inline sparkline
- Add `Sparkline` component to `apps/native/src/components/ui/` (path-based SVG rendered via `react-native-svg`)
- Upgrade `StatsCard` (native) to support `icon`, `color`/`tintColor`/`bgColor` theming, `sparklineData`, and a `zeego` dropdown menu for switching the time period (today / this week / this month / this year)
- Wire `moviePeriod` / `episodePeriod` state in the home screen and fetch `dashboard.watchHistory` for each so counts and sparklines react to the selected period
- Rewrite `apps/web/src/components/dashboard/sparkline.tsx` as a self-contained SVG sparkline (no Recharts), and update `stats-display.tsx` accordingly
- Remove `apps/web/src/components/ui/chart.tsx` (Recharts wrapper) and drop the `recharts` dependency from `apps/web/package.json`
- Add `packages/i18n/scripts/claude.ts` — a Bun script that translates empty PO entries via the Claude Code CLI, supporting batch mode, dry-run, optional AI context from `crowdin-context.jsonl`, and structured JSON output validation
- Add `i18n:claude` root script (`bun packages/i18n/scripts/claude.ts`) as a convenience alias
- Fill all previously empty msgstr entries across de, es, fr, it, pt PO catalogs and recompile `.ts` catalogs
- Polish README with badges (license, CI status, release, language, Crowdin localization) and minor formatting fixes
Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter)
and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule
coverage.
- Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins
and correctness/suspicious categories
- Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind
class sorting (all 30+ custom className attributes migrated)
- Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin
- Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`,
`oxfmt --check`
- Add `format:check` turbo task and CI step
- Update VS Code settings/extensions to use `oxc.oxc-vscode`
- Update CI path triggers from `biome.json` to new config files
- Remove all `biome-ignore` comments and fix shadowed variables
- Delete `biome.json` and `docs/biome.json`
- Reformat entire codebase with oxfmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add i18n with LingUI, Crowdin integration, and typed error codes
- Add `@sofa/i18n` shared package with LingUI v5 (v6-ready config using
`defineConfig` + `@lingui/format-po`), eager English + lazy-loaded
fr/de/es/it/pt catalogs, `Intl`-based date/number format utilities,
and test helpers
- Wire `@lingui/vite-plugin` + babel macro plugin for web, and
`@lingui/metro-transformer` + babel config for native
- Add `I18nProvider` to both app roots with locale auto-detection
(navigator.language / expo-localization) and persistence
(localStorage / MMKV)
- Wrap all ~512 user-facing strings across web and native with LingUI
macros (`<Trans>`, `useLingui`/`t`, `i18n._(msg`...`)`, `plural`)
- Add language switcher to Settings in both apps
- Add `@sofa/api/errors` with 13 typed `AppErrorCode` values and
`appErrorData()` helper for contract `.errors()` schemas
- Update all oRPC contract error definitions with typed `data` fields;
convert import procedure `throw new Error()` to `ORPCError` with codes
- Add per-app `error-messages.ts` utilities that map error codes to
localized strings; update global `QueryCache.onError` handlers to stop
leaking raw `error.message` to users
- Add `crowdin.yml` config and `.github/workflows/crowdin.yml` for
automated source upload on merge and translation pull via dispatch
- Add `lingui.config.ts` at repo root with `bun run i18n:extract` and
`bun run i18n:compile` convenience scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add Crowdin language mapping and ignore context file
* fix(i18n): address 20 localization quality issues
- Stop leaking raw error.message to users in change-password and
account-section; use localized fallbacks instead
- Add typed error data to discover contract's .errors() schema
- Replace all manual English plural suffixes with LingUI plural() macro
in episode counts, backup counts, star ratings, title/image counts
- Replace date-fns formatDistanceToNow and hardcoded date patterns with
locale-aware formatDate/formatRelativeTime from @sofa/i18n/format
- Convert integration-configs from module-scope translations (frozen at
import time) to lazy getIntegrationConfigs(i18n) function
- Combine split Trans fragments into single translatable units in
backup-schedule retention sentence and stats-display period labels
- Localize TV media type badge and full episode accessibilityLabel
- Replace Android-incompatible Alert.alert language picker (max 3
buttons) with zeego DropdownMenu
- Await async locale initialization before hiding splash screen so
non-English users don't see English flash on cold start
- Add @lingui/core as direct native app dependency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(i18n): add Intl polyfills for Hermes, replace DropdownMenu language picker, and remove date-fns
- Add `@formatjs/intl-*` polyfills loaded at app startup in `intl-polyfills.ts` so Hermes on Android has full `Intl` support for locale-aware formatting
- Alias `@formatjs/icu-messageformat-parser` to its `no-parser` variant in metro config to reduce bundle size
- Replace the zeego `DropdownMenu` language picker in Settings with a new `SelectModal` component that works reliably on all platforms without the Android 3-button limit
- Replace `date-fns` `format`/`parseISO` in `person/[id].tsx` with `formatDate` from `@sofa/i18n/format`; remove `date-fns` from native and web `package.json`
- Refactor `StatusActionButton` to use a `StatusLabel` component with `<Trans>` rather than a `getStatusConfig(t)` factory so labels are always reactive to locale changes
- Fix crowdin workflow to use root `bun run i18n:compile` script instead of `cd packages/i18n && bun run compile`
- Wrap root layout `GestureHandlerRootView` in `SafeAreaProvider` and add `.catch` to locale-ready promise to prevent unhandled rejections blocking the splash screen
* fix(i18n): localize integration status helpers, fix stats-display Trans wrapping, and clean up SelectModal
- Localize `webhookStatus` and `listStatus` in `integration-card.tsx` using `i18n._(msg`...`)` so status strings are translated instead of always rendering in English
- Wrap "Movies {select}" and "Episodes {select}" in a single `<Trans>` in `stats-display.tsx` so the period selector element is embedded inside the translatable unit rather than concatenated outside it
- Fix locale activation order in Settings: close the modal first, then `activateLocale`, and only call `setPersistedLocale` on success to avoid persisting a locale that failed to load
- Remove the redundant `SafeAreaProvider`/`SafeAreaView` wrapper from `SelectModal` — the root layout already provides `SafeAreaProvider`
- Recompile all six `.po`/`.ts` catalogs to pick up new and updated message strings
* chore(i18n): crowdin sync
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace `Alert` with `toast` for sign-in/register errors and add a `getFormErrors` utility to extract the first Zod validation message and field set
- Display the configured server host inline on the login screen and hide the navigation header in the auth layout (`headerShown: false`, `navigationBarHidden: true`)
- Delete `AuthStackHeader` and `DetailStackHeader`; introduce `ModalStackHeader` for screens that are pushed as modals
- Remove unused `StatusBadge` component and delete unused shadcn web components (`calendar`, `drawer`)
- Bump all Expo 55 SDK packages to their latest patch versions and add `expo-web-browser` plugin to `app.json` + `@react-navigation/native` as an explicit dependency
Replace the three `@expo-google-fonts` npm dependencies (dm-sans, dm-serif-display, geist-mono) with locally vendored TTF files under `assets/fonts/`. Update `app.json` font paths accordingly and consolidate the four separate DM Sans Android font-family entries into a single `"DM Sans"` family with multiple weight definitions. Rename font family identifiers throughout components and `global.css` to match the simplified names (`DM Sans`, `DM Serif Display`, `Geist Mono`).
- Add `apps/public-api` (`@sofa/public-api`) — minimal Hono app
deployable to Vercel; `GET /v1/version` fetches the latest GitHub
release and returns `{ version, releaseUrl }` with a 15-minute
CDN cache (`s-maxage=900, stale-while-revalidate=3600`)
- Update `packages/core/src/update-check.ts` to call
`PUBLIC_API_URL/v1/version` instead of the GitHub API directly;
`PUBLIC_API_URL` defaults to `https://public-api.sofa.watch`
Swap the hand-rolled `ToastView`/`ToastProvider`/queue system for the
`burnt` library, which delegates to native iOS/Android toast APIs.
- Add `burnt@0.13.0` dependency
- Delete `toast.tsx` and `toast-provider.tsx`; remove `<ToastProvider />`
from `_layout.tsx`
- Rewrite `lib/toast.ts` to wrap `burnt` — maps success/error/info/warning
to burnt presets and haptic types; duration converted from ms to seconds
Track titles and people visited in the native app and surface them
in the search tab when the query is empty, replacing the static
empty-state icon.
- Add `lib/recently-viewed.ts` — MMKV-backed store (max 20 items)
with `addRecentlyViewed`, `removeItem`, `clearAll`, and a
`useRecentlyViewed` hook
- Track visits via `useEffect` in `title/[id].tsx` and
`person/[id].tsx` once data resolves
- `RecentlyViewedList` — platform-split component:
- iOS (`.ios.tsx`): native SwiftUI List via `@expo/ui` with
system SF Symbols, swipe-to-delete, and a "Clear" button
- Android/fallback: FlatList with `SwipeableRow` for delete
- Add `SwipeableRow` component using `ReanimatedSwipeable`
- Clear recently viewed history on sign-out (both `HeaderAvatar`
and Settings screen)
- Add `@expo/ui@55.0.2` dependency
- Swap `runOnJS` for `scheduleOnRN` from `react-native-worklets` in
gesture handler `.onEnd()` callbacks (HeroBanner, PosterCard)
- Delete custom `timeAgo` util; use `date-fns` `formatDistanceToNow`
in integration status strings
- Enhance person detail screen: formatted birth date with age/death
calculation, place of birth with MapPin icon, department label
mapping, CalendarIcon for dates
- Fix `ExpandableText` truncation detection by measuring line count
on a hidden full-text node rather than the clamped one; add
`actionColor` prop so title accent color can theme the toggle
- Polish PosterCard metadata row: remove icon opacity, tighten gaps
* 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>
- 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
- 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
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>
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 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.
Background sparkline area charts on Movies and Episodes cards show watch
activity distribution over the selected period. Periods now use rolling
windows (past 24h/7d/30d/365d) instead of calendar boundaries. Bumped
stats grid breakpoint to lg. Upgraded recharts 2.15.4 → 3.7.0 and fixed
chart.tsx types for v3 (TooltipContentProps, LegendPayload).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title rows (TitleRow, ContinueWatchingList) now use the shadcn Carousel
component backed by Embla with embla-carousel-wheel-gestures, enabling
mouse wheel horizontal scrolling and drag-to-scroll on desktop while
preserving touch swipe, stagger animations, and existing layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap the hand-rolled Scheduler class (setInterval-based) for croner,
a battle-tested cron library with overlap protection and proper cron
expressions. Consolidate lib/jobs/ into a single lib/cron.ts and
remove the unused admin jobs API route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace @libsql/client with bun:sqlite for zero-dependency SQLite access,
swap drizzle-orm/libsql adapter for drizzle-orm/bun-sqlite, and rewrite
Dockerfile to use oven/bun:1-alpine. The raw Database instance is no longer
exported via Proxy (native C++ methods lose `this` binding through
Reflect.get); instead, a closeDatabase() helper handles graceful shutdown
and the health check uses Drizzle's db.run() directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>