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

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
#[AsCommand(
name: 'vitec:client-error-cleanup',
description: 'Delete client error reports (tx_vitec_clienterrorlog) older than 30 days; run daily via cron',
)]
final class ClientErrorCleanupCommand extends Command
{
private const TABLE = 'tx_vitec_clienterrorlog';
private const DEFAULT_DAYS = 30;
public function __construct(private readonly ConnectionPool $connectionPool)
{
parent::__construct();
}
protected function configure(): void
{
$this->addOption('days', 'd', InputOption::VALUE_REQUIRED, 'Keep entries younger than this many days', (string)self::DEFAULT_DAYS);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$days = (int)$input->getOption('days');
if ($days < 1) {
$output->writeln('<error>--days must be at least 1</error>');
return Command::INVALID;
}
$cutoff = time() - $days * 86400;
$conn = $this->connectionPool->getConnectionForTable(self::TABLE);
$deleted = $conn->executeStatement('DELETE FROM ' . self::TABLE . ' WHERE crdate < ?', [$cutoff]);
$remaining = (int)$conn->executeQuery('SELECT COUNT(*) FROM ' . self::TABLE)->fetchOne();
$output->writeln(sprintf(
'deleted %d client error report(s) older than %d days (before %s), %d remaining',
$deleted,
$days,
date('Y-m-d H:i', $cutoff),
$remaining
));
return Command::SUCCESS;
}
}