- 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>
Files are stored as <UPLOAD_DIR>/<id>/<filename>. The previous static
serving attempted to serve <UPLOAD_DIR>/<id> (a directory) which failed.
Custom /assets/:id route now looks up filename from DB and uses sendFile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pass FastifyRequest/FastifyReply directly to yoga.handleNodeRequestAndResponse
per the official graphql-yoga Fastify integration docs. Yoga v5 uses req.body
(already parsed by Fastify) when available, avoiding the dead raw stream issue.
- Add proper TypeScript generics for server context including db and redis
- Wrap sendVerification/sendPasswordReset in try/catch so missing SMTP
does not crash register/requestPasswordReset mutations
- Fix migrate.ts path resolution to work with both tsx (src/) and compiled (dist/)
- Expose postgres:5432 and redis:6379 ports in compose.yml for local dev
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
graphql-yoga's handleNodeRequestAndResponse returns a Response with a
Web API ReadableStream body. Fastify's reply.send() requires a Node.js
Readable stream, causing all GraphQL requests to hang indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nanoid v5 is ESM-only and cannot be require()'d in a CommonJS module.
v3 is the last version with native CJS support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Run pnpm install to update lockfile with packages/backend dependencies
- Add argon2 to root onlyBuiltDependencies (pnpm-workspace.yaml + package.json)
- Add explicit `pnpm rebuild argon2` in Dockerfile.backend to ensure native
bindings compile regardless of pnpm v10 build approval state
- Remove pnpm.onlyBuiltDependencies from packages/backend/package.json
(ineffective in workspace packages, warned by pnpm)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All custom logic (endpoints, hooks, gamification) has been ported to
packages/backend. The Directus bundle is no longer needed.
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>
Unwrap DeviceList wrapper message before passing to parseDeviceList(),
and rename FeatureDescriptor to FeatureDescription to match Rust v10 serde output.
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>
The likes_count field on videos was becoming inaccurate due to the manually
maintained counter getting out of sync with actual like records. Now we count
likes directly from the sexy_video_likes table for accurate counts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed from sexy_model_photos to junction_directus_users_files
which is the actual table Directus uses to store the many-to-many
relationship between users and their photo files.
🤖 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>
Change GET /sexy/models/:slug endpoint to query u.slug directly
instead of concatenating LOWER(first_name || ' ' || last_name).
This matches the actual slug field in the directus_users table.
🤖 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.