From ff4be391a9128457985ffca20603423da137f410 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:54:04 +1000 Subject: [PATCH] Add in-app feedback with attachments and renderer diagnostics --- services/supabase/README.md | 9 + .../migrations/202609230001_feedback.sql | 26 +++ src/components/feedback/FeedbackDialog.tsx | 212 ++++++++++++++++++ src/components/video-editor/TutorialHelp.tsx | 104 +-------- .../dashboard/DashboardSidebar.tsx | 2 + .../video-editor/layout/EditorHeader.tsx | 2 + src/lib/feedback/diagnostics.test.ts | 22 ++ src/lib/feedback/diagnostics.ts | 54 +++++ src/lib/feedback/submitFeedback.test.ts | 52 +++++ src/lib/feedback/submitFeedback.ts | 56 +++++ src/main.tsx | 3 + tests/ui/feedback.spec.ts | 40 ++++ 12 files changed, 479 insertions(+), 103 deletions(-) create mode 100644 services/supabase/README.md create mode 100644 services/supabase/migrations/202609230001_feedback.sql create mode 100644 src/components/feedback/FeedbackDialog.tsx create mode 100644 src/lib/feedback/diagnostics.test.ts create mode 100644 src/lib/feedback/diagnostics.ts create mode 100644 src/lib/feedback/submitFeedback.test.ts create mode 100644 src/lib/feedback/submitFeedback.ts create mode 100644 tests/ui/feedback.spec.ts diff --git a/services/supabase/README.md b/services/supabase/README.md new file mode 100644 index 00000000..d5c3d6a9 --- /dev/null +++ b/services/supabase/README.md @@ -0,0 +1,9 @@ +# Desktop feedback + +Apply `migrations/202609230001_feedback.sql` in the Supabase SQL editor for the project configured by `VITE_SUPABASE_URL` before releasing this feature. The desktop client uses its existing signed-in session; never put a service-role key in the app. + +Feedback is stored in `public.feedback_reports`. View reports through the Supabase dashboard and retrieve attachments from the private `feedback-attachments` bucket using each report's attachment paths. There is no email notification or public issue creation. Owners can read their own reports; other users cannot. Staff use server-side service-role access. + +On submission, the modal captures up to 100 recent renderer warnings/errors (2,000 characters each), browser/platform details, and a timestamp. Common secrets, URLs, emails, and local home paths are redacted. Arbitrary log objects are not serialized. Diagnostics are automatically included with each submission. Native-process logs are not collected. Attachments are chosen explicitly, up to five files and 10 MB total in the client, with a 10 MB per-object storage limit. + +A local demo account can preview the form but cannot submit without a real Supabase session. Failed submissions retain the draft in memory; closing and reopening the same modal keeps it, but restarting the app does not. Failed uploads are cleaned up on a best-effort basis; periodically remove unattached objects left by interrupted sessions. diff --git a/services/supabase/migrations/202609230001_feedback.sql b/services/supabase/migrations/202609230001_feedback.sql new file mode 100644 index 00000000..da9434a1 --- /dev/null +++ b/services/supabase/migrations/202609230001_feedback.sql @@ -0,0 +1,26 @@ +-- Apply to the Supabase project used by VITE_SUPABASE_URL before shipping feedback. +create table public.feedback_reports ( + id uuid primary key, + user_id uuid not null references auth.users(id) on delete cascade default auth.uid(), + created_at timestamptz not null default now(), + title text not null check (length(trim(title)) between 1 and 160), + subject text not null check (subject in ('bug', 'idea', 'question', 'other')), + message text not null check (length(trim(message)) between 1 and 10000), + logs text check (octet_length(logs) <= 300000), + attachments jsonb not null default '[]' check (jsonb_typeof(attachments) = 'array' and jsonb_array_length(attachments) <= 5) +); +alter table public.feedback_reports enable row level security; +grant select, insert on public.feedback_reports to authenticated; +create policy "Submit own feedback" on public.feedback_reports for insert to authenticated with check (user_id = auth.uid()); +create policy "Read own feedback" on public.feedback_reports for select to authenticated using (user_id = auth.uid()); +-- Reports are read by staff using service-role access, never publicly. +insert into storage.buckets (id, name, public, file_size_limit) values ('feedback-attachments', 'feedback-attachments', false, 10485760); +create policy "Upload own feedback files" on storage.objects for insert to authenticated +with check (bucket_id = 'feedback-attachments' and (storage.foldername(name))[1] = auth.uid()::text); +create policy "Remove unfinished feedback uploads" on storage.objects for delete to authenticated +using (bucket_id = 'feedback-attachments' and (storage.foldername(name))[1] = auth.uid()::text and not exists ( + select 1 from public.feedback_reports where id::text = (storage.foldername(name))[2] +)); +-- Allow owners to locate their uploads for cleanup; the bucket remains private. +create policy "Read own feedback uploads" on storage.objects for select to authenticated +using (bucket_id = 'feedback-attachments' and (storage.foldername(name))[1] = auth.uid()::text); diff --git a/src/components/feedback/FeedbackDialog.tsx b/src/components/feedback/FeedbackDialog.tsx new file mode 100644 index 00000000..075858d3 --- /dev/null +++ b/src/components/feedback/FeedbackDialog.tsx @@ -0,0 +1,212 @@ +import { useRef, useState } from "react"; +import { Button, Description, Form, Input, Label, TextArea, TextField } from "@heroui/react"; +import { useRecordlyAuth } from "@/components/auth/useRecordlyAuth"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { ChatDots, File, X } from "@/components/ui/icons"; +import { feedbackDiagnostics } from "@/lib/feedback/diagnostics"; +import { submitFeedback, validateAttachments } from "@/lib/feedback/submitFeedback"; + +export function FeedbackDialog({ + className, + showLabel = true, + onSignIn, +}: { + className?: string; + showLabel?: boolean; + onSignIn?: () => void; +}) { + const auth = useRecordlyAuth(); + const [open, setOpen] = useState(false); + const [subject, setSubject] = useState(""); + const [message, setMessage] = useState(""); + const [files, setFiles] = useState([]); + const [error, setError] = useState(""); + const [sending, setSending] = useState(false); + const [sent, setSent] = useState(false); + const sendingRef = useRef(false); + const picker = useRef(null); + function addFiles(incoming: File[]) { + const next = [...files, ...incoming]; + const issue = validateAttachments(next); + if (issue) { + setError(issue); + return; + } + setFiles(next); + setError(""); + } + return ( + { + if (sendingRef.current) return; + setOpen(value); + if (value) setSent(false); + }} + > + + + + + + {sent ? "Thanks for your feedback" : "Send feedback"} + + {sent ? ( + + ) : !auth.user ? ( +
+ Sign in to send feedback. + {onSignIn ? ( + + ) : ( + Open Home to sign in. + )} +
+ ) : ( +
{ + event.preventDefault(); + if (sendingRef.current) return; + if (!subject.trim() || !message.trim()) { + setError("Add a subject and description."); + return; + } + sendingRef.current = true; + setSending(true); + setError(""); + try { + await submitFeedback({ + title: subject, + subject: "other", + message, + files, + logs: feedbackDiagnostics(), + }); + setSent(true); + setSubject(""); + setMessage(""); + setFiles([]); + } catch { + setError("Could not send feedback. Please try again."); + } finally { + sendingRef.current = false; + setSending(false); + } + }} + > + + + + + + +