Questify

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 with user_id = auth.uid(), returns 201 (#326).

  • Added GET /api/admin/feedback — admin-only (requires feedback.read privilege) paginated feedback list with submitter profile; supports status, type, page, and limit query params; returns 403 for non-admins (#326).

  • Added PATCH /api/admin/feedback/[id] — admin-only (requires feedback.write privilege) triage endpoint; updates status and/or admin_notes; fires notifyFeedbackResolved() hook when status transitions to resolved or wontfix (#326).

  • Added the user-facing feedback entry point — a floating bottom-right FeedbackButton (server-side auth gate; hidden for unauthenticated visitors) that opens a FeedbackModal with a type radio (bug/suggestion), title (≤100), description (≤1000 with live counter), auto-captured read-only page_url, and optional screenshot upload (jpg/png/webp, ≤5MB) to the feedback-screenshots bucket at {userId}/{uuid}.ext before POST /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 by GET /api/admin/feedback, with status/type filters, status badges, pagination, row links to a feedback detail page, and /admin dashboard 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 to resolved or wontfix; notifications include the submitter greeting, feedback type/title, new status, optional admin notes, and are skipped once notification_sent_at is set (#330).

Security

  • Validated screenshot_path format ({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 new active subscription 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 going incomplete, a retry of an old cancellation — computed free and 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.feedback table (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 the Admin or SuperAdmin role, used by storage policies and other coarse staff checks.

  • Added feedback.read and feedback.write RBAC privileges seeded inline in the migration (and in seed.sql) so existing projects pick up the grants on supabase db push. Granted to SuperAdmin + Admin; feedback.read also granted to Support.

  • Hardened admin feedback updates with column-level UPDATE(status, admin_notes) grants so a client holding feedback.write cannot 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 existing users.read privilege (Support tier and up), so the admin feedback UI can display submitter context.

  • Added feedback.notification_sent_at to track the first resolved/wontfix status email sent to a feedback submitter (#330).

  • Added the private feedback-screenshots Supabase Storage bucket with a 5MB upload limit and image/jpeg | image/png | image/webp MIME 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 /admin redirects 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%) so test:coverage fails if coverage regresses; ratchet upward as coverage improves.

  • Added unit coverage for previously untested lib helpers: haversineDistanceM (great-circle distance), trackEvent/trackModelPersonaSelected analytics dispatch, mapRecapRow row 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.