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:
1 parent
32cb77b72e
commit
723680b7de
39 files changed
+2355
-27
No files matched your search
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Zentrale;
|
||||
|
||||
/**
|
||||
* SQLite in var/zentrale.db. Das Schema legt sich beim ersten Aufruf selbst an; spätere
|
||||
* Änderungen kommen als nummerierte Schritte in MIGRATIONEN dazu, nie als Änderung eines alten.
|
||||
*/
|
||||
final class Datenbank
|
||||
{
|
||||
private const MIGRATIONEN = [
|
||||
1 => [
|
||||
'CREATE TABLE konto (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, passwort TEXT NOT NULL, erstellt TEXT NOT NULL)',
|
||||
'CREATE TABLE anmeldeversuch (ip TEXT NOT NULL, zeit TEXT NOT NULL)',
|
||||
'CREATE INDEX anmeldeversuch_ip ON anmeldeversuch (ip, zeit)',
|
||||
'CREATE TABLE installation (
|
||||
id TEXT PRIMARY KEY,
|
||||
schluessel TEXT NOT NULL,
|
||||
adresse TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
domain_vorher TEXT,
|
||||
fassung TEXT NOT NULL,
|
||||
php TEXT NOT NULL DEFAULT \'\',
|
||||
datenbank TEXT NOT NULL DEFAULT \'\',
|
||||
art TEXT NOT NULL DEFAULT \'archiv\',
|
||||
wartung INTEGER NOT NULL DEFAULT 0,
|
||||
update_phase TEXT,
|
||||
update_meldung TEXT,
|
||||
update_zeit TEXT,
|
||||
notiz TEXT NOT NULL DEFAULT \'\',
|
||||
ip TEXT NOT NULL DEFAULT \'\',
|
||||
erste_meldung TEXT NOT NULL,
|
||||
letzte_meldung TEXT NOT NULL
|
||||
)',
|
||||
'CREATE TABLE auftrag (
|
||||
id INTEGER PRIMARY KEY,
|
||||
installation TEXT NOT NULL REFERENCES installation (id) ON DELETE CASCADE,
|
||||
aktion TEXT NOT NULL,
|
||||
fassung TEXT NOT NULL,
|
||||
erstellt TEXT NOT NULL,
|
||||
abgeholt TEXT,
|
||||
erledigt TEXT,
|
||||
ok INTEGER,
|
||||
meldung TEXT
|
||||
)',
|
||||
'CREATE INDEX auftrag_installation ON auftrag (installation, id)',
|
||||
],
|
||||
];
|
||||
|
||||
public static function oeffnen(string $datei): \PDO
|
||||
{
|
||||
$neu = !is_file($datei);
|
||||
if ($neu && !is_dir(dirname($datei)) && !@mkdir(dirname($datei), 0o775, true)) {
|
||||
throw new \RuntimeException(dirname($datei).' lässt sich nicht anlegen.');
|
||||
}
|
||||
$pdo = new \PDO('sqlite:'.$datei, null, null, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
$pdo->exec('PRAGMA busy_timeout = 5000');
|
||||
if ($neu) {
|
||||
$pdo->exec('PRAGMA journal_mode = WAL');
|
||||
}
|
||||
self::migrieren($pdo);
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
private static function migrieren(\PDO $pdo): void
|
||||
{
|
||||
$stand = (int) $pdo->query('PRAGMA user_version')->fetchColumn();
|
||||
foreach (self::MIGRATIONEN as $nummer => $anweisungen) {
|
||||
if ($nummer <= $stand) {
|
||||
continue;
|
||||
}
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
foreach ($anweisungen as $sql) {
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
$pdo->exec('PRAGMA user_version = '.$nummer);
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $fehler) {
|
||||
$pdo->rollBack();
|
||||
throw $fehler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function jetzt(): string
|
||||
{
|
||||
return gmdate('Y-m-d\TH:i:s\Z');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user