Files
KantSites/templates/admin/tasks.html.twig
T
kantarciandClaude Opus 5 05ea703655
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Fassung 1.6.0: der Aufgabenplan wird veroeffentlicht
Wiederkehrende Aufgaben ohne Cronjob (ADR 073): Sicherung, Statistik kuerzen,
geplante Veroeffentlichung, Updatepruefung und Aufraeumen laufen als Plan in
der Tabelle scheduled_task - eine Zeile je Aufgabe mit Faelligkeit, Sperre und
letztem Ergebnis. Auf geteiltem Hosting gibt es oft keinen Cronjob; der Plan
haengt stattdessen an Anfragen und an einem Takt aus dem geoeffneten Backend.

Diese Arbeit entstand in einer parallel laufenden Sitzung und lag bis hierher
unveroeffentlicht im Arbeitsverzeichnis: die Releases 1.5.0 bis 1.5.5 wurden
bewusst aus dem jeweiligen Tag gebaut und enthielten sie deshalb nicht.

Vor der Veroeffentlichung geprueft, nicht bloss eingepackt:

- phpunit 452/452, npm test 28/28, npm run test:editor vollstaendig.
- Die Migration 20260914000100 legt nur die Tabelle scheduled_task an und nimmt
  sie beim Zurueckrollen wieder weg. Sie fasst keine bestehenden Daten an.
- Seit einer Stunde keine Schreibzugriffe der anderen Sitzung mehr.

Dabei zwei Fehler behoben:

- config/services.yaml verwendet !tagged_iterator. Das ist gueltiges Symfony,
  aber "lint:yaml config" kennt eigene Tags nur mit --parse-tags und gab 1
  zurueck - der CI-Lauf waere daran gescheitert. Flag ergaenzt.
- AdminHeadersSubscriber.php (aus 1.5.3) entsprach nicht dem Stil von
  php-cs-fixer; composer lint gab 8 zurueck. Behoben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 08:30:13 +02:00

