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
+106
View File
@@ -0,0 +1,106 @@
import { NextRequest, NextResponse } from "next/server";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
const DATA_DIR = "/app/data";
const FEEDBACK_FILE = join(DATA_DIR, "feedback.jsonl");
interface FeedbackEntry {
id: string;
timestamp: string;
url: string;
error: string;
message: string;
userAgent: string;
}
function generateId(): string {
const ts = Date.now().toString(36);
const rand = Math.random().toString(36).substring(2, 8);
return `fb-${ts}-${rand}`;
}
function sanitizeUA(ua: string): string {
// Strip personal info from UA string
return ua.replace(/\(.*?\)/g, "(redacted)").substring(0, 200);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { url, error, message, userAgent } = body;
if (!url || !error) {
return NextResponse.json(
{ error: "url and error are required" },
{ status: 400 }
);
}
const entry: FeedbackEntry = {
id: generateId(),
timestamp: new Date().toISOString(),
url: url.substring(0, 500),
error: error.substring(0, 1000),
message: (message || "").substring(0, 2000),
userAgent: sanitizeUA(userAgent || ""),
};
// Ensure data dir exists
await mkdir(DATA_DIR, { recursive: true });
// Append to JSONL file
await writeFile(FEEDBACK_FILE, JSON.stringify(entry) + "\n", {
flag: "a",
});
return NextResponse.json({ id: entry.id });
} catch (err) {
// Silent failure — don't throw errors from error-reporting endpoint
console.error("Feedback save error:", err);
return NextResponse.json({ id: null }, { status: 200 });
}
}
export async function GET(request: NextRequest) {
// Admin-only endpoint — return paginated feedback
const authHeader = request.headers.get("authorization") || "";
const adminToken = process.env.FEEDBACK_ADMIN_TOKEN || "flh-feedback-admin";
// Simple token auth
if (authHeader !== `Bearer ${adminToken}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 200);
const offset = parseInt(searchParams.get("offset") || "0");
try {
if (!existsSync(FEEDBACK_FILE)) {
return NextResponse.json({ entries: [], total: 0 });
}
const raw = await readFile(FEEDBACK_FILE, "utf-8");
const lines = raw.trim().split("\n").filter(Boolean);
const entries = lines
.map((line) => {
try {
return JSON.parse(line) as FeedbackEntry;
} catch {
return null;
}
})
.filter(Boolean)
.reverse(); // newest first
const total = entries.length;
const page = entries.slice(offset, offset + limit);
return NextResponse.json({ entries: page, total });
} catch (err) {
console.error("Feedback read error:", err);
return NextResponse.json({ entries: [], total: 0 });
}
}