Commit Graph
100 Commits
Author SHA1 Message Date
jakeandClaude Sonnet 5.5 51d193b411 perf(metadata): remember empty TMDB lookups instead of refetching every view
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 14:01:22 -04:00
jakeandClaude Sonnet 5.5 edd6eb541d fix(imports): keep date-only values on their calendar day
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:51:49 -04:00
jakeandClaude Sonnet 5.5 1ccc138144 test(i18n): cover month labels when Intl defaults to UTC like the native polyfill
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:48:29 -04:00
jakeandClaude Sonnet 5.5 c40b6844a0 fix(i18n): format Upcoming month headers in UTC so native shows the right month
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:47:15 -04:00
jakeandClaude Sonnet 5.5 b7887eef70 fix(native): make select sheets and row actions usable with VoiceOver and TalkBack
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:43:21 -04:00
jakeandClaude Sonnet 5.5 851a188f00 fix(native): recover from render errors and let Close leave deep-linked titles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:40:39 -04:00
jakeandClaude Sonnet 5.5 9d0bede536 fix(native): keep Android headers from covering Upcoming filters and the modal Close button
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:37:47 -04:00
jakeandClaude Sonnet 5.5 9209214e98 fix(native): clear every episode's pending state and block taps during Mark All
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:35:27 -04:00
jakeandClaude Sonnet 5.5 7770e1a697 fix(native): let Android menus open from their trigger
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:33:15 -04:00
jakeandClaude Opus 5.5 ccaacfeae5 feat(native): follow the system language instead of an in-app picker
Declare the app's 12 languages to iOS and Android via expo-localization,
resolve the language from the device's preferred languages at start-up and
on Android runtime changes, and make Settings > Language open the system
settings page. Drops the MMKV-stored locale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:24:59 -04:00
jakeandClaude Sonnet 5.5 d012280f20 fix(native): add a local-network usage description for LAN servers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:17:20 -04:00
jakeandClaude Sonnet 5.5 55552cd1bf chore(native): require iOS 17
Raise ios.deploymentTarget from 16.4 to 17.0 (app and widget extension) and state the requirement in the mobile app docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:13:28 -04:00
jake 12294de522 chore(vscode): update TypeScript settings for improved path resolution and enable experimental features 2026-10-05 13:13:04 -04:00
jakeandClaude Sonnet 5.5 0afc60f28c fix(native): show account edits immediately and save name on first tap
Bypass the 5-minute session cookie cache when refetching after name/avatar edits, keep taps on Save/Cancel working with the keyboard open, add Return-to-save, trim the name, and guard against double submit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:07:54 -04:00
jakeandClaude Sonnet 5.5 fbc6ac90a3 fix(native): show "Session expired" for every session loss the user didn't start
Replace the session-rejected / server-change / cached-session-seeded flags with a single
session-end reason that deliberate sign-outs mark before signing out. The session-loss
effect now toasts only when no reason was recorded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 13:03:35 -04:00
jakeandClaude Sonnet 5.5 786a4ba7c5 fix(native): re-probe the server on retry and rebuild the auth client before marking a new server reachable
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 12:59:59 -04:00
jakeandClaude Sonnet 5.5 b46bc4cdcb fix(native): make the server-URL check reactive so the reachability monitor and banner work after first setup
With the React Compiler, hasServerUrl read from MMKV during render was
memoized forever at the root. Read it through useSyncExternalStore instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 12:54:17 -04:00
jakeandClaude Sonnet 5.5 8f4cd03715 fix(native): persist the query cache per server+user and clear it with the session
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 12:51:00 -04:00
jakeandClaude Opus 5.5 870d130261 chore(i18n): extract catalogs after sharing client helpers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:28:17 -04:00
jakeandClaude Opus 5.5 778a1356d5 refactor(api): share tracking-dependent query key lists between web and native
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:27:43 -04:00
jakeandClaude Opus 5.5 88e777d5ce refactor(api): share sparkline path and luminance helpers between web and native
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:27:15 -04:00
jakeandClaude Opus 5.5 4b38303404 refactor(i18n): re-export error helpers by name to satisfy no-barrel-file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:27:12 -04:00
jakeandClaude Opus 5.5 01a14157c6 refactor(i18n): share integration status text between web and native
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:26:05 -04:00
jakeandClaude Opus 5.5 4adaa4a73b refactor(i18n): share error message maps between web and native
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:25:28 -04:00
jakeandClaude Opus 5.5 d874ae9198 refactor(web): share one import-job flow between import dialogs
Extract the job loop into runImportJob (with tests), hold its UI state in
useImportJob, and move the importing/done steps and stat/option components
into import-dialog-parts. Both dialogs now close when an import continues in
the background or the connection drops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:11:48 -04:00
b63fbce60d feat(search): find titles by IMDb ID
Search queries that look like an IMDb ID (e.g. tt0133093) are now resolved
through TMDB's find endpoint, respecting the movie/tv type filter.

