logs: standardise log filenames to xamxam-{service}-{date}.log

This commit is contained in:
Pontoporeia
2026-08-24 11:34:34 +02:00
parent 7b6d79c133
commit d2cef85966
25 changed files with 442 additions and 83 deletions
+8 -1
View File
@@ -53,17 +53,24 @@ if ($activeTab === 'status' || !array_key_exists($activeTab, SystemController::L
$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;
if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
$selectedN = 100;
}
$logData = $_controller->getLogData($activeTab, $selectedN);
$logData = $_controller->getLogData($activeTab, $selectedN, $selectedDate);
$logLines = $logData['lines'];
$logError = $logData['error'];
$logFileMeta = $logData['meta'];
$logIsJson = $logData['isJson'] ?? false;
$notYet = $logData['notYet'] ?? false;
$logDates = SystemController::listLogDates($activeTab);
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
$statusInitiallyCollapsed = $collapsed === '1';
+8 -1
View File
@@ -33,6 +33,12 @@ if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
$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('X-Robots-Tag: noindex');
@@ -42,11 +48,12 @@ $_cache = new SystemCache($_db->getPDO());
$_controller = new SystemController($_db, $_cache);
// ── Render ─────────────────────────────────────────────────────────────────
$logData = $_controller->getLogData($activeTab, $selectedN);
$logData = $_controller->getLogData($activeTab, $selectedN, $selectedDate);
$logLines = $logData['lines'];
$logError = $logData['error'];
$logFileMeta = $logData['meta'];
$logIsJson = $logData['isJson'] ?? false;
$notYet = $logData['notYet'] ?? false;
$logDates = SystemController::listLogDates($activeTab);
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
+2 -2
View File
@@ -3,8 +3,8 @@
/**
* Admin audit logger.
*
* Writes JSON-lines to /var/log/xamxam.log (production) or
* storage/logs/admin.log (dev / cli-server).
* Writes JSON-lines to /var/log/xamxam/xamxam-admin-YYYY-MM-DD.log (production)
* or storage/logs/xamxam-admin.log (dev / cli-server).
*
* Each entry: timestamp, actor (admin IP/UA), action, resource, status, context.
*
+70 -11
View File
@@ -26,25 +26,39 @@ 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],
'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log', 'json' => false],
'nginx_error' => ['label' => 'nginx — erreurs','path' => '/var/log/nginx/xamxam_error.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-nginx-error.log', 'json' => false],
];
/**
* Resolve a log file path — app logs live under STORAGE_ROOT, system logs
* have hard-coded paths (only valid in production).
* 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).
*
* 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];
if ($def['path'] !== null) {
return $def['path'];
}
// App logs: storage/logs/{channel}.log (Monolog RotatingFileHandler uses
// this as base name; the current log is always at {channel}-YYYY-MM-DD.log)
$dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : APP_ROOT . '/storage/logs';
// 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
// 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
$base = $dir . '/' . $tab;
$dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
if (!empty($dated)) {
rsort($dated); // newest first
@@ -54,6 +68,49 @@ class SystemController
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];
// ── TTLs ──────────────────────────────────────────────────────────────────
@@ -157,11 +214,13 @@ class SystemController
/**
* 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}
*/
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;
$error = null;
$rawLines = null;
+10 -4
View File
@@ -55,7 +55,7 @@ class Logger
try {
$handler = new RotatingFileHandler(
$logDir . '/' . $channel . '.log',
$logDir . '/xamxam-' . $channel . '.log',
30, // keep 30 days of logs
self::level()
);
@@ -99,12 +99,18 @@ class Logger
/**
* 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
{
if (defined('STORAGE_ROOT')) {
return STORAGE_ROOT . '/logs';
// cli-server (dev) → app-local storage/logs; everything else (php-fpm
// production) → the standard system log directory.
if (php_sapi_name() === 'cli-server') {
return __DIR__ . '/../storage/logs';
}
return __DIR__ . '/../storage/logs';
return '/var/log/xamxam';
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ just migrate # run pending migrations
| `schema.sql` | Full, fully-migrated schema + seed data (regenerated from the local DB) |
| `backups/` | `just backup-snapshot` hot-backups (`*.db.gz`) |
| `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 |
| `theses/`, `tfe/`, `tmp/` | Uploaded files / staging |
+5 -5
View File
@@ -386,8 +386,8 @@
<p class="sys-refresh-note">
Affiché le <?= date('d/m/Y à H:i:s') ?> —
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;n=<?= $selectedN ?>">Rafraîchir</a> —
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;n=<?= $selectedN ?>&amp;refresh=1">Forcer actualisation</a>
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;date=<?= htmlspecialchars($selectedDate ?? '') ?>&amp;n=<?= $selectedN ?>">Rafraîchir</a> —
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;date=<?= htmlspecialchars($selectedDate ?? '') ?>&amp;n=<?= $selectedN ?>&amp;refresh=1">Forcer actualisation</a>
</p>
<div class="sys-status-header">
@@ -460,11 +460,11 @@
<nav class="sys-tabs" aria-label="Journaux et configuration">
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
<a href="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
<a href="?tab=<?= htmlspecialchars($key) ?>&amp;date=<?= htmlspecialchars($selectedDate ?? '') ?>&amp;n=<?= $selectedN ?>"
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&amp;date=<?= htmlspecialchars($selectedDate ?? '') ?>&amp;n=<?= $selectedN ?>"
hx-target="#sys-tab-panel"
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&amp;date=<?= htmlspecialchars($selectedDate ?? '') ?>&amp;n=<?= $selectedN ?>"
hx-swap="innerHTML"
hx-indicator="#sys-tab-panel"
data-tab="<?= htmlspecialchars($key) ?>"
@@ -4,7 +4,7 @@
hx-swap="innerHTML"
hx-indicator="#sys-tab-panel"
hx-trigger="change"
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>","date":"<?= htmlspecialchars($selectedDate ?? '') ?>"}'>
<label for="lines-select">Afficher</label>
<select id="lines-select" name="n" aria-label="Nombre de lignes">
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
@@ -12,6 +12,23 @@
<?php endforeach; ?>
</select>
</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): ?>
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
<?php endif; ?>
+2 -2
View File
@@ -2,7 +2,7 @@
# Installed to /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
#
# 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
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
+4 -3
View File
@@ -1,7 +1,8 @@
# XAMXAM — orphaned draft cleanup cron job
# 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.
# Runs every 4 hours — drafts only become eligible after 24h, so this is ample.
# Deletes draft theses older than 7 days (168h) that have no attached files.
# 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.
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
+53
View File
@@ -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
View File
@@ -153,12 +153,12 @@
- [ ] Add hourly and daily jobs:
```cron
# 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
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
View File
@@ -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-verify-permissions` | Check ownership / permissions on the server |
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` to install
> the backup script + cron jobs.
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` and
> `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
@@ -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-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-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 trigger-backup` | Run the backup script now |
| `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`),
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`.
---
+2 -2
View File
@@ -45,7 +45,7 @@ Do not replace any existing class yet. Just build the foundation.
```
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)
- 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')`
- 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
---
+2 -1
View File
@@ -59,7 +59,8 @@ spoofing.
files. The DocumentRoot is `app/public/` only.
- Restricted-file downloads are gated by a request/approval/token flow
(`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
+41 -15
View File
@@ -93,7 +93,7 @@ deploy: build deploy-code deploy-deps deploy-migrate
@just deploy-env
@just deploy-verify-permissions
@echo ""
@echo "ℹ️ First deploy? Also run: just deploy-backup"
@echo "ℹ️ First deploy? Also run: just deploy-all-first"
@echo ""
[group('deploy')]
@@ -294,6 +294,13 @@ deploy-verify-permissions:
err "storage/cache/rate_limit → NOT WRITABLE"
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_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
if [ "$env_perm" = "640" ]; then
@@ -335,6 +342,15 @@ deploy-script script_name:
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
@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')]
deploy-backup-script:
# 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
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 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 file: /etc/cron.d/xamxam-backup"
@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 "Verify with: just deploy-check-backup-log"
@@ -367,7 +383,7 @@ deploy-backup: deploy-backup-script deploy-backup-cron
[group('deploy')]
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')]
deploy-list-backups:
@@ -376,24 +392,33 @@ deploy-list-backups:
[group('deploy')]
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.
@echo "📋 Installing draft cleanup cron job…"
rsync -v scripts/cleanup-drafts.php xamxam:/var/www/xamxam/scripts/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"
rsync -v scripts/cleanup-drafts.php xamxam:/tmp/cleanup-drafts.php
ssh xamxam "chmod 755 /tmp/cleanup-drafts.php"
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 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 " Cron file: /etc/cron.d/xamxam-cleanup"
@echo " Script: /var/www/xamxam/scripts/cleanup-drafts.php"
@echo " Log file: /var/log/xamxam-cleanup.log"
@echo " Script: /tmp/cleanup-drafts.php"
@echo " Log file: /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log"
@echo ""
@echo "Verify with: just deploy-check-cleanup-log"
[group('deploy')]
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')]
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'
[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.
# ============================================================================
@@ -536,7 +561,8 @@ clean:
[group('utils')]
cleanup-drafts dry_run='':
# List (dry-run) or delete orphaned draft theses older than 24h.
# Pass --no-dry-run to actually delete:
# List (dry-run) or delete orphaned draft theses older than 7 days (168h).
# Pass --no-dry-run to actually delete. Override the age with OLDER_THAN_HOURS:
# 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
View File
@@ -100,8 +100,8 @@ sudo nginx -t
```bash
# Watch logs
sudo tail -f /var/log/nginx/xamxam_access.log
sudo tail -f /var/log/nginx/xamxam_error.log
sudo tail -f /var/log/nginx/xamxam-nginx-access.log
sudo tail -f /var/log/nginx/xamxam-nginx-error.log
# Check nginx status
sudo systemctl status nginx
+2 -2
View File
@@ -72,8 +72,8 @@ server {
client_body_timeout 600s;
# Logging
access_log /var/log/nginx/xamxam_access.log;
error_log /var/log/nginx/xamxam_error.log warn;
access_log /var/log/nginx/xamxam-nginx-access.log;
error_log /var/log/nginx/xamxam-nginx-error.log warn;
# Block access to hidden files (except .well-known for Let's Encrypt)
location ~ /\.(?!well-known).* {
+2 -2
View File
@@ -51,8 +51,8 @@ server {
client_body_timeout 120s;
# Logging
access_log /var/log/nginx/xamxam_access.log;
error_log /var/log/nginx/xamxam_error.log warn;
access_log /var/log/nginx/xamxam-nginx-access.log;
error_log /var/log/nginx/xamxam-nginx-error.log warn;
# Block access to hidden files (except .well-known for Let's Encrypt)
location ~ /\.(?!well-known).* {
+2 -2
View File
@@ -9,8 +9,8 @@
# RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh # 90 days
#
# Expected to be run from cron:
# 0 * * * * /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/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/xamxam-backup-$(date +%Y-%m-%d).log 2>&1
set -euo pipefail
+24 -6
View File
@@ -1,31 +1,49 @@
#!/usr/bin/env 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
* in ThesisCreateController. If the file phase throws after COMMIT, the
* 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:
* php scripts/cleanup-drafts.php # dry-run (list candidates)
* php scripts/cleanup-drafts.php --no-dry-run # actually delete
* php /tmp/cleanup-drafts.php # dry-run (list candidates)
* php /tmp/cleanup-drafts.php --no-dry-run # actually delete
*
* Exit codes: 0 on success, 1 on error.
*/
declare(strict_types=1);
$root = dirname(__DIR__);
define('APP_ROOT', $root . '/app');
// Resolve APP_ROOT robustly. In production the app code is deployed flat under
// /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';
$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 {
$db = new Database();
$result = $db->cleanupOrphanedDrafts(24, $dryRun);
$result = $db->cleanupOrphanedDrafts($olderThanHours, $dryRun);
} catch (Exception $e) {
error_log('[cleanup-drafts] Error: ' . $e->getMessage());
exit(1);
+6 -6
View File
@@ -8,12 +8,12 @@ echo "Creating investigation directory..."
mkdir -p ~/crash_investigation
echo "Copying nginx logs..."
sudo cp /var/log/nginx/xamxam_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_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_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-error.log ~/crash_investigation/
sudo cp /var/log/nginx/xamxam-nginx-error.log.1 ~/crash_investigation/ 2>/dev/null || true
sudo cp /var/log/nginx/xamxam-nginx-access.log ~/crash_investigation/
sudo cp /var/log/nginx/xamxam-nginx-access.log.1 ~/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-nginx-access.log.2.gz ~/crash_investigation/ 2>/dev/null || true
echo "Exporting journal from previous boot..."
sudo journalctl -b -1 --no-pager > ~/crash_investigation/journal_previous_boot.log 2>&1
+8
View File
@@ -89,6 +89,14 @@ chown www-data:xamxam /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"
# 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 ──────────────────────────────────────────────────────
printf "\n📋 Step 2: Deploying nginx configuration...\n"
echo "--------------------------------------------"
+151
View File
@@ -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
+6 -7
View File
@@ -88,13 +88,12 @@ chown -R "$WEB_USER:$APP_GROUP" "$APP_DIR/storage/cache"
chmod -R 2775 "$APP_DIR/storage/cache"
ok "Cache dirs: created and owned by $WEB_USER:$APP_GROUP"
# ── 8. Provision /var/log/xamxam.log ─────────────────────────────────────────
if [ ! -f /var/log/xamxam.log ]; then
touch /var/log/xamxam.log
fi
chown "$WEB_USER:$APP_GROUP" /var/log/xamxam.log
chmod 640 /var/log/xamxam.log
ok "/var/log/xamxam.log: owned by $WEB_USER:$APP_GROUP (640)"
# ── 8. Provision app log directory (/var/log/xamxam) ──────────────────────────────
LOG_DIR="/var/log/xamxam"
mkdir -p "$LOG_DIR"
chown "$WEB_USER:$APP_GROUP" "$LOG_DIR"
chmod 2775 "$LOG_DIR"
ok "/var/log/xamxam: owned by $WEB_USER:$APP_GROUP (2775)"
printf "\n"
ok "Setup complete."