mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
admin: backup logs via parameters.php, nextcloud secondary backup
- surface backup/cleanup cron logs + backup freshness status - email xamxam@erg.be when SQLite backups go stale (backup watchdog) - sync SQLite snapshots to Nextcloud WebDAV + remote-freshness watchdog - precise retention pruning, manual sync in check recipe, and Nextcloud-sync docs
This commit is contained in:
@@ -26,6 +26,8 @@ class SystemController
|
||||
'admin' => ['label' => 'Admin — actions', 'path' => null, 'json' => true],
|
||||
'error' => ['label' => 'Erreurs — application', 'path' => null, 'json' => true],
|
||||
'audit' => ['label' => 'Audit — données', 'path' => null, 'json' => true],
|
||||
'backup' => ['label' => 'Sauvegardes — SQLite', 'path' => null, 'json' => false, 'cron' => 'xamxam-backup'],
|
||||
'cleanup' => ['label' => 'Nettoyage — brouillons', 'path' => null, 'json' => false, 'cron' => 'xamxam-cleanup'],
|
||||
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam-nginx-access.log', 'json' => false],
|
||||
'nginx_error' => ['label' => 'nginx — erreurs','path' => '/var/log/nginx/xamxam-nginx-error.log', 'json' => false],
|
||||
];
|
||||
@@ -33,10 +35,11 @@ class SystemController
|
||||
/**
|
||||
* Resolve a log file path — app logs live under /var/log/xamxam in
|
||||
* production (and storage/logs in dev/cli-server); nginx logs have
|
||||
* hard-coded paths (only valid in production).
|
||||
* hard-coded paths (only valid in production); cron logs (backup, cleanup)
|
||||
* live directly under /var/log with the date in the filename.
|
||||
*
|
||||
* A $date (YYYY-MM-DD) selects a specific retained daily file for app
|
||||
* channels; when null the most recent file is returned.
|
||||
* A $date (YYYY-MM-DD) selects a specific retained daily file for app and
|
||||
* cron channels; when null the most recent file is returned.
|
||||
*/
|
||||
private static function resolveLogPath(string $tab, ?string $date = null): string
|
||||
{
|
||||
@@ -44,6 +47,25 @@ class SystemController
|
||||
if ($def['path'] !== null) {
|
||||
return $def['path'];
|
||||
}
|
||||
|
||||
// Cron logs: /var/log/xamxam-{cron}-YYYY-MM-DD.log — the date is part
|
||||
// of the filename (set by the cron job), not a Monolog rotation suffix.
|
||||
// These only exist in production; in dev there is no sensible fallback.
|
||||
if (isset($def['cron'])) {
|
||||
$base = '/var/log/' . $def['cron'];
|
||||
if ($date !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return $base . '-' . $date . '.log';
|
||||
}
|
||||
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
|
||||
if (!empty($dated)) {
|
||||
rsort($dated); // newest first
|
||||
return $dated[0];
|
||||
}
|
||||
// No log yet — return today's expected path so the caller can show
|
||||
// an empty-state (file_exists() will be false).
|
||||
return $base . '-' . date('Y-m-d') . '.log';
|
||||
}
|
||||
|
||||
// App logs: /var/log/xamxam/xamxam-{channel}.log (production) or
|
||||
// storage/logs/xamxam-{channel}.log (dev / cli-server). Monolog
|
||||
// RotatingFileHandler uses this as base name; the current log is always
|
||||
@@ -77,14 +99,16 @@ class SystemController
|
||||
public static function listLogDates(string $tab): array
|
||||
{
|
||||
$def = self::LOG_FILES[$tab];
|
||||
if (($def['json'] ?? false) !== true) {
|
||||
if (isset($def['cron'])) {
|
||||
$base = '/var/log/' . $def['cron'];
|
||||
} elseif (($def['json'] ?? false) === true) {
|
||||
$dir = php_sapi_name() === 'cli-server'
|
||||
? APP_ROOT . '/storage/logs'
|
||||
: '/var/log/xamxam';
|
||||
$base = $dir . '/xamxam-' . $tab;
|
||||
} else {
|
||||
return []; // nginx logs are fixed-name, not daily
|
||||
}
|
||||
|
||||
$dir = php_sapi_name() === 'cli-server'
|
||||
? APP_ROOT . '/storage/logs'
|
||||
: '/var/log/xamxam';
|
||||
$base = $dir . '/xamxam-' . $tab;
|
||||
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
|
||||
if (empty($dated)) {
|
||||
return [];
|
||||
@@ -229,12 +253,12 @@ class SystemController
|
||||
// App logs are rotated by Monolog; a missing file just means no
|
||||
// events have been logged yet. Show a friendly empty-state message
|
||||
// instead of a scary "fichier introuvable" error.
|
||||
if ($isJson) {
|
||||
if ($isJson || isset(self::LOG_FILES[$tab]['cron'])) {
|
||||
return [
|
||||
'lines' => [],
|
||||
'error' => null,
|
||||
'meta' => null,
|
||||
'isJson' => true,
|
||||
'isJson' => $isJson,
|
||||
'notYet' => true,
|
||||
];
|
||||
}
|
||||
@@ -536,6 +560,9 @@ class SystemController
|
||||
: 'Non accessible en écriture',
|
||||
];
|
||||
|
||||
// SQLite backups — surface staleness so admins catch a broken cron.
|
||||
$checks['backup'] = $this->backupStatus();
|
||||
|
||||
// Maintenance mode
|
||||
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
|
||||
$checks['maintenance'] = [
|
||||
@@ -547,6 +574,73 @@ class SystemController
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the freshness of SQLite backups in /var/backups/xamxam.
|
||||
*
|
||||
* Returns a status-check entry suitable for the system page. The hourly
|
||||
* cron should produce a fresh snapshot at least every ~2 hours, so we
|
||||
* flag a warning past that and an error past a full day.
|
||||
*/
|
||||
private function backupStatus(): array
|
||||
{
|
||||
$dir = '/var/backups/xamxam';
|
||||
$label = 'Sauvegardes SQLite';
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
// In dev (cli-server / local) the backup cron is not set up, so a
|
||||
// missing directory is expected — show inactive rather than failed.
|
||||
$isDev = php_sapi_name() === 'cli-server';
|
||||
return [
|
||||
'label' => $label,
|
||||
'status' => $isDev ? 'inactive' : 'failed',
|
||||
'detail' => $isDev
|
||||
? 'Cron de sauvegarde non configuré (environnement de dev)'
|
||||
: 'Dossier /var/backups/xamxam introuvable — cron non déployé ?',
|
||||
];
|
||||
}
|
||||
|
||||
$files = glob($dir . '/db-*.db.gz');
|
||||
if (empty($files)) {
|
||||
return [
|
||||
'label' => $label,
|
||||
'status' => 'warn',
|
||||
'detail' => 'Aucune sauvegarde trouvée dans /var/backups/xamxam',
|
||||
];
|
||||
}
|
||||
|
||||
// Newest backup by mtime
|
||||
$newest = null;
|
||||
$newestMtime = 0;
|
||||
foreach ($files as $f) {
|
||||
$m = filemtime($f);
|
||||
if ($m > $newestMtime) {
|
||||
$newestMtime = $m;
|
||||
$newest = $f;
|
||||
}
|
||||
}
|
||||
|
||||
$ageSec = time() - $newestMtime;
|
||||
$ageHours = (int) round($ageSec / 3600.0);
|
||||
$count = count($files);
|
||||
$human = $ageHours < 1
|
||||
? 'moins d\'une heure'
|
||||
: ($ageHours === 1 ? '1 heure' : "$ageHours heures");
|
||||
|
||||
if ($ageSec > 86400) {
|
||||
$status = 'failed';
|
||||
} elseif ($ageSec > 7200) {
|
||||
$status = 'warn';
|
||||
} else {
|
||||
$status = 'active';
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => $label,
|
||||
'status' => $status,
|
||||
'detail' => "$count fichier(s) — dernière il y a $human",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the tail of a log file, newest-first. Returns null on error.
|
||||
*
|
||||
|
||||
@@ -72,8 +72,16 @@
|
||||
|
||||
<?php elseif (!empty($notYet)): ?>
|
||||
<div class="log-empty">
|
||||
Aucune entrée pour le moment.<br>
|
||||
<span class="log-unavail-path">Le journal sera créé automatiquement au premier événement.</span>
|
||||
<?php if (isset(SystemController::LOG_FILES[$activeTab]['cron'])): ?>
|
||||
Aucun journal de cette tâche pour le moment.<br>
|
||||
<span class="log-unavail-path">
|
||||
Vérifiez que la tâche cron est installée (<code>just deploy-backup-cron</code> / <code>just deploy-cleanup-cron</code>)
|
||||
et qu'elle s'est bien exécutée. Le journal apparaîtra automatiquement à la prochaine exécution.
|
||||
</span>
|
||||
<?php else: ?>
|
||||
Aucune entrée pour le moment.<br>
|
||||
<span class="log-unavail-path">Le journal sera créé automatiquement au premier événement.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif (empty($logLines)): ?>
|
||||
|
||||
Reference in New Issue
Block a user