- 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.
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>
Remove the "On Watchlist" text badge overlay from title card posters.
Instead, indicate library status via a primary-colored card ring and a
small color-coded dot next to the title text with tooltip on hover.
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>
Replace the standalone GenreBrowser section with FilterableTitleRow components
that add genre chips directly beneath each row heading. Selecting a genre
replaces the carousel content inline; deselecting restores popular titles.
Also fix TitleCardSkeleton spacing to match the real card layout.
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>
- Show "Next backup in X" in scheduled backups description
- Add download button to manual backup success toast
- Remove redundant schedule description from header
- Reorder day-of-week picker above time picker for weekly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move /setup out of protected (pages) route group so it's accessible
without auth (fixes fresh install redirect loop)
- Redirect /login → /register when zero users exist
- Add "Get Started" button on landing page for fresh installs
- Hide register button/link when registration is closed
- Add auth redirects: logged-in users on /login or /register → /dashboard
- Convert register page to server component with server-side checks
- Fix animation snap on auth form buttons (transition-all → scoped)
- Update proxy middleware with /login and /register routes
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.
Background sparkline area charts on Movies and Episodes cards show watch
activity distribution over the selected period. Periods now use rolling
windows (past 24h/7d/30d/365d) instead of calendar boundaries. Bumped
stats grid breakpoint to lg. Upgraded recharts 2.15.4 → 3.7.0 and fixed
chart.tsx types for v3 (TooltipContentProps, LegendPayload).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Movies and Episodes cards now have an interactive dropdown to switch
between Today, This Week, This Month, and This Year. Extracts reusable
getWatchCount helper in discovery service and adds /api/stats route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace progress bar with watch/unwatch buttons on desktop hover.
Move episode counter to the right side next to progress bar. Rename
"Mark all"/"Unmark all" to "Watch all"/"Unwatch all".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clean up settings section design: consolidate backup cards, add
hover-reveal actions with tooltips, improve icon sizing across
components, and replace custom timeAgo helper with date-fns
formatDistanceToNow in webhook card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use date-fns format() instead of toLocaleDateString() for deterministic
server/client date rendering
- Set nativeButton={false} on download link Button to suppress base-ui warning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backup service using VACUUM INTO for WAL-safe atomic snapshots
- Server-side storage in DATA_DIR/backups with download/upload API routes
- Restore with integrity validation and automatic pre-restore safety backup
- Scheduled daily backups via cron with configurable retention
- Admin-only settings UI consolidated under single Server section
- Clean up account section sign-out button, fix switch sub-pixel rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wraps the hero banner h2 in a Link with a subtle primary color
hover transition, matching the app's warm-accent interaction pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title rows (TitleRow, ContinueWatchingList) now use the shadcn Carousel
component backed by Embla with embla-carousel-wheel-gestures, enabling
mouse wheel horizontal scrolling and drag-to-scroll on desktop while
preserving touch swipe, stagger animations, and existing layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-locate `StarRating` and `StatusButton` under the title detail
`_components/` directory since they're only used there. Delete the
unused `SearchAutocomplete` and `SearchBar` components from the shared
`components/` root.
Replaces the 335-line React Context provider with a scoped Jotai store,
giving consumers granular subscriptions and stable handler callbacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
drizzle-orm/bun-sqlite is fully synchronous — all queries return values
directly, not promises. Remove await from all db calls, drop async from
functions that no longer need it, simplify Promise.all patterns that
wrapped sync operations, and fix setSetting() which was missing .run()
(previously masked by await triggering execution via thenable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap the hand-rolled Scheduler class (setInterval-based) for croner,
a battle-tested cron library with overlap protection and proper cron
expressions. Consolidate lib/jobs/ into a single lib/cron.ts and
remove the unused admin jobs API route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Override CSS custom properties (--primary, --ring, --primary-foreground,
--status-watching) on the title detail page wrapper using the vibrant
color from the poster's node-vibrant palette. All Tailwind classes
referencing these variables automatically pick up the new color.
Also await lazy color extraction in getTitleWithChildren() so palettes
are available on first visit to never-before-opened titles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>