Alle drei von Umut auf seinem Hosting gefunden, keiner davon auf meinem Rechner sichtbar. - Der Updateknopf endete mit 500. Das Update lief vollstaendig durch - alle neun Schritte, Dateien getauscht, var/ und .env unberuehrt -, aber die Ergebnisseite starb daran: der Updater ueber Git nennt einen Schritt "step", der ueber das Archiv nannte ihn "label", beide legen ihn unter demselben Sitzungsschluessel ab, und die Vorlage liest "step". Damit war auch der zweite Knopf unerreichbar. Zehn Tests prueften das Rueckgabe-Array, keiner die Seite, die es anzeigt; der neue Test rendert sie (ADR 071). - Die Verwaltung zeigte Git-Fehler auf einer Installation ohne Git. "git ist installiert" und "das hier ist eine Arbeitskopie" waren derselbe Schalter; auf dem Hosting ist Git vorhanden, die Installation kam aber aus dem Archiv. Dort stand "fatal: Not a git repository" an der Stelle der Fassungsnummer. Ohne Arbeitskopie verschwinden die Git-Karten jetzt (ADR 072). - "Sicherung anlegen" endete mit 500: VACUUM INTO gibt es erst seit SQLite 3.27, das Hosting hat eine aeltere Fassung - alt genug fuer die Installation, zu alt fuer die Sicherung. Bei aelteren wird jetzt unter einer Schreibsperre kopiert, und der Knopf faengt jeden Fehler ab statt mit einer 500-Seite zu enden (ADR 072). Nachgestellt wurde das mit einer echten 1.4.3-Installation aus dem Releasearchiv, eigener Datenbank, eigenem Port. Der Archiv-Updater laeuft dort vollstaendig durch: 1.4.3 -> 1.4.4, neun Schritte, var/ und .env unberuehrt. Geprueft: 433 Tests auf PHP 8.2 und 8.4, Editor-Browsertest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
227 lines
14 KiB
Twig
227 lines
14 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, Dateien tauschen, migrieren.{% 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>Adresse der Verwaltung</dt>
|
||
<dd>
|
||
<code>/{{ adminPath }}</code>
|
||
{% if adminPath == 'admin' %}
|
||
<span class="muted">– die Adresse, die jeder Scanner zuerst probiert. Ändern mit <code>php bin/console cms:admin:path <neu></code>; sie steht danach in <code>.env.local</code> und in keiner öffentlichen Datei.</span>
|
||
{% else %}
|
||
<span class="muted">– geändert. Sie wird in der <code>robots.txt</code> nicht genannt; notiere sie dir an einem sicheren Ort. Zurück mit <code>php bin/console cms:admin:path admin</code>.</span>
|
||
{% endif %}
|
||
</dd>
|
||
{% if state.worktree %}
|
||
<dt>Stand</dt><dd><code>{{ state.describe }}</code>{% if state.commit != 'unbekannt' %} · Commit <code>{{ state.commit }}</code>{% endif %}</dd>
|
||
{% else %}
|
||
<dt>Eingerichtet über</dt>
|
||
<dd>das <strong>Releasearchiv</strong> <span class="muted">– kein Git in diesem Verzeichnis, und dafür ist es auch nicht gedacht. Updates laufen über die Karte weiter unten.</span></dd>
|
||
{% endif %}
|
||
{% if state.worktree %}
|
||
<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>
|
||
{% endif %}
|
||
</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>
|
||
|
||
{# Die Vorprüfung gehört zum Updateweg über Git: Arbeitsverzeichnis sauber, Datenbank
|
||
außerhalb von Git, Tag erreichbar. Auf einer Installation aus dem Releasearchiv prüft sie
|
||
Bedingungen, die dort keine sind - und zeigt lauter rote Ausrufezeichen für etwas, das
|
||
vollkommen in Ordnung ist. #}
|
||
{% if state.worktree %}
|
||
<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>
|
||
{% endif %}
|
||
|
||
{# Waehrend das Archiv-Update auf seinen zweiten Schritt wartet, gehoert der Knopf dorthin -
|
||
zwei Knoepfe, die dasselbe tun, sind eine Frage zu viel. #}
|
||
{% if pendingMigrations|default([]) and not archivePending %}
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<h2>Migrationen stehen aus</h2>
|
||
<span class="muted">{{ pendingMigrations|length }} Stück</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="notice">Der Code ist neuer als die Datenbank. Das passiert, wenn ein Update von Hand über FTP eingespielt wurde – dann fehlt der Schritt, den sonst der Updater übernimmt.</p>
|
||
<ul class="log-list">{% for name in pendingMigrations %}<li><code>{{ name }}</code></li>{% endfor %}</ul>
|
||
<p class="field-help">Lege vorher eine Sicherung an: Migrationen sind nicht umkehrbar, der Rückweg ist die Sicherung.</p>
|
||
<form action="{{ path('admin_system_migrate') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<button class="primary" type="submit">Migrationen jetzt ausführen →</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<h2>{{ state.worktree ? 'Update aus dem Releasearchiv' : 'Update einspielen' }}</h2>
|
||
<span class="muted">{{ state.worktree ? 'Alternative zum Weg über Git' : 'holt die neue Fassung und tauscht die Dateien' }}</span>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if archivePending %}
|
||
<p class="notice">Die Dateien der Fassung <strong>{{ archivePending.fassung }}</strong> liegen bereits (seit {{ archivePending.seit|slice(0, 16)|replace({"T": " "}) }} UTC). <strong>Es fehlen noch die Migrationen</strong> – bis dahin bleibt die Website im Wartungszustand.</p>
|
||
<form action="{{ path('admin_system_archive') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<input type="hidden" name="finish" value="1">
|
||
<button class="primary" type="submit">Migrationen ausführen und freigeben →</button>
|
||
</form>
|
||
{% else %}
|
||
<p class="muted">Dieser Weg braucht kein Git: Er lädt das Archiv der neuen Fassung von der Releaseseite, vergleicht die Prüfsumme, sichert Datenbank und Medien, schaltet den Wartungszustand ein und ersetzt die Dateien der Anwendung. <strong>Unberührt bleiben <code>var/</code>, <code>.env</code>, <code>.env.local</code>, die geladenen Schriften und das erzeugte Stylesheet-Bündel.</strong> Der vorherige Stand wird beiseitegelegt, nicht gelöscht.</p>
|
||
<p class="field-help">Danach folgt ein zweiter Knopf für die Migrationen. Das ist kein Umweg: Erst eine neue Anfrage startet mit dem Code der neuen Fassung – ein Prozess, der sich selbst ausgetauscht hat, arbeitet mit dem alten weiter.</p>
|
||
{% if archiveVersion %}
|
||
<p class="notice done">Verfügbar: <strong>{{ archiveVersion }}</strong> (installiert: {{ state.version }}).</p>
|
||
{% elseif asked %}
|
||
<p class="field-help">Die Releaseseite nennt nichts Neueres als {{ state.version }}.</p>
|
||
{% else %}
|
||
<p class="field-help"><a href="{{ path('admin_system', {pruefen: 1}) }}">Nachsehen, ob es etwas Neues gibt</a> – das fragt die Releaseseite und dauert einen Moment.</p>
|
||
{% endif %}
|
||
<form action="{{ path('admin_system_archive') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="archive-target">Fassung</label>
|
||
<input id="archive-target" name="target" maxlength="20" placeholder="{{ archiveVersion ?: '1.4.2' }}" pattern="[0-9]+\.[0-9]+\.[0-9]+">
|
||
<p class="field-help">Leer lassen für die neueste veröffentlichte Fassung.</p>
|
||
</div>
|
||
</div>
|
||
<button class="primary" type="submit">Update aus dem Archiv einspielen →</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if state.worktree %}
|
||
<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>
|
||
{% endif %}
|
||
|
||
{% 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 %}
|