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). Nurip_hashgespeichert, kein Klar-IP. - Konto-gebundene Reports (eingeloggt erstellt oder per
/r/{id}?claim=1beansprucht, Migration 0021):expires_at = NULL→ laufen nicht ab, abrufbar unter „Meine Reports" (/account/reports). Zusätzlichuser_idgespeichert;meta.saved=truesignalisiert den Zustand an UI/API. - Fail-open: scheitert die Persistenz, kommt der Report ohne
report_id(Kernfunktion unberührt).
Endpunkte
| Methode | Pfad | Ergebnis |
|---|---|---|
POST | /api/v1/report | Report persistiert + meta.permalink |
GET | /api/v1/report/{id} | gespeichertes REPORT-SPEC-JSON (404 wenn unbekannt/abgelaufen) |
GET | /api/v1/report/{id}/pdf | PDF (application/pdf), eigenes Tageslimit |
GET | /r/{id} | gerenderte Web-Ansicht + PDF-Download |
GET /api/v1/report?address=… (Query) bleibt flüchtig
(idempotent, schreibt nicht).
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:
| Profil | Vertrag |
|---|---|
free | Gespeicherter Free-Report mit mindestens zwei physischen Seiten; keine nur für Premium vorgesehenen Inhalte. |
premium | Gespeicherter 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:
- Das finale PDF wird mit
pdftoppm -grayund mitpdftocairo -gray -pngvollständig gerendert. Erfolg nur bei Exit-Code 0, identischer Seitenzahl zum PDF und genau einer nichtleeren Rasterdatei pro physischer Seite und Renderer.pdfinfo,pdftotextundpdfinfo -urlprüfen Struktur, Text und Linkziele unabhängig vom Bildrendering. - 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. - 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. - 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.
- Die PDF-Metadaten bleiben maschinenlesbar: Titel mit Standort,
LandNutzenals Autor, Betreff, Schlüsselwörter, Sprachede-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:
- 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.
- 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. - 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.mjsgibt 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.