Files
Deploymentcenter/public/api/health.php
T
Deploymentcenter BotandClaude Opus 5 3b56839a1e fix(2.7.1): Auskuenfte, die stimmen - Fassung, Health, doctor, Ansicht
Der erste Durchlauf der Testinstanz hat neun Bugs zutage gefoerdert, und fast
alle waren dieselbe Sorte Fehler: eine Auskunft, die nicht stimmte.

Am folgenreichsten die Fassung. config/config.php fuehrte app.version als feste
Zeichenkette und stand seit 2.0.0 darauf, waehrend 2.7.0 lief - unbemerkt, weil
die Datei gitignored ist und in keinem Diff auftaucht. Zwei gemeldete Bugs sind
daran fehldiagnostiziert worden ("der Server ist zu alt"). src/bootstrap.php
leitet die Fassung jetzt aus "current" in public/docs/changelog.json ab.

Im Einzelnen:

- #346 Fassung aus dem Changelog statt aus der Konfiguration
- #60  /api/health ueberlebt eine leere Datenbank: eigener Check "schema",
       healthy=false mit Begruendung statt HTTP 500
- #54  dc-testkit doctor prueft Mindestfassung (semantisch) und Rechte, statt
       sie nur anzuzeigen - dafuer neu: GET /api/tokens/v1/whoami
- #53  dc-testkit.sh unter /testkit/ erreichbar (Route fehlte, der
       dokumentierte Bezugsweg hat nie funktioniert) und mit LF ausgeliefert
       (deploy.py/deploy_dev.py entfernen CR bei .sh/.bash beim Upload)
- #288 Filter und Formulare werfen nicht mehr aufs Dashboard zurueck: die
       offene Ansicht wandert bei GET als "view" in den Query-String, und die
       Filter loesen ueber requestSubmit() aus - form.submit() erzeugt kein
       submit-Ereignis, weshalb der zentrale Zuhoerer dort nie lief
- #347 /api/bugtracker/v1/roadmap auch ohne .php erreichbar
- #55  Fassungshinweis in TESTUMGEBUNG.md 4.6 (serverseitig war das mit 2.6.0
       bereits behoben)

Nicht geprueft: php -l. Auf dem Arbeitsplatz ist kein PHP installiert; die
Abnahme laeuft ueber die Testinstanz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:06:33 +02:00

166 lines
5.7 KiB
PHP

