mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
logs: standardise log filenames to xamxam-{service}-{date}.log
This commit is contained in:
@@ -53,17 +53,24 @@ if ($activeTab === 'status' || !array_key_exists($activeTab, SystemController::L
|
|||||||
$activeTab = 'app';
|
$activeTab = 'app';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional date selection (YYYY-MM-DD) for daily app logs.
|
||||||
|
$selectedDate = $_GET['date'] ?? null;
|
||||||
|
if ($selectedDate !== null && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $selectedDate)) {
|
||||||
|
$selectedDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
||||||
if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
||||||
$selectedN = 100;
|
$selectedN = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
$logData = $_controller->getLogData($activeTab, $selectedN);
|
$logData = $_controller->getLogData($activeTab, $selectedN, $selectedDate);
|
||||||
$logLines = $logData['lines'];
|
$logLines = $logData['lines'];
|
||||||
$logError = $logData['error'];
|
$logError = $logData['error'];
|
||||||
$logFileMeta = $logData['meta'];
|
$logFileMeta = $logData['meta'];
|
||||||
$logIsJson = $logData['isJson'] ?? false;
|
$logIsJson = $logData['isJson'] ?? false;
|
||||||
$notYet = $logData['notYet'] ?? false;
|
$notYet = $logData['notYet'] ?? false;
|
||||||
|
$logDates = SystemController::listLogDates($activeTab);
|
||||||
|
|
||||||
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
||||||
$statusInitiallyCollapsed = $collapsed === '1';
|
$statusInitiallyCollapsed = $collapsed === '1';
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
|||||||
$selectedN = 100;
|
$selectedN = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional date selection (YYYY-MM-DD) for daily app logs.
|
||||||
|
$selectedDate = $_GET['date'] ?? null;
|
||||||
|
if ($selectedDate !== null && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $selectedDate)) {
|
||||||
|
$selectedDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
header('X-Robots-Tag: noindex');
|
header('X-Robots-Tag: noindex');
|
||||||
|
|
||||||
@@ -42,11 +48,12 @@ $_cache = new SystemCache($_db->getPDO());
|
|||||||
$_controller = new SystemController($_db, $_cache);
|
$_controller = new SystemController($_db, $_cache);
|
||||||
|
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
$logData = $_controller->getLogData($activeTab, $selectedN);
|
$logData = $_controller->getLogData($activeTab, $selectedN, $selectedDate);
|
||||||
$logLines = $logData['lines'];
|
$logLines = $logData['lines'];
|
||||||
$logError = $logData['error'];
|
$logError = $logData['error'];
|
||||||
$logFileMeta = $logData['meta'];
|
$logFileMeta = $logData['meta'];
|
||||||
$logIsJson = $logData['isJson'] ?? false;
|
$logIsJson = $logData['isJson'] ?? false;
|
||||||
$notYet = $logData['notYet'] ?? false;
|
$notYet = $logData['notYet'] ?? false;
|
||||||
|
$logDates = SystemController::listLogDates($activeTab);
|
||||||
|
|
||||||
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
|
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
/**
|
/**
|
||||||
* Admin audit logger.
|
* Admin audit logger.
|
||||||
*
|
*
|
||||||
* Writes JSON-lines to /var/log/xamxam.log (production) or
|
* Writes JSON-lines to /var/log/xamxam/xamxam-admin-YYYY-MM-DD.log (production)
|
||||||
* storage/logs/admin.log (dev / cli-server).
|
* or storage/logs/xamxam-admin.log (dev / cli-server).
|
||||||
*
|
*
|
||||||
* Each entry: timestamp, actor (admin IP/UA), action, resource, status, context.
|
* Each entry: timestamp, actor (admin IP/UA), action, resource, status, context.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -26,25 +26,39 @@ class SystemController
|
|||||||
'admin' => ['label' => 'Admin — actions', 'path' => null, 'json' => true],
|
'admin' => ['label' => 'Admin — actions', 'path' => null, 'json' => true],
|
||||||
'error' => ['label' => 'Erreurs — application', 'path' => null, 'json' => true],
|
'error' => ['label' => 'Erreurs — application', 'path' => null, 'json' => true],
|
||||||
'audit' => ['label' => 'Audit — données', 'path' => null, 'json' => true],
|
'audit' => ['label' => 'Audit — données', 'path' => null, 'json' => true],
|
||||||
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log', 'json' => false],
|
'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_error.log', 'json' => false],
|
'nginx_error' => ['label' => 'nginx — erreurs','path' => '/var/log/nginx/xamxam-nginx-error.log', 'json' => false],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a log file path — app logs live under STORAGE_ROOT, system logs
|
* Resolve a log file path — app logs live under /var/log/xamxam in
|
||||||
* have hard-coded paths (only valid in production).
|
* production (and storage/logs in dev/cli-server); nginx logs have
|
||||||
|
* hard-coded paths (only valid in production).
|
||||||
|
*
|
||||||
|
* A $date (YYYY-MM-DD) selects a specific retained daily file for app
|
||||||
|
* channels; when null the most recent file is returned.
|
||||||
*/
|
*/
|
||||||
private static function resolveLogPath(string $tab): string
|
private static function resolveLogPath(string $tab, ?string $date = null): string
|
||||||
{
|
{
|
||||||
$def = self::LOG_FILES[$tab];
|
$def = self::LOG_FILES[$tab];
|
||||||
if ($def['path'] !== null) {
|
if ($def['path'] !== null) {
|
||||||
return $def['path'];
|
return $def['path'];
|
||||||
}
|
}
|
||||||
// App logs: storage/logs/{channel}.log (Monolog RotatingFileHandler uses
|
// App logs: /var/log/xamxam/xamxam-{channel}.log (production) or
|
||||||
// this as base name; the current log is always at {channel}-YYYY-MM-DD.log)
|
// storage/logs/xamxam-{channel}.log (dev / cli-server). Monolog
|
||||||
$dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : APP_ROOT . '/storage/logs';
|
// RotatingFileHandler uses this as base name; the current log is always
|
||||||
|
// at xamxam-{channel}-YYYY-MM-DD.log.
|
||||||
|
$dir = php_sapi_name() === 'cli-server'
|
||||||
|
? APP_ROOT . '/storage/logs'
|
||||||
|
: '/var/log/xamxam';
|
||||||
|
$base = $dir . '/xamxam-' . $tab;
|
||||||
|
|
||||||
|
// Explicit date selection (validated YYYY-MM-DD)
|
||||||
|
if ($date !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||||
|
return $base . '-' . $date . '.log';
|
||||||
|
}
|
||||||
|
|
||||||
// Find the most recent dated log file for this channel
|
// Find the most recent dated log file for this channel
|
||||||
$base = $dir . '/' . $tab;
|
|
||||||
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
|
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
|
||||||
if (!empty($dated)) {
|
if (!empty($dated)) {
|
||||||
rsort($dated); // newest first
|
rsort($dated); // newest first
|
||||||
@@ -54,6 +68,49 @@ class SystemController
|
|||||||
return $base . '.log';
|
return $base . '.log';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the retained daily log files (dates) for an app channel.
|
||||||
|
*
|
||||||
|
* Returns [['date' => 'YYYY-MM-DD', 'label' => '...'], …] newest-first.
|
||||||
|
* Empty array for system logs (nginx) which have fixed names.
|
||||||
|
*/
|
||||||
|
public static function listLogDates(string $tab): array
|
||||||
|
{
|
||||||
|
$def = self::LOG_FILES[$tab];
|
||||||
|
if (($def['json'] ?? false) !== true) {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates = [];
|
||||||
|
foreach ($dated as $path) {
|
||||||
|
if (preg_match('/(\d{4}-\d{2}-\d{2})\.log$/', $path, $m)) {
|
||||||
|
$dates[] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rsort($dates); // newest first
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($dates as $d) {
|
||||||
|
$isToday = $d === date('Y-m-d');
|
||||||
|
$out[] = [
|
||||||
|
'date' => $d,
|
||||||
|
'label' => $isToday
|
||||||
|
? 'Aujourd\'hui (' . date('d/m/Y', strtotime($d)) . ')'
|
||||||
|
: date('d/m/Y', strtotime($d)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
public const ALLOWED_LINES = [50, 100, 200, 500];
|
public const ALLOWED_LINES = [50, 100, 200, 500];
|
||||||
|
|
||||||
// ── TTLs ──────────────────────────────────────────────────────────────────
|
// ── TTLs ──────────────────────────────────────────────────────────────────
|
||||||
@@ -157,11 +214,13 @@ class SystemController
|
|||||||
/**
|
/**
|
||||||
* Read and return data for a log tab.
|
* Read and return data for a log tab.
|
||||||
*
|
*
|
||||||
|
* @param string|null $date YYYY-MM-DD to select a specific daily file
|
||||||
|
* (app channels); null = most recent.
|
||||||
* @return array{lines: ?array, error: ?string, meta: ?array}
|
* @return array{lines: ?array, error: ?string, meta: ?array}
|
||||||
*/
|
*/
|
||||||
public function getLogData(string $tab, int $n): array
|
public function getLogData(string $tab, int $n, ?string $date = null): array
|
||||||
{
|
{
|
||||||
$logPath = self::resolveLogPath($tab);
|
$logPath = self::resolveLogPath($tab, $date);
|
||||||
$isJson = self::LOG_FILES[$tab]['json'] ?? false;
|
$isJson = self::LOG_FILES[$tab]['json'] ?? false;
|
||||||
$error = null;
|
$error = null;
|
||||||
$rawLines = null;
|
$rawLines = null;
|
||||||
|
|||||||
+10
-4
@@ -55,7 +55,7 @@ class Logger
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$handler = new RotatingFileHandler(
|
$handler = new RotatingFileHandler(
|
||||||
$logDir . '/' . $channel . '.log',
|
$logDir . '/xamxam-' . $channel . '.log',
|
||||||
30, // keep 30 days of logs
|
30, // keep 30 days of logs
|
||||||
self::level()
|
self::level()
|
||||||
);
|
);
|
||||||
@@ -99,12 +99,18 @@ class Logger
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the log directory.
|
* Resolve the log directory.
|
||||||
|
*
|
||||||
|
* Production: /var/log/xamxam/ (standard system log root, provisioned by
|
||||||
|
* setup-server.sh; owned by www-data:xamxam). Dev (cli-server): the
|
||||||
|
* app's local storage/logs/ so no root provisioning is needed.
|
||||||
*/
|
*/
|
||||||
private static function logDir(): string
|
private static function logDir(): string
|
||||||
{
|
{
|
||||||
if (defined('STORAGE_ROOT')) {
|
// cli-server (dev) → app-local storage/logs; everything else (php-fpm
|
||||||
return STORAGE_ROOT . '/logs';
|
// production) → the standard system log directory.
|
||||||
|
if (php_sapi_name() === 'cli-server') {
|
||||||
|
return __DIR__ . '/../storage/logs';
|
||||||
}
|
}
|
||||||
return __DIR__ . '/../storage/logs';
|
return '/var/log/xamxam';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ just migrate # run pending migrations
|
|||||||
| `schema.sql` | Full, fully-migrated schema + seed data (regenerated from the local DB) |
|
| `schema.sql` | Full, fully-migrated schema + seed data (regenerated from the local DB) |
|
||||||
| `backups/` | `just backup-snapshot` hot-backups (`*.db.gz`) |
|
| `backups/` | `just backup-snapshot` hot-backups (`*.db.gz`) |
|
||||||
| `cache/` | Runtime cache (rate limits, etc.) |
|
| `cache/` | Runtime cache (rate limits, etc.) |
|
||||||
| `logs/` | Runtime logs (admin, audit) — **outside the webroot** |
|
| `logs/` | Dev-only runtime logs (cli-server); production logs live in `/var/log/xamxam/` |
|
||||||
| `covers/` | Cover images |
|
| `covers/` | Cover images |
|
||||||
| `theses/`, `tfe/`, `tmp/` | Uploaded files / staging |
|
| `theses/`, `tfe/`, `tmp/` | Uploaded files / staging |
|
||||||
|
|
||||||
|
|||||||
@@ -386,8 +386,8 @@
|
|||||||
|
|
||||||
<p class="sys-refresh-note">
|
<p class="sys-refresh-note">
|
||||||
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
||||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&date=<?= htmlspecialchars($selectedDate ?? '') ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
||||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&date=<?= htmlspecialchars($selectedDate ?? '') ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="sys-status-header">
|
<div class="sys-status-header">
|
||||||
@@ -460,11 +460,11 @@
|
|||||||
|
|
||||||
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
||||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
<a href="?tab=<?= htmlspecialchars($key) ?>&date=<?= htmlspecialchars($selectedDate ?? '') ?>&n=<?= $selectedN ?>"
|
||||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||||||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&date=<?= htmlspecialchars($selectedDate ?? '') ?>&n=<?= $selectedN ?>"
|
||||||
hx-target="#sys-tab-panel"
|
hx-target="#sys-tab-panel"
|
||||||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&date=<?= htmlspecialchars($selectedDate ?? '') ?>&n=<?= $selectedN ?>"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-indicator="#sys-tab-panel"
|
hx-indicator="#sys-tab-panel"
|
||||||
data-tab="<?= htmlspecialchars($key) ?>"
|
data-tab="<?= htmlspecialchars($key) ?>"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-indicator="#sys-tab-panel"
|
hx-indicator="#sys-tab-panel"
|
||||||
hx-trigger="change"
|
hx-trigger="change"
|
||||||
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
|
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>","date":"<?= htmlspecialchars($selectedDate ?? '') ?>"}'>
|
||||||
<label for="lines-select">Afficher</label>
|
<label for="lines-select">Afficher</label>
|
||||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||||
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
||||||
@@ -12,6 +12,23 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<?php if (!empty($logDates)): ?>
|
||||||
|
<form id="date-form" hx-get="/admin/system-fragment.php"
|
||||||
|
hx-target="#sys-tab-panel"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#sys-tab-panel"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>","n":<?= $selectedN ?>}'>
|
||||||
|
<label for="date-select">Jour</label>
|
||||||
|
<select id="date-select" name="date" aria-label="Jour du journal">
|
||||||
|
<?php foreach ($logDates as $d): ?>
|
||||||
|
<option value="<?= htmlspecialchars($d['date']) ?>" <?= ($selectedDate ?? null) === $d['date'] ? 'selected' : '' ?>><?= htmlspecialchars($d['label']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Installed to /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
|
# Installed to /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
|
||||||
#
|
#
|
||||||
# Hourly snapshot — kept 30 days
|
# Hourly snapshot — kept 30 days
|
||||||
0 * * * * www-data /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
0 * * * * www-data /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1
|
||||||
|
|
||||||
# Daily snapshot at 2am — kept 90 days
|
# Daily snapshot at 2am — kept 90 days
|
||||||
0 2 * * * www-data RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
0 2 * * * www-data RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# XAMXAM — orphaned draft cleanup cron job
|
# XAMXAM — orphaned draft cleanup cron job
|
||||||
# Installed to /etc/cron.d/xamxam-cleanup (system cron format: minute hour dom month dow user command)
|
# Installed to /etc/cron.d/xamxam-cleanup (system cron format: minute hour dom month dow user command)
|
||||||
#
|
#
|
||||||
# Deletes draft theses older than 24h that have no attached files.
|
# Deletes draft theses older than 7 days (168h) that have no attached files.
|
||||||
# Runs every 4 hours — drafts only become eligible after 24h, so this is ample.
|
# Runs every 2 days — drafts only become eligible after 7 days, so this is ample.
|
||||||
# The script is a dry-run unless --no-dry-run is passed.
|
# The script is a dry-run unless --no-dry-run is passed.
|
||||||
0 */4 * * * www-data php /var/www/xamxam/scripts/cleanup-drafts.php --no-dry-run >> /var/log/xamxam-cleanup.log 2>&1
|
# Eligibility age is configurable via OLDER_THAN_HOURS (default 168).
|
||||||
|
0 0 */2 * * www-data php /tmp/cleanup-drafts.php --no-dry-run >> /var/log/xamxam-cleanup-$(date +\%Y-\%m-\%d).log 2>&1
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# XAMXAM — log rotation for application, nginx, and cron logs.
|
||||||
|
# Installed to /etc/logrotate.d/xamxam (run via `just deploy-logrotate`).
|
||||||
|
#
|
||||||
|
# Covers three roots:
|
||||||
|
# /var/log/xamxam/ app channels (xamxam-app/admin/error/audit-*.log)
|
||||||
|
# /var/log/nginx/xamxam-nginx-* nginx access + error logs
|
||||||
|
# /var/log/xamxam-backup-*.log SQLite backup cron
|
||||||
|
# /var/log/xamxam-cleanup-*.log draft cleanup cron
|
||||||
|
#
|
||||||
|
# The app channels additionally self-rotate via Monolog (RotatingFileHandler,
|
||||||
|
# 30 days) — logrotate here mainly compresses/prunes the nginx and cron files
|
||||||
|
# and any stragglers. Retention is aligned with the app's 30-day default.
|
||||||
|
|
||||||
|
/var/log/xamxam/*.log {
|
||||||
|
daily
|
||||||
|
rotate 30
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
create 664 www-data xamxam
|
||||||
|
su www-data xamxam
|
||||||
|
}
|
||||||
|
|
||||||
|
/var/log/nginx/xamxam-nginx-access.log /var/log/nginx/xamxam-nginx-error.log {
|
||||||
|
daily
|
||||||
|
rotate 52
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
create 640 root adm
|
||||||
|
su root adm
|
||||||
|
sharedscripts
|
||||||
|
postrotate
|
||||||
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active nginx >/dev/null 2>&1; then
|
||||||
|
systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
elif [ -x /usr/sbin/nginx ]; then
|
||||||
|
/usr/sbin/nginx -s reopen 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
endscript
|
||||||
|
}
|
||||||
|
|
||||||
|
/var/log/xamxam-backup-*.log /var/log/xamxam-cleanup-*.log {
|
||||||
|
daily
|
||||||
|
rotate 30
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
create 644 www-data www-data
|
||||||
|
su www-data www-data
|
||||||
|
}
|
||||||
+3
-3
@@ -153,12 +153,12 @@
|
|||||||
- [ ] Add hourly and daily jobs:
|
- [ ] Add hourly and daily jobs:
|
||||||
```cron
|
```cron
|
||||||
# Hourly snapshot — kept 30 days
|
# Hourly snapshot — kept 30 days
|
||||||
0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1
|
||||||
|
|
||||||
# Daily snapshot at 2am — kept 90 days
|
# Daily snapshot at 2am — kept 90 days
|
||||||
0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1
|
||||||
```
|
```
|
||||||
- [ ] Verify the log after the next hour: `tail -f /var/log/sqlite-backup.log`
|
- [ ] Verify the log after the next hour: `tail -f /var/log/xamxam-backup-$(date +%Y-%m-%d).log`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -55,8 +55,14 @@ just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron
|
|||||||
| `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) |
|
| `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) |
|
||||||
| `just deploy-verify-permissions` | Check ownership / permissions on the server |
|
| `just deploy-verify-permissions` | Check ownership / permissions on the server |
|
||||||
|
|
||||||
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` to install
|
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` and
|
||||||
> the backup script + cron jobs.
|
> `just deploy-cleanup-cron` to install the backup/cleanup cron jobs, and
|
||||||
|
> `just deploy-logrotate` to install log rotation. A single `just deploy-all-first`
|
||||||
|
> chains all of these together.
|
||||||
|
> The app log directory `/var/log/xamxam/` is provisioned automatically by
|
||||||
|
> `deploy-code` on every run (via `deploy-server.sh`), so no separate step is
|
||||||
|
> needed for it. To migrate logs written by an older build, run
|
||||||
|
> `just migrate-log-names --apply` once.
|
||||||
|
|
||||||
### Environment file & re-encryption
|
### Environment file & re-encryption
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ gzipped to `/var/backups/xamxam/` on the server.
|
|||||||
| `just deploy-backup` | Install backup script + cron jobs (one-shot) |
|
| `just deploy-backup` | Install backup script + cron jobs (one-shot) |
|
||||||
| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` |
|
| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` |
|
||||||
| `just deploy-backup-cron` | Install `/etc/cron.d/xamxam-backup` (hourly 30d + daily 90d) + dirs/log |
|
| `just deploy-backup-cron` | Install `/etc/cron.d/xamxam-backup` (hourly 30d + daily 90d) + dirs/log |
|
||||||
| `just deploy-check-backup-log` | Tail `/var/log/sqlite-backup.log` |
|
| `just deploy-check-backup-log` | Tail `/var/log/xamxam-backup-YYYY-MM-DD.log` |
|
||||||
| `just deploy-list-backups` | List backups on the server |
|
| `just deploy-list-backups` | List backups on the server |
|
||||||
| `just trigger-backup` | Run the backup script now |
|
| `just trigger-backup` | Run the backup script now |
|
||||||
| `just test-restore <path.gz>` | Fetch + decompress + verify a remote snapshot |
|
| `just test-restore <path.gz>` | Fetch + decompress + verify a remote snapshot |
|
||||||
@@ -91,7 +97,7 @@ Backup files: `/var/backups/xamxam/db-<timestamp>.db.gz`.
|
|||||||
|
|
||||||
Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`),
|
Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`),
|
||||||
installed via `just deploy-cleanup-cron`, logging to
|
installed via `just deploy-cleanup-cron`, logging to
|
||||||
`/var/log/xamxam-cleanup.log`. Verify with `just deploy-check-cleanup-log`.
|
`/var/log/xamxam-cleanup-YYYY-MM-DD.log`. Verify with `just deploy-check-cleanup-log`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Do not replace any existing class yet. Just build the foundation.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each channel gets:
|
Each channel gets:
|
||||||
- A `RotatingFileHandler` writing to `storage/logs/{channel}.log`, keeping 30 days
|
- A `RotatingFileHandler` writing to `xamxam-{channel}.log` (production: `/var/log/xamxam/`; dev: `storage/logs/`), keeping 30 days
|
||||||
- A `JsonFormatter` so log lines stay JSON (preserving the existing format contract)
|
- A `JsonFormatter` so log lines stay JSON (preserving the existing format contract)
|
||||||
- Log level set from an environment variable (`LOG_LEVEL`, defaulting to `WARNING` in production, `DEBUG` in dev)
|
- Log level set from an environment variable (`LOG_LEVEL`, defaulting to `WARNING` in production, `DEBUG` in dev)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ one file at a time without passing instances around.
|
|||||||
|
|
||||||
- Rewrite `AppLogger` as a thin wrapper that delegates to `Logger::get('app')`
|
- Rewrite `AppLogger` as a thin wrapper that delegates to `Logger::get('app')`
|
||||||
- Keep the existing public method signatures identical — no call sites change in this step
|
- Keep the existing public method signatures identical — no call sites change in this step
|
||||||
- Run the app, verify log output appears in `storage/logs/app.log`
|
- Run the app, verify log output appears in `xamxam-app.log` (see the log dir for the active SAPI)
|
||||||
- Delete the old file-writing implementation inside `AppLogger`, keep the class as a facade for now
|
- Delete the old file-writing implementation inside `AppLogger`, keep the class as a facade for now
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-1
@@ -59,7 +59,8 @@ spoofing.
|
|||||||
files. The DocumentRoot is `app/public/` only.
|
files. The DocumentRoot is `app/public/` only.
|
||||||
- Restricted-file downloads are gated by a request/approval/token flow
|
- Restricted-file downloads are gated by a request/approval/token flow
|
||||||
(`file_access_*` tables).
|
(`file_access_*` tables).
|
||||||
- Logs write to `app/storage/logs/` — outside the webroot, not publicly served.
|
- Logs write to `/var/log/xamxam/` in production (and `app/storage/logs/`
|
||||||
|
only in dev/cli-server) — outside the webroot, not publicly served.
|
||||||
|
|
||||||
## Injection & output
|
## Injection & output
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ deploy: build deploy-code deploy-deps deploy-migrate
|
|||||||
@just deploy-env
|
@just deploy-env
|
||||||
@just deploy-verify-permissions
|
@just deploy-verify-permissions
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "ℹ️ First deploy? Also run: just deploy-backup"
|
@echo "ℹ️ First deploy? Also run: just deploy-all-first"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
@@ -294,6 +294,13 @@ deploy-verify-permissions:
|
|||||||
err "storage/cache/rate_limit → NOT WRITABLE"
|
err "storage/cache/rate_limit → NOT WRITABLE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── /var/log/xamxam writable (app log dir — NullHandler if missing) ─────────
|
||||||
|
if ssh xamxam "[ -d /var/log/xamxam ] && [ -w /var/log/xamxam ]"; then
|
||||||
|
ok "/var/log/xamxam → writable"
|
||||||
|
else
|
||||||
|
err "/var/log/xamxam → MISSING or NOT WRITABLE (run: sudo bash /tmp/deploy-server.sh)"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── .env must be 640 ──────────────────────────────────────────────────────────
|
# ── .env must be 640 ──────────────────────────────────────────────────────────
|
||||||
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
|
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
|
||||||
if [ "$env_perm" = "640" ]; then
|
if [ "$env_perm" = "640" ]; then
|
||||||
@@ -335,6 +342,15 @@ deploy-script script_name:
|
|||||||
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
|
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
|
[group('deploy')]
|
||||||
|
migrate-log-names apply="":
|
||||||
|
# Rename pre-existing logs to the xamxam-{service}-{date}.log convention.
|
||||||
|
# Dry-run by default; pass apply="--apply" to actually rename on the server.
|
||||||
|
# Requires sudo — renames files under /var/log and /var/www/xamxam/storage/logs.
|
||||||
|
rsync -v scripts/migrate-log-names.sh xamxam:/tmp/migrate-log-names.sh
|
||||||
|
ssh -t xamxam "sudo bash /tmp/migrate-log-names.sh {{apply}}"
|
||||||
|
ssh xamxam "rm -f /tmp/migrate-log-names.sh"
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-backup-script:
|
deploy-backup-script:
|
||||||
# Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo)
|
# Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo)
|
||||||
@@ -353,11 +369,11 @@ deploy-backup-cron:
|
|||||||
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
|
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
|
||||||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
|
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
|
||||||
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam"
|
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam"
|
||||||
ssh -t xamxam "sudo touch /var/log/sqlite-backup.log && sudo chown www-data:www-data /var/log/sqlite-backup.log && sudo chmod 644 /var/log/sqlite-backup.log"
|
ssh -t xamxam "sudo touch /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
||||||
@echo "✅ Cron jobs installed."
|
@echo "✅ Cron jobs installed."
|
||||||
@echo " Cron file: /etc/cron.d/xamxam-backup"
|
@echo " Cron file: /etc/cron.d/xamxam-backup"
|
||||||
@echo " Backup dir: /var/backups/xamxam"
|
@echo " Backup dir: /var/backups/xamxam"
|
||||||
@echo " Log file: /var/log/sqlite-backup.log"
|
@echo " Log file: /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Verify with: just deploy-check-backup-log"
|
@echo "Verify with: just deploy-check-backup-log"
|
||||||
|
|
||||||
@@ -367,7 +383,7 @@ deploy-backup: deploy-backup-script deploy-backup-cron
|
|||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-check-backup-log:
|
deploy-check-backup-log:
|
||||||
ssh xamxam "tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
ssh xamxam "tail -20 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-list-backups:
|
deploy-list-backups:
|
||||||
@@ -376,24 +392,33 @@ deploy-list-backups:
|
|||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-cleanup-cron:
|
deploy-cleanup-cron:
|
||||||
# Install cron job for orphaned draft cleanup (every 4 hours, 24h threshold).
|
# Install cron job for orphaned draft cleanup (every 2 days, 7-day threshold).
|
||||||
# Creates /etc/cron.d/xamxam-cleanup and log file on the server.
|
# Creates /etc/cron.d/xamxam-cleanup and log file on the server.
|
||||||
@echo "📋 Installing draft cleanup cron job…"
|
@echo "📋 Installing draft cleanup cron job…"
|
||||||
rsync -v scripts/cleanup-drafts.php xamxam:/var/www/xamxam/scripts/cleanup-drafts.php
|
rsync -v scripts/cleanup-drafts.php xamxam:/tmp/cleanup-drafts.php
|
||||||
ssh xamxam "chown www-data:xamxam /var/www/xamxam/scripts/cleanup-drafts.php && chmod 755 /var/www/xamxam/scripts/cleanup-drafts.php"
|
ssh xamxam "chmod 755 /tmp/cleanup-drafts.php"
|
||||||
rsync -v deploy/xamxam-cleanup.cron xamxam:/tmp/xamxam-cleanup.cron
|
rsync -v deploy/xamxam-cleanup.cron xamxam:/tmp/xamxam-cleanup.cron
|
||||||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-cleanup.cron /etc/cron.d/xamxam-cleanup && rm -f /tmp/xamxam-cleanup.cron"
|
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-cleanup.cron /etc/cron.d/xamxam-cleanup && rm -f /tmp/xamxam-cleanup.cron"
|
||||||
ssh -t xamxam "sudo touch /var/log/xamxam-cleanup.log && sudo chown www-data:www-data /var/log/xamxam-cleanup.log && sudo chmod 644 /var/log/xamxam-cleanup.log"
|
ssh -t xamxam "sudo touch /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log"
|
||||||
@echo "✅ Cleanup cron installed."
|
@echo "✅ Cleanup cron installed."
|
||||||
@echo " Cron file: /etc/cron.d/xamxam-cleanup"
|
@echo " Cron file: /etc/cron.d/xamxam-cleanup"
|
||||||
@echo " Script: /var/www/xamxam/scripts/cleanup-drafts.php"
|
@echo " Script: /tmp/cleanup-drafts.php"
|
||||||
@echo " Log file: /var/log/xamxam-cleanup.log"
|
@echo " Log file: /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Verify with: just deploy-check-cleanup-log"
|
@echo "Verify with: just deploy-check-cleanup-log"
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-check-cleanup-log:
|
deploy-check-cleanup-log:
|
||||||
ssh xamxam "tail -20 /var/log/xamxam-cleanup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
ssh xamxam "tail -20 /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
||||||
|
|
||||||
|
[group('deploy')]
|
||||||
|
deploy-logrotate:
|
||||||
|
# Install /etc/logrotate.d/xamxam for app + nginx + cron logs.
|
||||||
|
@echo "📋 Installing logrotate config…"
|
||||||
|
rsync -v deploy/xamxam-logrotate xamxam:/tmp/xamxam-logrotate
|
||||||
|
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-logrotate /etc/logrotate.d/xamxam && sudo logrotate -d /etc/logrotate.d/xamxam && rm -f /tmp/xamxam-logrotate"
|
||||||
|
@echo "✅ logrotate config installed and validated (dry-run)."
|
||||||
|
@echo " Config: /etc/logrotate.d/xamxam"
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
test-restore remote_gz_path:
|
test-restore remote_gz_path:
|
||||||
@@ -427,7 +452,7 @@ deploy-migrate-storage dry_run='' target_host='xamxam':
|
|||||||
ssh {{target_host}} 'rm -f /var/www/xamxam/migrate-storage-paths.php'
|
ssh {{target_host}} 'rm -f /var/www/xamxam/migrate-storage-paths.php'
|
||||||
|
|
||||||
[group('deploy')]
|
[group('deploy')]
|
||||||
deploy-all-first: deploy deploy-backup deploy-cleanup-cron
|
deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-logrotate
|
||||||
# One-shot: full initial deploy including backup and cleanup cron jobs.
|
# One-shot: full initial deploy including backup and cleanup cron jobs.
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -536,7 +561,8 @@ clean:
|
|||||||
|
|
||||||
[group('utils')]
|
[group('utils')]
|
||||||
cleanup-drafts dry_run='':
|
cleanup-drafts dry_run='':
|
||||||
# List (dry-run) or delete orphaned draft theses older than 24h.
|
# List (dry-run) or delete orphaned draft theses older than 7 days (168h).
|
||||||
# Pass --no-dry-run to actually delete:
|
# Pass --no-dry-run to actually delete. Override the age with OLDER_THAN_HOURS:
|
||||||
# just cleanup-drafts --no-dry-run
|
# just cleanup-drafts --no-dry-run
|
||||||
@php -r 'define("APP_ROOT", getcwd()."/app");require APP_ROOT."/src/Database.php";$db=new Database();$dry="{{dry_run}}"!=="--no-dry-run";$res=$db->cleanupOrphanedDrafts(24,$dry);$c=count($res["candidates"]);if($c===0){echo"✅ No orphaned drafts found.\n";}elseif($dry){echo"🔍 Found {$c} orphaned draft(s):\n";foreach($res["candidates"]as$row){printf(" → #%d %s \"%s\" (submitted %s)\n",$row["id"],$row["identifier"],$row["title"],$row["submitted_at"]);}echo"\nRun \"just cleanup-drafts --no-dry-run\" to delete them.\n";}else{echo"🗑 Deleted {$res["deleted"]} orphaned draft(s).\n";}'
|
# OLDER_THAN_HOURS=24 just cleanup-drafts
|
||||||
|
@php scripts/cleanup-drafts.php {{dry_run}}
|
||||||
|
|||||||
+2
-2
@@ -100,8 +100,8 @@ sudo nginx -t
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Watch logs
|
# Watch logs
|
||||||
sudo tail -f /var/log/nginx/xamxam_access.log
|
sudo tail -f /var/log/nginx/xamxam-nginx-access.log
|
||||||
sudo tail -f /var/log/nginx/xamxam_error.log
|
sudo tail -f /var/log/nginx/xamxam-nginx-error.log
|
||||||
|
|
||||||
# Check nginx status
|
# Check nginx status
|
||||||
sudo systemctl status nginx
|
sudo systemctl status nginx
|
||||||
|
|||||||
+2
-2
@@ -72,8 +72,8 @@ server {
|
|||||||
client_body_timeout 600s;
|
client_body_timeout 600s;
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
access_log /var/log/nginx/xamxam_access.log;
|
access_log /var/log/nginx/xamxam-nginx-access.log;
|
||||||
error_log /var/log/nginx/xamxam_error.log warn;
|
error_log /var/log/nginx/xamxam-nginx-error.log warn;
|
||||||
|
|
||||||
# Block access to hidden files (except .well-known for Let's Encrypt)
|
# Block access to hidden files (except .well-known for Let's Encrypt)
|
||||||
location ~ /\.(?!well-known).* {
|
location ~ /\.(?!well-known).* {
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ server {
|
|||||||
client_body_timeout 120s;
|
client_body_timeout 120s;
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
access_log /var/log/nginx/xamxam_access.log;
|
access_log /var/log/nginx/xamxam-nginx-access.log;
|
||||||
error_log /var/log/nginx/xamxam_error.log warn;
|
error_log /var/log/nginx/xamxam-nginx-error.log warn;
|
||||||
|
|
||||||
# Block access to hidden files (except .well-known for Let's Encrypt)
|
# Block access to hidden files (except .well-known for Let's Encrypt)
|
||||||
location ~ /\.(?!well-known).* {
|
location ~ /\.(?!well-known).* {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
# RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh # 90 days
|
# RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh # 90 days
|
||||||
#
|
#
|
||||||
# Expected to be run from cron:
|
# Expected to be run from cron:
|
||||||
# 0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
# 0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1
|
||||||
# 0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
|
# 0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* cleanup-drafts.php — Delete orphaned draft theses older than 24h.
|
* cleanup-drafts.php — Delete orphaned draft theses older than a threshold.
|
||||||
*
|
*
|
||||||
* Draft theses are created with status='draft' during the two-phase commit
|
* Draft theses are created with status='draft' during the two-phase commit
|
||||||
* in ThesisCreateController. If the file phase throws after COMMIT, the
|
* in ThesisCreateController. If the file phase throws after COMMIT, the
|
||||||
* draft remains orphaned — no files attached, but blocks the identifier.
|
* draft remains orphaned — no files attached, but blocks the identifier.
|
||||||
*
|
*
|
||||||
|
* The eligibility age defaults to 7 days (168 hours) and can be overridden
|
||||||
|
* with the OLDER_THAN_HOURS environment variable:
|
||||||
|
* OLDER_THAN_HOURS=24 php scripts/cleanup-drafts.php
|
||||||
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* php scripts/cleanup-drafts.php # dry-run (list candidates)
|
* php /tmp/cleanup-drafts.php # dry-run (list candidates)
|
||||||
* php scripts/cleanup-drafts.php --no-dry-run # actually delete
|
* php /tmp/cleanup-drafts.php --no-dry-run # actually delete
|
||||||
*
|
*
|
||||||
* Exit codes: 0 on success, 1 on error.
|
* Exit codes: 0 on success, 1 on error.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$root = dirname(__DIR__);
|
// Resolve APP_ROOT robustly. In production the app code is deployed flat under
|
||||||
define('APP_ROOT', $root . '/app');
|
// /var/www/xamxam/ (src/, storage/, templates/ at the root, no app/ subdir), and
|
||||||
|
// this script may itself live in /tmp. Always point at /var/www/xamxam in
|
||||||
|
// non-CLI-SAPI contexts; local dev (cli-server) keeps the app/ subdir layout.
|
||||||
|
$prodRoot = '/var/www/xamxam';
|
||||||
|
if (is_dir($prodRoot . '/src') && is_file($prodRoot . '/src/Database.php')) {
|
||||||
|
define('APP_ROOT', $prodRoot);
|
||||||
|
} else {
|
||||||
|
define('APP_ROOT', dirname(__DIR__) . '/app');
|
||||||
|
}
|
||||||
|
|
||||||
require_once APP_ROOT . '/src/Database.php';
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
|
||||||
$dryRun = !in_array('--no-dry-run', $argv, true);
|
$dryRun = !in_array('--no-dry-run', $argv, true);
|
||||||
|
|
||||||
|
// Eligibility threshold: default 7 days (168h); overridable via env.
|
||||||
|
$olderThanHours = (int) (getenv('OLDER_THAN_HOURS') ?: 168);
|
||||||
|
if ($olderThanHours < 1) {
|
||||||
|
$olderThanHours = 168;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = new Database();
|
$db = new Database();
|
||||||
$result = $db->cleanupOrphanedDrafts(24, $dryRun);
|
$result = $db->cleanupOrphanedDrafts($olderThanHours, $dryRun);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('[cleanup-drafts] Error: ' . $e->getMessage());
|
error_log('[cleanup-drafts] Error: ' . $e->getMessage());
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ echo "Creating investigation directory..."
|
|||||||
mkdir -p ~/crash_investigation
|
mkdir -p ~/crash_investigation
|
||||||
|
|
||||||
echo "Copying nginx logs..."
|
echo "Copying nginx logs..."
|
||||||
sudo cp /var/log/nginx/xamxam_error.log ~/crash_investigation/
|
sudo cp /var/log/nginx/xamxam-nginx-error.log ~/crash_investigation/
|
||||||
sudo cp /var/log/nginx/xamxam_error.log.1 ~/crash_investigation/ 2>/dev/null || true
|
sudo cp /var/log/nginx/xamxam-nginx-error.log.1 ~/crash_investigation/ 2>/dev/null || true
|
||||||
sudo cp /var/log/nginx/xamxam_access.log ~/crash_investigation/
|
sudo cp /var/log/nginx/xamxam-nginx-access.log ~/crash_investigation/
|
||||||
sudo cp /var/log/nginx/xamxam_access.log.1 ~/crash_investigation/ 2>/dev/null || true
|
sudo cp /var/log/nginx/xamxam-nginx-access.log.1 ~/crash_investigation/ 2>/dev/null || true
|
||||||
sudo cp /var/log/nginx/xamxam_error.log.2.gz ~/crash_investigation/ 2>/dev/null || true
|
sudo cp /var/log/nginx/xamxam-nginx-error.log.2.gz ~/crash_investigation/ 2>/dev/null || true
|
||||||
sudo cp /var/log/nginx/xamxam_access.log.2.gz ~/crash_investigation/ 2>/dev/null || true
|
sudo cp /var/log/nginx/xamxam-nginx-access.log.2.gz ~/crash_investigation/ 2>/dev/null || true
|
||||||
|
|
||||||
echo "Exporting journal from previous boot..."
|
echo "Exporting journal from previous boot..."
|
||||||
sudo journalctl -b -1 --no-pager > ~/crash_investigation/journal_previous_boot.log 2>&1
|
sudo journalctl -b -1 --no-pager > ~/crash_investigation/journal_previous_boot.log 2>&1
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ chown www-data:xamxam /var/www/xamxam/storage/tmp/php-uploads
|
|||||||
chmod 2775 /var/www/xamxam/storage/tmp/php-uploads
|
chmod 2775 /var/www/xamxam/storage/tmp/php-uploads
|
||||||
ok "PHP upload temp dir: /var/www/xamxam/storage/tmp/php-uploads"
|
ok "PHP upload temp dir: /var/www/xamxam/storage/tmp/php-uploads"
|
||||||
|
|
||||||
|
# Ensure the application log directory exists and is writable by php-fpm.
|
||||||
|
# App logs (Monolog) go to /var/log/xamxam/xamxam-{channel}-YYYY-MM-DD.log in
|
||||||
|
# production; without this the logger falls back to NullHandler (silent).
|
||||||
|
mkdir -p /var/log/xamxam
|
||||||
|
chown www-data:xamxam /var/log/xamxam
|
||||||
|
chmod 2775 /var/log/xamxam
|
||||||
|
ok "Log dir: /var/log/xamxam owned by www-data:xamxam (2775)"
|
||||||
|
|
||||||
# ── Step 2: Nginx config ──────────────────────────────────────────────────────
|
# ── Step 2: Nginx config ──────────────────────────────────────────────────────
|
||||||
printf "\n📋 Step 2: Deploying nginx configuration...\n"
|
printf "\n📋 Step 2: Deploying nginx configuration...\n"
|
||||||
echo "--------------------------------------------"
|
echo "--------------------------------------------"
|
||||||
|
|||||||
Executable
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# migrate-log-names.sh — rename (and relocate) pre-existing log files to the
|
||||||
|
# xamxam-{service}-{date}.log convention under the standard /var/log root.
|
||||||
|
#
|
||||||
|
# Why: the app adopted a uniform log naming scheme where production app logs
|
||||||
|
# live under /var/log/xamxam/ (system standard), nginx under /var/log/nginx/,
|
||||||
|
# and cron jobs under /var/log/:
|
||||||
|
# /var/log/xamxam/xamxam-{channel}-YYYY-MM-DD.log (app/admin/error/audit)
|
||||||
|
# /var/log/nginx/xamxam-nginx-{access,error}.log (nginx, fixed live name)
|
||||||
|
# /var/log/xamxam-{backup,cleanup}-YYYY-MM-DD.log (cron jobs)
|
||||||
|
# but logs written before that change still carry the old names/locations
|
||||||
|
# (storage/logs/admin-*.log, xamxam_error.log, sqlite-backup.log, …). This
|
||||||
|
# script renames and moves those in place so they follow the same pattern and
|
||||||
|
# stay visible to the admin log viewer (which globs for xamxam-{channel}-*.log).
|
||||||
|
#
|
||||||
|
# Behaviour:
|
||||||
|
# - Idempotent: files already matching the target pattern are skipped.
|
||||||
|
# - Non-destructive: never overwrites an existing destination. If the
|
||||||
|
# destination already exists the source is left untouched and a warning
|
||||||
|
# is printed so you can merge/decide manually.
|
||||||
|
# - Dry-run by default; pass --apply to actually rename.
|
||||||
|
# - Uses each file's own mtime as its date (app/cron logs embed the date in
|
||||||
|
# their name already; nginx/cron flat files derive one from mtime).
|
||||||
|
#
|
||||||
|
# Usage (run on the server):
|
||||||
|
# sudo bash /tmp/migrate-log-names.sh # preview only
|
||||||
|
# sudo bash /tmp/migrate-log-names.sh --apply # perform the renames
|
||||||
|
#
|
||||||
|
# Install: just deploy-script migrate-log-names (uploads this file to /tmp)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||||
|
info() { printf "ℹ️ %s\n" "$*"; }
|
||||||
|
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
||||||
|
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
|
||||||
|
err() { printf "${RED}✗${NC} %s\n" "$*" >&2; }
|
||||||
|
|
||||||
|
APPLY=0
|
||||||
|
[ "${1:-}" = "--apply" ] && APPLY=1
|
||||||
|
|
||||||
|
APP_LOGS_OLD_DIR="/var/www/xamxam/storage/logs"
|
||||||
|
APP_LOGS_DIR="/var/log/xamxam"
|
||||||
|
NGINX_LOGS_DIR="/var/log/nginx"
|
||||||
|
|
||||||
|
# ── App channels (Monolog rotating files) ─────────────────────────────────────
|
||||||
|
# App logs now live under /var/log/xamxam (production). Handle both the old
|
||||||
|
# location (storage/logs, pre-move) and files already in the new location that
|
||||||
|
# still carry the old naming (missing xamxam- prefix).
|
||||||
|
# Old: {channel}-YYYY-MM-DD.log New: xamxam-{channel}-YYYY-MM-DD.log
|
||||||
|
# Old bare base also possible: {channel}.log (no date)
|
||||||
|
CHANNELS=(app admin error audit)
|
||||||
|
changed=0
|
||||||
|
|
||||||
|
for ch in "${CHANNELS[@]}"; do
|
||||||
|
# Source directory: prefer the old storage/logs location for files that
|
||||||
|
# predate the move; also scan the new /var/log/xamxam in case a previous
|
||||||
|
# partial migration already moved (but did not rename) some files.
|
||||||
|
for srcdir in "$APP_LOGS_OLD_DIR" "$APP_LOGS_DIR"; do
|
||||||
|
[ -d "$srcdir" ] || continue
|
||||||
|
|
||||||
|
# Dated files: {channel}-YYYY-MM-DD.log → xamxam-{channel}-YYYY-MM-DD.log
|
||||||
|
for src in "$srcdir"/"$ch"-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log; do
|
||||||
|
[ -e "$src" ] || continue
|
||||||
|
base=$(basename "$src")
|
||||||
|
dst="$APP_LOGS_DIR/xamxam-$base"
|
||||||
|
if [ -e "$dst" ]; then
|
||||||
|
warn "skip (dest exists): $base"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$APPLY" -eq 1 ]; then
|
||||||
|
mv "$src" "$dst" && ok "moved $srcdir/$base → $dst"
|
||||||
|
else
|
||||||
|
info "[dry-run] $srcdir/$base → $dst"
|
||||||
|
fi
|
||||||
|
changed=$((changed + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Bare base file (rare, pre-rotation): {channel}.log → xamxam-{channel}.log
|
||||||
|
src="$srcdir/$ch.log"
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
dst="$APP_LOGS_DIR/xamxam-$ch.log"
|
||||||
|
if [ -e "$dst" ]; then
|
||||||
|
warn "skip (dest exists): $ch.log"
|
||||||
|
elif [ "$APPLY" -eq 1 ]; then
|
||||||
|
mv "$src" "$dst" && ok "moved $srcdir/$ch.log → $dst"
|
||||||
|
else
|
||||||
|
info "[dry-run] $srcdir/$ch.log → $dst"
|
||||||
|
fi
|
||||||
|
changed=$((changed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
[ "$changed" -eq 0 ] && info " none found"
|
||||||
|
|
||||||
|
# ── nginx logs (fixed live name + logrotate .N / .N.gz rotations) ─────────────
|
||||||
|
# Old: xamxam_error.log[.N[.gz]] New: xamxam-nginx-error.log[.N[.gz]]
|
||||||
|
# Old: xamxam_access.log[.N[.gz]] New: xamxam-nginx-access.log[.N[.gz]]
|
||||||
|
info "nginx logs — $NGINX_LOGS_DIR"
|
||||||
|
for kind in access error; do
|
||||||
|
# Match the live file and any rotations (.1, .2.gz, …)
|
||||||
|
for src in "$NGINX_LOGS_DIR"/xamxam_"$kind".log*; do
|
||||||
|
[ -e "$src" ] || continue
|
||||||
|
base=$(basename "$src")
|
||||||
|
# strip the leading xamxam_{kind}.log, keep the rotation suffix
|
||||||
|
suffix=${base#xamxam_"$kind".log}
|
||||||
|
dst="$NGINX_LOGS_DIR/xamxam-nginx-$kind.log$suffix"
|
||||||
|
if [ -e "$dst" ]; then
|
||||||
|
warn "skip (dest exists): $base"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$APPLY" -eq 1 ]; then
|
||||||
|
mv "$src" "$dst" && ok "renamed $base → $(basename "$dst")"
|
||||||
|
else
|
||||||
|
info "[dry-run] $base → $(basename "$dst")"
|
||||||
|
fi
|
||||||
|
changed=$((changed + 1))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Flat cron logs (derive a date from mtime) ────────────────────────────────
|
||||||
|
# Old: /var/log/sqlite-backup.log → /var/log/xamxam-backup-YYYY-MM-DD.log
|
||||||
|
# Old: /var/log/xamxam-cleanup.log → /var/log/xamxam-cleanup-YYYY-MM-DD.log
|
||||||
|
info "cron logs — /var/log"
|
||||||
|
rename_cron_log() {
|
||||||
|
local src="$1" svc="$2"
|
||||||
|
[ -f "$src" ] || return 0
|
||||||
|
local d
|
||||||
|
d=$(date -r "$src" +%Y-%m-%d 2>/dev/null || stat -c %y "$src" | cut -d' ' -f1)
|
||||||
|
local dst="/var/log/xamxam-$svc-$d.log"
|
||||||
|
if [ -e "$dst" ]; then
|
||||||
|
warn "skip (dest exists): $(basename "$src")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$APPLY" -eq 1 ]; then
|
||||||
|
mv "$src" "$dst" && ok "renamed $(basename "$src") → $(basename "$dst")"
|
||||||
|
else
|
||||||
|
info "[dry-run] $(basename "$src") → $(basename "$dst")"
|
||||||
|
fi
|
||||||
|
changed=$((changed + 1))
|
||||||
|
}
|
||||||
|
rename_cron_log /var/log/sqlite-backup.log backup
|
||||||
|
rename_cron_log /var/log/xamxam-cleanup.log cleanup
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ "$APPLY" -eq 0 ]; then
|
||||||
|
info "Dry-run complete. Re-run with --apply to perform the renames."
|
||||||
|
else
|
||||||
|
ok "Renames complete ($changed changed)."
|
||||||
|
fi
|
||||||
@@ -88,13 +88,12 @@ chown -R "$WEB_USER:$APP_GROUP" "$APP_DIR/storage/cache"
|
|||||||
chmod -R 2775 "$APP_DIR/storage/cache"
|
chmod -R 2775 "$APP_DIR/storage/cache"
|
||||||
ok "Cache dirs: created and owned by $WEB_USER:$APP_GROUP"
|
ok "Cache dirs: created and owned by $WEB_USER:$APP_GROUP"
|
||||||
|
|
||||||
# ── 8. Provision /var/log/xamxam.log ─────────────────────────────────────────
|
# ── 8. Provision app log directory (/var/log/xamxam) ──────────────────────────────
|
||||||
if [ ! -f /var/log/xamxam.log ]; then
|
LOG_DIR="/var/log/xamxam"
|
||||||
touch /var/log/xamxam.log
|
mkdir -p "$LOG_DIR"
|
||||||
fi
|
chown "$WEB_USER:$APP_GROUP" "$LOG_DIR"
|
||||||
chown "$WEB_USER:$APP_GROUP" /var/log/xamxam.log
|
chmod 2775 "$LOG_DIR"
|
||||||
chmod 640 /var/log/xamxam.log
|
ok "/var/log/xamxam: owned by $WEB_USER:$APP_GROUP (2775)"
|
||||||
ok "/var/log/xamxam.log: owned by $WEB_USER:$APP_GROUP (640)"
|
|
||||||
|
|
||||||
printf "\n"
|
printf "\n"
|
||||||
ok "Setup complete."
|
ok "Setup complete."
|
||||||
|
|||||||
Reference in New Issue
Block a user