- Remove `{userImage && ...}` guards around AvatarImage; pass `src`
directly so AvatarFallback renders naturally when src is undefined
- In AccountSection, clear `src` during pending state instead of
conditionally rendering the image element; guard destructive overlay
color on `!isPending` so it doesn't flash while uploading
- 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 carousel.tsx and replace all Carousel/CarouselContent/CarouselItem
usage across title rows, cast carousel, continue watching, and explore
sections with ScrollArea + scrollFade horizontal scroll
- Update ScrollArea to accept a `scrollFade` prop that renders a
gradient edge overlay instead of the old per-site absolute div hack
- Remove "use client" directive from title-row and cast-carousel now
that they have no client-side hooks
- Delete unused resizable.tsx; inline TypeBadge icon into TitleHero
and delete the standalone type-badge.tsx file
- Add TitleTheme component for per-title accent color CSS injection
- Swap IconSparkles → IconThumbUp in recommendations section and grid
- Simplify ambient glow to a single size (remove mobile breakpoint override)
- Delete filterable-row, backup-schedule, integrations, and system-health
atom files; replace with useState/useTransition + server action calls
- Replace /api/explore/discover route with discoverByGenre server action;
refactor FilterableTitleRow to call it directly via useTransition
- Wrap PagesLayout and AuthLayout children in Suspense to fix dynamic
rendering errors during PPR static generation; remove StoreProvider
- Sequence ExplorePage session fetch before TMDB calls to prevent
build-time requests during static generation
- Drop unnecessary "use client" directives from dashboard and person
components that have no client-side hooks or browser API usage
- Improve Dockerfile: add bun install layer cache mount, copy
.next/cache from builder, reorder ENV declarations
- 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`
- useTimeAgo: replace per-instance intervals with shared useSyncExternalStore ticker
- useTiltEffect: gate to fine-pointer devices, skip motion graphs on touch
- Carousel: bake WheelGesturesPlugin into primitive, remove from all consumers
- TitleSeasons: use memoized Set + precomputed progress map instead of Array.includes
- useSearch: stabilize results identity with useMemo
- CommandPalette: hoist shortcut grouping to module scope, stabilize effect deps
- StarRating: hoist static spring transition to module constant
- Settings toggles: use useOptimistic + useTransition for auto-rollback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
* Fix mobile pinch zoom and content overlapping header/footer on iOS Safari
Add viewport export with maximumScale=1 and userScalable=false to prevent
pinch-to-zoom. Add viewportFit=cover for proper safe area support.
Fix content rendering above the sticky header and below the fixed mobile
tab bar on iOS Safari by adding relative z-0 to the content wrapper,
creating a stacking context that keeps page content below both navigation
elements.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Add safe area inset support for iOS 26 and fix film grain z-index
- Add safe-area-inset-top padding to NavBar for notch/Dynamic Island
- Add safe-area-inset-left/right to NavBar, main content, and
MobileTabBar for landscape mode
- Account for safe-area-inset-bottom in content wrapper padding so
content isn't hidden behind MobileTabBar + home indicator
- Lower film grain overlay z-index from 9999 to 1 to avoid triggering
iOS 26 Safari's fixed-element tab tinting behavior
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Hide ambient glow on mobile where it overwhelms the viewport
The 800x600px blurred glow blob dominates narrow mobile screens and
clashes with title backdrop images. Hidden below sm breakpoint.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
* Fix asymmetric safe-area insets using separate pl/pr padding
The previous px-[max(...,env(safe-area-inset-left))] shorthand applied
only the left inset to both sides. Split into separate pl/pr with the
correct env() variable for each side.
https://claude.ai/code/session_01Re8ndXPAMVMtiHZFVNDK7S
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replace mixed badge styles with a single icon type badge (with tooltip),
CSS-based dot separators, collapsed genres with hover popover, status
icons, and subtle TMDB link. Remove vote count for less noise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the `userStatus` guard on the "Mark All" button so unauthenticated
or unwatchlisted users can also access it. Drop the mobile percentage
label (redundant with the watch count) and make the watched/total count
always visible instead of hidden on small screens.
Introduces a local provider registry (lib/providers.ts) mapping TMDB
provider IDs to search URL templates for ~20 streaming services. Provider
badges on title pages are now clickable links that open the service's
search page with the title pre-filled. Categories with more than 4
providers show a "+N" overflow badge with a hover popover listing the rest.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
The proxy treated /setup as a protected route, redirecting unauthenticated
users to /login. This made first-run TMDB onboarding unreachable since
no users exist yet. Add /setup to public auth routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Person results from multi-search were using poster_path instead of
profile_path, causing missing profile images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base UI Select.Value renders raw values by default — add children render
functions to map values to human-readable labels (e.g. "this_month" →
"This Month", "0" → "unlimited"). Switch inline select underlines from
border-bottom to text-decoration for proper baseline alignment. Add TMDB
attribution with logo and disclaimer to settings footer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app/not-found.tsx: root 404 with cinematic atmosphere
- app/global-error.tsx: self-contained global error boundary
- app/(pages)/error.tsx: authenticated error boundary with retry
- app/(pages)/not-found.tsx: authenticated 404 with navigation
- Upgrade existing title and person not-found pages for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add global MotionConfig reducedMotion="user" in root layout
- Add color-scheme: dark, theme-color meta, skip-to-main-content link
- Add aria-hidden={true} to ~60+ decorative icons across 33 files
- Add aria-label to all icon-only buttons (logout, star rating, play, delete, etc.)
- Replace transition-all with specific properties (11 files)
- Add motion-safe: prefix for CSS hover transforms and animate-pulse
- Add prefers-reduced-motion: reduce override in globals.css
- Add useReducedMotion guard to status-dot pulsing animation
- Fix focus-visible styles on auth form inputs (focus → focus-visible, stronger ring)
- Add text-balance to all headings (13 files)
- Replace "..." with "…" (U+2026) in all loading/placeholder text
- Add autocomplete, spellCheck, role="alert" to auth form
- Add aria-label to Switch controls and filmography select
- Fix heading hierarchy (h3 → h2 where h2 was skipped)
- Add break-words to title overview, overscroll-contain to drawer
- Add confirmation dialog for destructive library removal
- Add group-focus-within:opacity-100 for keyboard-accessible backup actions
- Add role="radio" + aria-checked to star rating buttons
- Add aria-label to carousel region, role="img" to TMDB logo SVG
- Add text-foreground to native select for dark mode consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Context-based provider with useProgress() hook that auto-detects
link clicks and popstate, with manual start()/done()/set() for
router.push() calls. Uses useEffectEvent for clean effect deps.
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>
Spring-animated 3D tilt, parallax image shift, and glare reflection
follow the cursor using motion values for zero-rerender performance.
Respects prefers-reduced-motion. Carousel overflow adjusted to prevent
clipping of the tilt effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename the person detail route from `app/(pages)/person/[id]` to
`app/(pages)/people/[id]` and update all internal links accordingly.
Strip crew members from the cast carousel so only actors are shown;
remove the `crew` prop from `CastCarousel` and `TitleCast` entirely.
Move the trailer trigger from the metadata row into a centered overlay
on the backdrop image using a new `variant="backdrop"` prop on
`TrailerDialog`, replacing the previous inline button approach.
Swap several icons for better semantic matches: `IconBooks` for the
library section, `IconCheck` for the "Mark Watched" button,
`IconSparkles` on the Recommendations heading, and `IconDeviceTvOld`
on the Seasons heading.
Replace a nested `motion.p` with a plain `<p>` using CSS
`transition-opacity` in `StatsDisplay`. Remove the `animate-gentle-float`
keyframe and the `feed-scroll` utility from `globals.css`, replacing the
latter with the existing `no-scrollbar` class.