2.3.0
Released
Features
Added
POST /api/feedback— authenticated users submit bug reports or suggestions; validates body with Zod (type,title,description,page_url?,screenshot_path?), inserts withuser_id = auth.uid(), returns 201 (#326).Added
GET /api/admin/feedback— admin-only (requiresfeedback.readprivilege) paginated feedback list with submitter profile; supportsstatus,type,page, andlimitquery params; returns 403 for non-admins (#326).Added
PATCH /api/admin/feedback/[id]— admin-only (requiresfeedback.writeprivilege) triage endpoint; updatesstatusand/oradmin_notes; firesnotifyFeedbackResolved()hook when status transitions toresolvedorwontfix(#326).Added the user-facing feedback entry point — a floating bottom-right
FeedbackButton(server-side auth gate; hidden for unauthenticated visitors) that opens aFeedbackModalwith a type radio (bug/suggestion), title (≤100), description (≤1000 with live counter), auto-captured read-onlypage_url, and optional screenshot upload (jpg/png/webp, ≤5MB) to thefeedback-screenshotsbucket at{userId}/{uuid}.extbeforePOST /api/feedback. Mounted in the root layout so it appears on every authenticated page; success shows a transient toast, network/validation errors keep the modal open with inline messages mapped onto the relevant fields (#327).Added
/admin/feedback, a 25-per-page admin feedback table backed byGET /api/admin/feedback, with status/type filters, status badges, pagination, row links to a feedback detail page, and/admindashboard feedback summary cards (#328).Added
/admin/feedback/[id], an admin feedback detail view with submitter email, clickable source URL, private screenshot rendering via signed Supabase Storage URLs, editable status/admin notes, optimistic status display, and save confirmation toast (#329).Added Resend-backed feedback status emails from
PATCH /api/admin/feedback/[id]when feedback transitions toresolvedorwontfix; notifications include the submitter greeting, feedback type/title, new status, optional admin notes, and are skipped oncenotification_sent_atis set (#330).
Security
Validated
screenshot_pathformat ({user_id}/{uuid}.{ext}) and enforced that the path prefix matches the authenticated user's ID, preventing a caller from referencing another user's storage object in a feedback submission (#326).
Technical changes· 14 entries for database, tooling, CI, tests and docs
Database
apply_subscription_state()now records the Stripe event id itself, in the same transaction as the write it guards, and returns false when that id was already recorded. Recording it from a separate statement — as the webhook did — left a window in which a process killed between the record and the write leaves a record with no work behind it, and every Stripe retry then reads as a duplicate: a subscription that is paid for, never granted, and reports no error anywhere. A crash now rolls back both (#486).apply_subscription_state()takes a row lock on the owner's profile before it writes. Two events for different subscriptions of the same account each derived the tier from a snapshot that could not see the other's uncommitted rows, so whichever committed last won — a newactivesubscription racing an old one's cancellation could leave the account on Free while Stripe charged for Pro (#486).Added
profiles.plan_tier_source, recording whether the current tier was granted administratively or by billing, and taught reconciliation to revoke only what billing granted. The billing panel already surfaced accounts "granted Pro directly rather than through a subscription", and without an owner recorded on the tier the next lifecycle event for such an account — an abandoned checkout goingincomplete, a retry of an old cancellation — computedfreeand silently revoked the comp.set_user_plan_tier()marks the tier administratively owned, and the existing self-service guard now covers the new column, so an account holder cannot re-label a billing grant as permanent (#486).Added the
public.feedbacktable (bug + suggestion reports) with RLS that keeps reports owner-private and lets admins read all and update triage fields (#325).Added a
public.is_admin()SECURITY DEFINER helper that returns TRUE for users holding theAdminorSuperAdminrole, used by storage policies and other coarse staff checks.Added
feedback.readandfeedback.writeRBAC privileges seeded inline in the migration (and inseed.sql) so existing projects pick up the grants onsupabase db push. Granted toSuperAdmin+Admin;feedback.readalso granted toSupport.Hardened admin feedback updates with column-level
UPDATE(status, admin_notes)grants so a client holdingfeedback.writecannot rewrite submitter-owned fields (user_id,type,title,description,page_url,screenshot_path).Added a "Staff can read all profiles" RLS policy on
public.profiles, gated on the existingusers.readprivilege (Support tier and up), so the admin feedback UI can display submitter context.Added
feedback.notification_sent_atto track the first resolved/wontfix status email sent to a feedback submitter (#330).Added the private
feedback-screenshotsSupabase Storage bucket with a 5MB upload limit andimage/jpeg | image/png | image/webpMIME allow-list (#325).Added storage object policies so users can upload to and read from their own
{user_id}/prefix, and admins (is_admin()) can read all objects in the bucket.
Tests
Added feedback flow unit and Playwright coverage for submission payloads, admin feedback pagination/filter queries,
is_admin()checks, status-transition notification gating, authenticated submission, admin triage, and non-admin/adminredirects for issue #331.Added v8 coverage thresholds to
apps/web/vitest.config.ts, pinned to the current full-suite totals (statements/lines 20.65%, branches 67.03%, functions 41.95%) sotest:coveragefails if coverage regresses; ratchet upward as coverage improves.Added unit coverage for previously untested
libhelpers:haversineDistanceM(great-circle distance),trackEvent/trackModelPersonaSelectedanalytics dispatch,mapRecapRowrow mapping, the achievement definitions registry (ACHIEVEMENT_BY_KEY,isAchievementKey), AI prompt builders (buildQuestNarrativePrompt,buildTaskInvestigationPrompt,uniqueImageUrls), and the model-persona mappers/queries (mapModelPersona,listActiveModelPersonas,getActiveModelPersona,getActiveModelPersonaById,listActiveModelPersonasByIds) with mocked Supabase responses.