Client-Error-Endpoint POST /api/vitec/client-error (0072-FE / 0077-BE)

Neue PSR-15-Middleware ClientErrorMiddleware: nimmt Browser-Fehler des
React-Frontends als JSON {url, message, stack, release, userAgent}
entgegen und antwortet 204 ohne Body. Schutz: 30 Meldungen pro Minute
und IP (Sliding Window ueber TYPO3-RateLimiter, 429 + Retry-After),
8-KB-Body-Limit (413, Content-Length und tatsaechliche Bytes), kein
JSON-Objekt oder leere message = 400, GET = 405. Keine Session, keine
Cookies; der /_json-Praefix funktioniert ebenfalls.

Speicherung in der neuen Tabelle tx_vitec_clienterrorlog mit
anonymisierter IP (IPv4 letztes Oktett 0, IPv6 /48). Sichtbar als
Tabelle "Client errors (last 24 h)" im Modul VITEC SSR (mit Stack als
Aufklapper) und als Zeile "Client errors reported (24 h / 7 d)" in der
Dashboard-Kachel VITEC: SSR statistics.

CLI vitec:client-error-cleanup [--days=30] loescht Eintraege aelter als
30 Tage (taeglich per Cron).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-10 17:25:23 +01:00
1 parent ff6f86b829
commit 5121eaa000
7 files changed
+360

No files matched your search

@@ -140,4 +140,42 @@
</tbody>
</table>
<h3>Client errors <small>(last 24 h — {clientErrorCount24h} report<f:if condition="{clientErrorCount24h} != 1">s</f:if>, newest 50 shown)</small></h3>
<p class="text-muted" style="font-size:11px">
Browser-side failures reported by the React frontend via <code>POST /api/vitec/client-error</code>
(JS errors, unhandled promise rejections, element error boundaries and "chunk reload" after a deploy).
Limited to 30 reports per minute and IP, 8 KB per report; IPs are stored anonymized.
Kept for 30 days (cron <code>vitec:client-error-cleanup</code>).
</p>
<table class="table table-striped table-sm">
<thead><tr><th>Time</th><th>Release</th><th>Message</th><th>Page</th><th>Browser</th><th>IP</th></tr></thead>
<tbody>
<f:for each="{clientErrors}" as="row">
<tr>
<td style="white-space:nowrap"><f:format.date format="Y-m-d H:i:s">@{row.crdate}</f:format.date></td>
<td><code>{row.release}</code></td>
<td>
<f:if condition="{row.is_chunk_reload}">
<f:then><span class="badge badge-warning">chunk reload</span> </f:then>
<f:else><span class="badge badge-danger">error</span> </f:else>
</f:if>
{row.message}
<f:if condition="{row.stack}">
<details style="margin-top:4px">
<summary style="cursor:pointer" class="text-muted">stack</summary>
<pre style="font-size:11px;white-space:pre-wrap;max-height:240px;overflow:auto">{row.stack}</pre>
</details>
</f:if>
</td>
<td><a href="{row.url}" target="_blank" rel="noopener" title="{row.url}">{row.path}</a></td>
<td title="{row.user_agent}"><small>{row.ua_short}</small></td>
<td><small>{row.ip}</small></td>
</tr>
</f:for>
<f:if condition="!{clientErrors}">
<tr><td colspan="6" class="text-muted">No client errors reported in the last 24 hours.</td></tr>
</f:if>
</tbody>
</table>
</html>