Supersedes #60.

Co-authored-by: Gonçalo Fonseca <gcare.fonseca99@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:57:00 -04:00
jakeandClaude Opus 5.5 0f82de55cb refactor(core): move discover logic into @sofa/core/browse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:56:10 -04:00
jakeandClaude Opus 5.5 07e8e79e51 test(server): characterize discover procedures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:53:30 -04:00
jakeandClaude Opus 5.5 d6ac2324e7 fix(discover): map TV date sorts, clamp TMDB page counts, and pause infinite scroll after a failed page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:31:06 -04:00
jakeandClaude Opus 5.5 7269209d43 fix(web): load more titles in Explore rows and show status in Discover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:23:27 -04:00
jakeandClaude Opus 5.5 9aebca7121 perf(metadata): process episode art in the background with bounded concurrency
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:17:41 -04:00
jakeandClaude Opus 5.5 40517b5214 fix(native): add the Library tab and translated labels on Android
The Android tab bar only declared four tabs with hard-coded English
labels, so the Library route was never registered and "See all" on Home
did nothing. Match the iOS tab set and order and use LingUI labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:11:21 -04:00
jakeandClaude Opus 5.5 67766298d8 fix(native): block duplicate watch taps, stop invalidating title details, and show refresh spinner only on pull
- invalidateTitleQueries no longer invalidates titles.get and returns a promise for tracking/library refetches; mutations return it from onSuccess so isPending covers the refetch
- per-episode pending guard (disabled + dimmed row) and disabled season "Mark All Watched" while pending
- add usePullToRefresh so RefreshControl only reflects user-initiated pulls (Home, Explore, Library, Upcoming)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:07:24 -04:00
jakeandClaude Opus 5.5 bc3ea712f9 fix(imports): poll device codes one request at a time
The Trakt/Simkl sign-in dialog polled with setInterval and no in-flight
guard, so while the first "authorized" poll was still fetching the
library, the next tick re-sent the redeemed device code. Trakt answers
409, which was mapped to "expired", producing a false "Device code
expired" error.

- public-api: map Trakt 409 (already redeemed) to pending
- web: add a single-flight device-code poller that drops late responses
  after stop() and aborts the in-flight request
- web: use it in the imports dialog and ignore device-code responses
  that arrive after the dialog was closed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:00:42 -04:00
jakeandClaude Opus 5.5 feb41e3149 fix(native): sync React Query online state with server reachability
Probe the health endpoint while unreachable, refetch errored queries when the server
returns, and keep gcTime at least as long as the persister maxAge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:51:59 -04:00
jakeandClaude Opus 5.5 07ea8f68f2 fix(native): keep cached screens on refetch errors and show errors instead of empty states
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:51:57 -04:00
jakeandClaude Opus 5.5 25cf4f1bc8 fix(native): route to the right screen when the session is lost
The session-loss effect never ran because sessionLost was computed in a
render that also called setState, so the committed value was always false.
Compare against a ref inside the effect instead, re-validate the session on
401s, stop retrying 4xx errors, and show the change-server link on
OIDC-only login screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:39:54 -04:00
jakeandClaude Sonnet 5.5 e5209f80fe fix(auth): key rate limits on the resolved client IP and restrict the Expo auth proxy
Resolve the client IP from the TCP peer plus trusted-proxy X-Forwarded-For hops
(TRUSTED_PROXIES) and hand it to Better Auth in an internal header, so spoofed
forwarding headers can no longer rotate sign-in rate-limit buckets. The Expo
authorization proxy now only redirects to the OIDC provider's authorization endpoint.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:09:53 -04:00
jakeandClaude Opus 5.5 b8b5b31fa1 fix(i18n): render mapped error messages and stop showing raw server errors
Helpers that took Lingui's `t` as a parameter rendered empty strings because
the macro only transforms `t` imported from the macro. Use msg descriptors
plus i18n._ instead, so mapped error toasts and native server-URL errors are
real, extractable strings. Also map Better Auth error codes to localized
messages instead of showing raw server text, and send a REGISTRATION_CLOSED
code from the auth hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:44:10 -04:00
jakeandClaude Opus 5.5 4c58b550b1 fix(server): cap request bodies and require a session before large uploads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:33:36 -04:00
jakeandClaude Opus 5.5 e9fb4797e5 perf(imports): poll job progress without the payload and drop it when done
readImportJob now selects only progress columns instead of the multi-MB
payload, processImportJob empties the payload once a job reaches a terminal
status, and server startup clears payloads of previously finished jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:27:04 -04:00
jake 0b7d75a59b chore(native): bump version to 0.3.0 2026-10-04 23:26:50 -04:00
jakeandClaude Opus 5.5 82f0bb11e2 perf(db): collect planner statistics on connect and after imports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:18:25 -04:00
jakeandClaude Opus 5.5 6620f0b76f test(server): add test harness and auth coverage for every procedure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:14:56 -04:00
jakeandClaude Opus 5.5 810bbecf78 fix(imports): cap uncompressed size and entry count of uploaded ZIPs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:06:34 -04:00
jakeandClaude Sonnet 5.5 69b252e99b fix(native): serialize widget refreshes and fall back to other artwork
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:02:06 -04:00
jakeandClaude Sonnet 5.5 c62ef6b620 fix(native): keep widget timelines current and localize widget text
expo-widgets rebuilds each widget timeline from the stored entries when it
ends, so five entries 15 minutes apart left the widgets stuck on the last
entry (the 5th show, or the furthest-away release) after an hour.