<?php
/**
* GET /api/health
*
* Verfuegbarkeitspruefung fuer Monitoring und Agenten.
*
* Ohne Authentifizierung wird nur der Gesamtzustand gemeldet. Mit gueltigem
* Token, Shared Key oder angemeldeter Sitzung kommen Schema-Version,
* ausstehende Migrationen und Kennzahlen dazu.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../src/bootstrap.php';
use Deploymentcenter\Core\ApiAuth;
use Deploymentcenter\Core\Config;
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Http;
use Deploymentcenter\Core\Logger;
use Deploymentcenter\Core\Migrator;
use Deploymentcenter\Modules\Bugtracker\BugRepo;
Http::beginJson(['GET', 'OPTIONS'], true);
$checks = [];
$healthy = true;
// --- Datenbank ---
$db = null;
try {
$started = microtime(true);
$db = Db::init();
$db->query('SELECT 1')->fetchColumn();
$checks['database'] = [
'ok' => true,
'latency_ms' => (int)round((microtime(true) - $started) * 1000),
];
} catch (Throwable $e) {
Logger::error('Health-Check: Datenbank nicht erreichbar', ['error' => $e->getMessage()]);
$checks['database'] = ['ok' => false, 'error' => 'nicht erreichbar'];
$healthy = false;
}
// --- Schreibbarkeit des Log-Verzeichnisses ---
$logDir = DC_VAR . '/log';
$checks['log_writable'] = ['ok' => is_dir($logDir) ? is_writable($logDir) : is_writable(DC_ROOT)];
// --- Schema vorhanden? ---
// Dass die Verbindung steht, heisst noch nicht, dass die Migrationen gelaufen
// sind: SELECT 1 gelingt auch auf einer leeren Datenbank. Vor der ersten
// Migration gibt es dc_tokens nicht - und daran starb die Rechtepruefung
// weiter unten mit einer PDOException. Ausgerechnet der Endpunkt, der ueber
// kaputte Zustaende Auskunft geben soll, antwortete dann mit HTTP 500 statt
// mit healthy=false und einer Begruendung. Betroffen war jede Neuinstallation
// zwischen Deployment und erster Migration.
$schemaReady = false;
if ($db !== null) {
try {
$schemaReady = $db->query("SHOW TABLES LIKE 'dc_tokens'")->fetchColumn() !== false;
} catch (Throwable $e) {
Logger::error('Health-Check: Schemapruefung fehlgeschlagen', ['error' => $e->getMessage()]);
}
$checks['schema'] = $schemaReady
? ['ok' => true]
: [
'ok' => false,
'message' => 'Datenbankschema fehlt - die Migrationen wurden noch nicht ausgefuehrt (install_db.php aufrufen).',
];
if (!$schemaReady) {
$healthy = false;
}
}
$response = [
'healthy' => $healthy,
'app' => Config::get('app.name', 'Deploymentcenter'),
'version' => Config::get('app.version', 'unknown'),
'time_utc' => gmdate('c'),
'checks' => $checks,
];
// --- Detailinformationen nur fuer Authentifizierte ---
// Die Rechtepruefung selbst bleibt zusaetzlich abgesichert: ein halb
// migriertes Schema besteht die Pruefung oben, kann hier aber trotzdem ueber
// eine fehlende Spalte stolpern. Ein Fehlschlag wird wie "nicht
// authentifiziert" behandelt - dann kommt die Kurzfassung heraus, und die
// Antwort bleibt auswertbar.
$authenticated = false;
if ($db !== null && $schemaReady) {
try {
$authenticated = ApiAuth::resolve($db, 'bugtracker:read') !== null;
} catch (Throwable $e) {
Logger::warning('Health-Check: Rechtepruefung nicht moeglich', ['error' => $e->getMessage()]);
$response['checks']['schema'] = [
'ok' => false,
'message' => 'Datenbankschema unvollstaendig - die Rechtepruefung war nicht moeglich.',
];
$response['healthy'] = false;
}
}
if ($authenticated) {
try {
$status = Migrator::status($db);
$response['schema'] = [
'applied_count' => count($status['applied']),
'pending' => $status['pending'],
];
if ($status['pending'] !== []) {
$response['healthy'] = false;
$response['checks']['migrations'] = [
'ok' => false,
'message' => 'Ausstehende Migrationen: ' . implode(', ', $status['pending']),
];
} else {
$response['checks']['migrations'] = ['ok' => true];
}
$repo = new BugRepo($db);
$response['bugtracker'] = $repo->getStats();
$monitors = $db->query('
SELECT state, COUNT(*) AS total
FROM watchdog_monitors
GROUP BY state
')->fetchAll() ?: [];
$byState = [];
foreach ($monitors as $row) {
$byState[(string)$row['state']] = (int)$row['total'];
}
$response['watchdog'] = ['monitors_by_state' => $byState];
$lastRun = $db->query("
SELECT last_run_utc FROM watchdog_cron_jobs WHERE name = 'evaluator'
")->fetchColumn();
$response['watchdog']['evaluator_last_run_utc'] = $lastRun !== false ? $lastRun : null;
// Laeuft der Evaluator nicht, sind alle Monitor-Zustaende wertlos.
if ($lastRun === false || $lastRun === null) {
$response['checks']['evaluator'] = [
'ok' => false,
'message' => 'Der Evaluator lief noch nie. Cron-Job auf /api/watchdog/v1/evaluate einrichten.',
];
} else {
$age = time() - (int)strtotime((string)$lastRun . ' UTC');
$response['checks']['evaluator'] = [
'ok' => $age < 900,
'age_seconds' => $age,
'message' => $age < 900 ? null : 'Letzter Lauf liegt zu lange zurueck.',
];
}
} catch (Throwable $e) {
Logger::warning('Health-Check: Detailabfrage fehlgeschlagen', ['error' => $e->getMessage()]);
$response['detail_error'] = 'Detailinformationen konnten nicht ermittelt werden.';
}
}
Http::ok($response, $response['healthy'] ? 200 : 503);