- 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>
- Replace inline mobile dropdown with sliding flyout panel from right
- Hide burger menu on lg breakpoint, desktop auth buttons use hidden lg:flex
- Add backdrop overlay with opacity transition
- Remove logo from flyout panel header
- Fix backdrop div accessibility with role="presentation"
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>
- Remove outdated docs (COMPOSE.md, DOCKER.md, QUICKSTART.md, REBUILD_GUIDE.md)
- Remove build.sh, compose.production.yml, gamification-schema.sql, directus.yaml
- Simplify compose.yml for local dev (remove env var indirection)
- Add directus.yml schema snapshot and schema.sql from VPS
- Add schema:export and schema:import scripts to package.json
- Ignore .env files (vars set via compose environment)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed ?transform= to ?key= so Directus storage asset presets
(mini, thumbnail, preview, medium, banner) are actually applied.
Previously all images were served at full resolution.
Co-Authored-By: Claude Opus 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>
Remove size="sm" from SharingPopupButton to use default button height (h-9)
instead of small size (h-8), ensuring consistent button heights across all
action buttons on video, model, and article pages.
🤖 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>
- Add GET /sexy/videos/:slug endpoint in bundle that bypasses Directus permissions
- Simplify getVideoBySlug to use the new custom endpoint instead of direct API call
- Fixes "Video not found" error on production for public video pages
- Custom endpoint uses database query like other public endpoints (/sexy/models, etc)
- Ensures videos are accessible to unauthenticated users while respecting upload_date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added missing import of directusApiUrl from directus module to fix
ReferenceError in getVideoBySlug function.
Fixes: 5333bfd (fix: use native fetch for getVideoBySlug)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Convert getVideoBySlug from Directus SDK to native fetch API
- Fixes serialization errors when authenticated users view video pages
- Remove unused readUsers import from @directus/sdk
- Directus SDK returns non-serializable objects with circular refs
- Native fetch returns plain JSON that works with SvelteKit SSR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added null/undefined checks before mapping models array
- Filter out invalid entries in models junction table data
- Default to empty array if models is not an array
- Prevents "Cannot read properties of undefined" errors on video pages
This fix ensures the video detail page works even when model associations
are missing or malformed in the database.
🤖 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>