- System > This installation > Address has a field and a link to the other spelling (with or without www), like the WordPress address. It writes SITE_ORIGIN to .env through Environment::write and takes effect on the next request. With SITE_ORIGIN from the server environment there is no field, and a POST is refused. - Before saving, OriginCheck requests /.well-known/kantsites-origin under the new address; it has to return an HMAC of APP_SECRET that only this installation knows. Switching to www without DNS or certificate would otherwise send every page, the administration included, nowhere. "Save anyway" appears only after a refusal. The probe also answers during maintenance. - The message says when the administration changes host and needs a new sign-in, since the session cookie belongs to one spelling. Tests: 584 PHPUnit on PHP 8.2 and 8.4, 35 node, test:editor and test:browser green (test:browser reports only the known editor colour contrast on /grundlagen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
291 lines
18 KiB
Twig
291 lines
18 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'system' %}
|
||
{% block title %}System{% endblock %}
|
||
{% block heading %}System und Update{% endblock %}
|
||
{% block lead %}Fassung, Updateweg und Sicherungen dieser Installation.{% 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>Verwaltung</dt>
|
||
<dd>
|
||
<code>/{{ adminPath }}</code>
|
||
{% if adminPath == 'admin' %}
|
||
<span class="muted">– Standardpfad, den jeder Scanner zuerst probiert. Ändern: <code>php bin/console cms:admin:path <neu></code>.</span>
|
||
{% else %}
|
||
<span class="muted">– geändert; steht in keiner öffentlichen Datei. Zurück: <code>php bin/console cms:admin:path admin</code>.</span>
|
||
{% endif %}
|
||
</dd>
|
||
<dt>Datenbank</dt>
|
||
<dd>
|
||
<strong>{{ database.art }}</strong>
|
||
{% if database.datei %}
|
||
<code>{{ database.datei }}</code>
|
||
{% if database.bytes %}<span class="muted">· {{ (database.bytes / 1048576)|number_format(1, ',', '.') }} MB</span>{% endif %}
|
||
{% if not database.schreibbar %}<br><span class="check-bad"><strong>Nicht beschreibbar.</strong> SQLite braucht Schreibrechte auf die Datei <em>und</em> auf den Ordner darüber – nach einem FTP-Upload gehört sie dem FTP-Benutzer.</span>{% endif %}
|
||
{% else %}
|
||
<code>{{ database.adresse }}</code>
|
||
{% endif %}
|
||
<br><span class="muted">Eingestellt in <code>{{ database.quelle }}</code>; Server-Umgebungsvariablen haben Vorrang.</span>
|
||
</dd>
|
||
<dt>Adresse</dt>
|
||
<dd>
|
||
{% if origin.adresse %}<code>{{ origin.adresse }}</code>{% else %}<span class="check-bad"><strong>Nicht eingetragen.</strong></span>{% endif %}
|
||
{% if origin.passt %}
|
||
<span class="muted">– Canonical, Sitemap und Feed nennen sie; die Schreibweise {{ origin.adresse matches '{^https?://www\.}' ? 'ohne' : 'mit' }} www leitet hierher um.</span>
|
||
{% else %}
|
||
<br><span class="check-bad"><strong>Diese Verwaltung läuft gerade unter <code>{{ origin.host }}</code>.</strong> Canonical, Sitemap und Feed nennen trotzdem die eingetragene Adresse. Stimmt die nicht mehr, unten ändern.</span>
|
||
{% endif %}
|
||
{% if origin.quelle == '.env' %}
|
||
{# Wie „WordPress-Adresse“: eine Stelle für mit oder ohne www. Vor dem Speichern fragt
|
||
die Installation sich selbst unter der neuen Adresse an. #}
|
||
{% set host = origin.adresse|split('://')|last|split(':')|first %}
|
||
{% set twin = host starts with 'www.' ? host|slice(4) : 'www.' ~ host %}
|
||
<form class="origin-form" action="{{ path('admin_system_origin') }}" method="post">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('system') }}">
|
||
<label for="origin">Adresse der Website</label>
|
||
<div class="origin-row">
|
||
<input id="origin" name="origin" type="url" inputmode="url" maxlength="200" required placeholder="https://www.meine-firma.de" value="{{ originRejected ?: origin.adresse }}" aria-describedby="origin-help">
|
||
<button class="secondary" type="submit">Adresse speichern</button>
|
||
</div>
|
||
{% if originRejected %}
|
||
<label class="check-label" for="origin-force"><input id="origin-force" type="checkbox" name="force" value="1"> Trotzdem übernehmen – ich habe DNS und Zertifikat geprüft</label>
|
||
{% endif %}
|
||
<p class="field-help" id="origin-help">Nur Schema und Domain. Vor dem Speichern wird geprüft, ob die Website unter der neuen Adresse antwortet – sonst leitete die andere Schreibweise ins Leere, die Verwaltung eingeschlossen.{% if origin.adresse and host matches '/^[a-z0-9-]+(\.[a-z0-9-]+)+$/' and not (host matches '/^[0-9.]+$/') %} Umstellen auf <button class="link-button" type="submit" name="origin" value="{{ origin.adresse|split('://')|first }}://{{ twin }}" formnovalidate>{{ twin }}</button>.{% endif %}</p>
|
||
</form>
|
||
{% else %}
|
||
<br><span class="muted">Eingestellt in der Server-Umgebung; sie hat Vorrang vor <code>.env</code> und lässt sich nur beim Hoster ändern.</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><strong>Releasearchiv</strong> <span class="muted">– kein Git; Updates laufen über die Karte 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 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">{{ checks|length }} Bedingungen</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 – typisch nach einem Update von Hand über FTP.</p>
|
||
<ul class="log-list">{% for name in pendingMigrations %}<li><code>{{ name }}</code></li>{% endfor %}</ul>
|
||
<p class="field-help">Vorher sichern: 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' }}</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">Ohne Git: Archiv laden, Prüfsumme vergleichen, Datenbank und Medien sichern, Wartungszustand ein, Dateien tauschen. Der vorherige Stand wird beiseitegelegt, nicht gelöscht.</p>
|
||
{# Das Kleingedruckte steht zugeklappt: Wer aktualisiert, braucht zuerst den Knopf. #}
|
||
<details class="hint">
|
||
<summary>Was unberührt bleibt</summary>
|
||
<p class="field-help"><code>var/</code>, <code>.env</code>, <code>.env.local</code>, die geladenen Schriften und das erzeugte Stylesheet-Bündel.</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.</p>
|
||
</details>
|
||
{% 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> – 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">Sichern, Wartungszustand ein, auf den Tag wechseln, Abhängigkeiten, Migrationen, Cache, Antwortprüfung. Scheitert ein Schritt, wird zurückgesetzt – die Datenbank nur, wenn die Migration schon begonnen hatte.</p>
|
||
<details class="hint">
|
||
<summary>Zwei Hinweise vorab</summary>
|
||
<p class="field-help"><strong>Der Medienordner wird nicht gesichert.</strong> Ein Update ist keine Datensicherung.</p>
|
||
<p class="field-help">Bricht der Browseraufruf auf dem Hosting ab: <code>php bin/console cms:update</code> – dieselben Schritte mit <code>--check</code>, <code>--dry-run</code> und <code>--to=v1.2.0</code>.</p>
|
||
</details>
|
||
|
||
<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.</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">{{ steps|length }} Schritte</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>
|
||
{# `step.label` ist die Schreibweise bis 1.4.4. Sie muss hier stehen bleiben: Beim
|
||
Update von 1.4.4 auf 1.4.5 schreibt der **alte** Code die Schritte in die
|
||
Sitzung und der **neue** zeigt sie an - eine Fassung, die nur die neue
|
||
Schreibweise kennt, könnte das Update, das sie selbst einspielt, nicht
|
||
berichten. #}
|
||
<span><strong>{{ step.step|default(step.label|default('Schritt')) }}</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 und den Medienordner – nicht <code>.env</code>, <code>vendor/</code> und den Quellcode.</p>
|
||
<p class="field-help">Zurückspielen nur auf der Konsole: <code>php bin/console cms:restore <name></code>. Die tägliche Sicherung erledigt der <a href="{{ path('admin_tasks') }}">Aufgabenplan</a>.</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 table-stack">
|
||
<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 data-label="Name"><code>{{ backup.name }}</code></td>
|
||
<td data-label="Erstellt">{{ 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 data-label="Fassung">{{ backup.manifest ? backup.manifest.fassung : '—' }}</td>
|
||
<td data-label="Inhalt">{% if backup.manifest %}{{ backup.manifest.inhalt.seiten }} Seiten, {{ backup.manifest.medien.dateien }} Dateien{% else %}—{% endif %}</td>
|
||
<td data-label="Größe">{{ (backup.bytes / 1048576)|number_format(1, ',', '.') }} MB</td>
|
||
<td data-label="Zustand">{% 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 stimmt noch. 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 %}
|
||
{# Zwanzig Protokollzeilen sind auf einem Handy zwanzig Absätze. Die jüngsten fünf
|
||
stehen offen, der Rest liegt eine Umdrehung dahinter. #}
|
||
<ul class="log-list">
|
||
{% for line in state.log|slice(0, 5) %}<li><code>{{ line }}</code></li>{% endfor %}
|
||
</ul>
|
||
{% if state.log|length > 5 %}
|
||
<details class="hint">
|
||
<summary>Ältere Einträge ({{ state.log|length - 5 }})</summary>
|
||
<ul class="log-list">
|
||
{% for line in state.log|slice(5) %}<li><code>{{ line }}</code></li>{% endfor %}
|
||
</ul>
|
||
</details>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|