Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
207
app/Services/SystemHealthService.php
Normal file
207
app/Services/SystemHealthService.php
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class SystemHealthService
|
||||
{
|
||||
public static function report(): array
|
||||
{
|
||||
$checks = [];
|
||||
$root = dirname(__DIR__, 2);
|
||||
$db = DB::pdo();
|
||||
foreach (InstallationService::runtimeChecks() as $c) {
|
||||
$checks[] = self::check(
|
||||
$c['ok'] ? 'ok' : ($c['level'] === 'required' ? 'error' : 'warning'),
|
||||
'environment',
|
||||
$c['label'],
|
||||
$c['detail'],
|
||||
);
|
||||
}
|
||||
try {
|
||||
$integrity = (string) $db->query('PRAGMA integrity_check')->fetchColumn();
|
||||
$checks[] = self::check(
|
||||
$integrity === 'ok' ? 'ok' : 'error',
|
||||
'database',
|
||||
t('health.sqlite_integrity'),
|
||||
$integrity === 'ok' ? t('health.sqlite_ok') : $integrity,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$checks[] = self::check('error', 'database', t('health.sqlite_integrity'), $e->getMessage());
|
||||
}
|
||||
try {
|
||||
$fk = $db->query('PRAGMA foreign_key_check')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$checks[] = self::check(
|
||||
!$fk ? 'ok' : 'error',
|
||||
'database',
|
||||
t('health.foreign_keys'),
|
||||
!$fk ? t('health.no_fk_error') : t('health.fk_errors', ['count' => count($fk)]),
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$checks[] = self::check('error', 'database', t('health.foreign_keys'), $e->getMessage());
|
||||
}
|
||||
$migrationFiles = array_map('basename', glob($root . '/migrations/*.sql') ?: []);
|
||||
$applied = $db->query('SELECT name FROM _migrations')->fetchAll(PDO::FETCH_COLUMN);
|
||||
$pending = array_values(array_diff($migrationFiles, $applied));
|
||||
$checks[] = self::check(
|
||||
!$pending ? 'ok' : 'warning',
|
||||
'database',
|
||||
t('health.migrations'),
|
||||
!$pending
|
||||
? t('health.migrations_ok', ['count' => count($applied)])
|
||||
: t('health.migrations_pending', ['count' => count($pending)]),
|
||||
);
|
||||
$dbFile = $root . '/data/refuge.sqlite';
|
||||
$checks[] = self::check(
|
||||
is_file($dbFile) && is_readable($dbFile) ? 'ok' : 'error',
|
||||
'storage',
|
||||
t('health.database_file'),
|
||||
is_file($dbFile) ? self::bytes((int) filesize($dbFile)) : t('health.file_missing'),
|
||||
);
|
||||
$free = @disk_free_space($root);
|
||||
$status =
|
||||
$free === false
|
||||
? 'warning'
|
||||
: ($free >= 1_073_741_824
|
||||
? 'ok'
|
||||
: ($free >= 268_435_456
|
||||
? 'warning'
|
||||
: 'error'));
|
||||
$checks[] = self::check(
|
||||
$status,
|
||||
'storage',
|
||||
t('health.disk_space'),
|
||||
$free === false ? t('health.unknown') : self::bytes((int) $free),
|
||||
);
|
||||
$backups = BackupService::list();
|
||||
if (!$backups) {
|
||||
$checks[] = self::check('error', 'continuity', t('health.last_backup'), t('health.no_backup'));
|
||||
} else {
|
||||
$latest = $backups[0];
|
||||
$date = DateTimeImmutable::createFromFormat(
|
||||
'd/m/Y H:i:s',
|
||||
(string) $latest['date'],
|
||||
new DateTimeZone('Europe/Paris'),
|
||||
);
|
||||
$days = $date
|
||||
? (int) $date->diff(new DateTimeImmutable('now', new DateTimeZone('Europe/Paris')))->format('%a')
|
||||
: 999;
|
||||
$checks[] = self::check(
|
||||
$days <= 7 ? 'ok' : ($days <= 30 ? 'warning' : 'error'),
|
||||
'continuity',
|
||||
t('health.last_backup'),
|
||||
t('health.backup_age', ['date' => $latest['date'], 'days' => $days]),
|
||||
);
|
||||
}
|
||||
[$missing, $orphans] = self::photoConsistency($db, $root);
|
||||
$checks[] = self::check(
|
||||
$missing === 0 ? 'ok' : 'warning',
|
||||
'files',
|
||||
t('health.missing_photos'),
|
||||
t('health.file_count', ['count' => $missing]),
|
||||
);
|
||||
$checks[] = self::check(
|
||||
$orphans === 0 ? 'ok' : 'warning',
|
||||
'files',
|
||||
t('health.orphan_photos'),
|
||||
t('health.file_count', ['count' => $orphans]),
|
||||
);
|
||||
$display = (string) ini_get('display_errors');
|
||||
$production = getenv('APP_ENV') !== 'development';
|
||||
$checks[] = self::check(
|
||||
!$production || !in_array(strtolower($display), ['1', 'on', 'yes', 'true'], true) ? 'ok' : 'warning',
|
||||
'security',
|
||||
t('health.display_errors'),
|
||||
$production ? t('health.production_value', ['value' => $display ?: 'Off']) : t('health.development_mode'),
|
||||
);
|
||||
$logErrors = filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOL);
|
||||
$errorLog = trim((string) ini_get('error_log'));
|
||||
$checks[] = self::check(
|
||||
$logErrors ? 'ok' : 'warning',
|
||||
'security',
|
||||
t('health.error_log'),
|
||||
$logErrors ? ($errorLog ?: t('health.system_log')) : t('health.logging_disabled'),
|
||||
);
|
||||
$applicationLog = $root . '/storage/logs/php-errors.log';
|
||||
$recent = self::recentLogLines($applicationLog, 7);
|
||||
$checks[] = self::check(
|
||||
$recent === 0 ? 'ok' : 'warning',
|
||||
'security',
|
||||
t('health.recent_errors'),
|
||||
$recent ? t('health.recent_error_count', ['count' => $recent]) : t('health.no_recent_error'),
|
||||
);
|
||||
$summary = ['ok' => 0, 'warning' => 0, 'error' => 0];
|
||||
foreach ($checks as $c) {
|
||||
$summary[$c['status']]++;
|
||||
}
|
||||
return [
|
||||
'checks' => $checks,
|
||||
'summary' => $summary,
|
||||
'generated_at' => new DateTimeImmutable('now', new DateTimeZone('Europe/Paris'))->format('d/m/Y H:i:s'),
|
||||
'php' => PHP_VERSION,
|
||||
'sqlite' => (string) $db->query('SELECT sqlite_version()')->fetchColumn(),
|
||||
];
|
||||
}
|
||||
private static function photoConsistency(PDO $db, string $root): array
|
||||
{
|
||||
$expected = [];
|
||||
$missing = 0;
|
||||
try {
|
||||
foreach ($db->query('SELECT animal_id,filename FROM animal_photos')->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$path =
|
||||
$root .
|
||||
'/public/media/animals/' .
|
||||
(int) $row['animal_id'] .
|
||||
'/' .
|
||||
basename((string) $row['filename']);
|
||||
$expected[$path] = true;
|
||||
if (!is_file($path)) {
|
||||
$missing++;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return [0, 0];
|
||||
}
|
||||
$orphans = 0;
|
||||
foreach (glob($root . '/public/media/animals/*/*') ?: [] as $file) {
|
||||
if (is_file($file) && !isset($expected[$file]) && !str_starts_with(basename($file), '.')) {
|
||||
$orphans++;
|
||||
}
|
||||
}
|
||||
return [$missing, $orphans];
|
||||
}
|
||||
private static function check(string $status, string $group, string $title, string $detail): array
|
||||
{
|
||||
return compact('status', 'group', 'title', 'detail');
|
||||
}
|
||||
private static function recentLogLines(string $path, int $days): int
|
||||
{
|
||||
if (!is_file($path) || filesize($path) === 0) {
|
||||
return 0;
|
||||
}
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
$dates = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$dates[] = new DateTimeImmutable("-$i days")->format('d-M-Y');
|
||||
}
|
||||
$count = 0;
|
||||
foreach (array_slice($lines, -1000) as $line) {
|
||||
foreach ($dates as $date) {
|
||||
if (str_contains($line, $date)) {
|
||||
$count++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
private static function bytes(int $bytes): string
|
||||
{
|
||||
foreach (['o', 'Ko', 'Mo', 'Go', 'To'] as $unit) {
|
||||
if ($bytes < 1024 || $unit === 'To') {
|
||||
return number_format($bytes, $unit === 'o' ? 0 : 1, ',', ' ') . ' ' . $unit;
|
||||
}
|
||||
$bytes = (int) round($bytes / 1024);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue