feat(quick-260914-m97): Fehler-melden-Knopf in der Kopfzeile — Bildschirmfoto vor dem Dialog (html-to-image 1.11.13), Fehlerpuffer, Dialog mit Vorschau, i18n bugReport

- Knopf (Kaefer-Symbol) unmittelbar vor dem Erscheinungsbild-Schalter; captureScreenshot() laeuft VOR dem Oeffnen, Test pinnt es im toPng-Mock
- computeCaptureSize (laengste Kante 1600 px, reine Funktion, getestet), dataUrlToBlob, sendBugReport als Multipart mit credentials und ohne Content-Type-Header
- error-buffer: Ringpuffer 20, window error/unhandledrejection, console.error (Original bleibt), fetch-Wrapper nur bei !ok ohne Anfrage-Rumpf/Suchteil/Kopfzeilen (T-M97-02), idempotent, SSR-sicher; installiert in app-shell
- Dialog mit Vorschau, Haekchen, Beschreibung, Meldungen je Status 409/413/429/502/allgemein, Admin-Link auf /admin/smtp
- i18n bugReport de/en, Allowlist um "passiert"; html-to-image exakt 1.11.13, Lockfile aktualisiert

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 16:50:01 +02:00
parent 54121c1721
commit 60b0ee8409
13 changed files with 968 additions and 0 deletions
@@ -0,0 +1,72 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { captureScreenshot } from '@/lib/bug-report-api';
import { useAuthStore } from '@/lib/stores/auth-store';
import { BugReportDialog } from './bug-report-dialog';
/**
* Fehler-melden-Knopf in der Kopfzeile (quick-260914-m97), Stil wie
* `ThemeToggle`. Kernanforderung: das Bild der Seite wird aufgenommen,
* BEVOR der Dialog erscheint — sonst waere der Dialog im Bild.
*/
export function BugReportButton() {
const t = useTranslations('bugReport');
const user = useAuthStore((s) => s.user);
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
const [capturing, setCapturing] = useState(false);
const [open, setOpen] = useState(false);
const [screenshot, setScreenshot] = useState<string | null>(null);
const handleClick = async () => {
if (capturing) return;
setCapturing(true);
// Reihenfolge ist die Kernanforderung: erst aufnehmen, dann oeffnen.
const shot = await captureScreenshot();
setScreenshot(shot);
setOpen(true);
setCapturing(false);
};
return (
<>
<button
type="button"
onClick={handleClick}
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors disabled:opacity-50"
aria-label={t('button')}
title={t('button')}
disabled={capturing}
data-bug-report-ignore="true"
>
{/* Kaefer-Symbol nach dem lucide-Symbol "bug" */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m8 2 1.88 1.88" />
<path d="M14.12 3.88 16 2" />
<path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" />
<path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" />
<path d="M12 20v-9" />
<path d="M6.53 9C4.6 8.8 3 7.1 3 5" />
<path d="M6 13H2" />
<path d="M3 21c0-2.1 1.7-3.9 3.8-4" />
<path d="M20.97 5c0 2.1-1.6 3.8-3.5 4" />
<path d="M22 13h-4" />
<path d="M17.2 17c2.1.1 3.8 1.9 3.8 4" />
</svg>
</button>
<BugReportDialog open={open} screenshot={screenshot} isAdmin={isAdmin} onClose={() => setOpen(false)} />
</>
);
}