Show a maintenance page instead of a blank 500 and update in one click

Emergency layer logs fatal errors to var/log/notfall.log and answers 503.
The system page checks for a new release on load, shows its notes, runs
backup, download, switch, migration and health check in one flow with live
progress, and switches back automatically when the new version fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-09 18:33:53 +02:00
1 parent 339a48c810
commit a6a334fe68
15 files changed
+973 -42

No files matched your search

@@ -0,0 +1,50 @@
<?php
namespace App\EventSubscriber;
use App\Ops\Notfall;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\LogoutException;
/**
* Jeder Serverfehler, den das Framework abfängt, steht auch in var/log/notfall.log (ADR 113).
*
* Ohne das gab es auf dem Hosting kein eigenes Protokoll: Symfony schreibt nach stderr, und das
* landet - wenn überhaupt - im Fehlerlog des Hostings, das ein Betreiber erst suchen muss. 404 und
* andere gewollte Antworten gehören nicht hinein. Die Antwort selbst wird zur Wartungsseite.
*/
final class ErrorLogSubscriber implements EventSubscriberInterface
{
public function __construct(private bool $debug = false)
{
}
public static function getSubscribedEvents(): array
{
// Nach der Sicherheitsschicht (Priorität 1): „Zugriff verweigert“ ist dort eine
// Weiterleitung zur Anmeldung, kein Notfall. Vor der Fehlerseite des Frameworks (-128),
// damit der Eintrag auch dann entsteht, wenn sie selbst scheitert.
return [KernelEvents::EXCEPTION => ['onException', -64]];
}
public function onException(ExceptionEvent $event): void
{
$fehler = $event->getThrowable();
if ($event->hasResponse() || $fehler instanceof AuthenticationException || $fehler instanceof AccessDeniedException || $fehler instanceof LogoutException
|| ($fehler instanceof HttpExceptionInterface && $fehler->getStatusCode() < 500)) {
return;
}
Notfall::protokollieren($fehler, $event->getRequest()->getMethod().' '.$event->getRequest()->getRequestUri());
// Besucher sehen statt einer englischen „Oops“-Seite die Wartungsseite mit 503: für
// Suchmaschinen „gleich wieder da“. Im Entwicklungsmodus bleibt die ausführliche Seite.
if (!$this->debug) {
$event->setResponse(new Response(Notfall::html(), Response::HTTP_SERVICE_UNAVAILABLE, Notfall::KOPFZEILEN));
}
}
}