default: @just --list # XAMXAM Justfile # ============================================================================ # Development # ============================================================================ [group('dev')] setup: @bash scripts/setup-dev.sh # One-shot provisioning for a fresh clone: app/.env + APP_KEY (idempotent, never # overwrites an existing key), composer deps, npm deps, DB schema + migrations, # and a first-admin-password check. Prefer this over the stale `setup` recipe. [group('dev')] provision: @bash scripts/provision.sh # One-shot build of all frontend assets (run before `dev` if sources changed) [group('dev')] dev-build: @node scripts/build.mjs --quiet # 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 --quiet" [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 --quiet" & @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 # Diagnostic: report unused CSS symbols per bundle (rebuilds then reports). # Does NOT strip anything. Run after changing templates/JS to refresh the corpus. [group('build')] css-report: @node scripts/build-css.mjs @node scripts/css-unused-report.mjs [group('build')] build-install: @npm ci [group('build')] build-lint: @just lint-css @just lint-js [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-nginx deploy-deps deploy-migrate deploy-sudoers deploy-permissions @just deploy-env @just deploy-verify-permissions @echo "" @echo "ℹ️ First deploy? Also run: just deploy-all-first" @echo "" [group('deploy')] deploy-code: # Sync application code only (no Composer deps, no migrations, no nginx config). # nginx + server-side setup are handled by `deploy-nginx` (via deploy). # No -p/-t/-o/-g: the destination tree is owned by www-data:xamxam (setgid), # so this SSH user can read/write it but cannot chmod/chown/settime files it # doesn't own — preserving perms/times would fail every file with # "Operation not permitted" and exit rsync 23. Ownership/perms are restored # by `deploy-permissions` right after. Times are only used as a transfer # heuristic here; --size-only keeps unchanged files from being re-uploaded # since their remote mtimes are no longer preserved. rsync -rlDz --size-only --info=progress2 --delete \ --exclude-from=.rsync-exclude \ app/ xamxam:/var/www/xamxam/ # Plain rsync (as this user) leaves newly-synced files owned by the caller, # not www-data:xamxam — php-fpm then can't create SQLite journals in # storage/ → HTTP 500. Restore ownership right after, so `just deploy-code` # alone can never break the live site. @just deploy-permissions [group('deploy')] deploy-permissions: # Fix app-tree ownership/permissions on the host so www-data (php-fpm) can # read code and write storage (/sqlite wal+shm), cache/, tmp/, var/. Needs # sudo. Run after any deploy-code resync and as a dep of `deploy`. # # sudo here is NOPASSWD-scoped to /tmp/fix-permissions.sh via the # deploy/xamxam-fix-permissions.sudoers drop-in (installed once by `just # deploy-sudoers`). That avoids relying on an interactive remote pty, which # is fragile: `ssh -t` silently drops the pty when local stdin is not a TTY, # so sudo's prompt prints but accepts no input. If you have not installed # the drop-in yet, this step will prompt for your password interactively. @echo "🔒 Fixing www-data ownership/permissions…" rsync -v scripts/fix-permissions.sh xamxam:/tmp/fix-permissions.sh ssh -t xamxam "sudo bash /tmp/fix-permissions.sh && rm -f /tmp/fix-permissions.sh" [group('deploy')] deploy-sudoers: # One-time install: scoped NOPASSWD sudo so `deploy-permissions` never needs # an interactive remote pty (see deploy/xamxam-fix-permissions.sudoers for # rationale). Privileged write to /etc/sudoers.d requires an interactive sudo # password, so run this from a real terminal the first time. @echo "🔒 Installing NOPASSWD sudo rule for fix-permissions.sh…" rsync -v deploy/xamxam-fix-permissions.sudoers xamxam:/tmp/xamxam-fix-permissions.sudoers ssh -t xamxam "sudo install -o root -g root -m 0440 /tmp/xamxam-fix-permissions.sudoers /etc/sudoers.d/xamxam-fix-permissions && sudo visudo -c -f /etc/sudoers.d/xamxam-fix-permissions && rm -f /tmp/xamxam-fix-permissions.sudoers" @echo "✅ NOPASSWD rule installed. deploy-permissions will run without a password prompt." [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-cover-webp dry_run='': # Backfill small WebP cover thumbnails on the server (idempotent). # Dry-run by default; pass --no-dry-run to actually generate. Runs as the # SSH user (member of the xamxam group, dirs are setgid group-writable) so # no sudo is needed. Deploy as www-data if group-write ever changes. rsync -v scripts/generate-cover-webp.php xamxam:/tmp/generate-cover-webp.php ssh xamxam "cd /var/www/xamxam && php /tmp/generate-cover-webp.php {{dry_run}}" ssh xamxam "rm -f /tmp/generate-cover-webp.php" [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 # 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 " 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 # ── /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 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')] 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) # 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/" @echo "📋 Deploying backup watchdog…" rsync -v scripts/backup-watchdog.php xamxam:/tmp/backup-watchdog.php ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-watchdog.php /usr/local/bin/backup-watchdog.php && rm -f /tmp/backup-watchdog.php" @echo "✅ backup-watchdog.php installed to /usr/local/bin/" @echo "📋 Deploying Nextcloud sync…" rsync -v scripts/nextcloud-sync.php xamxam:/tmp/nextcloud-sync.php ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/nextcloud-sync.php /usr/local/bin/nextcloud-sync.php && rm -f /tmp/nextcloud-sync.php" @echo "✅ nextcloud-sync.php 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:xamxam /var/backups/xamxam && sudo chmod 775 /var/backups/xamxam" 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/xamxam-backup-\$(date +%Y-%m-%d).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 sync='': # Tail the backup log, then optionally run the Nextcloud sync manually. # Usage: `just deploy-check-backup-log` (tail only) or `just deploy-check-backup-log 1` (tail + sync). 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)'" # Accept both `1` and `sync=1` for convenience; anything non-empty other than # the literal "sync=1" positional string is also treated as a truthy request. @case "{{sync}}" in \ 1|sync|sync=1) \ echo "===== Running manual Nextcloud sync ====="; \ ssh -t xamxam "sudo -u www-data php /usr/local/bin/nextcloud-sync.php; echo exit=\$?";; \ ''|0|no) \ echo "(sync skipped — pass 1 to also run it)";; \ *) \ echo "===== Running manual Nextcloud sync ====="; \ ssh -t xamxam "sudo -u www-data php /usr/local/bin/nextcloud-sync.php; echo exit=\$?";; \ esac [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 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:/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-\$(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: /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-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" deploy-tmp-cleanup-cron: # Install cron job for abandoned-upload garbage collection (hourly, 2h threshold). # Creates /etc/cron.d/xamxam-tmp-cleanup and log file on the server. @echo "📋 Installing abandoned-upload cleanup cron job…" rsync -v scripts/cleanup-tmp-uploads.php xamxam:/tmp/cleanup-tmp-uploads.php ssh xamxam "chmod 755 /tmp/cleanup-tmp-uploads.php" rsync -v deploy/xamxam-tmp-cleanup.cron xamxam:/tmp/xamxam-tmp-cleanup.cron ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-tmp-cleanup.cron /etc/cron.d/xamxam-tmp-cleanup && rm -f /tmp/xamxam-tmp-cleanup.cron" ssh -t xamxam "sudo touch /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log" @echo "✅ Abandoned-upload cleanup cron installed." @echo " Cron file: /etc/cron.d/xamxam-tmp-cleanup" @echo " Script: /tmp/cleanup-tmp-uploads.php" @echo " Log file: /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log" @echo "" @echo "Verify with: just deploy-check-tmp-cleanup-log" [group('deploy')] deploy-check-tmp-cleanup-log: ssh xamxam "tail -20 /var/log/xamxam-tmp-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: # 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 deploy-tmp-cleanup-cron deploy-logrotate # One-shot: full initial deploy including backup and cleanup cron jobs. # One-shot remote provisioning for a fresh xamxam server. # Chains: server .env/APP_KEY (idempotent, never overwrites an existing key) → # full deploy → nginx → backup + cleanup + logrotate, then prints the # first-admin and service-credential follow-ups. [group('deploy')] provision-server: @bash scripts/provision-server-env.sh xamxam @just deploy @just deploy-backup @just deploy-cleanup-cron @just deploy-logrotate @echo "" @echo "✅ Server provisioning complete." @echo "" @echo "Next, from the server's /admin/account:" @echo " 1. Set the admin password (fresh DB starts unauthenticated)" @echo " 2. Configure SMTP + PeerTube credentials (stored encrypted in the DB)" @echo " and verify Nextcloud sync in /admin" # ============================================================================ # 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')] smoke-password-reset: # End-to-end smoke test for the password-reset flow (throwaway DB, no email). @php scripts/smoke-test-password-reset.php [group('test')] smoke-session-keepalive: # Smoke test: admin session keepalive refreshes activity but idle sessions still time out. @php scripts/smoke-test-session-keepalive.php [group('test')] lint-php: # Static analysis (phpstan) + coding standards (php-cs-fixer) @vendor/bin/phpstan analyse --memory-limit=512M @vendor/bin/php-cs-fixer check --no-interaction [group('test')] lint-css: # Lint CSS with biome @npx biome lint app/public/assets/css/ [group('test')] lint-js: # Lint JS/build scripts with biome @npx biome lint app/public/assets/js/app/ scripts/ [group('test')] lint: # Run all linters @just lint-php @just lint-css @just lint-js [group('test')] fix: # Auto-fix: biome (JS/CSS formatting + lint) + php-cs-fixer (PHP coding standards) @npx biome check --write --unsafe app/public/assets/css/ app/public/assets/js/app/ scripts/ @vendor/bin/php-cs-fixer fix --no-interaction # ============================================================================ # 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 # ============================================================================ # Test environment (podman compose — fresh Debian box, host-driven via SSH) # ============================================================================ # Boot a throwaway podman-compose server that simulates a FRESH Debian machine # with nothing preinstalled (see test-env/README.md). Generates a test SSH key, # starts the systemd+sshd container, and renders test-env/ssh/config. [group('test-env')] test-env-up: @bash test-env/scripts/setup.sh # Run any just recipe against the TEST box. Puts the test-env ssh/rsync shims # on PATH so every `ssh xamxam …` / `rsync … xamxam:/…` in the recipes is routed # to the podman container instead of production. The host's ~/.ssh/config is # left untouched. Default recipe: `deploy`. # just test-env-run # just deploy # just test-env-run recipe=deploy-nginx [group('test-env')] test-env-run recipe='deploy': @PATH="$(cd test-env && pwd)/bin:$PATH" just {{recipe}} # Bootstrap the LAMP stack (nginx + php8.4-fpm + composer) on the fresh box. # Runs the module's package script through the shim so it targets the test box. [group('test-env')] test-env-provision: @PATH="$(cd test-env && pwd)/bin:$PATH" bash test-env/scripts/provision-server-packages.sh # Run the project's real role/user/dir setup (scripts/setup-server.sh) against # the test box through the shim, via the module's own deploy-script flow. [group('test-env')] test-env-setup-server: @echo "▶ Running scripts/setup-server.sh on the test box…" @PATH="$(cd test-env && pwd)/bin:$PATH" bash -c \ 'rsync -a scripts/setup-server.sh xamxam:/tmp/setup-server.sh && \ ssh -t xamxam "sudo DEPLOY_USER=deploy bash /tmp/setup-server.sh" && \ ssh xamxam "rm -f /tmp/setup-server.sh"' # Check the test box is healthy: ssh, then nginx + php-fpm status + nginx -t. [group('test-env')] test-env-status: @PATH="$(cd test-env && pwd)/bin:$PATH" bash -c \ 'ssh xamxam "echo reachable as \$(whoami); sudo nginx -t 2>&1 | tail -1; systemctl is-active nginx php8.4-fpm 2>/dev/null || true"' # Tear down the whole stack (add -- --keys to also drop the test keypair). [group('test-env')] test-env-teardown tidy='': @bash test-env/scripts/teardown.sh {{tidy}} # ============================================================================ # Utils # ============================================================================ [group('utils')] creds-test: # Probe the SMTP + PeerTube credentials stored in the DB (gum UI). # --instance / --channel to override the stored PeerTube values # --show-pwd to reveal the decrypted password (interactive confirm) @bash scripts/creds-test.sh [group('utils')] app-token: # Probe whether a long-lived PeerTube app token is obtainable. # --instance https://videos.erg.be --client --secret @bash scripts/app-token.sh [group('utils')] sso-diagnose: # Verify every claim in docs/peertube-sso-incident.md (DNS, IdP discovery, # PeerTube OAuth, SMTP AUTH, header propagation) and append to sso-diagnose.log. # --instance --idp --smtp --db --log @bash scripts/sso-diagnose.sh [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 7 days (168h). # Pass --no-dry-run to actually delete. Override the age with OLDER_THAN_HOURS: # just cleanup-drafts --no-dry-run # OLDER_THAN_HOURS=24 just cleanup-drafts @php scripts/cleanup-drafts.php {{dry_run}} cleanup-tmp-uploads dry_run='': # List (dry-run) or delete abandoned FilePond uploads (>2h or missing session). # Pass --no-dry-run to actually delete. Override the age with # TMP_UPLOAD_MAX_AGE_SECONDS: # just cleanup-tmp-uploads --no-dry-run @php scripts/cleanup-tmp-uploads.php {{dry_run}}