- Add `nuqs` dependency; define `dashboardSearchParams` and
`exploreSearchParams` loaders in new `search-params.ts` files
- Thread `moviePeriod`/`episodePeriod` from URL into `StatsSection`
and `StatsDisplay`, replacing local `useState` with `useQueryStates`
- Rename `getUserStats` fields `moviesThisMonth`/`episodesThisWeek` →
`movieCount`/`episodeCount` and accept period args so SSR stats
match the active URL params
- Pre-fetch genre-filtered results server-side on the explore page
when `movieGenre`/`tvGenre` params are present; pass as
`initialGenreItems` to `FilterableTitleRow` so first paint is
already populated
- Render genre chips as `<Link>` elements (with click-handler
fallback) so genre selections produce shareable, bookmarkable URLs
- Add `openapi-fetch` dependency; generate `lib/tmdb/schema.d.ts` from
TMDB's OpenAPI spec and delete `lib/tmdb/types.ts`
- Rewrite `lib/tmdb/client.ts` to use the typed fetch client against
the generated schema, exporting `TmdbMovieDetails`, `TmdbTvDetails`,
`TmdbVideo`, and `TmdbGenre` directly
- Fix null-safety across services (`credits`, `availability`,
`metadata`, `person`, `webhooks`) and call sites (explore page,
search route, explore actions) to handle optional fields produced
by the stricter generated types
- Update `metadata.ts` and `metadata.test.ts` to import shared types
from `@/lib/tmdb/client` instead of the removed `types.ts`
- Exclude `lib/tmdb/schema.d.ts` from Biome linting
- Change primary argument from TMDB size string (`w500`, `w1280`, etc.)
to category name (`posters`, `backdrops`, `stills`, `logos`,
`profiles`); optional size override remains as third argument
- Update all call sites across services, actions, pages, and route
handlers to use the new category-based API
- Update image.test.ts to match the new signature and replace
size-mapping test descriptions with category-name descriptions
- Rename `lib/types/title.ts` → `lib/types.ts` (remove nested directory)
- Update all imports across services, atoms, components, and utils
from `@/lib/types/title` to `@/lib/types`
- Rename `lib/test-preload.ts` → `test/preload.ts` and
`lib/test-utils.ts` → `test/sqlite.ts`
- Update `bunfig.toml` preload path to `./test/preload.ts`
- Update all service test imports from `@/lib/test-utils` to
`@/test/sqlite`
- Add `revalidate` option to `refreshCredits` and `refreshRecommendations`;
pass `{ revalidate: false }` from `ensureEnriched` to avoid calling
`updateTag` outside Server Actions/Route Handlers
- Add `hideScrollbar` prop to all horizontal ScrollArea instances
(continue-watching, title rows, filterable row, cast carousel)
- Replace text-only type badge in HeroBanner with icon + label using
IconMovie / IconDeviceTv
- Fix StatusButton destructive hover overrides with `!important` so
they correctly supersede the status-specific background/text/ring
- Add `updateNameAction` server action that calls `auth.api.updateUser`
with validation (non-empty, max 100 chars)
- Replace static name display in AccountSection with an inline edit
mode: click pencil icon to activate, Enter/blur to save, Escape to
cancel
- Use auto-sizing ghost input (invisible span grid trick) so the field
matches text width
- Animate between display and edit states with AnimatePresence fade;
show Spinner while transition is pending
- Add `lib/constants.ts` exporting `DATA_DIR`, `DATABASE_URL`, `CACHE_DIR`,
`BACKUP_DIR`, `AVATAR_DIR`, `TMDB_API_BASE_URL`, and `TMDB_IMAGE_BASE_URL`
- Replace inline `process.env` derivations in `db/client`, `backup`,
`image-cache`, `system-health`, `tmdb/client`, `actions/settings`,
and `api/avatars` with imports from the new module
- Add `uploadAvatarAction` and `removeAvatarAction` server actions;
store files under `DATA_DIR/avatars/{userId}.{ext}` via Bun.write
- Add `GET /api/avatars/[userId]` route that scans the avatar dir with
Bun.Glob and serves with immutable Cache-Control (cache-busted via
query param)
- Refactor AccountSection with hover overlay, AnimatePresence fade,
and file input; click avatar to upload (no image) or remove (has image)
- Replace NavBar user initial badge with Avatar + DropdownMenu showing
name, email, settings link, and sign-out
- Thread `userEmail` and `userImage` props through AuthenticatedShell
→ NavBar and SettingsPage → AccountSection
- Delete filterable-row, backup-schedule, integrations, and system-health
atom files; replace with useState/useTransition + server action calls
- Replace /api/explore/discover route with discoverByGenre server action;
refactor FilterableTitleRow to call it directly via useTransition
- Wrap PagesLayout and AuthLayout children in Suspense to fix dynamic
rendering errors during PPR static generation; remove StoreProvider
- Sequence ExplorePage session fetch before TMDB calls to prevent
build-time requests during static generation
- Drop unnecessary "use client" directives from dashboard and person
components that have no client-side hooks or browser API usage
- Improve Dockerfile: add bun install layer cache mount, copy
.next/cache from builder, reorder ENV declarations
- Add `resolveTitle` and `resolvePerson` server actions that import
from TMDB and return the internal DB id
- Remove `tmdb-{id}-{type}` URL pattern and server-side redirect logic
from TitleDetailPage and PersonDetailPage
- Rename `importTitle` → `getOrFetchTitleByTmdbId`; add `getOrFetchTitle`
combining fetch + children lookup
- Update HeroBanner, TitleCard, and CommandPalette to call resolve
actions client-side before pushing to router
- Batch availability offer inserts into a single transaction
- Stack title-hero poster/metadata vertically on mobile (flex-col md:flex-row),
switch backdrop tall breakpoint from sm to md
- Extract bio expand/collapse logic into reusable ExpandableText component;
use it in PersonHero and TitleHero
- Fix ContinueWatchingCard image to use fill + sizes instead of fixed
width/height to avoid layout shift
- Add fade-out gradient on genre chip scrollbar edge on mobile
- Show scaled-down ambient glow on mobile instead of hiding it entirely
- Humanize knownForDepartment labels (Acting→Actor, Directing→Director, etc.)
- Change cast member name from truncate to line-clamp-2 for wrapping
- Hide tooltip arrow via [&>:last-child]:hidden instead of color-matching it
- Apply safe-area-inset padding to HeroBanner content on notched devices
- Swap Jotai `movieStatsAtom`/`episodeStatsAtom` atoms for `useState` +
`useEffect` calling `getStatsAction` directly on period change
- Remove dependency on `lib/atoms/stats` module
- Add touch-friendly tap target padding to `inlineTriggerClass` via
negative margin compensation on mobile
- Delete 10 API routes (stats, system-health, update-check, jobs/trigger,
backup/restore, person, titles import/resolve, registration/status) and
move logic into server actions in lib/actions/settings.ts and
lib/actions/watchlist.ts
- Refactor SystemHealthCards to accept initialData prop and hydrate Jotai
atoms via useHydrateAtoms; replace useSystemHealth SWR hook with a
lightweight useSystemHealthRefresh that calls getSystemHealthAction
- Convert BackupRestoreSection to use restoreBackupAction + useTransition
instead of a raw fetch call
- Split SystemStatusCard, BackgroundJobsCard, and StorageCard into
standalone components reading from systemHealthDataAtom
- Make SetupPage async with connection() to opt into dynamic rendering
- Overhaul `lib/auth/server.ts` and `lib/auth/session.ts`; update all API
routes and server actions to use the revised session pattern
- Refactor server actions (settings, titles, watchlist, setup) for
consistency with new auth layer
- Extract `SetupForm` into its own client component with `useActionState`,
animated steps, and copyable env snippets
- Move landing page redirect logic into `app/page.tsx`; slim down
`LandingPage` component
- Add `proxy.ts` for local dev proxying
- Minor cleanup to `NavBar`, `MobileTabBar`, and `TitleCard`
Add covering indexes for recommendation rank ordering, title staleness scans,
and type+status filtering. Replace N+1 per-row staleness checks in cron jobs
with set-based batch queries. Wrap availability refresh in a transaction for
atomicity. Batch episode stills query and use existence check instead of
loading all rows for count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New `lists` service fetches Sonarr/Radarr library via their REST APIs
and auto-imports matching titles from TMDB into the user's watchlist
- `app/api/lists/[token]/route.ts` webhook endpoint triggers a list sync
- Unified `IntegrationCard` component replaces `WebhookCard`, handling
both webhook-style (Plex/Jellyfin/Emby) and list-style (Sonarr/Radarr)
integrations with per-type config forms
- Schema migration adds `sonarr` and `radarr` to the integration type
enum and a `listConnections` table for list-based integrations
- 212 tests added for the lists service covering import, deduplication,
and error handling
bun's mock.module persists across test files in the same process,
causing image.test.ts to receive the stub instead of the real
tmdbImageUrl. Since tmdbImageUrl has no heavy runtime dependencies,
service tests work fine with the real implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 211 tests across 15 files covering services (tracking, discovery,
metadata, backup, credits, webhooks, settings, update-check, colors,
person), utilities (config, cron, providers, title-theme), and TMDB
image URL helpers
- `lib/test-preload.ts` + `bunfig.toml` wire up a global in-memory
SQLite DB with migrations for all DB-backed tests
- `lib/test-utils.ts` provides `clearAllTables()` and seed helpers
(insertUser, insertTitle, insertTvShow, insertMovieWatch, etc.)
- Export `getBackupSource`, `isKnownBackup`, `isValidBackupFilename`,
`buildBackupCron`, `performUpdateCheck` internals for direct testing
- GitHub Actions workflow runs `bun test --coverage` on push/PR to main
- Backup and update check jobs show as "disabled" (not "succeeded") when
their respective settings are off
- Disabled jobs excluded from "x of y jobs healthy" count, sorted to
bottom of list, with schedule/next-run hidden and trigger button disabled
- Active jobs sorted by next run time
- "Never run" uses amber dot to differentiate from gray "disabled" dot
- Extract RefreshButton component and add it to all three health cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the static package.json import in lib/version.ts and instead
pass APP_VERSION as a Docker build arg extracted from package.json by
the CI workflow. Both the builder and runner stages receive APP_VERSION
and GIT_COMMIT_SHA as environment variables, making runtime version
info available via process.env without bundling package.json into the
standalone output.
Build a provider registry mapping TMDB provider IDs to search URL
templates. `generateProviderUrl()` resolves a URL for a given provider
and title name using URL-encoded search queries. `readAvailability()`
now accepts the title name and attaches `watchUrl` to each offer.
Provider badges with a resolved URL render as `<a>` links opening in a
new tab; tooltip copy switches from the provider name to "Watch on
{name}". Badges without a known URL remain non-interactive as before.
When a show had status "watchlist", logging episode watches did not
transition it to "in_progress", so it never appeared in the Continue
Watching feed which filters for in_progress titles only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast, availability, recommendations, colors, and trailers were missing
on first load because importTitle fire-and-forgot enrichment tasks.
getTitleWithChildren now calls ensureEnriched() to backfill any missing
data before rendering — works for new imports, old titles, and shells.
Also removes the awaitEnrichment option from importTitle (redundant now)
and optimizes DB queries: reuses already-read data for existence checks,
caches fetchSeasonsFromDb result, and skips re-reads when no backfill ran.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store TMDB genres in normalized tables (genres + titleGenres) and content
ratings as a column on titles. Both are fetched during import/refresh using
append_to_response for content ratings (no extra API calls). Displayed in
the title hero between type badge and year. Also fixes pre-existing type
error for profile_path on TmdbSearchResult.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add composite indexes on userEpisodeWatches and userMovieWatches for hot queries
- Batch episode tracking: wrap season/batch watches in single transaction (~8 queries vs 8*N)
- Fix N+1 patterns in credits, recommendations, and filmography with batch prefetch+insert
- Stream TV season hydration via Suspense instead of blocking page render
- Optimize webhook logs (per-connection LIMIT 10) and system health queries
- Merge genre filter waterfalls into single Promise.all fetch
- Migrate deprecated Jotai loadable() to unwrap()
- Replace isolated createStore()+Provider with useHydrateAtoms on root store
- Remove unnecessary atomWithStorage SSR guards (handled by Jotai internally)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Coalesces concurrent imports via an in-flight promise map, and catches
SQLITE_CONSTRAINT_UNIQUE as a safety net to return the existing row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Motion's initial="hidden" rendered opacity:0 in server HTML, making content
invisible until JS hydrated — causing a multi-second blank gap after skeleton
disappearance. Replaced with CSS @keyframes stagger animation that plays
immediately from SSR. Also made color extraction fire-and-forget, merged
duplicate stats queries, and removed redundant dashboard loading.tsx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate TMDB credits data into the app: cast carousels on title detail
pages, person detail pages with biography and filmography, and person
search results in the command palette. Includes new persons/titleCast
DB tables, profile image caching, and a nightly credits refresh cron job.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetch trailer video keys from TMDB videos API during title import/refresh,
store in DB, and display a "Trailer" button on title pages that opens a
media-chrome Sutro-themed YouTube player in a fullwidth dialog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add try/catch around `request.json()` in all POST routes to return a
400 instead of crashing on malformed bodies. Validate `type` as a
strict `"movie" | "tv"` enum in search, discover, import, and resolve
routes. Validate `tmdbId` as a positive integer. Add a
`SORT_BY_PATTERN` regex and page-range check (1–500) to the discover
route. Wrap all outbound TMDB calls in try/catch and return 502 on
failure so clients get a structured error rather than an unhandled
rejection.
Fix optimistic-update rollbacks in `use-title-actions`: capture
`prevStatus` and `prevWatches` before each mutation and restore both
atoms in the catch block for catchUp, handleMarkSeason,
handleUnmarkSeason, and single-episode toggle.
Fix a bug in `getContinueWatchingFeed` where the watchDateMap could
hold a stale date for episodes watched more than once; the map now
keeps the most-recent `watchedAt` per episode.
Serialize all backup operations through a promise-based queue
(`withBackupLock`) to prevent race conditions under concurrent
requests. Extract internal helpers so `pruneBackups` and
`deleteBackup` can participate in the same lock.
Strengthen `restoreFromBackup`: write the temp file alongside the
live database (`dbDir`), validate it with a full integrity check,
foreign key check, and a required-tables list before touching the
live DB, then use a synchronous `renameSync` for an atomic swap.
Clear WAL/SHM files synchronously in the same tick to avoid
interleaving. Run migrations after restore so older backups are
brought up-to-date automatically.
Add milliseconds to all backup timestamps (`HHmmssSSS`) to avoid
filename collisions when backups are created in rapid succession.
Update filename regexes to accept both the old and new formats.
Add createLogger to tmdb client, image-cache, availability, and
webhooks services. Replace all silent .catch(() => {}) in metadata.ts
with debug-level error logging. Add progress and success logs in cron
job bodies, colors extraction, and webhook processing. Fix pre-existing
unused parameter lint warning in title-card.tsx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual fetch/useState/useEffect patterns with SWR hooks in
system health section and command palette search, gaining automatic
caching, request deduplication, and stale-while-revalidate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace nested loop queries with batch fetches using inArray() across
discovery, tracking, metadata, settings, and system-health services.
The biggest win is getContinueWatchingFeed() going from ~375+ queries
to 5. Add a cached getSession() wrapper via React.cache() to deduplicate
auth lookups shared between layouts and pages. Parallelize independent
async operations in importTitle(), cacheImagesJob(), and the explore page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rebuild the background jobs card as a sortable table showing each job's
schedule, last run time (live-updating via a new useTimeAgo hook), last
duration, and a manual trigger button backed by a new POST
/api/admin/jobs/trigger route. Extract StatusDot into a shared
component. Add cronToHuman() to display schedule patterns as readable
strings (e.g. "Every 6h", "Daily at 03:00"). Replace static
formatDistanceToNow calls throughout the health section with a
LiveTimeAgo component that refreshes every 30 seconds. Also swap a
handful of section icons for better visual matches across settings cards.
Checks current version against latest GitHub release every 6 hours via
cron job, caches result in appSettings, and surfaces updates through a
toast notification (once per browser session) and an animated badge in
the settings footer. Includes an admin toggle to enable/disable checks
(enabled by default). Also renames ServerSection to RegistrationSection
now that update checks are in their own card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows a subtle progress bar at the bottom of title cards indicating
watched/total episodes, with a tooltip for exact counts. Uses a single
efficient SQL query with JOINs to batch-fetch progress for all visible
titles on the Explore page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title cards now show the user's existing watch status (watchlist,
watching, completed) with a color-coded badge on the poster and a
status-aware quick-add button that prevents re-adding tracked titles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace ad-hoc useState/useEffect fetch patterns with Jotai atoms and
loadables in StatsDisplay, FilterableTitleRow, BackupScheduleSection,
IntegrationsSection, and CommandPalette. Each component now gets a
scoped Jotai Provider with a pre-initialized store so server-rendered
initial values hydrate correctly. Async data fetching moves into
atom-level loadables, eliminating manual loading flags and cancellation
logic throughout.
Add Emby as a third media server integration alongside Plex and Jellyfin.
Emby webhooks use a similar payload format (JSON with nested Item object
and ProviderIds). Also removes the mediaServerUsername field from all
webhook connections since it was never used for authentication — the
token-in-URL is the sole auth mechanism. This simplifies the connection
UX to a single "Connect" button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all server actions from scattered page-level files into shared lib/actions/
directory (settings.ts, titles.ts, watchlist.ts) so they can be reused across
the app. Add a hover-triggered plus button on explore page title cards that lets
users add titles to their watchlist without navigating away.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add admin-only system health API and UI showing database stats, TMDB
connection status, background job history, storage usage, and environment
variables (with redacted secrets). Split monolithic backup card into
three focused cards (backups, schedule, restore) with dedicated section
headers for Server, Security, and Backups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "unlimited" (value 0) to the keeping-last dropdown
- Skip pruning when retention is set to unlimited
- Show toast confirmation when changing retention
- Update description to read "Keeping unlimited backups" or "Keeping last N backups"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make backup frequency (6h/12h/1d/7d), time, and day-of-week
configurable via settings, stored in appSettings key-value table
- Add rescheduleBackup() to dynamically update cron job at runtime
- Differentiate manual (sofa-manual-*) vs scheduled (sofa-scheduled-*)
backup filenames with source icons and tooltips in the backup list
- Pruning now only targets scheduled backups, leaving manual ones intact
- Merge backup-actions.ts into actions.ts, use shadcn DropdownMenu for
all selectors (consistent with dashboard stats cards)
- Fix hydration mismatch on relative backup timestamps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created a new `cronRuns` table to track job execution history, including job name, status, timestamps, and error messages.
- Updated job scheduling logic to log job status in the `cronRuns` table, capturing success and error states.
- Enhanced error handling to log detailed error messages in the database.
This commit establishes a foundation for monitoring scheduled jobs and their outcomes.