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:
1 parent
339a48c810
commit
a6a334fe68
15 files changed
+973
-42
No files matched your search
@@ -3,6 +3,8 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Command\SiteOriginCommand;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Domain\InvalidPage;
|
||||
use App\Ops\ArchiveUpdate;
|
||||
use App\Ops\Backup;
|
||||
@@ -11,8 +13,10 @@ use App\Ops\Installation;
|
||||
use App\Ops\OriginCheck;
|
||||
use App\Ops\Updater;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
@@ -24,7 +28,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
*/
|
||||
final class SystemController extends AbstractController
|
||||
{
|
||||
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin, private OriginCheck $originCheck)
|
||||
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin, private OriginCheck $originCheck, private HttpKernelInterface $kernel, private PageRepository $pages, private SiteSettings $settings)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -189,6 +193,113 @@ final class SystemController extends AbstractController
|
||||
return $this->redirectToRoute('admin_system');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Update mit einem Klick (ADR 113), nach dem Vorbild von reiseSoft: Die Systemseite fragt
|
||||
// beim Öffnen selbst nach einer neuen Fassung, ein Knopf startet das Update, und die Seite
|
||||
// zeigt Schritt für Schritt, was gerade passiert - bis „abgeschlossen“ oder „zurückgeschaltet“.
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
||||
/** Gibt es eine neue Fassung? Für die Systemseite, die das beim Öffnen fragt. */
|
||||
#[Route('/%kant.admin_path%/system/update/pruefen', name: 'admin_update_check', methods: ['GET'])]
|
||||
public function updateCheck(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$installiert = $this->updater->version();
|
||||
$neueste = $this->archive->latest();
|
||||
$antwort = ['installiert' => $installiert, 'status' => $this->archive->status(), 'offen' => null !== $this->archive->pending()];
|
||||
if (null === $neueste) {
|
||||
$antwort['fehler'] = 'Die Releaseseite antwortet gerade nicht. Später noch einmal versuchen.';
|
||||
} elseif (version_compare($neueste['version'], $installiert, '>')) {
|
||||
$antwort['neu'] = ['version' => $neueste['version'], 'name' => $neueste['name'], 'notizen' => $neueste['notes'], 'datum' => substr($neueste['published'], 0, 10)];
|
||||
}
|
||||
|
||||
return new JsonResponse($antwort, 200, ['Cache-Control' => 'no-store']);
|
||||
}
|
||||
|
||||
/** Der Fortschritt, alle zwei Sekunden abgefragt, solange das Update läuft. */
|
||||
#[Route('/%kant.admin_path%/system/update/status', name: 'admin_update_status', methods: ['GET'])]
|
||||
public function updateStatus(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
return new JsonResponse(['status' => $this->archive->status(), 'installiert' => $this->updater->version()], 200, ['Cache-Control' => 'no-store']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet das Update: Sichern, Laden, Prüfen, Umschalten. Die Antwort kommt nach dem Tausch
|
||||
* als reines JSON ohne Framework (siehe sendJsonAndStop); den Abschluss ruft die Seite
|
||||
* danach selbst auf.
|
||||
*/
|
||||
#[Route('/%kant.admin_path%/system/update/starten', name: 'admin_update_start', methods: ['POST'])]
|
||||
public function updateStart(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
if (!$this->isCsrfTokenValid('system', $request->headers->get('X-CSRF-Token'))) {
|
||||
return new JsonResponse(['ok' => false, 'meldung' => 'Deine Sitzung ist abgelaufen. Bitte die Seite neu laden.'], 403);
|
||||
}
|
||||
@set_time_limit(0);
|
||||
$ergebnis = $this->archive->run($request->request->getString('version') ?: null);
|
||||
if ($ergebnis['migrate'] ?? false) {
|
||||
$this->sendJsonAndStop(['ok' => true, 'token' => $ergebnis['token'], 'fassung' => $ergebnis['fassung']]);
|
||||
}
|
||||
$letzter = end($ergebnis['steps']) ?: ['detail' => ''];
|
||||
|
||||
return new JsonResponse(['ok' => $ergebnis['ok'], 'meldung' => $letzter['detail'], 'status' => $this->archive->status()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Abschluss in einer frischen Anfrage mit dem neuen Code: Migrationen, Prüfung der
|
||||
* Startseite, Wartung aus - oder Zurückschalten auf die vorherige Fassung. Die Antwort ist
|
||||
* wieder reines JSON, denn nach einem Zurückschalten liegt erneut anderer Code auf der Platte.
|
||||
*/
|
||||
#[Route('/%kant.admin_path%/system/update/abschliessen', name: 'admin_update_finish', methods: ['POST'])]
|
||||
public function updateFinish(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$offen = $this->archive->pending();
|
||||
if (null === $offen) {
|
||||
return new JsonResponse(['ok' => true, 'meldung' => 'Das Update ist bereits abgeschlossen.', 'status' => $this->archive->status()]);
|
||||
}
|
||||
if (!is_string($offen['token'] ?? null) || !hash_equals($offen['token'], $request->request->getString('token'))) {
|
||||
return new JsonResponse(['ok' => false, 'meldung' => 'Dieser Abschluss gehört zu keinem offenen Update.'], 403);
|
||||
}
|
||||
@set_time_limit(0);
|
||||
$ergebnis = $this->archive->finish($this->installation, $this->pruefen(...));
|
||||
$this->sendJsonAndStop(['ok' => $ergebnis['ok'], 'status' => $this->archive->status()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Läuft die neue Fassung wirklich? Datenbank, Startseite in der Datenbank und die Startseite
|
||||
* als Seite - als Unteranfrage, die an der Wartungsseite vorbeigeht. null heißt: alles gut.
|
||||
*/
|
||||
private function pruefen(): ?string
|
||||
{
|
||||
$home = $this->settings->homePath();
|
||||
if (null === $this->pages->findPublished($home)) {
|
||||
return sprintf('Die Startseite /%s ist nicht lesbar.', $home);
|
||||
}
|
||||
$antwort = $this->kernel->handle(Request::create('/'), HttpKernelInterface::SUB_REQUEST, false);
|
||||
if ($antwort->getStatusCode() >= 400) {
|
||||
return sprintf('Die Startseite antwortet mit %d.', $antwort->getStatusCode());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Wie sendAndStop, nur als JSON für die Systemseite. */
|
||||
private function sendJsonAndStop(array $daten): never
|
||||
{
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Cache-Control: no-store');
|
||||
header('X-Robots-Tag: noindex');
|
||||
echo json_encode($daten, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Der zweite Schritt des Archivupdates, aufgerufen von der Zwischenseite selbst (ADR 112).
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user