Error Feedback Service + friendly error handling across all pages

- Error Feedback Service: POST /api/feedback saves to JSONL, GET with admin token
- ErrorFeedback component: warm amber tones, friendly messages, 'Report Issue' button
- Replaced ALL red-themed error displays across 15+ pages with ErrorFeedback
- Kind classification: network, auth, location, upgrade, default messages
- QA test suite v2: 8-layer testing (system, API, render, scenario, edge, mobile, perf, security)
- Browser-based visual QA with Playwright (Layer 9)
This commit is contained in:
root
2026-06-18 16:24:47 +02:00
parent 28be776c84
commit 14d7127e41
19 changed files with 1913 additions and 165 deletions
+175
View File
@@ -0,0 +1,175 @@
"use client";
import { useState, useCallback } from "react";
import { AlertTriangle, Send, X, Bug } from "lucide-react";
// ── Feedback submission API ──
async function submitFeedback(data: {
url: string;
error: string;
message?: string;
userAgent: string;
}) {
try {
const res = await fetch("/mobile/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.ok;
} catch {
return false; // silently fail — we don't want a feedback-on-feedback loop
}
}
// ── Friendly error messages map ──
const FRIENDLY_MESSAGES: Record<string, { title: string; description: string }> = {
default: {
title: "A small glitch 🛠️",
description:
"Something didn't load quite right. Don't worry — we're on it! Try again or drop us a report so we can fix it faster.",
},
location: {
title: "Location not available 📡",
description:
"We couldn't detect your location. Make sure location services are enabled, or try refreshing. The compass will work even without location!",
},
network: {
title: "Connection hiccup 🌐",
description:
"Looks like the signal dropped for a moment. Check your connection and try again. If it persists, let us know!",
},
auth: {
title: "Session expired 🔑",
description:
"Your session ran out — it happens! Just sign in again and you'll be right back where you were.",
},
upgrade: {
title: "Premium feature ✨",
description:
"This feature needs an upgrade. Check your plan details to unlock it!",
},
};
type ErrorKind = keyof typeof FRIENDLY_MESSAGES;
interface ErrorFeedbackProps {
/** The raw error message from the system */
error: string;
/** Category of error for friendly message mapping */
kind?: ErrorKind;
/** Callback to retry the action */
onRetry?: () => void;
/** Additional context for debugging */
context?: string;
}
export default function ErrorFeedback({
error,
kind = "default",
onRetry,
context,
}: ErrorFeedbackProps) {
const [showForm, setShowForm] = useState(false);
const [userMessage, setUserMessage] = useState("");
const [sent, setSent] = useState(false);
const [sending, setSending] = useState(false);
const friendly = FRIENDLY_MESSAGES[kind] || FRIENDLY_MESSAGES.default;
const handleSend = useCallback(async () => {
setSending(true);
const ok = await submitFeedback({
url: window.location.href,
error: error + (context ? ` [${context}]` : ""),
message: userMessage,
userAgent: navigator.userAgent,
});
setSent(true);
setSending(false);
if (ok) {
setTimeout(() => setShowForm(false), 2000);
}
}, [error, context, userMessage]);
return (
<div className="mx-4 mb-4">
<div className="bg-amber-900/10 border border-amber-700/30 rounded-2xl p-5">
{/* ── Friendly header ── */}
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-900/20 flex items-center justify-center shrink-0 mt-0.5">
<AlertTriangle size={20} className="text-amber-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-amber-200 mb-1">
{friendly.title}
</h3>
<p className="text-xs text-amber-300/70 leading-relaxed">
{friendly.description}
</p>
</div>
</div>
{/* ── Actions ── */}
<div className="flex flex-wrap gap-2 mt-4">
{onRetry && (
<button
onClick={onRetry}
className="flex-1 min-w-[80px] px-4 py-3 bg-amber-900/20 hover:bg-amber-900/30 border border-amber-700/30 text-amber-300 text-sm font-medium rounded-xl active:scale-95 transition min-h-[44px]"
>
Try Again
</button>
)}
<button
onClick={() => setShowForm((s) => !s)}
className="flex-1 min-w-[80px] px-4 py-3 bg-gray-800/60 hover:bg-gray-700/60 border border-gray-700/40 text-gray-300 text-sm rounded-xl active:scale-95 transition min-h-[44px] flex items-center justify-center gap-1.5"
>
<Bug size={14} />
Report Issue
</button>
</div>
{/* ── Feedback form (expandable) ── */}
{showForm && (
<div className="mt-4 pt-4 border-t border-amber-800/20">
{sent ? (
<p className="text-xs text-emerald-400 text-center">
Thanks! Your report helps us improve.
</p>
) : (
<>
<p className="text-xs text-amber-300/60 mb-2">
What happened? (optional helps us fix it faster)
</p>
<textarea
value={userMessage}
onChange={(e) => setUserMessage(e.target.value)}
placeholder="e.g. The page was blank, then this showed up..."
rows={2}
className="w-full bg-gray-900/60 border border-gray-700/40 rounded-xl px-3 py-2 text-xs text-gray-300 placeholder:text-gray-600 resize-none focus:outline-none focus:border-amber-700/50"
/>
<div className="flex gap-2 mt-2">
<button
onClick={handleSend}
disabled={sending}
className="flex-1 px-4 py-2.5 bg-amber-600/20 hover:bg-amber-600/30 border border-amber-600/30 text-amber-300 text-xs font-medium rounded-xl active:scale-95 transition min-h-[40px] disabled:opacity-50 flex items-center justify-center gap-1.5"
>
<Send size={12} />
{sending ? "Sending..." : "Send Report"}
</button>
<button
onClick={() => setShowForm(false)}
className="px-3 py-2.5 bg-gray-800/40 border border-gray-700/30 text-gray-500 text-xs rounded-xl active:scale-95 transition min-h-[40px]"
>
<X size={14} />
</button>
</div>
</>
)}
</div>
)}
</div>
</div>
);
}