Continue Watching now rotates through up to five shows every 30 minutes for
12 hours, then rests on the top show. Upcoming writes one entry per local
midnight at which the shown item or its Today/Tomorrow label changes, moves
on after each air date and ends on the empty state once everything has aired.

All widget text is now produced in widgets.ts through LingUI and passed to the
widget extension as string props, replacing the hard-coded English labels and
month table. The widget catalogs gain "S{seasonNum} E{epNum}", "Nothing to
watch" and "Nothing upcoming"; the rest reuse existing messages. The refresh
now waits for the locale catalog so English labels are not baked in first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:47:52 -04:00
jakeandClaude Sonnet 5.5 b4fdf6164f fix(native): cache widget artwork at its real pixel size
UIGraphicsImageRenderer(size:) uses the screen scale by default, so the cached JPEGs came out about 1395px wide instead of the intended 465px. WidgetKit refuses to render images whose pixel area is far beyond the widget's, and the extension has a tight memory limit, so oversized art could blank the widget.

Render at scale 1 into a center-cropped 510px square (systemSmall is at most 170pt, or 510px at 3x) and bump JPEG quality to 0.8 now that the files are much smaller.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:38:04 -04:00
jakeandClaude Sonnet 5.5 9bbe36b2d4 fix(native): give home-screen widgets a container background and full-bleed artwork
iOS 17+ rendered 'Please adopt containerBackground API' because expo-widgets 56 dropped its hard-coded container background and neither widget applied the modifier. Artwork also drew at its raw pixel size because the images were never resizable, and the layout assumed a fixed 155pt box with default content margins.

