Files
KantSites/zentrale/app/Bestand.php
T
kantarciandClaude Opus 5.5 7cdb431fd4
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
Add search, filters and pages to the central registry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:04:46 +02:00

154 lines
6.1 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();
// Natürlich sortiert und ohne www: kunde-2 vor kunde-10, www.ebsolar.de bei e.
usort($zeilen, static fn (array $a, array $b): int => strnatcasecmp(preg_replace('/^www\./', '', (string) $a['domain']), preg_replace('/^www\./', '', (string) $b['domain'])));
$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'])];
}
}