Every installation now reports address and version to sites.kant-it.de every 15 minutes and picks up update orders there (ADR 123). Reports and answers are HMAC-signed with a per-installation key sent on first contact; an order can only install a published release with checksum. The next request after the file swap runs the migrations, so a remote update finishes without anyone in the admin. The platform itself lives in zentrale/ (plain PHP + SQLite, not part of the release archive): overview with outdated, silent and unassigned installations, single and bulk updates, per-site detail and history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
5.9 KiB
PHP
152 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Zentrale;
|
|
|
|
/**
|
|
* Die Installationen und ihre Aufträge - was die Übersicht zeigt und was ihre Knöpfe tun.
|
|
*/
|
|
final class Bestand
|
|
{
|
|
/** Ohne Meldung seit so vielen Stunden gilt eine Installation als verstummt. */
|
|
public const STILL_STUNDEN = 48;
|
|
|
|
/** Ein abgeholter Auftrag ohne Ergebnis nach so vielen Stunden: keine Rückmeldung. */
|
|
public const AUFTRAG_STUNDEN = 2;
|
|
|
|
public function __construct(private \PDO $db)
|
|
{
|
|
}
|
|
|
|
/** @return list<array<string, mixed>> */
|
|
public function alle(?string $neueste): array
|
|
{
|
|
$zeilen = $this->db->query('SELECT * FROM installation ORDER BY domain, erste_meldung')->fetchAll();
|
|
$auftraege = [];
|
|
foreach ($this->db->query('SELECT a.* FROM auftrag a JOIN (SELECT installation, MAX(id) AS id FROM auftrag GROUP BY installation) j ON j.id = a.id')->fetchAll() as $auftrag) {
|
|
$auftraege[$auftrag['installation']] = $auftrag;
|
|
}
|
|
|
|
return array_map(fn (array $zeile): array => $this->anreichern($zeile, $auftraege[$zeile['id']] ?? null, $neueste), $zeilen);
|
|
}
|
|
|
|
/** @return array<string, mixed>|null */
|
|
public function eine(string $id, ?string $neueste): ?array
|
|
{
|
|
$abfrage = $this->db->prepare('SELECT * FROM installation WHERE id = ?');
|
|
$abfrage->execute([$id]);
|
|
$zeile = $abfrage->fetch() ?: null;
|
|
if (null === $zeile) {
|
|
return null;
|
|
}
|
|
$verlauf = $this->db->prepare('SELECT * FROM auftrag WHERE installation = ? ORDER BY id DESC LIMIT 30');
|
|
$verlauf->execute([$id]);
|
|
$auftraege = $verlauf->fetchAll();
|
|
$eine = $this->anreichern($zeile, $auftraege[0] ?? null, $neueste);
|
|
$eine['verlauf'] = array_map(fn (array $a): array => [...$a, 'zustand' => $this->auftragZustand($a)], $auftraege);
|
|
|
|
return $eine;
|
|
}
|
|
|
|
/**
|
|
* Ein Update in Auftrag geben. Gibt null zurück oder den Grund, warum nicht.
|
|
*/
|
|
public function beauftragen(string $id, string $fassung): ?string
|
|
{
|
|
$eine = $this->eine($id, $fassung);
|
|
if (null === $eine) {
|
|
return 'Diese Installation gibt es nicht.';
|
|
}
|
|
if (!$eine['kann_update']) {
|
|
return $eine['kein_update_grund'];
|
|
}
|
|
$offen = $eine['auftrag'];
|
|
if (null !== $offen && null === $offen['abgeholt'] && null === $offen['erledigt']) {
|
|
$this->db->prepare('UPDATE auftrag SET fassung = ?, erstellt = ? WHERE id = ?')->execute([$fassung, Datenbank::jetzt(), $offen['id']]);
|
|
|
|
return null;
|
|
}
|
|
$this->db->prepare('INSERT INTO auftrag (installation, aktion, fassung, erstellt) VALUES (?, ?, ?, ?)')
|
|
->execute([$id, 'update', $fassung, Datenbank::jetzt()]);
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Einen noch nicht abgeholten Auftrag zurückziehen. */
|
|
public function zurueckziehen(string $id): bool
|
|
{
|
|
$abfrage = $this->db->prepare('DELETE FROM auftrag WHERE installation = ? AND abgeholt IS NULL AND erledigt IS NULL');
|
|
$abfrage->execute([$id]);
|
|
|
|
return $abfrage->rowCount() > 0;
|
|
}
|
|
|
|
public function notiz(string $id, string $notiz): void
|
|
{
|
|
$this->db->prepare('UPDATE installation SET notiz = ? WHERE id = ?')->execute([mb_substr(trim($notiz), 0, 200), $id]);
|
|
}
|
|
|
|
public function entfernen(string $id): void
|
|
{
|
|
$this->db->prepare('DELETE FROM installation WHERE id = ?')->execute([$id]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $zeile
|
|
* @param array<string, mixed>|null $auftrag
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function anreichern(array $zeile, ?array $auftrag, ?string $neueste): array
|
|
{
|
|
$alter = time() - (int) strtotime((string) $zeile['letzte_meldung']);
|
|
$veraltet = null !== $neueste && 'unbekannt' !== $zeile['fassung'] && version_compare((string) $zeile['fassung'], $neueste, '<');
|
|
$zustand = null === $auftrag ? null : $this->auftragZustand($auftrag);
|
|
// Ein wartender Auftrag lässt sich noch ändern; ein abgeholter läuft.
|
|
$laeuft = 'laeuft' === ($zustand['art'] ?? null);
|
|
|
|
$grund = match (true) {
|
|
null === $neueste => 'Die neueste Fassung ist gerade nicht bekannt.',
|
|
!$veraltet => 'Diese Installation ist auf dem neuesten Stand.',
|
|
'git' === $zeile['art'] => 'Eine Git-Arbeitskopie wird über Git aktualisiert, nicht aus der Ferne.',
|
|
$laeuft => 'Ein Update läuft gerade.',
|
|
default => '',
|
|
};
|
|
|
|
return [
|
|
...$zeile,
|
|
'alter' => $alter,
|
|
'still' => $alter > self::STILL_STUNDEN * 3600,
|
|
'veraltet' => $veraltet,
|
|
'unbekannt' => '' === trim((string) $zeile['notiz']),
|
|
'auftrag' => $auftrag,
|
|
'auftrag_zustand' => $zustand,
|
|
'kann_update' => '' === $grund,
|
|
'kein_update_grund' => $grund,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $auftrag
|
|
*
|
|
* @return array{art: string, text: string}
|
|
*/
|
|
private function auftragZustand(array $auftrag): array
|
|
{
|
|
if (null !== $auftrag['erledigt']) {
|
|
return (int) $auftrag['ok'] === 1
|
|
? ['art' => 'ok', 'text' => sprintf('Update auf %s erledigt', $auftrag['fassung'])]
|
|
: ['art' => 'fehler', 'text' => sprintf('Update auf %s gescheitert', $auftrag['fassung'])];
|
|
}
|
|
if (null === $auftrag['abgeholt']) {
|
|
return ['art' => 'wartet', 'text' => sprintf('Update auf %s wartet auf die nächste Meldung', $auftrag['fassung'])];
|
|
}
|
|
if ((int) strtotime((string) $auftrag['abgeholt']) < time() - self::AUFTRAG_STUNDEN * 3600) {
|
|
return ['art' => 'fehler', 'text' => sprintf('Update auf %s: keine Rückmeldung', $auftrag['fassung'])];
|
|
}
|
|
|
|
return ['art' => 'laeuft', 'text' => sprintf('Update auf %s läuft', $auftrag['fassung'])];
|
|
}
|
|
}
|