132 lines
8.3 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'admin/shell.html.twig' %}
{% set nav = 'tasks' %}
{% block title %}Aufgaben{% endblock %}
{% block heading %}Wiederkehrende Aufgaben{% endblock %}
{% block lead %}Was regelmäßig laufen soll, wer es anstößt und was zuletzt dabei herauskam. Alle Zeiten in UTC.{% endblock %}
{% block content %}
{# Der wichtigste Satz zuerst, und zwar als Einschränkung und nicht als Werbung: ohne Anfrage
passiert nichts. Wer das nicht weiß, sucht später den Fehler an der falschen Stelle. #}
<div class="card">
<div class="card-head">
<h2>Wer die Arbeit anstößt</h2>
<span class="muted">{{ next is null ? 'Zeitpunkt noch unbekannt' : 'Nächster Lauf: ' ~ next|date('Y-m-d H:i:s', 'UTC') ~ ' UTC' }}</span>
</div>
<div class="card-body">
<p class="muted">Ein Webhosting ohne Cronjob hat nur eines: Anfragen. Deshalb erledigt eine Anfrage die Arbeit – nach der ausgelieferten Antwort, sodass niemand darauf wartet. <strong>Ohne Besuch und ohne geöffnete Verwaltung passiert nichts.</strong> Eine Aufgabe für 03:00 Uhr läuft dann beim ersten Aufruf danach, auf einer stillen Website vielleicht um 07:15 Uhr. Wer es genauer braucht, richtet einen der beiden unteren Wege ein.</p>
<form action="{{ path('admin_tasks_configure') }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('tasks') }}">
<fieldset class="field-set">
<legend>Auslöser in Anfragen</legend>
<label class="check-label"><input type="checkbox" name="traffic" value="1"{{ traffic ? ' checked' : '' }}> Besuche der Website</label>
<p class="field-help">Jeder Seitenaufruf schaut nach, ob etwas fällig ist. Der Normalfall kostet eine gelesene Datei und keine Datenbankabfrage. Ausschalten, wenn ein echter Cronjob die Arbeit übernimmt.</p>
<label class="check-label"><input type="checkbox" name="admin" value="1"{{ admin ? ' checked' : '' }}> Geöffnete Verwaltung</label>
<p class="field-help">Solange dieser Bildschirm offen und sichtbar ist, fragt er jede Minute nach. So läuft der Plan auch auf einer Website, die gerade niemand besucht.</p>
</fieldset>
<div class="section-head"><h2>Der Plan</h2><span class="muted">Abstand in Minuten</span></div>
<table class="table tasks-table">
<thead>
<tr><th scope="col">Aufgabe</th><th scope="col" class="check-cell">An</th><th scope="col">Abstand</th><th scope="col">Nächster Lauf</th><th scope="col">Zuletzt</th><th scope="col"></th></tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>
<strong>{{ task.label }}</strong>
<p class="field-help">{{ task.explanation }}</p>
<p class="field-help"><code>{{ task.name }}</code></p>
</td>
<td class="check-cell">
<input type="checkbox" name="enabled[{{ task.name }}]" value="1"{{ task.enabled ? ' checked' : '' }} aria-label="{{ task.label }} ausführen">
</td>
<td><input type="number" name="interval[{{ task.name }}]" value="{{ (task.interval / 60)|round }}" min="1" max="43200" step="1" aria-label="Abstand für {{ task.label }} in Minuten"></td>
<td>
{% if not task.enabled %}<span class="pill pill-off">aus</span>
{% elseif task.running %}<span class="pill pill-draft">läuft</span>
{% elseif task.due %}<span class="pill pill-draft">fällig</span>
{% else %}<span class="muted">{{ task.next }}</span>{% endif %}
</td>
<td>
{% if task.status is null %}
<span class="muted">noch nie</span>
{% else %}
<span class="pill {{ task.status == 'ok' ? 'pill-live' : 'pill-off' }}">{{ task.status }}</span>
<p class="field-help">{{ task.last }} · {{ task.ms }} ms{% if task.trigger %} · durch {{ task.trigger }}{% endif %}</p>
<p class="field-help{{ task.status == 'ok' ? '' : ' danger-text' }}">{{ task.message }}</p>
{% if task.failures > 1 %}<p class="field-help danger-text">{{ task.failures }} Fehlschläge nacheinander – der Abstand bis zum nächsten Versuch wächst.</p>{% endif %}
{% endif %}
</td>
<td>
<button class="secondary" type="submit" form="run-{{ task.name }}">Jetzt ausführen</button>
{% if task.running %}<button class="text-button" type="submit" form="release-{{ task.name }}">Sperre aufheben</button>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6">Keine Aufgaben. Fehlt die Migration? Der Bildschirm <a href="{{ path('admin_system') }}">System</a> führt sie aus.</td></tr>
{% endfor %}
</tbody>
</table>
<button class="primary" type="submit">Plan speichern</button>
</form>
{# Die Formulare der Knöpfe stehen außerhalb der Tabelle: ein Formular im Formular gibt es
in HTML nicht, und `form="..."` verbindet Knopf und Formular über die Tabelle hinweg. #}
{% for task in tasks %}
<form id="run-{{ task.name }}" action="{{ path('admin_tasks_run') }}" method="post" hidden>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('tasks') }}">
<input type="hidden" name="name" value="{{ task.name }}">
</form>
{% if task.running %}
<form id="release-{{ task.name }}" action="{{ path('admin_tasks_release') }}" method="post" hidden>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('tasks') }}">
<input type="hidden" name="name" value="{{ task.name }}">
</form>
{% endif %}
{% endfor %}
</div>
</div>
<div class="card">
<div class="card-head"><h2>Der zuverlässige Weg</h2><span class="muted">falls dieses Hosting ihn anbietet</span></div>
<div class="card-body">
<p class="muted">Beide Wege sind unabhängig von den Schaltern oben – sie laufen auch dann, wenn Besuch und Verwaltung ausgeschaltet sind. Wer einen davon einrichtet, sollte den Besuchs-Auslöser abschalten: die Arbeit ist dann schon getan, und die Website antwortet ein wenig schneller.</p>
<dl class="facts">
<dt>URL-Cronjob</dt>
<dd>
<code>{{ cronUrl }}</code>
<p class="field-help">Diese Adresse beim Hoster als Cronjob eintragen, zum Beispiel alle fünf Minuten. Der Schlüssel ist aus dem Anwendungsgeheimnis abgeleitet, steht in keiner Datenbank und ändert sich, wenn das Geheimnis gewechselt wird. Er gehört nicht in eine E-Mail und nicht in ein Ticket.</p>
</dd>
<dt>Konsole</dt>
<dd>
<code>php bin/console cms:tasks:run</code>
<p class="field-help">Für einen echten Cronjob, jede Minute. <code>cms:tasks</code> zeigt denselben Plan wie dieser Bildschirm, <code>cms:tasks:run --loop=60</code> läuft dauerhaft.</p>
</dd>
</dl>
</div>
</div>
<div class="card">
<div class="card-head"><h2>Protokoll</h2><span class="muted">die letzten Läufe, neueste zuerst</span></div>
<div class="card-body">
{% if protocol is empty %}
<p class="muted">Noch nichts gelaufen. Steht in der Spalte „Nächster Lauf“ ein Zeitpunkt, ist alles in Ordnung – der Plan wartet.</p>
{% else %}
<div class="table-wrap">
<table class="table">
<tbody>
{% for line in protocol %}
<tr><td><code>{{ line }}</code></td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="field-help">Aus <code>var/tasks.log</code>. Die Datei kürzt sich selbst; die Aufgabe „Aufräumen“ tut es ebenfalls.</p>
{% endif %}
</div>
</div>
{% endblock %}