Report-Output: Permalink & PDF

Jeder über POST /api/v1/report erzeugte Report wird persistiert und ist teilbar — als Web-Permalink und als PDF (ADR-0022). Vierfach verfügbar (AI-Parität, ADR-0018): UI, API, MCP, Skill.

Permalink

POST /api/v1/report ergänzt im Ergebnis:

"meta": { "report_id": "…", "permalink": "/r/…" }
  • /r/{id} — öffentliche, gerenderte Ansicht (Marken-Layout, PDF-Button). noindex (kein Suchmaschinen-Index).
  • ID = CSPRNG ≥128 Bit base62 → nicht erratbar (IDOR-Schutz, ADR-0023).
  • Retention 90 Tage (expires_at) für anonyme Reports, danach 404 + Hard-Delete (täglicher Cron). Nur ip_hash gespeichert, kein Klar-IP.
  • Konto-gebundene Reports (eingeloggt erstellt oder per /r/{id}?claim=1 beansprucht, Migration 0021): expires_at = NULLlaufen nicht ab, abrufbar unter „Meine Reports" (/account/reports). Zusätzlich user_id gespeichert; meta.saved=true signalisiert den Zustand an UI/API.
  • Fail-open: scheitert die Persistenz, kommt der Report ohne report_id (Kernfunktion unberührt).

Endpunkte

MethodePfadErgebnis
POST/api/v1/reportReport persistiert + meta.permalink
GET/api/v1/report/{id}gespeichertes REPORT-SPEC-JSON (404 wenn unbekannt/abgelaufen)
GET/api/v1/report/{id}/pdfPDF (application/pdf), eigenes Tageslimit
GET/r/{id}gerenderte Web-Ansicht + PDF-Download

GET /api/v1/report?address=… (Query) bleibt flüchtig (idempotent, schreibt nicht).

PDF

Server-seitig via @react-pdf/renderer (reines JS, kein Headless-Chromium) — robust/kalt-startarm auf Vercel. Bewusst nüchternes Layout; Zahlen 1:1 aus dem gespeicherten Report. Rate-limitiert (eigener pdf:-Bucket).

