176 lines
9.3 KiB
PHP
176 lines
9.3 KiB
PHP
<?php
|
|
/**
|
|
* includes/status_page.php
|
|
*
|
|
* Statusseite für Kunden (status.php): Zustand der Dienste („Komponenten“) und Meldungen zu Störungen und Wartungen.
|
|
* Verwaltet wird sie im Mitarbeiterbereich (Adminportal → Statusseite) und über die Apps.
|
|
*/
|
|
|
|
function status_page_enabled(): bool
|
|
{
|
|
return get_setting('status_page_enabled', '0') === '1';
|
|
}
|
|
|
|
/** Zustände einer Komponente, vom besten zum schlechtesten. Schlüssel => [Beschriftung, Farbe, Rang]. */
|
|
function status_component_states(): array
|
|
{
|
|
return [
|
|
'operational' => ['In Betrieb', '#157a45', 0],
|
|
'maintenance' => ['Wartung', '#2563eb', 1],
|
|
'degraded' => ['Eingeschränkt', '#b45309', 2],
|
|
'outage' => ['Ausfall', '#dc2626', 3],
|
|
];
|
|
}
|
|
|
|
/** Stufen einer Störungsmeldung. */
|
|
function status_incident_states(): array
|
|
{
|
|
return [
|
|
'investigating' => 'Wird untersucht',
|
|
'identified' => 'Ursache gefunden',
|
|
'monitoring' => 'Wird beobachtet',
|
|
'resolved' => 'Behoben',
|
|
];
|
|
}
|
|
|
|
function status_components(PDO $pdo): array
|
|
{
|
|
return $pdo->query('SELECT * FROM status_components ORDER BY sort_order, name')->fetchAll();
|
|
}
|
|
|
|
/** Gesamtzustand: der schlechteste Zustand aller Komponenten und offener Störungen. @return array{key:string,label:string,color:string} */
|
|
function status_overall(PDO $pdo): array
|
|
{
|
|
$states = status_component_states();
|
|
$worst = 'operational';
|
|
foreach (status_components($pdo) as $c) {
|
|
if (($states[$c['status']][2] ?? 0) > $states[$worst][2]) $worst = $c['status'];
|
|
}
|
|
// Eine offene Störung ohne zugeordnete Komponente zählt mindestens als „eingeschränkt“
|
|
$open = (int) $pdo->query("SELECT COUNT(*) FROM status_incidents WHERE status <> 'resolved'")->fetchColumn();
|
|
if ($open > 0 && $states[$worst][2] < $states['degraded'][2] && $worst !== 'maintenance') $worst = 'degraded';
|
|
return ['key' => $worst, 'label' => $worst === 'operational' ? 'Alle Systeme laufen' : ($worst === 'maintenance' ? 'Wartungsarbeiten' : ($worst === 'outage' ? 'Ausfall' : 'Teilweise eingeschränkt')), 'color' => $states[$worst][1]];
|
|
}
|
|
|
|
/** Offene Störungen und die der letzten Tage (neueste zuerst), jeweils mit ihren Updates. */
|
|
function status_incidents(PDO $pdo, int $days = 14, bool $onlyOpen = false): array
|
|
{
|
|
$since = date('Y-m-d H:i:s', time() - max(1, $days) * 86400);
|
|
$sql = "SELECT i.*, c.name AS component_name FROM status_incidents i LEFT JOIN status_components c ON c.id = i.component_id WHERE "
|
|
. ($onlyOpen ? "i.status <> 'resolved'" : "(i.status <> 'resolved' OR i.created_at >= ?)") . ' ORDER BY i.created_at DESC, i.id DESC LIMIT 50';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($onlyOpen ? [] : [$since]);
|
|
$rows = $stmt->fetchAll();
|
|
$up = $pdo->prepare('SELECT * FROM status_incident_updates WHERE incident_id = ? ORDER BY created_at DESC, id DESC');
|
|
foreach ($rows as &$r) {
|
|
$up->execute([$r['id']]);
|
|
$r['updates'] = $up->fetchAll();
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
function status_set_component(PDO $pdo, int $id, string $status): bool
|
|
{
|
|
if (!isset(status_component_states()[$status])) return false;
|
|
$s = $pdo->prepare('UPDATE status_components SET status = ? WHERE id = ?');
|
|
$s->execute([$status, $id]);
|
|
return $s->rowCount() > 0 || (bool) $pdo->query('SELECT 1 FROM status_components WHERE id = ' . (int) $id)->fetchColumn();
|
|
}
|
|
|
|
function status_save_component(PDO $pdo, ?int $id, string $name, string $description, int $sort): ?int
|
|
{
|
|
$name = trim(mb_substr($name, 0, 120));
|
|
if ($name === '') return null;
|
|
$description = trim(mb_substr($description, 0, 255));
|
|
if ($id) {
|
|
$pdo->prepare('UPDATE status_components SET name = ?, description = ?, sort_order = ? WHERE id = ?')->execute([$name, $description !== '' ? $description : null, $sort, $id]);
|
|
return $id;
|
|
}
|
|
$pdo->prepare('INSERT INTO status_components (name, description, status, sort_order, created_at) VALUES (?,?,?,?,?)')->execute([$name, $description !== '' ? $description : null, 'operational', $sort, db_now()]);
|
|
return (int) $pdo->lastInsertId();
|
|
}
|
|
|
|
function status_delete_component(PDO $pdo, int $id): void
|
|
{
|
|
$pdo->prepare('UPDATE status_incidents SET component_id = NULL WHERE component_id = ?')->execute([$id]);
|
|
$pdo->prepare('DELETE FROM status_components WHERE id = ?')->execute([$id]);
|
|
}
|
|
|
|
/**
|
|
* Neue Störungs- oder Wartungsmeldung. Mit $componentState wird die zugeordnete Komponente gleich auf diesen Zustand gesetzt.
|
|
* @return array{ok:bool,id?:int,error?:string}
|
|
*/
|
|
function status_create_incident(PDO $pdo, string $title, string $body, string $status, ?int $componentId, ?string $componentState, ?int $staffId): array
|
|
{
|
|
$title = trim(mb_substr($title, 0, 200));
|
|
if ($title === '') return ['ok' => false, 'error' => 'Bitte einen Titel angeben.'];
|
|
if (!isset(status_incident_states()[$status])) $status = 'investigating';
|
|
if ($componentId !== null) {
|
|
$c = $pdo->prepare('SELECT COUNT(*) FROM status_components WHERE id = ?');
|
|
$c->execute([$componentId]);
|
|
if (!(int) $c->fetchColumn()) $componentId = null;
|
|
}
|
|
$now = db_now();
|
|
$pdo->prepare('INSERT INTO status_incidents (title, body, status, impact, component_id, created_by, created_at, updated_at, resolved_at) VALUES (?,?,?,?,?,?,?,?,?)')
|
|
->execute([$title, trim($body) !== '' ? trim($body) : null, $status, 'minor', $componentId, $staffId, $now, $now, $status === 'resolved' ? $now : null]);
|
|
$id = (int) $pdo->lastInsertId();
|
|
if (trim($body) !== '') {
|
|
$pdo->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')->execute([$id, $status, trim($body), $staffId, $now]);
|
|
}
|
|
if ($componentId !== null && $componentState !== null && isset(status_component_states()[$componentState])) status_set_component($pdo, $componentId, $componentState);
|
|
return ['ok' => true, 'id' => $id];
|
|
}
|
|
|
|
/**
|
|
* Update zu einer Meldung. Wird sie als „Behoben“ markiert, setzt die zugeordnete Komponente zurück auf „In Betrieb“, wenn $restore true ist.
|
|
* @return array{ok:bool,error?:string}
|
|
*/
|
|
function status_update_incident(PDO $pdo, int $id, string $status, string $body, ?int $staffId, bool $restore = true): array
|
|
{
|
|
if (!isset(status_incident_states()[$status])) return ['ok' => false, 'error' => 'Unbekannter Status.'];
|
|
$s = $pdo->prepare('SELECT * FROM status_incidents WHERE id = ?');
|
|
$s->execute([$id]);
|
|
$inc = $s->fetch();
|
|
if (!$inc) return ['ok' => false, 'error' => 'Meldung nicht gefunden.'];
|
|
$now = db_now();
|
|
$pdo->prepare('UPDATE status_incidents SET status = ?, updated_at = ?, resolved_at = ? WHERE id = ?')
|
|
->execute([$status, $now, $status === 'resolved' ? ($inc['resolved_at'] ?: $now) : null, $id]);
|
|
$pdo->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')
|
|
->execute([$id, $status, trim(mb_substr($body, 0, 4000)), $staffId, $now]);
|
|
if ($status === 'resolved' && $restore && $inc['component_id']) {
|
|
// Nur zurücksetzen, wenn keine andere offene Meldung dieselbe Komponente betrifft
|
|
$o = $pdo->prepare("SELECT COUNT(*) FROM status_incidents WHERE component_id = ? AND status <> 'resolved' AND id <> ?");
|
|
$o->execute([$inc['component_id'], $id]);
|
|
if (!(int) $o->fetchColumn()) status_set_component($pdo, (int) $inc['component_id'], 'operational');
|
|
}
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function status_delete_incident(PDO $pdo, int $id): void
|
|
{
|
|
$pdo->prepare('DELETE FROM status_incident_updates WHERE incident_id = ?')->execute([$id]);
|
|
$pdo->prepare('DELETE FROM status_incidents WHERE id = ?')->execute([$id]);
|
|
}
|
|
|
|
/** Zusammenfassung für die Apps. */
|
|
function status_summary(PDO $pdo): array
|
|
{
|
|
$states = status_component_states();
|
|
$overall = status_overall($pdo);
|
|
return [
|
|
'enabled' => status_page_enabled(),
|
|
'title' => (string) get_setting('status_page_title', 'Systemstatus'),
|
|
'url' => base_url() . '/status.php',
|
|
'overall' => $overall,
|
|
'componentStates' => array_map(fn($k, $v) => ['key' => $k, 'label' => $v[0]], array_keys($states), $states),
|
|
'incidentStates' => array_map(fn($k, $v) => ['key' => $k, 'label' => $v], array_keys(status_incident_states()), status_incident_states()),
|
|
'components' => array_map(fn($c) => ['id' => (int) $c['id'], 'name' => (string) $c['name'], 'description' => (string) ($c['description'] ?? ''), 'status' => (string) $c['status'], 'statusLabel' => $states[$c['status']][0] ?? $c['status']], status_components($pdo)),
|
|
'incidents' => array_map(fn($i) => [
|
|
'id' => (int) $i['id'], 'title' => (string) $i['title'], 'status' => (string) $i['status'], 'statusLabel' => status_incident_states()[$i['status']] ?? $i['status'],
|
|
'component' => $i['component_name'] ?? null, 'componentId' => $i['component_id'] !== null ? (int) $i['component_id'] : null,
|
|
'createdAt' => (string) $i['created_at'], 'updatedAt' => (string) $i['updated_at'], 'resolvedAt' => $i['resolved_at'],
|
|
'updates' => array_map(fn($u) => ['status' => (string) $u['status'], 'statusLabel' => status_incident_states()[$u['status']] ?? $u['status'], 'body' => (string) ($u['body'] ?? ''), 'createdAt' => (string) $u['created_at']], $i['updates']),
|
|
], status_incidents($pdo, 14)),
|
|
];
|
|
}
|