100 Commits
Author SHA1 Message Date
jakeandGitHub 553fe98013 feat: add "Mark Unwatched" toggle for movies on web and native (#52)
- Replace the static "Mark Watched" button with a toggle that shows "Mark Unwatched" (destructive style) when a movie is already completed
- Add `handleUnwatchMovie` to web `useTitleActions` with optimistic update (reverts status to `in_watchlist` on error)
- Add `unwatchMovie` mutation to native `useTitleActions` hook and `titleActions` imperative helper
- Update poster-card and upcoming-row context menus to show the correct action based on current watch status
- Wire the `M` keyboard shortcut on the title detail page to unwatch when the movie is already marked watched
- Add i18n strings for "Mark Unwatched", "Marked as unwatched", and "Failed to mark as unwatched"
2026-07-12 16:53:52 -04:00
jakeandGitHub 0c747a9275 fix(webhooks): ignore episode-level Plex TMDB GUIDs when parsing scrobble events (#53)
Plex attaches TMDB GUIDs that identify the individual episode, not the parent series, so using them as `tmdbId` caused incorrect series lookups. Now only movie scrobbles populate `tmdbId` from the TMDB GUID; episode scrobbles rely on IMDB/TVDB IDs (and the title) to resolve the show via `resolveShowTmdbId`.

- Extract IMDB/TMDB/TVDB prefix constants to avoid magic strings
- Add `parsePlexPayload` test asserting `tmdbId` is `undefined` for episode events
- Add `processWebhook` integration test verifying `resolveShowTmdbId` is called without a TMDB ID for Plex episodes
- Hoist `makePlexForm` helper to module scope so it's available across all describe blocks
2026-07-12 16:53:04 -04:00
jake bb3b7891d4 v0.2.0 2026-03-27 15:52:22 -04:00
dce333e128 refactor: organize API around operation domains (#24)
* feat: reorganize API around operation domains

Restructure the oRPC contract from 14 resource-oriented routers to 8
domain-oriented routers for a cleaner public API surface.

- Consolidate 7 watch procedures into unified tracking.watch/unwatch
  with scope + ids input (movie, episode, season, series)
- Split dashboard across tracking (stats, history), library
  (continueWatching, upcoming), and discover (recommendations)
- Merge explore + search + discover into single discover router
- Absorb integrations into account.integrations
- Merge system.authConfig into system.publicInfo
- Collapse 6 admin setting endpoints into admin.settings.get/update
- Deduplicate platforms.list + explore.watchProviders into
  discover.platforms
- Add symmetric unwatchMovie/unwatchSeries core functions
- Rename titles.detail→get, titles.recommendations→similar,
  people.detail→get

BREAKING CHANGE: All client API paths have changed. REST paths now
mirror router structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback on API reorganization

- Fix updateRating invalidation in native app — was only invalidating
  title queries, now calls invalidateTitleQueries() to also refresh
  tracking.userInfo (drives the rating UI)
- Make unwatchMovie status revert consistent with unwatchSeries — revert
  any non-watchlist status, not just "completed"
- Add sync invariant comment on handleWatch/handleUnwatch loops
- Remove unused queryClient import in native use-title-actions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updateStatus NOT_FOUND error + rate() invalidation in native

- updateStatus now throws NOT_FOUND when quickAddTitle returns null
  (title doesn't exist), instead of silently succeeding
- Add NOT_FOUND error to updateStatus contract definition
- Fix rate() in native title-actions.ts to call invalidateTitleQueries()
  instead of only invalidating orpc.titles.key() — mirrors the fix
  already applied to the hook-based path in d2ddc0f

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: expand mobile app docs and add Play Store badge

- Add the Google Play badge to the README alongside the App Store badge
- Expand the mobile app docs with the Android/Play Store release details
- Refresh docs site dependencies and related package versions for the updated docs stack

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:44:05 -04:00
jake 3906f6021d v0.1.3 2026-03-24 21:02:45 -04:00
jake b0172a2b25 feat: refactor platforms to support multiple TMDB provider IDs per platform
- Replace `tmdbProviderId` (single int) with a many-to-many `platformTmdbProviders` join table so one platform entry can map to multiple TMDB provider IDs (e.g. a single "Max" platform covers several regional TMDB IDs)
- Add `isSubscription` flag to platforms to distinguish subscription services from transactional ones, replacing the old `displayOrder` field in API responses
- Add `getPlatformTmdbIds` / `getPlatformTmdbIdMap` helpers in `@sofa/core/platforms` and thread them through discover, explore, and platform list procedures
- Replace `providerId: number` with `platformId: string` (UUID) in discover input schema so the client never needs to resolve TMDB IDs
- Add `scripts/sync-tmdb-providers.ts` script to pull live provider data from TMDB and update `platforms.json`
- Split native title detail availability into separate "Stream" and "Buy or Rent" sections matching the web pattern
- Add two new DB migrations for the join table and platform schema changes
2026-03-24 20:54:34 -04:00
jake 7fedccb4fd v0.1.2 2026-03-24 16:08:33 -04:00
jake c940e4cdc9 chore(docs): sync API spec 2026-03-24 16:07:34 -04:00
jake 66d2de3020 fix(i18n): wrap remaining hardcoded strings in web and native with LingUI macros
- Wrap star rating and decade labels in library screen/filter-sheet with `t` macro so they're translatable (and move decade labels out of static const into a reactive map)
- Wrap accessibility labels in `integration-card`, `swipeable-row` with `t` macro
- Refactor `SHORTCUT_DESCRIPTIONS` in command-palette into `SHORTCUT_GROUPS` with `msg` descriptors; render scope/description/connector via `i18n._()` and `<Trans>` so all shortcut help text is extractable
- Wrap "Command Palette" dialog title, media type badges, and "then" connector in `<Trans>`/`t`
- Fix `system-health-section` backup count string: move `plural()` outside `<Trans>` to avoid untranslatable interpolation
- Wrap sr-only strings ("More", "Close") in breadcrumb, dialog, pagination, sheet, and sidebar with `<Trans>`
- Wrap setup route strings with `<Trans>`/`t` macros
- Update `en.po` with newly extracted message IDs
2026-03-24 14:38:12 -04:00
jake b8f3468283 fix(native): add Apple privacy manifest and fix Sentry metro config
- Add `privacyManifests` to `app.json` declaring crash/performance/diagnostic data collection (unlinked, non-tracking, app functionality only) and required API access reasons for UserDefaults, SystemBootTime, and FileTimestamp
- Switch metro config from `withSentryConfig` wrapper to `getSentryExpoConfig` (Sentry's recommended Expo integration) and remove redundant `withSentryConfig` wrapping
2026-03-24 13:39:00 -04:00
jake 34b9ae3620 fix(native): fix widget image management and add tests
- Fix `copyBundledAsset` to accept a URI instead of a named bundle asset, resolving via `resolveAssetURL` with support for remote downloads
- Read app group identifier from Info.plist (`ExpoWidgetsAppGroupIdentifier`) instead of a hardcoded string
- Add `pruneWidgetImages(maxAgeSeconds)` to age-based cache cleanup, skipping the widget icon file
- Add `normalizedFileName` helper to sanitize cache keys and a `log` helper with consistent prefix
- Extract `getWidgetIconAsset()` into `widget-assets.ts` so the asset require is testable in isolation
- Add Vitest config for the native app and unit tests covering widget data-fetch, image download, and prune logic
2026-03-24 11:13:09 -04:00
jake 1bf12d3884 fix: update Dockerfile and turbo.json to include APP_VERSION and GIT_COMMIT_SHA as environment variables
- Added APP_VERSION and GIT_COMMIT_SHA arguments to Dockerfile for versioning and commit tracking.
- Updated turbo.json to pass APP_VERSION and GIT_COMMIT_SHA as environment variables for build tasks.
2026-03-24 11:08:30 -04:00
jakeandClaude Opus 4.6 2cd1964ca8 fix(native): overhaul library screen, fix upcoming filters, improve SelectModal
Library screen:
- Replace broken bottom sheet with inline filter strip (horizontal scroll
  chips matching the web pattern) — type pills, status/genre/rating/year/
  content rating dropdown chips with SelectModal pickers
- Replace TabStack with custom layout so filter sort controls appear in
  the native header alongside the avatar
- Fix grid: 3-column layout with computed item widths, proper spacing
- Remove redundant search bar (Search tab already exists)
- Sync sort/filter state via Jotai atoms between layout and screen

Upcoming screen:
- Move filter chips into SectionList ListHeaderComponent so they scroll
  with content and don't overlap the native header
- Single horizontal scroll row with divider between type and status groups
- Status chips toggle on/off instead of redundant dual "All" buttons
- Tighten section header spacing

SelectModal:
- Add ScrollView for long option lists (genres, content ratings)
- Respect safe areas with dynamic max height from screen dimensions
- Add optional multiSelect mode (stays open, toggles checkmarks)
- Add optional clearLabel/onClear for a "Clear" button in the header
- Accept string | string[] for selection to support multi-select

Other:
- Remove Settings tab from tab bar (accessible via avatar dropdown)
- Simplify tab layout by removing unused update check query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:17:26 -04:00
jakeandClaude Opus 4.6 647784d8fc feat(native): add Sentry native crash reporting with privacy-conscious defaults
Adds @sentry/react-native for native crash reporting (segfaults, OOM,
ANRs, watchdog kills) that PostHog's JS-level tracking can't capture.

Privacy measures:
- sendDefaultPii: false, no tracing/replay/profiling/screenshots
- beforeSend strips user/request data from JS errors
- beforeBreadcrumb drops navigation/http breadcrumbs
- Screenshot, ViewHierarchy, UserInteraction integrations filtered out

Crash reporting has its own toggle in Settings, independent from the
existing analytics toggle. Enabled by default, takes effect on restart
since native crash handlers must be installed at init time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:38:53 -04:00
4fc6b2f8d1 feat: add filtering and sorting across library, explore, and upcoming (#21)
* feat: add filtering and sorting across library, explore, and upcoming

Add comprehensive filtering and sorting to all list views on both web and mobile.

Library (new dedicated page):
- New /library route (web) with URL search param persistence
- New Library tab (5th tab, mobile) with FlashList grid
- Filters: status (multi-select), type, genre (library-only), user rating range,
  release year (decade presets + custom), content rating, available to stream
- Sort by: title, date added, release date, popularity, user rating, TMDB rating
- Text search within collection
- Filter popover (web) / bottom sheet with Apply (mobile)
- Remove old dashboard.library endpoint in favor of library.list

Explore/Discover:
- New Discover section on Explore page with inline filter controls
- TMDB filters: genre (now optional), year range, min rating, sort order,
  original language, streaming provider
- Watch provider list endpoint (WATCH_REGION env var, default "US")
- Default to popular content when no filters selected

Upcoming:
- Type filter (All/Movies/TV Shows) and status filter (All/Watching/Watchlist)
- Toggle chips on both web (URL params) and mobile
- Backend: mediaType and statusFilter params on upcoming endpoint

Navigation:
- Library added to web nav bar (desktop + mobile tab bar)
- Library added as 5th tab on mobile (between Home and Explore)
- Dashboard library section simplified to compact preview with "See all" link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add user streaming platform subscriptions and redesign filter UI

Replace the denormalized availabilityOffers table with a normalized
platforms + titleAvailability + userPlatforms schema. Users can now
declare which streaming services they subscribe to, enabling
personalized "On your services" availability display and filtered
library/explore results.

Backend: new platforms table seeded from TMDB provider data on startup,
new API endpoints (platforms.list, account.platforms, account.updatePlatforms),
title detail annotates availability with isUserSubscribed flag, library
"on my services" filter checks user's subscribed platforms.

Web UI: post-registration onboarding page for platform selection,
streaming services settings section, title detail split into "On your
services" / "Also available on", explore dropdown uses platforms table
with active filter highlighting. Library filters redesigned from
cluttered popover to clean collapsible inline strip with consolidated
dropdowns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address race conditions, stale state, and edge cases in platforms feature

- Fix debounced autosave race in streaming services settings: use ref
  for latest selection + counter guard so only the most recent save
  updates UI. Clean up timers on unmount.
- Fix explore provider dropdown desyncing from query filter when
  switching Movie/TV type: reset selectedPlatformId alongside providerId.
- Fix platformIdsExist rejecting duplicate valid IDs by deduplicating
  input before comparing against DB results.
- Fix stale platform metadata: always upsert name/logo on TMDB refresh
  instead of skipping when platform already exists.
- Fix invisible ratingMax filter: clear ratingMax when user changes
  ratingMin since the UI only exposes a single rating dropdown now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear stale saved-state timer and preserve falsy filter values

- Clear previous savedTimerRef timeout before scheduling a new one in
  streaming-services-section, preventing an older timer from hiding the
  "Saved" indicator too early on rapid successive saves
- Replace `value || undefined` with explicit empty-value check in
  library filter handler so numeric 0 (e.g. ratingMin=0) is not
  incorrectly dropped

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add TanStack Form and migrate auth + change password forms

Set up TanStack Form v1 composition layer with createFormHook, pre-bound
field components (TextField, TextareaField, CheckboxField, SwitchField,
SubmitButton), and migrate the two traditional submit-based forms:

- AuthForm: replace 4 useState calls with useAppForm, use form.Field
  for each input while preserving motion animation layout
- ChangePasswordDialog: replace 6 useState calls with useAppForm + Zod
  schema validation (cross-field password match, min length), per-field
  error display, form.reset() on dialog close

Also adds @tanstack/react-form to the monorepo catalog and updates both
web and native apps to reference it via catalog:.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:07:14 -04:00
jake d1b0d952bd v0.1.1 2026-03-23 16:39:36 -04:00
jakeandClaude Opus 4.6 eb9c7ae6df fix(i18n): comprehensive audit of translated strings across web and native
- Replace hand-rolled pluralization (ternary suffix hacks) with `plural` macro
- Wrap 40+ untranslated user-facing strings with LingUI macros: aria-labels,
  accessibilityLabels/Hints, placeholders, error fallbacks, toast overrides
- Unify episode progress format to `${watched}/${plural(total, ...)}` across
  all 4 call sites (title-card, continue-watching, season-accordion)
- Unify capitalization: "Sign in", "Sign out", "Create account",
  "Change password", "Update password", "Image cache", "Remove from library",
  "Mark Watched", "Trending Today", "On Watchlist", "or"
- Translate cron schedule strings in system health via `i18n._(msg)` with
  locale-aware day names via `Intl.DateTimeFormat`
- Move Zod validation schemas inside components with `useMemo(() => ..., [t])`
  so validation messages are translatable at runtime
- Unify "Watched all of" variable naming to `seasonLabel` across all call
  sites to produce a single msgid
- Remove trailing period from lone "Failed to..." error message
- Add EAS build i18n template extraction step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:21:54 -04:00
jake b289b7abed refactor: optimize cron scheduling, DB maintenance, and TMDB reliability
- Skip recommendations refresh for titles fetched within the last 7 days via `getTitlesWithFreshRecommendations`; only stale titles are re-fetched
- Replace `Promise.allSettled` in `refreshTvChildren` with a `mapWithConcurrency` helper (concurrency = 5) to stay within TMDB's rate limit
- Add 30s request timeout middleware to TMDB client to prevent indefinitely hung fetches
- Expand `optimizeDb` weekly job to prune 30-day-old cron runs, integration events, expired sessions, and expired verifications
- Add `wal_checkpoint(TRUNCATE)` to `optimizeDatabase` so the WAL is flushed before `PRAGMA optimize`
- Add `recoverStaleImportJobs` called on server startup to mark any `pending`/`running` import jobs as errored after a crash/restart
- Return `false` from `triggerJob` immediately if the job is already running
2026-03-23 13:59:31 -04:00
jake d9be566863 refactor: clean up orphaned cached images on cache purge and metadata updates
- Add `deleteOrphanedImage(category, path)` helper to `image-cache.ts` for fire-and-forget unlink of stale cached files
- Extend `purgeShellTitlesTransaction` to collect poster, backdrop, still, and profile image paths from deleted titles/persons before cascade deletes, returning them as `orphanedImages` in `PurgeResult`
- Wire orphaned image cleanup into `purgeMetadataCache` (cache purge), `batchUpsertPersons` (profile path changes), `updateTitleWithArtInvalidation` (poster/backdrop changes), and `refreshTvChildren` (season posters and episode stills)
- Remove `getTitlesWithMissingThumbhashes`, `getTitleIdsWithMissingSeasonThumbhashes`, `getTitleIdsWithMissingEpisodeThumbhashes`, and `getTitleIdsWithMissingProfileThumbhashes` from `@sofa/db/queries/cron` — `getThumbhashBackfillTitleIds` now delegates to `getLibraryTitleIds` only
- Fix episode still cleanup to call `deleteOrphanedImage` even when `stillThumbHash` is null (previously skipped)
2026-03-23 13:18:06 -04:00
jake cc8909d64e fix: remove @sofa/core and @sofa/api deps from public-api to fix Vercel runtime compatibility
- Drop `@sofa/core` and `@sofa/api` from `apps/public-api` — importing workspace packages with DB/Node dependencies broke the Vercel edge runtime
- `fetchUserData` on Trakt and Simkl importers now returns raw `unknown` API responses; parsing is deferred to the self-hosted server
- Add `source` + `rawPayload` fields to `ParsePayloadInput`; `parsePayload` procedure now dispatches to `parseTraktPayload`/`parseSimklPayload` and returns full warnings + diagnostics
- Export `ProviderEnum` from `apps/public-api/src/importers/index.ts` instead of defining it inline in `app.ts`
2026-03-23 12:48:04 -04:00
jake 9df1075040 fix: remove App Tracking Transparency (ATT) from native analytics flow
- Drop `expo-tracking-transparency` dependency and plugin config from `app.json`
- Replace `applyTrackingTransparency` with `initAnalytics()` that simply syncs PostHog opt-in/out from the stored preference on startup
- Remove ATT permission prompt, IDFA/AAID identification, and one-time migration logic from `posthog.ts`
- Analytics now starts disabled on both iOS and Android; user opts in via Settings toggle
- Update telemetry docs and privacy policy to reflect the simplified consent flow (no ATT prompt, no advertising IDs)
2026-03-22 19:03:28 -04:00
jake ddaa194f5d fix: close cache purge dialogs on success and disable actions while pending 2026-03-22 19:00:13 -04:00
jake 91ca6ab3c0 fix(native): add podspec to sofa-widgets-support module to resolve CocoaPods module map build failure 2026-03-22 18:40:36 -04:00
jake f2f5e823f4 fix: UX polish across web and native — error boundaries, query tuning, and i18n improvements
- Add `RouteError` component for route-level error boundaries (web) with "Try again" and "Dashboard" links; wire it into dashboard, explore, upcoming, people, and settings routes
- Add `await queryClient.cancelQueries()` before every optimistic update in `useTitleActions` to prevent in-flight refetches from overwriting local state
- Cap all `useInfiniteQuery` calls at `maxPages: 10` (web and native) to bound memory growth on scroll-heavy pages
- Replace `ActivityIndicator` with `Spinner` in search, person detail, and list footer components (native)
- Tune native query client defaults: add `staleTime: 60_000` and lower `gcTime` to `300_000` (was 24h)
- Darken `--color-muted-foreground` for better contrast in the native theme
- Improve error message copy in `error-messages.ts` for clarity (TMDB, import, registration errors)
- Update i18n `.po` files across all 12 locales with new strings (route error, improved error messages)
2026-03-22 16:46:39 -04:00
jake 1d67b201a0 chore: extract shared tsconfig, move display-status to core, add i18n tests
- Add `@sofa/tsconfig` package with a shared `base.json` (target, lib, strict, moduleResolution, etc.) and migrate all packages/apps to `"extends": "@sofa/tsconfig/base.json"` to eliminate duplicated compiler options
- Move `display-status.ts` (and its `DisplayStatus` type + `getDisplayStatus` fn) from `@sofa/api` to `@sofa/core`; remove the `./display-status` export from `@sofa/api` and update all import sites in `discovery.ts` and `tracking.ts`
- Add `packages/core/test/display-status.test.ts`: 15 tests covering `ONGOING_TMDB_STATUSES` and all `getDisplayStatus` branches (movie, TV watchlist/in_progress/caught_up/completed, edge cases)
- Add `packages/i18n/vitest.config.ts` and two new test files: `date-buckets.test.ts` and `format.test.ts` covering date bucketing and locale formatting utilities
2026-03-22 14:46:25 -04:00
jake 6be586bd53 chore: dependency updates and minor refactors
- Bump `@tanstack/react-query` and related packages to 5.95.0
- Add `@orpc/json-schema`, `@orpc/openapi`, `@orpc/server`, `@orpc/zod` to catalog and use catalog refs in `apps/server`
- Move `ORPCError` throws for backup not-found/invalid into `@sofa/core/backup` so the server procedure can simply `await deleteBackup()` without re-wrapping errors
- Add `@orpc/server` dep to `packages/core` to support the above
- Add `expo-updates` to native deps; switch `requireNativeModule` import to `expo` in `WidgetImagesModule`
- Pin native `react`/`react-dom` to catalog to avoid duplicate copies
- Add `@lingui/core` to web and `react` to i18n package deps
2026-03-22 14:11:45 -04:00
jake 2c7068ced3 chore: migrate oxlint config to e18e plugin and fix lint violations
- 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
2026-03-22 13:50:24 -04:00
jakeandClaude Opus 4.6 373a5d3caf test: add integrations, cache, cron, and telemetry tests
- 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>
2026-03-22 12:39:04 -04:00
jakeandClaude Opus 4.6 554271a888 test: add webhook, credits, image-cache, and metadata tests
- New webhooks.test.ts: 34 tests covering all 3 payload parsers
  (Plex/Jellyfin/Emby), toOptionalInt, and processWebhook end-to-end
  (movie/episode watch logging, deduplication, resolution failures)
- Expand credits.test.ts: 6 new tests for refreshCredits (movie/TV
  cast+crew upsert, person deduplication, cast limit, notable crew filter)
- Expand image-cache.test.ts: 8 new tests for imageCacheEnabled,
  getLocalImagePath, and loadImageBuffer error handling
- Expand metadata.test.ts: 4 new tests for ensureBrowseTitlesExist
  (shell title creation, deduplication, mixed existing/new)

304 → 345 tests passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:23:50 -04:00
jakeandClaude Opus 4.6 5f3e14b415 feat: migrate test runner from Bun to Vitest
- 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>
2026-03-22 12:03:06 -04:00
jake 3f05e08fea fix: misc bug fixes and reliability improvements
- Fix `refreshCreditsJob` to treat `null` `lastFetchedAt` as needing refresh (not just missing cast entry)
- Fix `buildBackupCron` 6h expression to correctly fire every 6 hours using explicit hour list instead of `*/6`
- Fix `vacuumDatabase` to pass the path directly as a bound parameter (the manual escape was redundant and incorrect)
- Clean up orphaned `.tmp` files when image cache writes fail in both `downloadAndCacheImage` and `fetchAndMaybeCache`
- Surface import job payload parse/validation errors as job-level `error` status instead of throwing unhandled exceptions; check for cancellation before the processing loop starts
- Replace bare `Error` throws in `readImportJob` with `ORPCError` (`NOT_FOUND` / `FORBIDDEN`) so oRPC maps them to correct HTTP status codes
- Auto-promote title status to `in_progress` when `logEpisodeWatchBatch` is called for titles that have no status or are only on the watchlist
- Refresh seasons from TMDB when a webhook references a season not yet in the DB (handles newly-released seasons)
- Upgrade fire-and-forget enrichment failures and webhook processing errors from `log.debug` → `log.warn` so they surface in production logs
2026-03-22 10:57:48 -04:00
jake 72f26f822c fix: harden stale import job detection and cancellation cleanup
- Extend stale-job auto-cancel to cover `running` jobs (30 min threshold) in addition to `pending` jobs (5 min), so crashed mid-import workers don't permanently block new imports
- Use `startedAt` instead of `createdAt` when calculating age for running jobs
- Scope `readImportJob` in the SSE loop to the authenticated user's ID to prevent cross-user job reads
- Persist progress counters (`processedItems`, `importedCount`, `skippedCount`, `failedCount`) when a job is cancelled mid-run so the UI shows accurate final stats
2026-03-22 10:17:17 -04:00
jake fd4a8119ce feat(native): add iOS home screen widgets for continue watching and upcoming
- 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)
2026-03-21 21:03:58 -04:00
jake 61013ee41f feat: add user data export and sofa export re-import support
- Add `GET /api/export/user-data` route that streams a JSON attachment of the authenticated user's full library data, named `sofa-export-<name>-<date>.json`
- Add `generateUserExport` in `@sofa/core/export` and a matching `parseSofaExport` parser so exported files can be re-imported via the existing import pipeline
- Register `sofa` as a new `ImportSource` in the API contract/schemas and wire up `parseSofaExport` in the `parseFile` procedure
- Add `EXPORT_FAILED` error code to the API error registry and surface it in web and native error-message maps
- Update the account settings section with export/import UI (download button, import progress)
- Add tests for `generateUserExport`, `parseSofaExport`, and round-trip fidelity
2026-03-21 17:12:15 -04:00
jake 58cf2e689f fix: harden backup restore, first-user race, and several reliability issues
- Block all non-health API requests with 503 while a database restore is in progress (`withDatabaseAccessBlocked` in `@sofa/db/client`); pause and resume cron jobs around the restore window
- Replace the two-hook first-user admin promotion with an atomic `claimInitialAdmin` query that uses a DB-level unique constraint so concurrent sign-ups during the first-run window can't each see `userCount === 0`
- Fix `refreshAvailability` to always call `replaceAvailabilityTransaction` (clearing stale rows) even when TMDB returns no US providers, instead of returning early and leaving old data in place
- Fix `performUpdateCheck` to read `release_url` (snake_case) from the public API response instead of `releaseUrl`
- Fix `createJob` import handler to catch `SQLITE_CONSTRAINT_UNIQUE` and surface it as an `IMPORT_ALREADY_RUNNING` conflict error instead of a 500
- Relax public API telemetry schema to accept `string | number` for `users` and `titles` fields
- Add tests for availability clearing, import deduplication, and `claimInitialAdmin`
2026-03-21 16:16:37 -04:00
jake f676cea237 fix: throw notFound() for missing titles/people and improve error handling
- Web: wrap title and person detail loaders in try/catch; throw `notFound()` when the API returns `TITLE_NOT_FOUND` or `PERSON_NOT_FOUND` so TanStack Router renders the 404 component instead of crashing
- Web: fix `QuickAddButton` to reset local status to `null` (not skip the update) when `userStatus` becomes undefined, so removing a title from the library reflects immediately
- Native: add a generic error state to the title detail screen with a "Try again" / "Go back" recovery UI, distinct from the existing "title not found" state
- Native: pass `getItemType` to `FlashList` in the search screen and recently-viewed list to enable correct item recycling across person and title cell types
2026-03-21 16:02:59 -04:00
jake 37c35488fd feat(analytics): proxy PostHog ingestion through public-api on Vercel
- Add Vercel rewrites to proxy `/ingest/*` and `/ingest/static/*` to PostHog's US endpoints
- Update native `.env.example` to point `EXPO_PUBLIC_POSTHOG_HOST` at the self-hosted proxy URL
2026-03-21 15:39:56 -04:00
jake 31572816aa chore(ci): simplify Crowdin workflow and bump checkout action
- Upgrade `actions/checkout` from v4 → v6 in both upload and download jobs
- Remove manual `bun install` + compile + git commit steps from the download job — Crowdin's own action now handles committing updated PO files via `push_translations: true`
- Allow the download job to run on `schedule` events in addition to `workflow_dispatch`
2026-03-21 13:02:01 -04:00
jakeandGitHub 1a503df2ae feat: add upcoming episodes and movies timeline (#17) 2026-03-21 12:31:53 -04:00
jake 5aaf88591b feat(i18n): add RTL layout support for Arabic and other RTL locales
- 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
2026-03-20 19:25:04 -04:00
jake b540a9111a refactor(i18n): replace compiled .ts catalogs with direct .po imports
- Remove `lingui compile` / `i18n:compile` script — Vite and Metro LingUI plugins now compile `.po` files on the fly at dev/build time; no manual compile step needed
- Import `.po` files directly in `@sofa/i18n` (`./po/en.po` etc.) and add `po.d.ts` ambient module declaration so TypeScript accepts them
- Add `fallbackLocales: { default: "en" }` and `lineNumbers: false` to `lingui.config.ts`; prefix catalog paths with `<rootDir>` for correctness
- Derive `SupportedLocale` and `SUPPORTED_LOCALES` from `LOCALE_INFO` in `locales.ts` to eliminate the circular import between `index.ts` and `locales.ts`
- Update Crowdin config to use `project_id_env` instead of a hardcoded ID, drop `languages_mapping` (no longer needed), and add `preserve_hierarchy: true`
- Add a daily cron trigger (`0 6 * * *`) to the Crowdin GitHub Actions workflow and drop the now-redundant compile step from it
- Update docs and `AGENTS.md` to reflect that no compile step is required after `i18n:extract`
2026-03-20 16:36:02 -04:00
jakeandGitHub 5c70d29fac refactor: consolidate watchlist states (#18)
- Rename `watchlist` → `in_watchlist` and `in_progress` → `watching` across the full stack (DB migration, Drizzle schema, core services, API contract, web, native)
- Add a new `caught_up` status for TV shows where all aired episodes are watched but the show is still airing
- Add `titles.watchAll` procedure and `markAllWatched` action to mark every episode of a TV show as watched in one step; replace the old "Mark as Watching" / "Mark as Completed" context-menu actions with "Mark All Watched" (TV) and "Mark as Watched" (movie)
- Extract `display-status.ts` in `@sofa/api` to share status → display label/color logic between web and native
- Add `getDisplayStatusesByTitleIds` to `@sofa/core/tracking` and wire it into the dashboard library feed so clients receive resolved display statuses
- Show a destructive Alert confirmation before removing a title from the library (native)
- Remove "Mark as Completed" from the continue-watching card context menu
- Update i18n catalogs for all 6 locales (de, en, es, fr, it, pt)
2026-03-20 15:31:31 -04:00
jake 4b4d35ce31 perf(native): memoize list headers and card components to reduce unnecessary re-renders
- Wrap `ContinueWatchingCard` with `memo()` so it only re-renders when its `item` prop changes
- Extract `ListHeaderComponent` in `RecentlyViewedList` into a `useMemo` to avoid recreating the JSX tree on every render
- Move `listHeader` in the person detail screen from a plain JSX variable to `useMemo`, and relocate `departmentLabels` inside it so the `t` reference is stable
- Store the "copied" reset timer in a `useRef` in `IntegrationCard` and clear it on unmount to prevent setState-after-unmount warnings
2026-03-19 16:53:14 -04:00
jake 1670c6f494 feat(web): add page titles, staleTime, and lift auth redirect into layout
- Add `head()` with document titles to dashboard, explore, settings, setup, login, register, and person detail routes
- Return `personName` from the person detail loader so the title can be set server-side instead of after data loads
- Add `staleTime` to all loader routes (30s for dashboard/settings, 60s for explore/titles/people)
- Move the "redirect if already authenticated" `beforeLoad` guard from individual login/register routes into the shared `_auth` layout so it runs once for all unauthenticated routes
- Add a Cloud deploy section to the README with a one-click Railway button
2026-03-19 16:47:23 -04:00
jake e7da1e5109 chore: bump deps 2026-03-19 15:15:39 -04:00
jake 78266cf202 feat(native): internationalize tab bar labels and improve recently-viewed row
- Wrap tab bar trigger labels and stack screen titles with `useLingui()` / `t` so they translate correctly across all 6 supported locales
- Swap `IconDeviceTv` → `IconDeviceTvOld` in the recently-viewed badge to match the icon used elsewhere in the app
- Show the type icon inside the pill badge and display a department label (Actor, Director, Writer, etc.) for person results instead of the generic "Person" / "TV" label
- Suppress the subtitle row for person results (department is shown in the badge instead)
- Merge the separate iOS/Android `headerTitleStyle` variables in `TabStack` into a single shared style
- Mark `packages/i18n/src/po/*` as readonly/excluded in VS Code settings alongside `routeTree.gen.ts`
- Extract and compile updated PO/TS catalogs for all locales (de, en, es, fr, it, pt)
2026-03-19 15:06:02 -04:00
jakeandClaude Opus 4.6 fb4b521450 fix(native): fix modal close button behind status bar on Android
Replace the separate ModalStackHeader + ModalCloseButton components with
a single ModalLayout wrapper. On iOS the native toolbar close button is
used; on Android a floating close button is rendered with proper
safe-area offset, bypassing the unreliable react-native-screens
transparent header inset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:16:47 -04:00
jakeandClaude Opus 4.6 98aa7326d5 fix(server): store raw TMDB paths in browse upserts to prevent double-resolution
The explore, discover, and search procedures were calling tmdbImageUrl()
on raw TMDB paths before passing them to ensureBrowseTitlesExist() and
ensureBrowsePersonsExist(). This stored already-resolved paths like
/images/posters/abc.jpg in the DB. When getOrFetchTitle() later read
these rows and called tmdbImageUrl() again, the result was a doubled
path (/images/posters/images/posters/abc.jpg) that returned 404.

Fix: pass raw TMDB paths for DB storage, resolve via tmdbImageUrl() only
when building the client response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:00:45 -04:00
jake 885b594a97 fix(native): inline department labels and dim age text in person detail screen
- Move `getDepartmentLabels` helper inline as `departmentLabels` object inside the component so `t` from `useLingui()` is already in scope
- Wrap the birthday age/death annotation in a nested `<Text>` with `text-muted-foreground/60` to visually de-emphasise it
2026-03-19 11:57:27 -04:00
jakeandClaude Opus 4.6 8d1cc46875 fix(db): use (tmdbId, type) composite keys for title lookups
The unique constraint on titles is (tmdbId, type), meaning the same
numeric TMDB ID can exist as both a movie and TV show. Several places
were keying maps by tmdbId alone, which caused UNIQUE constraint
violations when an UPDATE inadvertently changed a row's type, and
silently dropped titles when both types existed for the same ID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:47:14 -04:00
jake 1f8b3d8f37 feat: redesign dashboard stats cards with sparklines, icons, and period selection
- 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`
2026-03-19 11:42:30 -04:00
jake 1050a0de00 feat: redesign dashboard stats cards with sparklines, icons, and period selection
- 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`
2026-03-19 11:31:15 -04:00
jake f4c6b27cd9 refactor(web): replace ProgressProvider context with a self-contained NavigationProgress component driven by TanStack Router lifecycle events
- Remove `ProgressContext`, `useProgress`, and manual `progress.start()` calls from `CommandPalette`, `TitleKeyboardShortcuts`, and `__root.tsx`
- Replace DOM click/popstate listeners with `router.subscribe("onBeforeLoad")` and `router.subscribe("onResolved")` so progress starts/stops automatically on every navigation
- Render `<NavigationProgress />` as a sibling in the root instead of wrapping the tree in a provider
2026-03-19 11:05:33 -04:00
jake 320852b035 fix(web): import globals.css directly instead of as a URL stylesheet link 2026-03-19 10:55:01 -04:00
jake d94ec16fae feat(web): add route loaders to prefetch dashboard, explore, and settings queries 2026-03-18 22:04:19 -04:00
jake 9db4285bc9 fix(core): validate SQLite magic bytes before opening backup database files 2026-03-18 21:12:10 -04:00
jake 7b0c07ad3c chore(i18n): add Claude-powered translation script and fill missing PO translations
- 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
2026-03-18 20:42:01 -04:00
jake 9b2d7caa3f fix(native): add .js extensions to all @formatjs polyfill imports for Metro compatibility 2026-03-18 17:42:44 -04:00
jake 466aa5464e fix(native): rename DetailStackHeader to ModalStackHeader and remove hero fade-in animation and continue-watching overlay
- Rename `DetailStackHeader` → `ModalStackHeader` in `modal-stack-header.tsx` and update all import sites (`title/[id].tsx`, `person/[id].tsx`)
- Remove `FadeIn` animation wrapper around `HeroBanner` in the explore screen
- Remove the dark overlay (`rgba(0,0,0,0.4)`) from `ContinueWatchingCard` thumbnail
2026-03-18 17:36:45 -04:00
jake 3c1b7cfe11 refactor: enforce layered architecture by extracting all DB queries into @sofa/db/queries/*
- Create 18 new query modules under `packages/db/src/queries/` covering every domain (availability, cache, colors, credits, cron, discovery, image-cache, imports, integrations, lists, metadata, person, settings, system-health, thumbhash, title, tracking, webhooks)
- Remove all direct `db` client imports and raw Drizzle expressions from `@sofa/core` services and `apps/server` procedures/cron; replace with named query functions
- Extract cron DB helpers (`startCronRun`, `completeCronRun`, `failCronRun`, `getLibraryTitleIds`, `getStaleLibraryTitles`, etc.) into `packages/core/src/cron.ts`
- Extract integration DB helpers into `packages/core/src/integrations.ts`
- Add `drizzle-orm` as a direct dependency of `@sofa/db` and remove it from `@sofa/core`
- Update `AGENTS.md` to document the strict layered architecture rule
2026-03-18 17:31:18 -04:00
jake 3a0374c825 chore(lint): add perf category to oxlint and fix resulting warnings
- Enable `perf: warn` in `.oxlintrc.json` and add `builtin` env; disable `no-await-in-loop` and `react/no-array-index-key` globally
- Reorder plugins so `oxc` is first
- Add `.oxfmtrc.json` override to suppress trailing commas in JSON/JSONC files
- Replace spread-into-new-object patterns (`{ ...x, key: val }`) with `Object.assign(x, { key: val })` in discover, explore, integrations, search, credits, and image-cache to satisfy perf rules
- Memoize `ChartContext` and `ToggleGroupContext` provider values to avoid unnecessary re-renders
2026-03-18 15:48:47 -04:00
jakeandClaude Opus 4.6 2b0c683b7b refactor: replace Biome with oxlint + oxfmt
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>
2026-03-18 13:26:50 -04:00
jake a80c8c9a0c fix(docker): include lingui.config.ts in production image 2026-03-18 12:20:12 -04:00
ad0497ccd6 feat: add i18n with LingUI, Crowdin integration, and typed error codes (#16)
* 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>
2026-03-18 12:16:10 -04:00
jake e50ab44783 refactor(native): consolidate server utilities into a single lib/server.ts module
- Merge `auth-client.ts`, `cached-session.ts`, `server-reachability.ts`, and `server-url.ts` into `lib/server.ts`; update all import sites to use `@/lib/server`
- Replace the standalone `registerServer` + `setServerUrl` calls with a `serverManager.connectToServer()` method and expose `serverManager.normalizeUrl`, `serverManager.validateServerUrl`, and `serverManager.hasStoredServerUrl`
- Move `use-server-connection.ts` from `lib/` to `hooks/` and remove the `seedSessionFromCache` export; replace with an `initialize()` helper called at module scope in the root layout
- Add `getScopeKey()` to derive the TanStack Query persistence scope key, removing the inline computation from `QueryProvider`
- Clear the query cache on server URL change inside `useServerConnection`
2026-03-17 20:57:50 -04:00
jake f4199b595b fix(native): change person detail screen presentation from slide animation to modal 2026-03-17 20:19:57 -04:00
jake b2a5c298c5 refactor(native): centralize title mutations into useTitleActions, extract server-connection logic, and unify hero banner
- Add `useTitleActions` hook that owns all title mutation side-effects (toast messages, query invalidation) and replace inline `useMutation` calls in the title detail, search, and person screens, as well as `usePosterActions`, with it
- Extract the server URL watching, instance-ID provisioning, storage-scope management, reachability monitor, and session-reconciliation logic from `_layout.tsx` into a `useServerConnection` hook and a `seedSessionFromCache` helper in `use-server-connection.ts`; `AppContent` now calls a single hook
- Delete the iOS-only `hero-banner.ios.tsx` and consolidate into `hero-banner.tsx`
- Extract `usePressAnimation` into its own hook file
- Move shared action helpers (quick-add, watch-movie, etc.) into `lib/title-actions.ts`
2026-03-17 20:01:03 -04:00
jake 1257a485e9 feat(native): add context menus to poster cards, hero banners, and continue-watching cards; remove failedKey error state
- Add `Link.Menu` with contextual actions to `PosterCard` (add to watchlist, mark completed, remove), `HeroBanner` ("Add to Watchlist"), and `ContinueWatchingCard` ("Mark as Completed", "Remove from Library") — all invalidate the relevant `titles` and `dashboard` query keys on success
- Remove the `failedKey`/`resetError` error-state pattern from `usePosterActions` and strip the now-unused `failedKey` and `onQuickAddFailed` props from every call site (`HorizontalPosterRow`, `person/[id].tsx`, `PosterCard`)
- Restructure `ContinueWatchingCard` JSX so `GestureDetector` wraps the outer `Animated.View` rather than sitting inside `Link`, fixing gesture/preview layering
- Simplify single-argument plugin entries in `app.json` from `["plugin-name"]` to `"plugin-name"`
2026-03-17 19:32:00 -04:00
jake c6ae58f41d refactor(native): overhaul auth screens, add ModalStackHeader, and bump Expo patch releases
- 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
2026-03-17 18:33:11 -04:00
jakeandGitHub daf90a4f39 fix(native): prevent iOS black screen by avoiding provider tree remount on login (#14) 2026-03-17 14:40:04 -04:00
jake ca44354240 refactor: move system health procedure from system to admin router
- Rename `system.health` → `admin.systemHealth` and gate it behind the admin middleware
- Remove `tmdbConfigured` from `system.status` response; update description to reflect its purpose
- Update all call sites on web and native to use `orpc.admin.systemHealth`
- Rename and relocate the docs page from `system/system.health.mdx` to `admin/admin.systemHealth.mdx`
2026-03-17 11:14:03 -04:00
jakeandGitHub 710c43a01f feat: add watch history import from Trakt, Simkl, and Letterboxd (#13) 2026-03-17 11:02:58 -04:00
jake 25736f684a refactor: return pre-resolved internal IDs from all listing endpoints and remove client-side resolve mutations
All explore, discover, search, recommendation, and person-credit listing procedures now include the internal database `id` on every item so clients can navigate and act without a separate resolve round-trip.

- Remove `titles.resolve` and `people.resolve` mutation calls from the native search screen, hero banners, poster rows, and cast cards; replace with direct `Link` navigation using the pre-returned `id`.
- Change `titles.quickAdd` to accept `{ id }` instead of `{ tmdbId, type }` and update every call site on native and web.
- Key all user-status and episode-progress lookups by `id` instead of `tmdbId-type` composite strings across `PosterCard`, `HorizontalPosterRow`, `FilterableTitleRow`, and `usePosterActions`; drop the `tmdbId` prop from `PosterCard` entirely.
- Remove the `titles.hydrateSeasons` auto-trigger from the title detail screen; season hydration now happens server-side on resolve.
- Delete the `browse-thumbhashes` and `browse-title-ids` server procedures and remove them from the router.
- Extend `@sofa/api` schemas with an `id` field on all listing-item types; update `packages/core` services and add a DB migration accordingly.
2026-03-16 16:35:50 -04:00
jake ccc7922737 refactor(native): centralize navigation header configuration into dedicated stack components
Replace the `useTabScreenOptions` hook (and its `.android` variant) with a `TabStack` component that encapsulates all tab-level header styling in one place. Introduce `AuthStackHeader` and `DetailStackHeader` components to own header configuration for auth and detail screens respectively, removing scattered `Stack.Screen options` overrides from individual screen files.

- Delete `use-tab-screen-options.tsx` and `use-tab-screen-options.android.tsx`; all four tab layouts now render `<TabStack title="…" />` instead of manually composing `<Stack screenOptions={useTabScreenOptions()}>`.
- Add `AuthStackHeader` and swap every `<Stack.Screen options={{ title: "…" }}>` inside auth screens for it; strip the now-redundant header option overrides from the auth `_layout`.
- Add `DetailStackHeader` and remove the hardcoded `headerTransparent`/`headerTintColor`/`headerBackButtonDisplayMode` options from the title and person detail routes in `_layout.tsx`.
- Replace remaining `Stack.Screen options={{ title }}` patterns in non-tab screens (`change-password`, `+not-found`) with the `Stack.Screen.Title` helper.
- Refactor the search screen to use `Stack.Header`, `Stack.Screen.Title`, and `Stack.SearchBar` instead of an inline `Stack.Screen options` object; add `placement` and `allowToolbarIntegration` for the iOS integrated search bar.
- Add `browseTitleIds` server procedure and extend `explore` / `discover` procedures; add corresponding schema fields in `@sofa/api`.
2026-03-16 15:14:32 -04:00
jake e5fc18178b docs: add privacy policy page and footer link 2026-03-16 14:17:31 -04:00
jake d8711b09c4 chore(native): remove unstable Metro optimize flags from EAS production build 2026-03-16 14:06:38 -04:00
jake ed3fd54896 refactor(native): replace hardcoded pixel font sizes with semantic Tailwind classes and add ScaledIcon
Replace all `text-[Npx]` arbitrary size utilities with their nearest semantic Tailwind equivalents (`text-sm`, `text-base`, `text-xs`, `text-3xl`, etc.) across every component so text participates properly in the system font-scale pipeline.

Introduce a `ScaledIcon` component backed by a `useScaledSize` hook that multiplies a base icon size by the current font-scale factor, then replace every bare Tabler icon with `ScaledIcon` so icons grow and shrink in proportion with the surrounding text.

- Add `maxFontSizeMultiplier={1.0}` to badge and label text that must not reflow at large accessibility sizes (the "Admin" pill, type/rating chips, provider name labels, and the TMDB attribution line).
2026-03-16 12:46:55 -04:00
jake 923651a7ae refactor(native): bundle fonts locally and replace @expo-google-fonts packages
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`).
2026-03-16 12:02:14 -04:00
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