Verbindlicher Mehrseiten-Viewervertrag (#332)

Der PDF-Output ist erst viewerfest abgenommen, wenn npm run qa:pdf:poppler über scripts/verify-pdf-poppler.ts denselben produktiven renderReportPdf-Pfad für zwei feste, netzunabhängige semantische QA-Profile prüft. Beide Fixtures haben coordinate: null sowie feste Reportzeit- und Quellwerte, damit weder Karten- noch Provider-Netzaufrufe in den Nachweis gelangen:

ProfilVertrag
freeGespeicherter Free-Report mit mindestens zwei physischen Seiten; keine nur für Premium vorgesehenen Inhalte.
premiumGespeicherter Premium-Report mit mindestens zwei physischen Seiten und Premium-Inhalten.

Eine feste Seitenzahl gehört bewusst nicht zum Vertrag: neue Reportbausteine dürfen den Umfang verändern. Die Prüfung muss aber für beide Profile tatsächlich mehrseitig bleiben und jede physische Seite einbeziehen. Das Harness pinnt deshalb die aktuelle semantische Seitenzuordnung fail-closed auf Free 3 und Premium 4: Ändert ein beabsichtigter Layout- oder Inhaltsumbau den Umfang, müssen die seitenspezifischen Body-Marker im selben Diff bewusst nachgezogen werden. Diese Fixture-Map ist eine Regressionserwartung, keine dauerhafte Produktzusage über exakt 3 beziehungsweise 4 Seiten.

Automatisches Poppler-Gate

Für beide Profile gelten in Linux-/CI-Umgebungen dieselben Pflichten:

  1. Das finale PDF wird mit pdftoppm -gray und mit pdftocairo -gray -png vollständig gerendert. Erfolg nur bei Exit-Code 0, identischer Seitenzahl zum PDF und genau einer nichtleeren Rasterdatei pro physischer Seite und Renderer. pdfinfo, pdftotext und pdfinfo -url prüfen Struktur, Text und Linkziele unabhängig vom Bildrendering.
  2. Jeder Seitenhintergrund bleibt deckend weiß. Schwarze oder transparente Flächen im geprüften Außenrand, ein zu hoher Dunkelanteil, schwarze Ganzseiten und das frühere alternierende Schwarzbild sind Fehler. Eine unerwartet inhaltsleere weiße Seite ist ebenfalls ein Fehler; „weißer Hintergrund“ bedeutet nicht „leere Seite“. Zusätzlich muss der um Kopf, Fuß und Seitenrand beschnittene Body-Crop pro Backend mindestens 0,150 % sichtbar dunkle Pixel mit einer Helligkeit von höchstens 220 enthalten.
  3. Die Textschicht bleibt selektier- und extrahierbar. Der extrahierte Text enthält pro physischer Seite mehrere seitenspezifische Body-Marker sowie die lückenlose Folge Seite X / Y. Die Raster- und Textgates wirken gemeinsam: unsichtbare Textobjekte können sichtbaren Body-Inhalt nicht ersetzen.
  4. Mindestens der Report-Permalink bleibt als echte URI-Linkannotation erhalten. Weitere im Fixture enthaltene Quellen- und Lizenzlinks dürfen nicht zu reinem Bildtext degradiert werden.
  5. Die PDF-Metadaten bleiben maschinenlesbar: Titel mit Standort, LandNutzen als Autor, Betreff, Schlüsselwörter, Sprache de-AT, Creator und Producer. Die vom Parser gemeldete Seitenzahl stimmt mit beiden Renderern und den Seitenzahlen im Dokument überein.

Das Gate prüft den fertigen Output nach React-PDF-Layout und PDF-Finalisierung. Ein erfolgreicher Komponenten-Snapshot oder eine Prüfung nur der ersten Seite genügt nicht.

Manueller Viewer-Check

Vor Abschluss einer Änderung am Seiten-Chrome werden Free- und Premium-PDFs mit demselben Renderer und denselben festen Fixture-Eingaben wie im automatischen Gate zusätzlich vollständig in beiden Viewern geprüft. Das Gate selbst hält keine Reportinhalte als Artefakt zurück; die für den manuellen Check neu erzeugten Dateien werden deshalb über ihren SHA-256 eindeutig protokolliert:

  • Chromium mit PDFium: jede Seite sichtbar, weißer Hintergrund, konsistenter Kopf/Fuß, Textauswahl, anklickbare Links und lückenlose Seitenzahlen.
  • Apple Vorschau: dieselben Sichtprüfungen sowie Titel, Autor und Betreff in den Dokumentinformationen.

Das Protokoll nennt Datum, macOS-/Chromium-/Viewer-Version, Profil, Seitenzahl und SHA-256 des geprüften PDFs. Eine Sichtprüfung in nur einem Viewer oder nur eines Profils erfüllt den Vertrag nicht.

Temporäre QA-Artefakte und Abschlussnachweis

Generierte PDFs, Rasterseiten und extrahierte Textdateien sind temporäre QA-Artefakte. Das Harness erstellt dafür ausschließlich ein validiertes, eindeutiges mkdtemp-Verzeichnis, räumt es auch im Fehlerfall im finally auf und verwendet für die Rasteranalyse das gemeinsame Modul scripts/lib/pdf-poppler-regression.mjs. Die Artefakte werden nicht eingecheckt und enthalten keine echten Kunden- oder Kontodaten. CI installiert poppler-utils und führt denselben npm run qa:pdf:poppler-Befehl aus; es veröffentlicht die temporären Reportinhalte nicht als Job-Artefakt.

Die technische Abnahmematrix für Issue #332 ist auf Produktionscommit 734f22f3b17de1593da72ef997f7f1eac5e38e50 erfüllt. Free und Premium bestanden beide Poppler-Renderer in PR- und Main-CI; PDFium, Chromium und Apple Vorschau wurden über alle jeweiligen Seiten geprüft. Der zwölfseitige Live-Readback bestand Text-, Link-, Metadaten-, Seitenzahl- und Rasterprüfung auf demselben Commit. Evidenz-PR #531 bindet die Issue-Schließung mit Closes #332 atomar an seinen Merge und bestätigt das ergänzte Body-Ink-/Semantikgate im separaten Poppler-CI-Lauf 32944989218. Versionen, SHA-256-Werte der konkret geprüften Dateien, Seitenzahlen, Deployment und Datenschutzgrenze stehen im Produktionsnachweis.

PDF/UA-1-Machbarkeitsgate (#333, Slice A)

Der Viewer-Vertrag aus #332 belegt Darstellung, Textschicht, Links, Metadaten und Seitenzahlen, aber noch keinen semantischen Tag-Baum und keine PDF/UA-Konformität. Issue #333 bleibt deshalb getrennt offen. ADR-0059 definiert als ersten Slice ausschließlich ein synthetisches, nicht produktives PDF/UA-1-Machbarkeits- und Vertragsprüfgate:

  1. Eine feste, per SHA-256 gebundene und ohne ausführbare Inhalte netzwerkfreie HTML-Fixture mit erfundenen Daten deckt Überschriften, Absätze, Liste, Tabelle, Link, informative Abbildung mit Alternativtext sowie dekorative und wiederkehrende Artefakte ab.
  2. Ein Chromium-basierter Kandidatenpfad muss daraus einen semantisch getaggten PDF-1.7-Output mit Sprache, MarkInfo/Marked, PDF/UA-1-XMP, Dokumenttitel und -metadaten, Outline, Strukturbaum in der festgelegten Fixture-Lesereihenfolge, seitenweise gebundener physischer Textschicht, exaktem Linkziel und genau einer informativen Figure mit Alternativtext erzeugen. Ein gepinnter veraPDF-Lauf muss ohne fehlgeschlagene PDF/UA-1-Regel enden; die Textmarkerprüfung behauptet keine Zuordnung einzelner Texte zu konkreten MCIDs.
  3. Eine semantisch entsprechende negative React-PDF-Gegenprobe muss am selben Prüfgate scheitern. Ein Prüfgate, das Kandidat und ungetaggten Bestandsoutput gleichermaßen akzeptiert, ist nicht trennscharf und damit rot.

Der lokale Validatornachweis dieses Slices ist ausgeführt: Der Kandidat bestand 106 Regeln und 4.679 Checks, die React-PDF-Gegenprobe scheiterte erwartungsgemäß an 9 Regeln und 189 Checks. Linux-CI-Lauf 32951907614 bestätigte denselben Vertrag mit 106 Regeln und 4.645 Checks sowie einer trennscharfen Gegenprobe; die Run-Artefaktliste blieb leer. Slice A ist damit auf dem Harness-Commit dbd46f3c3a94 commit- und CI-gebunden abgeschlossen. Der vollständige Lauf steht im Machbarkeitsnachweis. Der Produktionsrenderer bleibt unverändert @react-pdf/renderer; es gibt noch keinen produktiven PDF/UA-Report und keine Screenreader-Abnahme. Ein späterer Rendererwechsel benötigt getrennte Free-/Standard-/Premium-/Planer-Integration, Runtime- und Viewergates, kontrolliertes Rollout, Live-Readback und eine manuelle Screenreader-Prüfung.

Zugänge (Parität)

  • UI: /report → nach Erzeugung „Permalink" + „PDF herunterladen".
  • API: siehe Tabelle, OpenAPI unter /docs/api.
  • MCP: Tool export_report{ permalink, pdf_url }.
  • Skill: report.mjs gibt am Ende Permalink + PDF-URL aus.

Grenzen

„Wer den Link hat, sieht den Report" (Teilen-Use-Case) — Schutz = unerratbare ID + noindex + 90-Tage-Retention. Privatere Modi (Auth/Ablauf-Token) sind als Folge möglich.