Commit Graph
100 Commits
Author SHA1 Message Date
jake 753bd2d0cc refactor(native): extract shared RecentlyViewedRowContent and polish iOS search list
Extract the thumbnail + text content into a shared `RecentlyViewedRowContent` component so both the iOS SwiftUI list and the Android/default `RecentlyViewedRow` render identical row content without duplication.

Rework the iOS recently-viewed list header to use an `RNHostView`-backed React Native view instead of SwiftUI `Section`/`HStack`/`Button`, giving it the app's display font, a history icon, and a native `Pressable` clear button. Switch each list row to the same `RNHostView` pattern so rows render with the app's background color and consistent insets. Replace SF Symbols type icons with Tabler icons (`IconMovie`, `IconDeviceTv`, `IconUser`) to match the rest of the app.
2026-03-16 11:54:54 -04:00
jake 32cfbc2e86 fix(native): change background color to pure black for OLED screens 2026-03-16 11:42:16 -04:00
jake 1d1555ccf7 fix(native): clear transparent header titles and minor UI polish
Remove the hardcoded "Title" and "Person" header titles (and the person's name) from transparent headers so the back button floats cleanly over the hero image. Simplify `StatusActionButton` by dropping the unused `--color-status-watchlist` CSS variable lookup and collapsing the icon color logic; also rename the watchlist label to "Watchlisted". Fix `ExpandableText` to only mark the text `selectable` on iOS, since Android's `EditText`-backed selectable text ignores `numberOfLines` and scrolls instead of truncating.
2026-03-16 11:21:59 -04:00
jake 1c3c74fb0a fix(native): make person filmography grid responsive to screen width
Replace the hardcoded 2-column filmography grid with a responsive layout that uses 2, 3, or 4 columns based on screen width (600px and 900px breakpoints). Switch from per-item fixed widths to horizontal gutter padding so columns are evenly distributed across all screen sizes.
2026-03-16 11:07:49 -04:00
jake 3e9d7efd94 refactor(native): replace polling-based server reachability with passive fetch interception
Swap the periodic health-probe loop for a `serverFetch` wrapper that updates reachability state as a side-effect of every real request: any HTTP response marks the server reachable; a network-level error marks it unreachable.

Wire TanStack Query's `onlineManager` and `focusManager` to `expo-network` and `AppState` so queries and mutations are automatically paused when the device goes offline and resumed on reconnect. Set `networkMode: "online"` on both queries and mutations to opt in to this behaviour.

Move `isNetworkError` from `query-client.ts` into `server-reachability.ts` and extend its pattern list. Export `serverFetch` and use it as the custom fetch implementation in both the oRPC `RPCLink` and the Better Auth client so all network traffic flows through the same reachability tracker.

Move the retry logic out of `useServerReachability` and into the banner components directly — on press, resume paused mutations, refetch active queries, and refresh the auth session.

Add a unit test that verifies a transport failure sets the server unreachable and any subsequent HTTP response (even a 503) clears it.
2026-03-16 11:03:24 -04:00
jake 88780c337e fix(native): enhance horizontal list components with consistent spacing
Introduce a new `horizontal-list-spacing` module to standardize spacing and separator components across various screens. Update `DashboardScreen`, `TitleDetailScreen`, and `HorizontalPosterRow` to utilize the new styles and separators, improving layout consistency. Refactor `CastCard` to use `useRouter` for navigation instead of `Link` for better performance and accessibility.
2026-03-16 10:58:18 -04:00
jake 639fa34585 fix(native): add change password screen and update navigation
Implement a new Change Password screen with form validation and user feedback. Adjust navigation to include the change password option in the settings. Update the layout to present the change password screen as a modal on Android and a form sheet on iOS. Clean up previous settings layout code by removing redundant options.
2026-03-16 10:49:40 -04:00
jake 8dfa54a60c refactor(native): polish UI, add accessibility labels, and improve auth navigation
Add `accessibilityLabel` (and `accessibilityHint`) to all form inputs across auth and settings screens. Switch the auth stack from a hidden header to a transparent header with back-button support so deep-linked screens navigate correctly. Present the change-password screen as a form sheet on iOS and a modal on Android, and wrap its body in a `ScrollView` to handle the keyboard and small screens. Move inline `contentContainerStyle` objects in the explore and home screens to module-level constants to avoid unnecessary re-renders. Enable `EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH` and `EXPO_UNSTABLE_TREE_SHAKING` in EAS production builds. Also apply miscellaneous layout and styling fixes across `poster-card`, `switch`, `text-field`, `hero-banner`, and several other UI components.
2026-03-16 00:15:35 -04:00
jake 7995790864 chore(deps): upgrade vite 8, @vitejs/plugin-react 6, @base-ui/react 1.3, hono 4.12.8, and related packages 2026-03-15 20:07:39 -04:00
jake 6c193a9271 feat(core): add pagination to explore, search, person, and library endpoints
Convert trending, search, people.detail, and dashboard.library to page-based responses (`page` / `totalPages`) so clients can load results incrementally.

Update `@sofa/core` discovery and person services to accept a `page` / `limit` input and slice results accordingly. Add a new DB migration to persist the data needed to back paginated filmography queries.

On the web, introduce a `useInfiniteScroll` hook and wire up `useInfiniteQuery` in the explore, person detail, and filterable title row components with an intersection-observer sentinel. On native, switch the same screens from `useQuery` to `useInfiniteQuery` with `onEndReached` / `ListFooterComponent` loading indicators. Also replace remaining `FlatList` usages with `FlashList` in the home and title detail screens.
2026-03-15 17:37:39 -04:00
jakeandClaude Opus 4.6 d0eca7cf32 feat(core): use TMDB combined credits for full person filmography
The people.detail endpoint now fetches a person's complete filmography
from TMDB's combined_credits API (cast + crew) instead of only returning
locally-enriched titles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 16:13:43 -04:00
jake 4ce285f721 feat(docs): use parameterized OpenAPI server URL
Replace the static `https://sofa.example.com/api/v1` server entry with a variable-based URL (`{protocol}://{host}:{port}/api/v1`) so the spec works against any instance out of the box. Also move the output path from `docs/openapi.json` to `docs/public/openapi.json` so the file is served as a static asset by the docs site.
2026-03-15 14:40:33 -04:00
jake d885fc524b refactor(native): extract NativeTabBar component and add Android-specific tab bar implementation 2026-03-15 13:25:02 -04:00
jake a2862e5948 feat(config): update turbo.json to include UI setting and modify web package.json dev script to allow external access 2026-03-15 12:16:39 -04:00
jake c201006ca2 test(core): replace image-cache module mocks with globalThis.fetch spies
Switch `mock.module("../src/image-cache", ...)` stubs in thumbhash, person-detail, colors, and credits tests to `spyOn(globalThis, "fetch")` instead. This avoids the cross-file persistence problem with `mock.module` and tests the real code paths more faithfully.

Update `image-cache.ts` to call `globalThis.fetch` so the spy intercepts it, add `mock.restore()` to the global `afterEach` in `preload.ts`, and set `IMAGE_CACHE_ENABLED=false` in affected tests to avoid disk writes during test runs.
2026-03-15 11:38:35 -04:00
jake d060177c67 refactor(web): consolidate change password into account section dialog
Replace the standalone `ChangePasswordSection` card with a `ChangePasswordDialog` triggered from a button next to "Sign out" in the account section. This removes the separate card, merges the dialog logic into `account-section.tsx`, and cleans up the settings page layout accordingly.
2026-03-15 11:32:18 -04:00
jake fe81a541b4 refactor(docs): split grouped API reference pages into per-procedure MDX files
Replace the single MDX file per API tag (e.g. `admin.mdx`, `titles.mdx`) with individual files for each procedure (e.g. `admin/admin.backups.create.mdx`). Each file renders a single `<APIPage>` with one operation, includes an inline description, and carries its own `_openapi` frontmatter.

Update `generate-api-docs.ts` to emit this per-procedure structure and adjust `source.ts` and `next.config.mjs` to resolve the new paths correctly.
2026-03-15 11:02:58 -04:00
jake 297eb6b550 feat(admin): add cache purge controls to settings
Add `purgeMetadataCache` and `purgeImageCache` procedures that let admins free disk space on demand. Metadata purge removes un-enriched stub titles not in any user's library plus orphaned person records; image purge deletes all cached TMDB files from disk.

Expose both as POST endpoints under `/admin/cache/` via the oRPC contract, implement the core logic in a new `@sofa/core/cache` module, and surface them in a new "Danger Zone" section on the Settings page with individual confirmation dialogs and a combined "Purge all" action.
2026-03-15 10:37:35 -04:00
jake 33fd871114 feat(server): serve normalized OpenAPI spec at /api/v1/spec.json with shared schema components
Extract spec generation into `openapi-spec.ts` with a `generateOpenApiSpec` function that registers common `$ref` schemas (Title, Person, Episode, Season, etc.) to deduplicate inline definitions across the spec. Add a `/api/v1/spec.json` interceptor that returns the fully normalized document, and point the Scalar docs UI at it instead of the plugin's raw generator output.

Also strip oRPC's impossible `{ not: {} }` void placeholders from request/response bodies via `normalizeOpenApiSpec`, and fix the handler registration so both `/api/v1` and `/api/v1/*` routes are matched.
2026-03-14 20:05:44 -04:00
jake aede4fc90a feat: generate and display thumbhash blur placeholders for TMDB images 2026-03-14 18:58:09 -04:00
jake 76648a6023 refactor(native): replace FlatList with FlashList for search and person screens 2026-03-14 14:36:37 -04:00
jakeandClaude Opus 4.6 9b29da21bf feat(native): gracefully handle unreachable servers on app startup
Seed the Better Auth session atom from SecureStore cache before React
renders so the app shows cached data instantly instead of hanging on the
splash screen for 30-60s when the server is down. Add a server
reachability monitor that probes /api/health and displays an amber
"Can't reach server" banner (distinct from the offline banner). When the
server recovers, the session is re-validated automatically — if expired,
a toast explains the sign-out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:52:38 -04:00
jake c6cd061175 refactor(native): list optimizations and cleanup 2026-03-14 13:46:12 -04:00
jakeandClaude Opus 4.6 3148574d5b feat(docs): add OpenAPI API reference from oRPC contract
Generate an OpenAPI 3.1 spec from the oRPC contract at build time
using @orpc/openapi, then use fumadocs-openapi to render interactive
API reference pages grouped by tag (Titles, Episodes, Seasons, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:07:42 -04:00
jake 17ddffef41 docs: replace GitHub-hosted screenshot with local image in README 2026-03-14 11:08:46 -04:00
jake 5acc33e3bf chore: exclude docs app from Docker build context 2026-03-14 11:01:28 -04:00
jake 7c3f39f2f0 feat(docs): replace default Fumadocs OG image with custom branded template
Swap the generic `fumadocs-ui/og/takumi` default for a hand-crafted OG image using the Sofa cinema theme — dark background, amber accent, DM Serif Display title, DM Sans body, and a radial gradient — matching the app's visual identity.

- Load DM Sans (400/500) and DM Serif Display (400) at runtime from Google Fonts via old Safari UA to get TrueType URLs
- Render header with Sofa logo SVG + "Sofa Docs" label, centered title/description block, and an amber accent bar
- Scale title font size based on length (68px → 56px for longer strings)
- Define theme constants (BG, FG, PRIMARY, MUTED) from the app's oklch palette
2026-03-14 10:55:11 -04:00
jake b25e395909 feat(docs): add Vercel Analytics to docs app 2026-03-14 10:44:19 -04:00
jake 07dc3923c4 feat: add docs app with Fumadocs
- Add `docs/` — Next.js 15 app powered by Fumadocs with a landing page, docs layout, search, OG image generation, and LLM text routes
- Cover getting started, configuration, integrations (Plex, Emby, Jellyfin, Radarr, Sonarr), mobile app setup, and telemetry in MDX
- Exclude `docs/` from root Biome config since it has its own `biome.json`
2026-03-14 10:39:37 -04:00
jake 98c1143540 refactor(native): scope MMKV storage per server instance and user
Isolate the query cache and recently-viewed history by instance ID +
user ID so that switching servers or accounts never bleeds data
between them.

- Split `mmkv.ts` into `globalStorage` (app-wide settings, analytics)
  and a scoped store keyed by `${instanceId}_${userId}`; add
  `setStorageScope`, `clearStorageScope`, `onStorageScopeChange`
- Add `QueryProvider` component — mounts `PersistQueryClientProvider`
  keyed by `${instanceId}_${userId}` when scoped storage is ready,
  falls back to plain `QueryClientProvider` otherwise; remounts on
  scope change to restore from the correct MMKV partition
- `registerServer` in `server-url.ts` persists the instance ID
  returned from the health check; `ensureInstanceId` fetches it from
  the server for upgrade paths and env-based URLs
- Auth client `storagePrefix` now includes the instance ID so
  SecureStore tokens are namespaced per server
- Export `rebuildAuthClient` for use when instance ID resolves after
  initial mount
- Remove explicit `clearRecentlyViewed` calls from sign-out — scoped
  storage naturally separates history per account
- Update `recently-viewed.ts` and `posthog.ts` to use the appropriate
  storage tier (`scopedStorage` / `globalStorage`)
2026-03-14 09:24:51 -04:00
jake 9278ea6e73 feat: add opt-in anonymous telemetry reporting
Add an opt-in telemetry system that sends anonymised instance
statistics to the public API once per day, which proxies them to
PostHog when `POSTHOG_API_KEY` is configured.

- Add `packages/core/src/telemetry.ts` — `performTelemetryReport`
  and `isTelemetryEnabled`; user and title counts are bucketed
  before sending to avoid exposing exact figures
- Add `getInstanceId()` to `settings.ts` — generates and persists a
  stable UUIDv7 for the instance
- Schedule a daily `telemetryReport` cron job (00:30)
- Add `POST /v1/telemetry` to `apps/public-api`; forwards payload
  to PostHog or returns 204 silently if key is absent
- Add `admin.telemetry` and `admin.toggleTelemetry` oRPC procedures
  for inspecting and toggling the setting
- Expose `instanceId` on `system.publicInfo`
2026-03-13 21:14:56 -04:00
jakeandClaude Opus 4.6 b7bacb352f feat: add change password to web and mobile settings
Add a change password feature using Better Auth's built-in
changePassword endpoint. On web, a dialog opens from a new card in the
Account section. On mobile, a dedicated screen is pushed from a new
SettingsRow. Both use listAccounts to detect credential-based accounts
and only show the option when the user has a password and password login
is enabled (respecting OIDC-only configurations).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 20:43:34 -04:00
jake 1798107afc feat: add public-api app and route update checks through it
- 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`
2026-03-13 19:53:51 -04:00
jake e7ffceb278 refactor(native): replace custom toast implementation with burnt
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
2026-03-13 19:09:48 -04:00
jake b4c2a1d343 feat(native): add recently viewed history to search screen
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
2026-03-13 19:08:13 -04:00
jake 35bdfc171e refactor(native): replace runOnJS with scheduleOnRN and custom timeAgo with date-fns
- 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
2026-03-13 18:11:05 -04:00
jakeandClaude Opus 4.6 baf203516a refactor(native): align title action buttons with web app design
Replace three separate status toggle buttons (Watchlist/Watching/Completed)
with a single status button matching the web pattern: "+ Watchlist" when
inactive, "Watching"/"Completed" when active (tap to remove). Consolidate
actions into a single row with separator and star rating. Use title-accent
color for theming consistency with other native components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:55:01 -04:00
jake c037a64028 feat(native): add HeroBanner and OfflineBanner components with haptic feedback
- Introduced HeroBanner component for displaying movie or TV show details with animated press effects and navigation.
- Added OfflineBanner component to notify users of network connectivity issues, utilizing glass effect when available.
- Implemented haptic feedback utilities for Android to enhance user interactions.
2026-03-13 17:43:46 -04:00
jake d63fb7bac2 refactor(native): move liquid glass effect from backdrop to play button
Replace the conditional GlassView backdrop overlay with plain tinted
Views, and apply the liquid glass effect to the play button circle
instead when supported.
2026-03-13 16:30:54 -04:00
jake b6eb53e9b8 fix(deps): mismatched/multiple react versions 2026-03-13 16:28:13 -04:00
jake c021b8157b chore: bump deps 2026-03-13 15:55:25 -04:00
jake 893f4c1b76 refactor(native): remove refresh control from TitleDetailScreen 2026-03-13 15:51:36 -04:00
jake c3e2d7def6 feat(native): sync Stack header title with active tab selection 2026-03-13 15:29:56 -04:00
jake 985030ad26 feat(native): introduce title-accent CSS variable for per-title color theming
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`
2026-03-13 15:20:43 -04:00
jake 7b941a8739 refactor(native): enhance title detail screen gradient and remove ambient glow effect 2026-03-13 15:13:32 -04:00
jake 781f843372 Limit Docker build workflow to relevant path changes (#12) 2026-03-13 12:40:06 -04:00
jake 912b2766ff feat(native): use App Tracking Transparency to gate PostHog analytics (#10) 2026-03-13 12:30:38 -04:00
jake 6252ace4c0 Delete .github/workflows/eas-update.yml 2026-03-13 12:30:24 -04:00
jake c5d12482e2 feat(native): port title page color tinting from web to mobile (#11) 2026-03-13 11:19:24 -04:00
jake 3a57de2ed6 Improve server URL input UX on native app launch (#9) 2026-03-13 10:32:38 -04:00
jake 83839a0cd7 Add @sofa/native Expo React Native app (#7) 2026-03-12 19:39:54 -04:00
jakeandClaude Opus 4.6 a326c968b7 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>
2026-03-10 16:50:34 -04:00
jakeandClaude Opus 4.6 26558b29e4 Migrate from server actions & REST APIs to oRPC for type-safe RPC (#5)
* 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>
2026-03-10 10:26:26 -04:00
jakeandClaude Opus 4.6 dfda2da827 Fix updateTag errors when called from cron jobs and detached promises
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>
2026-03-08 19:29:18 -04:00
jake aba84c6979 Update README.md 2026-03-08 19:27:46 -04:00
jake e19f29a90f Remove auth redirect logic from proxy middleware
- 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
2026-03-08 19:17:52 -04:00
jake 271069cc0c Replace server actions with API routes and add SWR data-fetching hooks
- 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
2026-03-08 19:09:21 -04:00
jake 1f3e5e976f Enhance season watch tracking in useTitleActions
- 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.
2026-03-08 17:52:05 -04:00
jake 29b80e304e Add modal={false} to Select/DropdownMenu and polish cast carousel layout
- 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
2026-03-08 16:02:47 -04:00
jake 5fdc7ebd97 Revert "Persist dashboard periods and explore genre filters in URL via nuqs"
This reverts commit 2bb5af042b.
2026-03-08 15:14:03 -04:00
jake 2bb5af042b Persist dashboard periods and explore genre filters in URL via nuqs
- 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
2026-03-08 14:32:09 -04:00
jake 0838e5fb74 Replace hand-written TMDB types with openapi-fetch + generated schema
- 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
2026-03-08 14:07:32 -04:00
jake ad8ec62317 Refactor tmdbImageUrl to accept semantic category instead of size string
- 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
2026-03-08 12:33:07 -04:00
jake a7b3600d15 Flatten lib/types/title.ts → lib/types.ts and update all imports
- 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`
2026-03-08 12:26:00 -04:00
jake 609f984aa0 Move test utilities from lib/ to test/ directory
- 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`
2026-03-08 12:18:21 -04:00
jake 1328619c43 Merge MobileTabBar into nav-bar and refactor nav indicator to ResizeObserver
- 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`
2026-03-08 12:10:28 -04:00
jake 66f2447efe Add admin role badge to NavBar and normalize command palette text size
- 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
2026-03-08 11:46:01 -04:00
jake d75d40e63a Fix avatar conditional rendering in NavBar and AccountSection
- 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
2026-03-08 11:39:46 -04:00
jake 69f04937bc Fix updateTag during render, hide scrollbars, polish type badge and hover styles
- 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
2026-03-08 11:24:07 -04:00
jake 1407571b64 Add inline name editing to account settings
- 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
2026-03-08 11:11:44 -04:00
jake 17fe19e85b Extract shared constants into lib/constants.ts
- 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
2026-03-08 11:06:29 -04:00
jake 84c4356a9f Add user avatar upload to account settings and nav bar
- 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
2026-03-08 11:02:33 -04:00
jake 02466e94f9 Replace Carousel with ScrollArea, remove embla-carousel dependency
- 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)
2026-03-08 09:53:12 -04:00
jake 7ed172d675 Replace Jotai atoms with local state, add Suspense for PPR compatibility
- 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
2026-03-07 16:00:19 -05:00
jake 8a45aa1c36 Replace tmdb-* URL routing with resolveTitle/resolvePerson server actions
- 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
2026-03-07 14:27:03 -05:00
jake ccd482db4e Update StarRating animation to simplify scale transition logic 2026-03-07 13:30:58 -05:00
jake a5393792c7 Refactor seed script to use 'integrations' and 'integrationEvents' instead of 'webhookConnections' and 'webhookEventLog'. Update logging messages and variable names for clarity. 2026-03-07 12:48:27 -05:00
jakeandClaude c6efbacc1c Add database seed script for demo data (#4)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-07 12:34:21 -05:00
jake 383f0b819b Polish mobile layouts, extract ExpandableText, fix image sizing
- 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
2026-03-07 12:09:51 -05:00
jake 5d63ad699f Replace stats atoms with local state and server action in StatsDisplay
- 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
2026-03-07 11:26:37 -05:00
jake 8d8c49a7f0 Replace API route handlers with server actions across the app
- 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
2026-03-06 17:31:14 -05:00
jake 73b07f5ff6 Refactor auth session handling and server actions across app
- 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`
2026-03-06 17:05:49 -05:00
jakeandClaude Opus 4.6 7a3052250d Optimize frontend: memoization, modern React hooks, reduced render overhead
- 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>
2026-03-06 16:15:08 -05:00
jake d805b8b738 Add pre-commit checks section to CLAUDE.md 2026-03-06 16:06:25 -05:00
jakeandClaude Opus 4.6 29449fd02a Optimize database queries: add indexes, batch cron scans, transactional writes
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>
2026-03-06 15:47:51 -05:00
jake 49cea568a9 Add Sonarr and Radarr list integrations
- 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
2026-03-06 15:34:34 -05:00
jakeandClaude Opus 4.6 8a3717cea7 Remove tmdbImageUrl mocks to fix cross-file mock poisoning in CI
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>
2026-03-06 13:51:55 -05:00
jake 4cf326fa00 Add comprehensive test suite with CI workflow
- 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
2026-03-06 13:42:58 -05:00
jake 965e8353ac Update job health reporting to use finishedAt timestamp
- Modify the lastRunAt property to prioritize finishedAt over startedAt for job health data, ensuring more accurate reporting of job execution times.
2026-03-06 13:05:25 -05:00
jakeandClaude Opus 4.6 a263a6536a Show disabled jobs properly in admin health dashboard
- 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>
2026-03-06 13:02:04 -05:00
jake fb24b25d34 Replace lib/version.ts with APP_VERSION build arg injected via Docker
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.
2026-03-06 13:00:41 -05:00
jakeandClaude 57ce0fd85d Fix mobile pinch zoom and content overlapping header/footer on iOS Safari (#3)
* 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>
2026-03-06 09:41:47 -05:00
jake 4d966bfbc2 "Claude Code Review workflow" 2026-03-06 08:41:19 -05:00
jake 0a06117278 "Claude PR Assistant workflow" 2026-03-06 08:41:19 -05:00
jakeandClaude Opus 4.6 11143d0803 Declutter title page metadata row with cleaner visual hierarchy
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>
2026-03-05 21:26:47 -05:00
jake 9c68768295 Fix episode progress display in seasons: show mark-all for all users, simplify progress indicators
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.
2026-03-05 20:53:05 -05:00
jakeandClaude Opus 4.6 522c03588d Add provider registry with direct watch links and overflow popover
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>
2026-03-05 20:46:53 -05:00
jake cd4b2908d8 Add clickable watch links to streaming provider badges
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.
2026-03-05 20:35:59 -05:00
jakeandClaude Opus 4.6 926ddfef34 Fix watchlisted shows not appearing in Continue Watching after logging episodes
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>
2026-03-05 20:27:29 -05:00
jakeandClaude Opus 4.6 70a2a9f28d Fix missing userStatus on title cards across dashboard, recommendations, and filmography
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:24:17 -05:00