Files
xamxam/justfile
Pontoporeia 6ecd3d4540 Fix biome lint errors: remove duplicate CSS properties, apply safe auto-fixes
CSS:
- Remove duplicate 'background' fallbacks in base.css, header.css, search.css
  (solid color declared before gradient — gradient always wins)
- Remove duplicate 'padding' in admin.css .admin-import-log

JS (biome --write safe fixes applied):
- function() → arrow functions in all IIFEs and callbacks
- forEach/callback → arrow functions
- evaluePtrn → parseInt(x, 10) in admin-contacts-form.js
- Cleaned label text in build.mjs lint step

Remaining warnings are intentional: !important overrides, descending
specificity (admin.css cascade), noUnusedVariables (functions exported
to window/onclick), useTemplate style preference.
2026-06-24 13:57:00 +02:00

525 lines
21 KiB
Makefile
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
default:
@just --list
# XAMXAM Justfile
# ============================================================================
# Development
# ============================================================================
[group('dev')]
setup:
@bash scripts/setup-dev.sh
# One-shot build of all frontend assets (run before `dev` if sources changed)
[group('dev')]
dev-build:
@node scripts/build.mjs
# Watch CSS/JS sources and rebuild on change.
# Run in a separate terminal alongside `just dev`.
[group('dev')]
dev-watch:
@npx chokidar \
"app/public/assets/css/**/*.css" \
"app/public/assets/js/app/**/*.js" \
--initial \
-c "node scripts/build.mjs"
[group('dev')]
dev: dev-build migrate
@xdg-open http://127.0.0.1:8000 &
@xdg-open http://127.0.0.1:8000/admin/ &
@npx chokidar \
"app/public/assets/css/**/*.css" \
"app/public/assets/js/app/**/*.js" \
-c "node scripts/build.mjs" &
@sleep 0.5
@php \
-d upload_max_filesize=8192M \
-d post_max_size=8704M \
-d memory_limit=512M \
-d max_execution_time=600 \
-d max_input_time=600 \
-S 127.0.0.1:8000 -t app/public/ app/router.php 2>&1 \
| stdbuf -oL grep -Ev '(Accepted|Closing|live-reload\.php|assets/|favicon)'
[group('dev')]
stop:
@pkill -f "php -S 127.0.0.1:8000" 2>/dev/null; \
pkill -f "chokidar.*assets/(css|js)" 2>/dev/null; \
echo "stopped"
[group('dev')]
logs:
@tail -n 20 error.log 2>/dev/null || echo "no error log"
# ============================================================================
# Build (JS/CSS bundling & minification)
# ============================================================================
[group('build')]
build:
@node scripts/build.mjs
[group('build')]
build-css:
@node scripts/build-css.mjs
[group('build')]
build-js:
@node scripts/build-js.mjs
[group('build')]
build-install:
@npm ci
[group('build')]
build-lint:
@npx biome lint app/public/assets/css/ app/public/assets/js/app/ scripts/
[group('build')]
build-check:
@echo "Checking if build output is up to date…"
@node scripts/check-build.mjs
# ============================================================================
# Deploy
# ============================================================================
[group('deploy')]
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 ""
[group('deploy')]
deploy-code:
# Sync app code + nginx config + permissions (no Composer deps, no migrations)
rsync -vur --progress --delete \
--chown="www-data:xamxam" \
--exclude '/vendor' \
--exclude 'tests' \
--exclude '*.md' \
--exclude '.git*' \
--exclude '.jj' \
--exclude '.claude' \
--exclude '.pi' \
--exclude '.DS_Store' \
--exclude '.env' \
--exclude 'storage/xamxam.db' \
--exclude 'storage/tfe/' \
--exclude 'storage/these/' \
--exclude 'storage/frart/' \
--exclude 'storage/theses' \
--exclude 'storage/covers' \
--exclude 'storage/backup_*' \
--exclude 'storage/cache/*' \
--exclude 'storage/maintenance.flag' \
--exclude 'storage/fixtures' \
--exclude 'storage/docs' \
--exclude 'storage/tmp/' \
--exclude 'storage/documents/' \
--exclude 'storage/theses/' \
--exclude 'storage/triage/' \
--exclude 'storage/backups/' \
--exclude 'storage/logs/' \
--exclude 'var/' \
--exclude 'composer.json' \
--exclude 'composer.lock' \
app/ xamxam:/var/www/xamxam/
# Deploy nginx config + fix permissions + reload (single server-side run)
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh -t xamxam "sudo bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
[group('deploy')]
deploy-deps:
# Sync composer.json + composer.lock to server, then run composer install
# (only if composer.lock checksum changed — skip expensive install otherwise)
rsync -v composer.json composer.lock xamxam:/var/www/xamxam/
ssh xamxam 'cd /var/www/xamxam && \
sed -i "s|\"app/src/\"|\"src/\"|" composer.json && \
if [ ! -f vendor/.composer-lock-checksum ] || \
[ "$(sha256sum composer.lock | cut -d" " -f1)" != "$(cat vendor/.composer-lock-checksum)" ]; then \
echo "→ composer.lock changed, installing dependencies…"; \
composer install --no-dev --no-interaction --optimize-autoloader && \
sha256sum composer.lock | cut -d" " -f1 > vendor/.composer-lock-checksum; \
else \
echo "→ composer.lock unchanged, dumping autoloader (new classes may exist)…"; \
composer dump-autoload --optimize --no-interaction; \
fi'
[group('deploy')]
deploy-migrate:
# Run pending DB migrations (creates DB from schema if missing, idempotent)
rsync -v scripts/migrate.sh xamxam:/tmp/migrate.sh
ssh xamxam "cd /var/www/xamxam && REPO_ROOT=/var/www/xamxam bash /tmp/migrate.sh"
ssh xamxam "rm -f /tmp/migrate.sh"
[group('deploy')]
deploy-env:
#!/usr/bin/env bash
set -euo pipefail
# Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY.
if [ ! -f app/.env ]; then
echo "WARNING: app/.env not found locally — skipping."
exit 0
fi
if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then
echo "Remote .env already exists — skipping to avoid overwriting key."
echo "Run 'just reencrypt-password' if you rotated APP_KEY."
else
rsync -v --progress app/.env xamxam:/var/www/xamxam/.env
ssh -t xamxam "sudo chmod 640 /var/www/xamxam/.env && sudo chown www-data:xamxam /var/www/xamxam/.env"
echo ".env uploaded."
fi
[group('deploy')]
reencrypt-password new_key_b64="":
#!/usr/bin/env bash
set -euo pipefail
# Re-encrypt the SMTP password in the remote DB after rotating APP_KEY.
# Usage:
# 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));"
# 2. Run: just reencrypt-password <new_base64_key>
# 3. Update app/.env locally with the new key, then run: just deploy-env
if [ -z "{{new_key_b64}}" ]; then
echo "Usage: just reencrypt-password <new_base64_key>"
echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\""
exit 1
fi
# Run the re-encryption script on the server using the current key (from remote .env)
# and the supplied new key.
ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db"
[group('deploy')]
deploy-db:
@ssh xamxam '[ ! -f /var/www/xamxam/storage/xamxam.db ]' || (echo "ERROR: remote database already exists. Remove it manually if you intend to overwrite." && exit 1)
rsync -v --progress app/storage/xamxam.db xamxam:/var/www/xamxam/storage/xamxam.db
ssh xamxam "chown www-data:xamxam /var/www/xamxam/storage/xamxam.db && chmod 660 /var/www/xamxam/storage/xamxam.db"
[group('deploy')]
deploy-verify-permissions:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/xamxam"
WEB_USER="www-data"
APP_GROUP="xamxam"
ERRORS=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ok() { printf "${GREEN}${NC} %s\n" "$*"; }
err() { printf "${RED}${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR"
# ── Ownership ──────────────────────────────────────────────────────────────────
echo "── Ownership ───────────────────────────────────"
while IFS= read -r line; do
owner=$(echo "$line" | awk '{print $1}')
group=$(echo "$line" | awk '{print $2}')
path=$(echo "$line" | awk '{print $NF}')
if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then
err "$path$owner:$group (expected $WEB_USER:$APP_GROUP)"
else
ok "$path$owner:$group"
fi
done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null")
# ── Key directories: 2775 ─────────────────────────────────────────────────────
echo "── Directory permissions (expected 2775) ───────"
while IFS= read -r line; do
perms=$(echo "$line" | awk '{print $1}')
path=$(echo "$line" | awk '{print $NF}')
if [ "$perms" != "drwxrwsr-x" ]; then
err "$path$perms (expected drwxrwsr-x / 2775)"
else
ok "$path$perms"
fi
done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort")
# ── Key files: 664 ────────────────────────────────────────────────────────────
echo "── File permissions (expected 664 / 660) ───────"
# Spot-check a few critical files
while IFS= read -r path; do
perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING")
if [ "$perms" = "MISSING" ]; then
err "$path → FILE MISSING"
else
perm_num=$(echo "$perms" | awk '{print $1}')
owner=$(echo "$perms" | awk '{print $2}')
group=$(echo "$perms" | awk '{print $3}')
case "$path" in
*/storage/xamxam.db|*/storage/*.db)
expected_perm="660" ;;
*)
expected_perm="664" ;;
esac
if [ "$perm_num" != "$expected_perm" ]; then
err "$path$perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP"
elif [ "$owner" != "$WEB_USER" ]; then
err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)"
else
ok "$path$perm_num $owner:$group"
fi
fi
done < <(printf '%s\n' \
"$APP_DIR/storage/xamxam.db")
# ── var/ subdirectories must be writable ──────────────────────────────────────
echo "── var/ writability ────────────────────────────"
for subdir in cache logs tmp; do
if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then
ok "var/$subdir → writable"
else
err "var/$subdir → NOT WRITABLE"
fi
done
# ── storage/cache/rate_limit writable ─────────────────────────────────────────
if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then
ok "storage/cache/rate_limit → writable"
else
err "storage/cache/rate_limit → NOT WRITABLE"
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
ok ".env → 640"
elif [ -z "$env_perm" ]; then
warn ".env → MISSING"
else
err ".env → $env_perm (expected 640)"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
if [ "$ERRORS" -eq 0 ]; then
printf "${GREEN}✅ All permissions OK${NC}\n"
else
printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS"
printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n"
exit 1
fi
[group('deploy')]
deploy-nginx:
# Upload nginx config to the server, test it, and reload.
# Uses the scripts/deploy-server.sh helper that handles the nginx
# config installation and reload (steps 2-4).
@echo "📋 Deploying nginx configuration…"
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh -t xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
[group('deploy')]
deploy-script script_name:
# Generic script deployer (e.g., just deploy-script setup-server)
rsync -v scripts/{{script_name}}.sh xamxam:/tmp/{{script_name}}.sh
@echo ""
@echo "Script uploaded. SSH into the server and run:"
@echo ""
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
@echo ""
[group('deploy')]
deploy-backup-script:
# Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo)
# Run once after initial deploy or when the backup script changes.
@echo "📋 Deploying backup script…"
rsync -v scripts/backup-sqlite.sh xamxam:/tmp/backup-sqlite.sh
ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-sqlite.sh /usr/local/bin/backup-sqlite.sh && rm -f /tmp/backup-sqlite.sh"
@echo "✅ backup-sqlite.sh installed to /usr/local/bin/"
[group('deploy')]
deploy-backup-cron:
# Install cron jobs for hourly (30d retention) and daily (90d) backups.
# Uses /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
# Creates backup directory and log file on the server.
@echo "📋 Installing backup cron jobs…"
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"
@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 ""
@echo "Verify with: just deploy-check-backup-log"
[group('deploy')]
deploy-backup: deploy-backup-script deploy-backup-cron
# One-shot: deploy backup script + install cron jobs + set up directories.
[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)'"
[group('deploy')]
deploy-list-backups:
# List all existing backups on the server (most recent last).
ssh xamxam "ls -lth /var/backups/xamxam/ 2>/dev/null || echo 'No backups yet.'"
[group('deploy')]
deploy-cleanup-cron:
# Install cron job for orphaned draft cleanup (every 4 hours, 24h 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 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"
@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 ""
@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)'"
[group('deploy')]
test-restore remote_gz_path:
# Test-restore a production backup snapshot to a local temp DB and verify.
# Usage: just test-restore /var/backups/xamxam/db-2026-05-11T14-00-00.db.gz
@scp xamxam:'{{remote_gz_path}}' /tmp/xamxam-restore-test.db.gz
@gunzip -c /tmp/xamxam-restore-test.db.gz > /tmp/xamxam-restore-test.db
@echo "Tables in snapshot:"
@sqlite3 /tmp/xamxam-restore-test.db ".tables"
@echo ""
@echo "Thesis count:"
@sqlite3 /tmp/xamxam-restore-test.db "SELECT COUNT(*) FROM theses WHERE deleted_at IS NULL;"
@echo ""
@echo "✅ Snapshot is valid. Remove temp files:"
@echo " rm /tmp/xamxam-restore-test.db /tmp/xamxam-restore-test.db.gz"
@rm -f /tmp/xamxam-restore-test.db.gz
[group('deploy')]
trigger-backup:
# Manually trigger the backup script on the server now (doesn't wait for cron).
ssh -t xamxam "sudo -u www-data /usr/local/bin/backup-sqlite.sh"
[group('deploy')]
deploy-migrate-storage dry_run='' target_host='xamxam':
# Run the storage path migration on the remote server.
# Usage:
# just deploy-migrate-storage # apply migration
# just deploy-migrate-storage --dry-run # dry-run only
rsync -v scripts/migrate-storage-paths.php {{target_host}}:/var/www/xamxam/migrate-storage-paths.php
ssh {{target_host}} 'cd /var/www/xamxam && php migrate-storage-paths.php {{dry_run}}'
ssh {{target_host}} 'rm -f /var/www/xamxam/migrate-storage-paths.php'
[group('deploy')]
deploy-all-first: deploy deploy-backup deploy-cleanup-cron
# One-shot: full initial deploy including backup and cleanup cron jobs.
# ============================================================================
# Testing
# ============================================================================
[group('test')]
test:
# Run all PHPUnit tests
@vendor/bin/phpunit tests/phpunit/
[group('test')]
test-coverage:
# Generate HTML coverage report in coverage/
@vendor/bin/phpunit --coverage-html coverage/ tests/phpunit/
[group('test')]
lint-biome:
@biome lint app/public/assets/js/
[group('test')]
lint-php:
# Static analysis + coding standards check
@vendor/bin/phpstan analyse --memory-limit=512M
@vendor/bin/php-cs-fixer check --no-interaction
[group('test')]
cs-fix:
@vendor/bin/php-cs-fixer fix --no-interaction
[group('test')]
phpstan: lint-php
[group('test')]
cs-check: lint-php
[group('test')]
syntax:
@find app/ -name '*.php' -exec php -l {} \; 2>/dev/null | grep -v 'No syntax errors' || true
@echo '✅ Syntax OK'
# ============================================================================
# Database
# ============================================================================
[group('database')]
migrate:
@echo "Running migrations…"
@bash scripts/migrate.sh
[group('database')]
init-db:
@sqlite3 app/storage/xamxam.db < app/storage/schema.sql
@sqlite3 app/storage/xamxam.db "SELECT COUNT(*) || ' tables' FROM sqlite_master WHERE type='table';"
[group('database')]
reset-db:
@rm -f app/storage/xamxam.db
@just init-db
[group('database')]
query:
@sqlite3 app/storage/xamxam.db
[group('database')]
backup:
@sqlite3 app/storage/xamxam.db .dump > app/storage/backup_$(date +%Y%m%d_%H%M%S).sql
[group('database')]
fix-finality-types:
# Rename finality types from old forms to canonical names
# Approfondi → Approfondie, Didactique → Enseignement, Spécialisé → Spécialisée
@php scripts/fix-finality-types.php
[group('database')]
backup-snapshot:
# Hot backup using SQLite's .backup API (WAL-safe), then gzip.
@DB_PATH=app/storage/xamxam.db BACKUP_DIR=app/storage/backups RETENTION_DAYS=30 bash scripts/backup-sqlite.sh
# ============================================================================
# Utils
# ============================================================================
[group('utils')]
clean:
@rm -f app/error.log
@rm -rf app/storage/cache/rate_limit/*
@rm -f /tmp/xamxam-*.log /tmp/xamxam-*.pid
[group('utils')]
cleanup-drafts dry_run='':
# List (dry-run) or delete orphaned draft theses older than 24h.
# Pass --no-dry-run to actually delete:
# 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";}'