Commit Graph

27 Commits

Author SHA1 Message Date
722392d19e chore: lint and format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:32:39 +01:00
ea23233645 feat: add BullMQ job queue with admin monitoring UI
All checks were successful
Build and Push Backend Image / build (push) Successful in 48s
Build and Push Buttplug Image / build (push) Successful in 3m26s
Build and Push Frontend Image / build (push) Successful in 1m11s
- Add BullMQ to backend; mail jobs (verification, password reset) now enqueued instead of sent inline
- Mail worker processes jobs with 3-attempt exponential backoff retry
- Admin GraphQL resolvers: adminQueues, adminQueueJobs, adminRetryJob, adminRemoveJob, adminPauseQueue, adminResumeQueue
- Admin frontend page at /admin/queues: queue cards with counts, job table with status filter, retry/remove/pause actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:25:09 +01:00
4d81266cb1 feat: add dedicated model photo separate from avatar
Adds a `photo` field to the users table (and a migration) that serves
as a dedicated profile/card image for models. This is now used in model
cards and on the model single page, while `avatar` is reserved for
comments, article authors, and the user profile page.

- DB: `photo` column on `users` with FK to `files`
- GraphQL: exposed on ModelType, UserType, AdminUserDetailType; photoId arg on adminUpdateUser
- Services: photo field in MODELS_QUERY, MODEL_BY_SLUG_QUERY, ADMIN_GET/UPDATE_USER
- Frontend: model cards and single page use `photo ?? avatar` fallback
- Admin: model photo upload section in user edit page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 10:54:27 +01:00
90497e9e7c fix: resolve TypeScript build errors from leftJoin nullable types
Some checks failed
Build and Push Backend Image / build (push) Successful in 42s
Build and Push Frontend Image / build (push) Has been cancelled
Non-null assert photo/achievement ids that are structurally non-null
due to FK constraints but nullable in Drizzle's leftJoin return type.
Add missing description field to enrichVideo model select and map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 19:33:16 +01:00
e236ced12a refactor: replace all explicit any types with proper TypeScript types
Backend resolvers: typed enrichArticle/enrichVideo/enrichModel with DB
and $inferSelect types, SQL<unknown>[] for conditions arrays, proper
enum casts for status/role fields, $inferInsert for .set() updates,
typed raw SQL result rows in gamification, ReplyLike interface for
ctx.reply in auth. Frontend: typed catch blocks with Error/interface
casts, isActiveLink param, adminGetUser response, tags filter callback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 19:25:04 +01:00
8313664d70 chore: fix all lint errors and format codebase
- Remove unused `or` import in comments resolver
- Remove unused `users` import in recordings resolver
- Add index keys to pagination {#each} loops in videos, models, magazine
- Remove stale svelte-ignore comment in header (a11y warnings no longer fired)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 19:06:57 +01:00
b78831231d fix: select description from users in article enrichArticle query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 19:02:53 +01:00
f90b045ca5 fix: add description to VideoModel type and GraphQL schema
Requesting description on the article author caused a GraphQL error
which the page.server.ts caught as a 404.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 19:01:52 +01:00
56b57486dc fix: add upload/delete file endpoints and wire avatar update through profile
- Add POST /upload and DELETE /assets/:id routes to backend (session auth via session_token cookie)
- Add avatar arg to updateProfile GraphQL mutation and resolver
- Fix frontend to pass avatarId correctly on save, preserve existing avatar when unchanged
- Ignore 404 on file delete (already gone is fine)
- Remove broken folder lookup (getFolders is a stub, backend has no folder concept)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:22:22 +01:00
fde0d63271 feat: remove archived status from recordings, deletions are now immediate
All checks were successful
Build and Push Backend Image / build (push) Successful in 45s
Build and Push Frontend Image / build (push) Successful in 4m3s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:42:13 +01:00
754a236e51 feat: add admin tables for comments and recordings
All checks were successful
Build and Push Backend Image / build (push) Successful in 44s
Build and Push Frontend Image / build (push) Successful in 4m20s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:29:48 +01:00
9ba848372a fix: make gamification calls non-blocking so errors don't fail core mutations
Some checks failed
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Has been cancelled
awardPoints/checkAchievements were awaited inline, so any gamification error
(DB constraint, missing table, etc.) would propagate as INTERNAL_SERVER_ERROR
on comment creation, recording plays, etc. Now they run fire-and-forget with
error logging, so the core action always succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:16:27 +01:00
dcf2fbd3d4 feat: enhance session security and freshness
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Successful in 4m15s
- Sliding expiration: reset 24h TTL on every Redis session access
- SameSite=Strict on login and logout cookies (was Lax)
- Secure flag on logout cookie in production (was missing)
- Re-fetch user from DB on every request in buildContext so role/avatar/
  admin changes take effect immediately without requiring re-login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:10:01 +01:00
bff354094e fix: add adminGetVideo/adminGetArticle queries to fix 404 on edit pages
Some checks failed
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Has been cancelled
The edit page loaders were calling adminListVideos/adminListArticles with the
old pre-pagination signatures and filtering by ID client-side, which broke
after pagination limited results to 50. Now fetches the single item by ID
directly via new adminGetVideo and adminGetArticle backend queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:05:21 +01:00
6f2f3b3529 fix: deduplicate model photos in public resolver to match admin behavior
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Successful in 4m10s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:54:29 +01:00
9c5dba5c90 feat: add server-side pagination, search, and filtering to all collection and admin pages
- Public pages (videos, magazine, models): URL-driven search, sort, category/duration
  filters, and Prev/Next pagination (page size 24)
- Admin tables (videos, articles): search input, toggle filters, and pagination (page size 50)
- Tags page: tag filtering now done server-side via DB arrayContains query instead of
  fetching all items and filtering client-side
- Backend resolvers updated for videos, articles, models with paginated { items, total }
  responses and filter/sort/tag args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:43:26 +01:00
95fd9f48fc refactor: align article author with VideoModel, streamline selects, fix flyout inert
- Remove ArticleAuthor type; article.author now reuses VideoModel (id, artist_name, slug, avatar)
- updateArticle accepts authorId; author selectable in admin article edit page
- Article edit: single Select with bind:value + $derived selectedAuthor display
- Video edit: replace pill toggles with Select type="multiple" bind:value for models
- Video table: replace inline badge spans with Badge component
- Magazine: display artist_name throughout, author bio links to model profile
- Fix flyout aria-hidden warning: replace with inert attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:31:41 +01:00
670c18bcb7 feat: refactor role system to is_admin flag, add Badge component, fix native dialogs
- 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>
2026-03-06 16:14:00 +01:00
9ef490c1e5 fix: make deleteRecording a hard delete instead of soft archive
Previously deleteRecording set status to "archived", leaving the row
in the DB and visible in queries without a status filter. Now it hard-
deletes the row. Also excludes archived recordings from the default
recordings query so any pre-existing archived rows no longer appear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:45:59 +01:00
d021acaf0b feat: add admin user edit page with avatar, banner, and photo gallery
- 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>
2026-03-06 13:18:43 +01:00
ad7ceee5f8 fix: resolve lint errors from ACL/admin implementation
- Remove unused requireOwnerOrAdmin import from videos.ts
- Remove unused requireAuth import from users.ts
- Remove unused GraphQLError import from articles.ts
- Replace URLSearchParams with SvelteURLSearchParams in admin users page
- Apply prettier formatting to all changed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:35:11 +01:00
c1770ab9c9 feat: role-based ACL + admin management UI
Backend:
- Add acl.ts with requireAuth/requireRole/requireOwnerOrAdmin helpers
- Gate premium videos from unauthenticated users in videos query/resolver
- Fix updateVideoPlay to verify ownership before updating
- Add admin mutations: adminListUsers, adminUpdateUser, adminDeleteUser
- Add admin mutations: createVideo, updateVideo, deleteVideo, setVideoModels, adminListVideos
- Add admin mutations: createArticle, updateArticle, deleteArticle, adminListArticles
- Add deleteComment mutation (owner or admin only)
- Add AdminUserListType to GraphQL types
- Fix featured filter on articles query

Frontend:
- Install marked for markdown rendering
- Add /admin/* section with sidebar layout and admin-only guard
- Admin users page: paginated table with search, role filter, inline role change, delete
- Admin videos pages: list, create form, edit form with file upload and model assignment
- Admin articles pages: list, create form, edit form with split-pane markdown editor
- Add admin nav link in header (desktop + mobile) for admin users
- Render article content through marked in magazine detail page
- Add all admin GraphQL service functions to services.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:31:33 +01:00
97269788ee feat: add shared @sexy.pivoine.art/types package and fix type safety across frontend/backend
- 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>
2026-03-05 11:01:11 +01:00
efc7624ba3 style: apply prettier formatting to all files
All checks were successful
Build and Push Backend Image / build (push) Successful in 46s
Build and Push Frontend Image / build (push) Successful in 5m12s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:27:54 +01:00
1e930baccb fix: resolve GraphQL request hang in Fastify integration
All checks were successful
Build and Push Backend Image / build (push) Successful in 39s
Build and Push Frontend Image / build (push) Successful in 4m7s
- 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>
2026-03-04 20:31:18 +01:00
4102f9990c fix: switch backend to CommonJS, generate Drizzle migrations, add migrate script
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 4m23s
- Remove "type": "module" and switch tsconfig to CommonJS/Node resolution
  to fix drizzle-kit ESM/CJS incompatibility
- Strip .js extensions from all backend TypeScript imports
- Fix gamification resolver: combine two .where() calls using and()
- Fix index.ts: wrap top-level awaits in async main(), fix Fastify+yoga
  request handling via handleNodeRequestAndResponse
- Generate initial Drizzle SQL migration (0000_pale_hellion.sql) for all
  15 tables
- Add src/scripts/migrate.ts: programmatic Drizzle migrator for production
- Copy migrations folder into Docker image (Dockerfile.backend)
- Add schema:migrate npm script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:42:58 +01:00
9d7afbe1b5 feat: replace Directus with custom Node.js GraphQL backend
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>
2026-03-04 18:07:18 +01:00