Add the central site registry and remote updates

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>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-14 10:43:13 +02:00
1 parent 32cb77b72e
commit 723680b7de
39 files changed
+2355 -27

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
<?php /** @var Zentrale\Ansicht $v */ ?>
<p class="zurueck"><a href="/">← Alle Installationen</a></p>
<div class="titelzeile">
<h1><?= $v->e($i['domain']) ?></h1>
<?php if ($i['unbekannt']) { ?><span class="marke-fremd">Unbekannt</span><?php } ?>
</div>
<div class="raster">
<section class="karte" aria-labelledby="t-stand">
<h2 id="t-stand">Stand</h2>
<dl class="fakten">
<dt>Adresse</dt><dd><a href="<?= $v->e($i['adresse']) ?>" rel="noopener noreferrer"><?= $v->e($i['adresse']) ?></a></dd>
<?php if (null !== $i['domain_vorher']) { ?><dt>Vorher gemeldet als</dt><dd><?= $v->e($i['domain_vorher']) ?></dd><?php } ?>
<dt>Fassung</dt><dd><span class="fassung <?= $i['veraltet'] ? 'f-alt' : 'f-ok' ?>"><?= $v->e($i['fassung']) ?></span><?= $i['veraltet'] && null !== $neueste ? ' – neueste ist '.$v->e($neueste['version']) : '' ?></dd>
<dt>Aktualisiert über</dt><dd><?= 'git' === $i['art'] ? 'Git-Arbeitskopie' : 'Releasearchiv' ?></dd>
<dt>PHP</dt><dd><?= $v->e($i['php'] ?: '–') ?></dd>
<dt>Datenbank</dt><dd><?= $v->e($i['datenbank'] ?: '–') ?></dd>
<dt>Wartungsseite</dt><dd><?= $i['wartung'] ? 'an' : 'aus' ?></dd>
<dt>Letzte Meldung</dt><dd><?= $v->e($v->zeit($i['letzte_meldung'])) ?> (<?= $v->e($v->vor($i['alter'])) ?>)<?= $i['still'] ? ' – <strong>seit über 48 Stunden still</strong>' : '' ?></dd>
<dt>Erste Meldung</dt><dd><?= $v->e($v->zeit($i['erste_meldung'])) ?></dd>
<dt>IP-Adresse</dt><dd><?= $v->e($i['ip'] ?: '–') ?></dd>
<?php if (null !== $i['update_phase']) { ?>
<dt>Letztes Update</dt><dd><?= $v->e($i['update_meldung']) ?> <span class="klein">(<?= $v->e($v->zeit($i['update_zeit'])) ?>)</span></dd>
<?php } ?>
</dl>
</section>
<section class="karte" aria-labelledby="t-update">
<h2 id="t-update">Update</h2>
<?php if (null !== $i['auftrag_zustand']) { ?>
<p><span class="pille p-<?= $v->e($i['auftrag_zustand']['art']) ?>"><?= $v->e($i['auftrag_zustand']['text']) ?></span></p>
<?php } ?>
<?php if ('wartet' === ($i['auftrag_zustand']['art'] ?? null)) { ?>
<p>Die Website holt den Auftrag mit ihrer nächsten Meldung ab – meist innerhalb von 15 Minuten.</p>
<?php } elseif ($i['kann_update'] && null !== $neueste) { ?>
<form method="post" action="/auftrag">
<?= $v->csrf() ?>
<input type="hidden" name="id" value="<?= $v->e($i['id']) ?>">
<input type="hidden" name="zurueck" value="detail">
<p>Die Website holt den Auftrag mit ihrer nächsten Meldung ab, legt eine Sicherung an, spielt <?= $v->e($neueste['version']) ?> ein und schaltet bei einem Fehler zurück.</p>
<button type="submit" class="knopf">Auf <?= $v->e($neueste['version']) ?> aktualisieren</button>
</form>
<?php } else { ?>
<p><?= $v->e($i['kein_update_grund']) ?></p>
<?php } ?>
<?php if (null !== $i['auftrag'] && null === $i['auftrag']['abgeholt'] && null === $i['auftrag']['erledigt']) { ?>
<form method="post" action="/auftrag/zurueckziehen" class="abstand">
<?= $v->csrf() ?>
<input type="hidden" name="id" value="<?= $v->e($i['id']) ?>">
<button type="submit" class="knopf knopf-leise">Auftrag zurückziehen</button>
</form>
<?php } ?>
</section>
<section class="karte" aria-labelledby="t-kunde">
<h2 id="t-kunde">Kunde</h2>
<form method="post" action="/notiz">
<?= $v->csrf() ?>
<input type="hidden" name="id" value="<?= $v->e($i['id']) ?>">
<label for="notiz">Kunde oder Notiz</label>
<p class="feldhilfe" id="notiz-hilfe">Installationen ohne Eintrag stehen in der Übersicht als „Unbekannt“ – so fällt eine fremde Nutzung auf.</p>
<input id="notiz" name="notiz" type="text" maxlength="200" value="<?= $v->e($i['notiz']) ?>" aria-describedby="notiz-hilfe">
<button type="submit" class="knopf abstand">Speichern</button>
</form>
</section>
</div>
<section class="karte" aria-labelledby="t-verlauf">
<h2 id="t-verlauf">Aufträge</h2>
<?php if ([] === $i['verlauf']) { ?>
<p>Noch kein Auftrag.</p>
<?php } else { ?>
<div class="tabelle-rahmen">
<table class="tabelle">
<thead><tr><th scope="col">Beauftragt</th><th scope="col">Fassung</th><th scope="col">Abgeholt</th><th scope="col">Ergebnis</th></tr></thead>
<tbody>
<?php foreach ($i['verlauf'] as $a) { ?>
<tr>
<td data-label="Beauftragt"><?= $v->e($v->zeit($a['erstellt'])) ?></td>
<td data-label="Fassung"><?= $v->e($a['fassung']) ?></td>
<td data-label="Abgeholt"><?= $v->e($v->zeit($a['abgeholt'])) ?></td>
<td data-label="Ergebnis"><span class="pille p-<?= $v->e($a['zustand']['art']) ?>"><?= $v->e($a['zustand']['text']) ?></span><?php if ('' !== (string) $a['meldung']) { ?><span class="klein block"><?= $v->e($a['meldung']) ?></span><?php } ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
</section>
<details class="karte gefahr">
<summary>Eintrag entfernen</summary>
<form method="post" action="/entfernen">
<?= $v->csrf() ?>
<input type="hidden" name="id" value="<?= $v->e($i['id']) ?>">
<p>Entfernt die Installation und ihre Aufträge aus dieser Liste. An der Website ändert sich nichts; meldet sie sich wieder, erscheint sie neu.</p>
<label class="bestaetigen"><input type="checkbox" name="bestaetigt" value="ja" required> Ja, <?= $v->e($i['domain']) ?> aus der Liste entfernen</label>
<button type="submit" class="knopf knopf-gefahr">Entfernen</button>
</form>
</details>