Apply containerBackground, make the artwork resizable, aspect-filled and clipped, replace the flat overlay with a gradient scrim, left-align the text block, use a native linear ProgressView, and disable content margins via the plugin's ios config. Also correct the Continue Watching description, since the feed only contains TV episodes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:37:08 -04:00
jakeandClaude Sonnet 5.5 6a1b753a49 fix(public-api): paginate Trakt sign-in import and keep it under the response limit
Follow Trakt's pagination headers, slim items to the fields Sofa reads,
keep the most recent plays when the payload would exceed Vercel's 4.5 MB
response limit, surface failed/truncated lists as warnings, and return a
readable fetch_error instead of an oversized response for every provider.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:27:26 -04:00
jakeandClaude Sonnet 5.5 57aaa5e7d2 fix(core): allow scheduler jitter in update-check and telemetry throttles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:17:18 -04:00
jakeandClaude Sonnet 5.5 98fd236b5c fix(auth): refresh the session cookie cache after first-admin promotion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:10:49 -04:00
jakeandClaude Sonnet 5.5 d42a022a30 fix(i18n): format date-only values in UTC and share an age helper
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:05:33 -04:00
jakeandClaude Sonnet 5.5 5a96fd39f1 fix(webhooks): accept native Emby payloads, ignore episode TMDB ids, refresh on missing episodes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:46:00 -04:00
jakeandClaude Sonnet 5.5 f05e63bab7 fix(public-api): treat Simkl KO as pending and unwrap all-items responses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:41:00 -04:00
jakeandClaude Sonnet 5.5 96a0771e8c fix(web): keep subscription settings in sync and report save failures
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:34:31 -04:00
jakeandClaude Sonnet 5.5 d84126f51a fix(discover): pass explicit browse fields from search
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:30:11 -04:00
jakeandClaude Sonnet 5.5 dea8ea74f7 fix(metadata): enrich and fully populate TV shows on first view
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:28:12 -04:00
jakeandClaude Sonnet 5.5 57bac67b36 fix(tmdb): keep the /3 prefix for custom base URLs and trailing slashes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:21:18 -04:00
jakeandClaude Sonnet 5.5 8b10b5cb45 fix: stop infinite lists from evicting their first page
Every infinite list set maxPages: 10 without getPreviousPageParam, so TanStack Query dropped the first page once an 11th was fetched and it could never be scrolled back to. These lists are forward-only, so remove maxPages.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:17:03 -04:00
jakeandClaude Sonnet 5.5 68693415bf fix(tracking): skip watched episodes in bulk marks; read ratings without status
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:10:45 -04:00
jakeandClaude Sonnet 5.5 65d070ff23 fix(credits): replace title cast on refresh and remove duplicate crew rows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:06:00 -04:00
jakeandClaude Sonnet 5.5 4addc32e76 fix(native): invalidate discover and people after title actions
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:59:38 -04:00
jakeandClaude Sonnet 5.5 a0159a3d4b fix(web): make Retry and Try again actually refetch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:59:08 -04:00
jakeandClaude Sonnet 5.5 8a7dc2c8c0 fix(web): refresh tracking-dependent queries after mutations
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:58:41 -04:00
jakeandClaude Sonnet 5.5 923924a76f fix(native): reset home-screen widgets when the session ends
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:52:54 -04:00
jakeandClaude Sonnet 5.5 4ab152cbcc fix(web): clear cached user data on sign-out and sign-in
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:52:52 -04:00
jakeandClaude Opus 5.5 a2724abd89 fix(imports): show a readable file format in the upload hint
The upload hint interpolated the raw `accept` string, so Trakt rendered
as "Upload a .json,.zip export…". Derive a label from `accept` with a new
locale-aware `formatList` helper ("JSON or ZIP"), and wrap the source
card descriptions in `msg` so they are translatable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:46:53 -04:00
jakeandClaude Sonnet 5.5 0c9ef44ea4 fix(imports): tolerate a UTF-8 BOM in uploaded Trakt JSON
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:40:23 -04:00
jakeandClaude Sonnet 5.5 2767b495e3 fix(imports): accept Trakt's official export ZIP and JSON files
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:39:15 -04:00
jakeandClaude Sonnet 5.5 9b1a8b07a6 fix(imports): accept Simkl's nested item shape in uploaded backups
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:35:02 -04:00
jakeandClaude Sonnet 5.5 405ced71e8 fix(imports): normalize nulls and drop invalid items instead of failing the parse
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:31:08 -04:00
jakeandClaude Sonnet 5.5 2598124772 fix(imports): keep rewatches and added dates; honor late cancellation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:27:15 -04:00
jakeandClaude Sonnet 5.5 c8c1bf801b fix(imports): map Trakt season/episode items and normalize imported statuses
Trakt season/episode watchlist items now add the parent show instead of an
unrelated movie sharing the show's TMDB id; season/episode ratings are skipped
and reported as unsupported. The import processor normalizes statuses per
title type (TV never 'completed', movies never 'in_progress') and a data
migration repairs rows already stored wrongly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 15:22:11 -04:00
jakeandClaude Opus 5.5 2529b132ef fix(discovery): only offer aired episodes in Continue Watching; unify "today"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:17:11 -04:00
jakeandClaude Opus 5.5 72d9f8f2d1 fix(backup): accept backups from older schema versions on restore
Validate only a baseline of tables present in every release since v0.1.0
(plus __drizzle_migrations) instead of the full current schema, so restore's
post-swap runMigrations() can bring older backups up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:12:51 -04:00
jakeandClaude Opus 5.5 d73529586b fix(upcoming): make feed cursor unicode-safe and consistent with sort order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:06:59 -04:00
jakeandClaude Opus 5.5 2e7f2667dc fix(cron): isolate per-title failures in refresh jobs
Wrap the four title-looping cron jobs in a runIsolated helper so one
failing title (e.g. removed from TMDB) no longer aborts the whole run;
the job only fails when every item fails. Also include library shell
titles (NULL lastFetchedAt) in the nightly stale query so they get hydrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:03:18 -04:00
jakeandClaude Opus 5.5 4e4a4903f9 fix: resolve @types/bun when Vercel compiles public-api with TypeScript 7
With TypeScript 7, @vercel/node compiles via a temporary tsconfig in the OS
temp dir that extends the project config, so default type roots resolve
from that temp dir and `types: ["bun"]` fails with TS2688. Pin typeRoots
relative to the public-api tsconfig so they survive being extended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:00:21 -04:00
jake fefd0ee032 chore: pin Bun version to 1.4.x in Vercel deployments 2026-10-04 14:54:03 -04:00
jake 75ede459d2 chore: upgrade Expo SDK 55 → 57 and React Native 0.83 → 0.86 (#54) 2026-10-04 13:47:13 -04:00
jake 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
jake 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
jakeandClaude Opus 4.6 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
jakeandClaude Opus 4.6 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