- Move @import "@maizzle/tailwindcss" + @theme tokens to packages/email/email.css
- Layout uses <link rel="stylesheet" href="{{ cssPath }}" inline> — Maizzle's
expandLinkTag reads the absolute path and expands it to a <style> tag, which
the second compileCss pass then processes with @tailwindcss/postcss + LightningCSS
- render.ts passes cssPath as a local so the expression resolves inside the layout
- Layout head is now clean HTML with no inline style logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- @theme now mirrors all :root variables from app.css (background, foreground,
card, muted, muted-foreground, border, primary, primary-foreground)
- Replaced all zinc-* utilities with semantic token classes (bg-background,
bg-card, bg-muted, text-foreground, text-muted-foreground, border-border, etc.)
- Added Noto Sans via Google Fonts import (progressive enhancement — skips
Tailwind processing via `plain` attribute)
- Font family @theme token set to Noto Sans with system-font fallbacks
- Button inline styles updated to use hex equivalent of --primary-foreground
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New @sexy.pivoine.art/email package with @maizzle/framework@6.0.0-15
- Uses @maizzle/tailwindcss (TW v4 preset) with @theme brand tokens
derived from the frontend's app.css oklch primary color
- LightningCSS automatically lowers oklch/lab to hex for email clients
- Real HTML template files (templates/layouts/main.html, verification.html,
password-reset.html) — not JS template strings
- PostCSS `from` override so @import "@maizzle/tailwindcss" resolves from
the email package's own node_modules
- Backend lib/email.ts now calls renderVerification/renderPasswordReset
instead of inline HTML strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add UnauthorizedError class exported from services.ts
- loggedApiCall now detects Unauthorized GraphQL errors, logs at DEBUG
instead of ERROR, and throws UnauthorizedError (no more stack dumps)
- hooks.server.ts catches UnauthorizedError from any load function and
redirects to /login?redirect=<original-path>
- getRecordings, getRecording, getAnalytics now accept an optional token
and use getAuthClient server-side so cross-origin cookie forwarding works
- Update play/recordings, play/buttplug, me/analytics page.server.ts to
pass the session token — prevents Unauthorized on auth-protected pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker/metadata-action uses github.* context vars which are empty in
Gitea Actions. Explicitly set org.opencontainers.image.source using
gitea.server_url and gitea.repository so the container registry links
each image back to this repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use CardHeader + CardTitle instead of an h3 inside CardContent,
so both cards get the same pt-0 treatment on CardContent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- px-2 py-2 / gap-2 on mobile (was p-4 / gap-4)
- Rank badge w-8 on mobile (was w-14), font scaled down
- Avatar h-9 w-9 on mobile (was h-12 w-12)
- Score text-lg on mobile (was text-2xl), "points" label hidden on mobile
- Stats always visible, icons/gaps scaled down for mobile
- Arrow indicator hidden on mobile (hover-only, useless on touch)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add overflow-hidden to outer wrapper so the absolutely-positioned
SexyBackground is clipped, matching how public pages handle it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New lib/components/pagination/pagination.svelte with numbered pages,
ellipsis for large ranges, and prev/next buttons
- All 6 admin pages (users, articles, videos, recordings, comments,
queues) now show enumerated page numbers next to the "Showing X–Y of Z"
label; offset is derived from page number * limit
- Public pages (videos, models, magazine) replace their inline
totalPages/pageNumbers derived state with the shared component
- Removes ~80 lines of duplicated pagination logic across 9 files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move queue, status, and offset to URL search params (?queue=&status=&offset=)
- Load jobs server-side in +page.server.ts with auth token (matches other admin pages)
- Derive total from adminQueues counts (waiting+active+completed+failed+delayed)
so pagination knows total without an extra query
- Add fetchFn/token params to getAdminQueueJobs for server-side use
- Retry/remove/pause/resume actions now use invalidateAll() instead of local state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- revokePoints now accepts optional recordingId; when absent it deletes
one matching row (for actions like COMMENT_CREATE that have no recording)
- deleteComment queues revokePoints + checkAchievements so leaderboard
and social achievements stay in sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deleteRecording now queues revokePoints for RECORDING_CREATE (and
RECORDING_FEATURED if applicable) before deleting a published recording,
so leaderboard points are correctly removed
- Fix comment stat/achievement queries using collection "recordings" instead
of "videos" — comments are stored under collection "videos", so the count
was always 0, breaking COMMENT_CREATE stats and social achievements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Once an achievement is unlocked, preserve date_unlocked permanently
instead of clearing it to null when the user drops below the threshold
(e.g. on unpublish). This prevents the wasUnlocked check from returning
false on republish, which was causing achievement points to be re-awarded
on every publish/unpublish cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostgreSQL cannot resolve the type of a parameterized $1 = 0.005 in
-$1 * EXTRACT(EPOCH ...) and fails with an operator type error. Using
sql.raw() embeds the constant directly in the query string so userId
is the only parameter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Passing a JS Date to a Drizzle sql template serializes it as a locale
string (e.g. "Mon Mar 09 2026 19:51:22 GMT+0100") which PostgreSQL
cannot parse as timestamptz, causing the gamification worker to fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add migration 0004: partial unique index on user_points (user_id, action, recording_id)
for RECORDING_CREATE and RECORDING_FEATURED to prevent earn-on-republish farming
- Add revokePoints() to gamification lib; awardPoints() now uses onConflictDoNothing
- Add gamificationQueue (BullMQ) with 3-attempt exponential backoff
- Add gamification worker handling awardPoints, revokePoints, checkAchievements jobs
- Move all inline gamification calls in recordings + comments resolvers to queue
- Revoke RECORDING_CREATE points when a recording is unpublished (published → draft)
- Register gamification worker at server startup alongside mail worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace custom inline span+getStatusColor with Badge component in recording card
- Align admin recordings table badge to same style (outline, green/yellow)
- Use i18n label in admin table instead of raw status string
- Remove unused cn import and getStatusColor helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove back button from admin entity edit pages (sidebar handles navigation)
- Remove cancel button from video/article forms, make submit button full-width
- Show actual entity title + subtitle on video/article edit pages
- Remove asterisks from Title/Slug field labels in i18n
- Remove px-3 sm:px-0 from all admin list page headers/filters (fixes mobile padding)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace ← text with icon-[ri--arrow-left-line] in admin and me layouts
- Add avatar + admin shield badge to admin sidebar header
- Wrap all admin edit forms in Card (bg-card/50 border-primary/20) with styled inputs
- Fix sm:pl-6 → lg:pl-6 so extra left padding only applies when sidebar is visible
- Update security form submit button to gradient style matching profile
- Remove "View Public Profile" button from me/profile
- Use shadcn-svelte Empty component for recordings empty state
- Install empty component via shadcn-svelte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>