Fassung 1.6.0: der Aufgabenplan wird veroeffentlicht
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

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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-14 08:30:13 +02:00
1 parent d1ac9d760b
commit 05ea703655
35 files changed
+2229 -13

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
<?php
namespace App\Command;
use App\Ops\Scheduler;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Was der Plan vorhat und was zuletzt passiert ist.
*
* Dieselbe Auskunft wie der Bildschirm „Aufgaben“, nur für den, der ohnehin auf der Konsole
* steht – und für den Fall, dass die Verwaltung gerade nicht erreichbar ist.
*/
#[AsCommand(name: 'cms:tasks', description: 'Zeigt den Aufgabenplan: Abstand, Fälligkeit und das letzte Ergebnis.')]
final class TasksCommand extends Command
{
public function __construct(private Scheduler $scheduler)
{
parent::__construct();
}
protected function configure(): void
{
$this->addOption('log', null, InputOption::VALUE_NONE, 'Zusätzlich die letzten Protokollzeilen.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
// Erst der Abgleich (er legt fehlende Zeilen an und schreibt die Zustandsdatei neu), dann
// die Auskunft daraus - sonst zeigt der erste Aufruf nach einem Update einen Zeitpunkt,
// der eine Sekunde spaeter schon nicht mehr stimmt.
$tasks = $this->scheduler->overview();
$state = $this->scheduler->fastState();
$io->writeln(sprintf(
'Auslöser: Besuch <info>%s</info>, Verwaltung <info>%s</info>. Konsole und URL-Cron laufen immer.',
$state['traffic'] ? 'an' : 'aus',
$state['admin'] ? 'an' : 'aus',
));
$io->writeln(sprintf(
'Jetzt ist %s UTC; das nächste Mal steht %s an.',
gmdate('Y-m-d H:i:s'),
null === $state['next'] ? 'unbekannt' : gmdate('Y-m-d H:i:s', $state['next']),
));
$rows = [];
foreach ($tasks as $task) {
$rows[] = [
$task['name'],
$task['enabled'] ? ($task['due'] ? 'fällig' : 'an') : 'aus',
$this->minutes($task['interval']),
$task['next'],
$task['running'] ? 'läuft' : ($task['status'] ?? '–'),
mb_substr($task['message'], 0, 60),
];
}
if ([] === $rows) {
$io->warning('Keine Aufgaben. Fehlt die Migration? `php vendor/bin/doctrine-migrations migrate`');
} else {
$io->table(['Name', 'Zustand', 'Abstand', 'Nächster Lauf (UTC)', 'Zuletzt', 'Meldung'], $rows);
}
if ($input->getOption('log')) {
$io->section('Protokoll');
$lines = $this->scheduler->protocol(30);
$io->writeln([] === $lines ? 'Noch nichts protokolliert.' : $lines);
}
return Command::SUCCESS;
}
private function minutes(int $seconds): string
{
return $seconds >= 86400 && 0 === $seconds % 86400
? sprintf('%d Tag(e)', intdiv($seconds, 86400))
: sprintf('%d Min.', max(1, intdiv($seconds, 60)));
}
}