Arbeitspaket O. Der Teil, um den es geht, ist nicht das Sichern, sondern das Zurueckspielen: eine Sicherungsfunktion, deren Wiederherstellung nie gelaufen ist, ist eine Vermutung. Die Datenbank wird nicht kopiert, sondern von SQLite ausgeschrieben (VACUUM INTO). Ein copy() waehrend eines Schreibvorgangs ergibt eine Datei, die vorhanden aussieht und sich am Tag der Wiederherstellung nicht oeffnen laesst. Der Medienordner gehoert dazu - ohne die Dateien stellt die Sicherung eine Website voller kaputter Bilder her. .env gehoert nicht dazu: darin steht APP_SECRET, und eine Sicherung wird heruntergeladen und weitergereicht. Was nicht drin ist, steht im Manifest. Jede Sicherung traegt Zeitpunkt, Anlass, Fassung, Migrationsstand, Inhaltszahlen und die SHA-256 der Datenbank. Die Uebersicht zeigt "unversehrt" oder "Pruefsumme falsch" - eine Sicherung, von der man erst beim Zurueckspielen erfaehrt, dass sie kaputt ist, waere schlimmer als keine. Beim Zurueckspielen: neuere Fassung wird abgewiesen (mit --force geht es), zuerst wird der aktuelle Stand gesichert, Wartungszustand an, Medien werden ergaenzt statt ersetzt - eine Datei, die nach der Sicherung hochgeladen wurde, bleibt liegen. Die Massnahme gegen Datenverlust darf nicht selbst welchen verursachen. In der Verwaltung gibt es Liste und Anlegen-Knopf, aber kein Zurueckspielen: ein Knopf, der die ganze Datenbank ersetzt, gehoert nicht neben einen, der eine Seite speichert. Nachgewiesen, zweimal: acht Tests gegen eine wegwerfbare Installation (Sicherung, Seite und Bilddatei loeschen, zurueckspielen, beides wieder da) - und von Hand auf der Entwicklungsinstallation mit einer eigens angelegten Testseite: 5 Seiten, gesichert, geloescht (4), zurueckgespielt (5, Seite mit ihrer Adresse wieder da), Testseite entfernt (4). Damit ist der aelteste offene Punkt aus Paket I erledigt. Ein Fall, den erst der Test zeigte: zwei Sicherungen in derselben Sekunde - nicht akademisch, denn die Wiederherstellung legt selbst eine Sicherheitskopie an. Der Name bekommt jetzt eine Nummer, statt dass die zweite scheitert. Offen: MySQL (mysqldump fehlt) und ein Download ueber den Browser. 344 PHP-Tests / 1397 Assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
8.4 KiB
Twig
145 lines
8.4 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'system' %}
|
||
{% block title %}System{% endblock %}
|
||
{% block heading %}System und Update{% endblock %}
|
||
{% block lead %}Welche Fassung läuft, ob ein Update möglich ist, und der Weg dorthin: sichern, Wartungszustand, Code wechseln, migrieren, Rauchtest.{% endblock %}
|
||
{% block content %}
|
||
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<h2>Diese Installation</h2>
|
||
<span class="muted">{{ state.maintenance ? 'Wartungszustand eingeschaltet' : 'im Betrieb' }}</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<dl class="facts">
|
||
<dt>Fassung</dt><dd><strong>{{ state.version }}</strong></dd>
|
||
<dt>Stand</dt><dd><code>{{ state.describe }}</code>{% if state.commit != 'unbekannt' %} · Commit <code>{{ state.commit }}</code>{% endif %}</dd>
|
||
<dt>Verfügbar</dt>
|
||
<dd>
|
||
{% if available %}
|
||
<strong>{{ available }}</strong>{% if available == 'v' ~ state.version %} – das ist die installierte Fassung.{% endif %}
|
||
{% elseif asked %}
|
||
Kein Tag der Form <code>v1.2.3</code> in der Herkunft gefunden.
|
||
{% else %}
|
||
<a href="{{ path('admin_system', {pruefen: 1}) }}">Beim Repository nachfragen</a>
|
||
{% endif %}
|
||
</dd>
|
||
</dl>
|
||
|
||
{% if state.maintenance %}
|
||
<p class="notice error" role="alert">Die Website antwortet Besuchern gerade mit 503. Wenn das nicht beabsichtigt ist, hebe den Wartungszustand auf – ein gescheitertes Update kann ihn eingeschaltet gelassen haben.</p>
|
||
{% endif %}
|
||
|
||
<div class="sheet-actions">
|
||
<form action="{{ path('admin_system_maintenance') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<input type="hidden" name="state" value="{{ state.maintenance ? 'aus' : 'an' }}">
|
||
<button class="secondary" type="submit">Wartungszustand {{ state.maintenance ? 'aufheben' : 'einschalten' }}</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head"><h2>Vorprüfung</h2><span class="muted">Jede Bedingung wird geprüft, nicht angenommen</span></div>
|
||
<div class="card-body">
|
||
<ul class="check-list">
|
||
{% for check in checks %}
|
||
<li class="{{ check.ok ? 'check-ok' : 'check-bad' }}">
|
||
<span class="check-mark" aria-hidden="true">{{ check.ok ? '✓' : '!' }}</span>
|
||
<span><strong>{{ check.name }}</strong> <span class="visually-hidden">{{ check.ok ? 'erfüllt' : 'nicht erfüllt' }}:</span> {{ check.detail }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head"><h2>Update einspielen</h2><span class="muted">{{ ready ? 'Vorprüfung bestanden' : 'erst die Vorprüfung erfüllen' }}</span></div>
|
||
<div class="card-body">
|
||
<p class="muted">Der Updater sichert zuerst Datenbank und Umgebungsdateien, schaltet den Wartungszustand ein, wechselt auf den Tag, installiert Abhängigkeiten, migriert, räumt den Cache und prüft, ob die Installation antwortet. Scheitert ein Schritt, wird auf den vorherigen Stand zurückgesetzt – und nur wenn die Migration schon begonnen hatte, auch die Datenbank zurückgeschrieben.</p>
|
||
<p class="field-help"><strong>Der Medienordner wird nicht gesichert.</strong> Er ist zu groß, und kein Update schreibt darin. Ein Update ist keine Datensicherung.</p>
|
||
<p class="field-help">Auf manchen Hostings bricht ein Browseraufruf nach kurzer Zeit ab. Dann ist <code>php bin/console cms:update</code> der verlässliche Weg – dort gibt es dieselben Schritte mit <code>--check</code>, <code>--dry-run</code> und <code>--to=v1.2.0</code>.</p>
|
||
|
||
<form action="{{ path('admin_system_update') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="target">Ziel</label>
|
||
<input id="target" name="target" maxlength="40" placeholder="{{ available ?: 'v1.2.0' }}" pattern="v[0-9]+\.[0-9]+\.[0-9]+">
|
||
<p class="field-help">Leer lassen für den höchsten verfügbaren Tag.</p>
|
||
</div>
|
||
<div>
|
||
<label class="check-label" for="dryRun"><input id="dryRun" name="dryRun" type="checkbox" value="1" checked> Nur Probelauf</label>
|
||
<p class="field-help">Prüft alles und verändert nichts. Nimm das Häkchen weg, wenn es wirklich laufen soll.</p>
|
||
</div>
|
||
</div>
|
||
<button class="primary" type="submit"{{ ready ? '' : ' disabled' }}>Update ausführen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{% if steps is not empty %}
|
||
<div class="card">
|
||
<div class="card-head"><h2>Letzter Durchlauf</h2><span class="muted">Schritt für Schritt</span></div>
|
||
<div class="card-body">
|
||
<ol class="check-list">
|
||
{% for step in steps %}
|
||
<li class="{{ step.ok ? 'check-ok' : 'check-bad' }}">
|
||
<span class="check-mark" aria-hidden="true">{{ step.ok ? '✓' : '!' }}</span>
|
||
<span><strong>{{ step.step }}</strong> {{ step.detail }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<div class="card-head"><h2>Sicherungen</h2><span class="muted">{{ backups|length }} vorhanden · var/backups</span></div>
|
||
<div class="card-body">
|
||
<p class="muted">Eine Sicherung enthält die Datenbank als vollständige Kopie – nicht als Dateikopie während des Schreibens, die unbrauchbar sein kann – und den Medienordner. <strong>Nicht enthalten sind <code>.env</code>, <code>vendor/</code> und der Quellcode</strong>: die kommen aus dem Repository, und <code>.env</code> enthält das Geheimnis dieser Installation.</p>
|
||
<p class="field-help">Zurückspielen geht bewusst nur auf der Konsole: <code>php bin/console cms:restore <name></code>. Ein Knopf, der die ganze Datenbank ersetzt, gehört nicht neben einen, der eine Seite speichert. Für den täglichen Cronjob: <code>php bin/console cms:backup</code>.</p>
|
||
<form action="{{ path('admin_system_backup') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<button class="primary" type="submit">Sicherung jetzt anlegen</button>
|
||
</form>
|
||
|
||
{% if backups is not empty %}
|
||
<table class="table">
|
||
<caption class="visually-hidden">Vorhandene Sicherungen</caption>
|
||
<thead><tr><th scope="col">Name</th><th scope="col">Erstellt</th><th scope="col">Fassung</th><th scope="col">Inhalt</th><th scope="col">Größe</th><th scope="col">Zustand</th></tr></thead>
|
||
<tbody>
|
||
{% for backup in backups %}
|
||
<tr>
|
||
<td><code>{{ backup.name }}</code></td>
|
||
<td>{{ backup.manifest ? backup.manifest.erstellt|date('d.m.Y H:i') : '—' }}{% if backup.manifest %}<br><span class="muted">{{ backup.manifest.anlass }}</span>{% endif %}</td>
|
||
<td>{{ backup.manifest ? backup.manifest.fassung : '—' }}</td>
|
||
<td>{% if backup.manifest %}{{ backup.manifest.inhalt.seiten }} Seiten, {{ backup.manifest.medien.dateien }} Dateien{% else %}—{% endif %}</td>
|
||
<td>{{ (backup.bytes / 1048576)|number_format(1, ',', '.') }} MB</td>
|
||
<td>{% if backup.intact %}<span class="pill pill-live">unversehrt</span>{% else %}<span class="pill pill-off">Prüfsumme falsch</span>{% endif %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<p class="field-help">„Unversehrt“ heißt: die Prüfsumme der gesicherten Datenbank stimmt noch mit dem Manifest überein. Stimmt sie nicht, wird die Sicherung nicht eingespielt.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head"><h2>Verlauf</h2><span class="muted">var/updates.log</span></div>
|
||
<div class="card-body">
|
||
{% if state.log is empty %}
|
||
<p class="muted">Noch kein Update über diesen Weg eingespielt.</p>
|
||
{% else %}
|
||
<ul class="log-list">
|
||
{% for line in state.log %}<li><code>{{ line }}</code></li>{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|