- Replace static account card in mobile flyout with swipe-to-logout widget
- Remove redundant logout button from flyout bottom
- Make LogoutButton full-width via class prop and dynamic maxSlide
- Extract clean GraphQL error messages instead of raw JSON in all auth forms
- Add i18n keys for known backend errors (invalid credentials, email taken, invalid token)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace large gradient title with simple text-2xl font-bold heading,
matching the header pattern used across admin pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mobile: remove horizontal padding so tables fill full width with top/bottom
borders only. Desktop: keep left padding, table extends to right edge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add calendar + popover components and a custom DateTimePicker wrapper.
Video forms use date-only; article forms include a time picker.
Also add video player preview to the video edit form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Separate admin identity from role: viewer|model + is_admin boolean flag
- DB migration 0001_is_admin: adds column, migrates former admin role users
- Update ACL helpers, auth session, GraphQL types and all resolvers
- Admin layout guard and header links check is_admin instead of role
- Admin users table: show Admin badge next to name, remove admin from role select
- Admin user edit page: is_admin checkbox toggle
- Install shadcn Badge component; use in admin users table
- Fix duplicate photo keys in adminGetUser resolver
- Replace confirm() in /me recordings with Dialog component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Layout: sidebar hidden on mobile, replaced with horizontal top nav strip
- Tables: overflow-x-auto + hide secondary columns (email/category/dates/
plays/likes) on small screens; show email inline under name on mobile
- Forms: grid-cols-2 → grid-cols-1 sm:grid-cols-2 on all admin forms
- Markdown editor: Write/Preview tab toggle on mobile, side-by-side on sm+
- Padding: p-3 sm:p-6 on all admin pages for tighter mobile layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: adminGetUser query returns user + photos; adminUpdateUser now
accepts avatarId/bannerId; new adminAddUserPhoto and adminRemoveUserPhoto
mutations; AdminUserDetailType added to GraphQL schema
- Frontend: /admin/users/[id] page for editing name, avatar, banner, and
managing the model photo gallery (upload multiple, delete individually)
- Admin users list: edit button per row linking to the detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The full-screen glassmorphism overlay had backdrop-blur-[0.5px] which
triggered GPU compositing on the entire viewport, degrading subpixel
text rendering inconsistently. Also use globalThis.fetch (not SvelteKit
fetch) when forwarding session token in admin SSR calls to avoid header
stripping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin list queries (users, videos, articles) were using getGraphQLClient
without auth credentials, causing silent 403s on server-side loads. Now
extract session_token cookie and pass it to getAuthClient so the backend
sees the admin session on SSR requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.)
- Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:*
- Update backend Pothos objectRef types to use shared interfaces instead of inline types
- Update frontend $lib/types.ts to re-export from shared package
- Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[])
- Add artist_name to comment user select in backend resolver
- Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Root-level eslint.config.js (flat config): typescript-eslint,
eslint-plugin-svelte, eslint-config-prettier, @eslint/js
- Root-level prettier.config.js with prettier-plugin-svelte
- svelte-check added to frontend for Svelte/TS type checking
- lint, lint:fix, format, format:check, check scripts in root
and both packages
- All 60 lint errors fixed across backend and frontend:
- Consistent type imports
- Removed unused imports/variables
- Added keys to all {#each} blocks for Svelte performance
- Replaced mutable Set/Map with SvelteSet/SvelteMap
- Fixed useless assignments and empty catch blocks
- 64 remaining warnings are intentional any usages in the
Pothos/Drizzle GraphQL resolver layer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
thumbnail (300x300 square) was double-cropping inside the wide h-48
container. preview (800px, aspect-ratio preserved) lets object-cover
do the only crop, matching the videos and model pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes Directus 11 and replaces it with a lean, purpose-built backend:
- packages/backend/: Fastify v5 + GraphQL Yoga v5 + Pothos (code-first)
with Drizzle ORM, Redis sessions (session_token cookie), argon2 auth,
Nodemailer, fluent-ffmpeg, and full gamification system ported from bundle
- Frontend: @directus/sdk replaced by graphql-request v7; services.ts fully
rewritten with identical signatures; directus.ts now re-exports from api.ts
- Cookie renamed directus_session_token → session_token
- Dev proxy target updated 8055 → 4000
- compose.yml: Directus service removed, backend service added (port 4000)
- Dockerfile.backend: new multi-stage image with ffmpeg
- Dockerfile: bundle build step and ffmpeg removed from frontend image
- data-migration.ts: one-time script to migrate all Directus/sexy_ tables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ButtplugMessageSorter never deleted entries from _waitingMsgs after
resolving, causing unsolicited DeviceList messages (with reused Ids)
to be swallowed. Also fix battery level not updating in UI by accessing
the device through the Svelte $state proxy array.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add play count display below video title with play icon
- Query actual plays count from sexy_video_plays table for accuracy
- Apply same pattern as likes_count for consistency
- Show singular/plural ("play" vs "plays") based on count
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed from max-w-lg to max-w-3xl to accommodate 3-column grid
with better spacing and prevent cramped appearance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Photos are returned from backend as { directus_files_id: fileObject }
but frontend was trying to access p.id directly. Updated to use
p.directus_files_id.id to properly display model photos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace customEndpoint() with native fetch in getFeaturedModels() and
getFeaturedVideos() to return plain JSON instead of non-serializable
Directus SDK objects. This resolves the SvelteKit serialization error:
"Cannot stringify arbitrary non-POJOs".
Changes:
- Use native fetch with PUBLIC_URL instead of getDirectusInstance()
- Return plain JSON via response.json() instead of SDK request objects
- Remove JSON.parse(JSON.stringify()) serialization hack from +page.server.ts
- Rename fetch parameter to fetchFn for clarity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed serialization error by converting Directus SDK response objects to
plain JSON using JSON.parse(JSON.stringify()).
This resolves the error:
'Data returned from load while rendering / is not serializable:
Cannot stringify arbitrary non-POJOs (data.models)'
Also improved performance by using Promise.all to fetch models and videos
in parallel instead of sequentially.
Backend Changes:
- Added original_recording_id field to sexy_recordings table to track duplicates
- Added indexes for original_recording_id and public fields
- Implemented /sexy/community-recordings endpoint to list public shared recordings
- Implemented /sexy/recordings/:id/duplicate endpoint to duplicate community recordings
- Community recordings filtered by status=published AND public=true
- Duplication creates a private draft copy for the current user
Frontend Changes:
- Added leaderboard and profile quick links to play view header
- Added navigation buttons for better UX on play page
- Added translations: my_profile, anonymous, load_more
Database Schema:
- ALTER TABLE sexy_recordings ADD COLUMN original_recording_id uuid
- Created foreign key and indexes for efficient queries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add gamification card to user profile showing:
- Total weighted points and rank
- Recordings and playbacks count
- Unlocked achievements with icons and dates
- Link to leaderboard
Updates user profile page server load to fetch gamification data
from /api/sexy/gamification/user/:id endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added Directus hooks for automatic point awards:
- Recording creation/publishing (50 points)
- Recording featured status (100 points bonus)
- Comments on recordings (5 points)
- Created /leaderboard route with full UI
- Server-side data loading with authentication guard
- Responsive design with medal emojis for top 3
- User stats display (recordings, plays, achievements)
- Pagination support
- "How It Works" info section
- Added comprehensive gamification translations
- Time-weighted scoring displayed for rankings
- Automatic achievement checking on point awards
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added user profile feature allowing authenticated users to view profiles of other users. Key changes:
**New Routes:**
- `/users/[id]/+page.server.ts` - Server-side load function with authentication guard and user data fetching
- `/users/[id]/+page.svelte` - User profile UI component displaying avatar, stats, and bio
**Features:**
- Authentication required - redirects to /login if not authenticated
- Shows user display name (first_name + last_name or email fallback)
- Displays join date, location, and description
- Statistics: comments count and likes count
- "Edit Profile" button visible only for own profile (links to /me)
- Responsive layout with avatar placeholder for users without profile images
**Comment Integration:**
- Updated video comment section to link user avatars to their profiles
- Added hover effects on avatars (ring-primary/40 transition)
- Username in comments now clickable and links to `/users/[id]`
**Translations:**
- Added `profile` section to en.ts locales with:
- member_since: "Member since {date}"
- comments: "Comments"
- likes: "Likes"
- edit: "Edit Profile"
- activity: "Activity"
**Design:**
- Simplified layout (no cover banner) compared to model profiles
- Peony background with card-based UI
- Primary color theme with gradient accents
- Consistent with existing site design patterns
This creates a clear distinction between:
- Model profiles (`/models/[slug]`) - public, content-focused
- User profiles (`/users/[id]`) - authenticated only, viewer-focused
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Redirect unauthenticated users to /login
- Add error handling for getFolders API call
- Prevent 500 errors from accessing undefined user properties
This fixes the issue where clicking the header button to access the dashboard
would show a 500 error for unauthenticated users.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Backend changes:
- Added /sexy/analytics endpoint to fetch detailed creator analytics
- Calculates total likes, plays, completion rates, and avg watch times
- Groups analytics by date for timeline visualization
- Provides video-specific performance metrics
Frontend changes:
- Added Analytics TypeScript types and service function
- Created Analytics tab in /me dashboard (visible only for Models)
- Displays overview stats: total videos, likes, and plays
- Added detailed video performance table with:
- Individual video metrics
- Color-coded completion rates (green >70%, yellow >40%, red <40%)
- Average watch time per video
- Links to video pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added "Most Liked" and "Most Played" sorting options to video listing
- Display total likes and plays on model profile pages
- Show individual video like/play counts on model profile video cards
- Added i18n translation keys for new sort options
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Video Detail Page (/videos/[slug]):
- Load like status from server on page load
- Add functional like button with heart icon
- Show likes count with real-time updates
- Track video plays when user clicks play
- Record watch progress every 10 seconds
- Mark video as completed at 90% watched
- Show toast notifications for like/unlike actions
- Require authentication to like videos
Video Listing Page (/videos):
- Display play count badge on video thumbnails
- Show play icon with count in top-right corner
Features:
- Like/unlike videos with live count updates
- Play tracking with analytics data
- Progress tracking for completion metrics
- Authentication-gated liking functionality
- User-friendly toast feedback
All UI components working and integrated with backend API
- Remove bits-ui Select component dependency
- Use native HTML select element for device selection
- Simplify state management (single mappings Map)
- Fix selection handling with direct onchange event
- Add visual indicator for exact name matches
- Resolves selection not working issue