mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add periodic cleanup of orphaned drafts: cleanup job, just command, deploy cron
This commit is contained in:
8
TODO.md
8
TODO.md
@@ -1,19 +1,17 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
> Last updated: 2026-06-11 12:10
|
> Last updated: 2026-06-11
|
||||||
> Context: Form Accessibility & Resilience improvements for XAMXAM thesis submission platform
|
> Context: Form Accessibility & Resilience improvements for XAMXAM thesis submission platform
|
||||||
|
|
||||||
## In Progress
|
|
||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
- [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows
|
- [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows
|
||||||
- [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES`
|
- [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES`
|
||||||
- [ ] #cleanup-drafts Add periodic cleanup job for orphaned drafts (`just cleanup-drafts`)
|
|
||||||
- [ ] #form-setup-helper Add `ThesisFormSetup` helper class to reduce bootstrap duplication across add/edit/partage `(partage/index.php)` `(admin/add.php)` `(admin/edit.php)`
|
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
- [x] #cleanup-drafts Add periodic cleanup job for orphaned drafts (`Database.php`, `justfile`, `deploy/xamxam-cleanup.cron`, `scripts/cleanup-drafts.php`) ✓
|
||||||
|
- [x] #form-setup-helper Add `FormBootstrap` helper class to reduce bootstrap duplication across add/edit/partage `(admin/add.php)` `(admin/edit.php)` ✓
|
||||||
- [x] #two-phase-commit Add two-phase commit: INSERT thesis `status='draft'`, COMMIT, move files, UPDATE to `active` `(ThesisCreateController.php)` ✓
|
- [x] #two-phase-commit Add two-phase commit: INSERT thesis `status='draft'`, COMMIT, move files, UPDATE to `active` `(ThesisCreateController.php)` ✓
|
||||||
- [x] #filepond-preserve Preserve FilePond temp file IDs on partage validation redirect `(partage/index.php)` `(FilepondHandler.php)` ✓
|
- [x] #filepond-preserve Preserve FilePond temp file IDs on partage validation redirect `(partage/index.php)` `(FilepondHandler.php)` ✓
|
||||||
- [x] #refactor-partage Extract partage form page chrome to `templates/partage/form-page.php` `(partage/index.php)` ✓
|
- [x] #refactor-partage Extract partage form page chrome to `templates/partage/form-page.php` `(partage/index.php)` ✓
|
||||||
|
|||||||
@@ -2307,6 +2307,59 @@ class Database
|
|||||||
return $newId;
|
return $newId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and optionally delete orphaned draft theses older than a threshold.
|
||||||
|
*
|
||||||
|
* Draft theses are created with status='draft' before file operations
|
||||||
|
* (two-phase commit). If the file phase throws after COMMIT, the draft
|
||||||
|
* remains orphaned indefinitely — no files are attached but the row blocks
|
||||||
|
* the identifier number.
|
||||||
|
*
|
||||||
|
* @param int $olderThanHours Drafts older than this many hours are candidates.
|
||||||
|
* @param bool $dryRun When true, only list candidates without deleting.
|
||||||
|
* @return array{deleted: int, candidates: array} Deleted count + list of IDs found.
|
||||||
|
*/
|
||||||
|
public function cleanupOrphanedDrafts(int $olderThanHours = 24, bool $dryRun = true): array
|
||||||
|
{
|
||||||
|
$cutoff = date('Y-m-d H:i:s', strtotime("-{$olderThanHours} hours"));
|
||||||
|
|
||||||
|
// Draft theses with no files attached = orphaned submissions.
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT t.id, t.identifier, t.title, t.submitted_at
|
||||||
|
FROM theses t
|
||||||
|
WHERE t.status = 'draft'
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND t.submitted_at < ?
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM thesis_files tf WHERE tf.thesis_id = t.id
|
||||||
|
)
|
||||||
|
ORDER BY t.submitted_at ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute([$cutoff]);
|
||||||
|
$candidates = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
return ['deleted' => 0, 'candidates' => $candidates];
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
require_once __DIR__ . '/Audit.php';
|
||||||
|
$actor = Audit::actor();
|
||||||
|
foreach ($candidates as $row) {
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$old = $this->fetchRow('theses', $id);
|
||||||
|
// Hard-delete the orphan row and its junction records.
|
||||||
|
// Cascade covers thesis_authors, thesis_tags, thesis_files (empty),
|
||||||
|
// thesis_supervisors, thesis_languages, thesis_formats.
|
||||||
|
$this->pdo->prepare('DELETE FROM theses WHERE id = ?')->execute([$id]);
|
||||||
|
Audit::log($this, $actor, 'DELETE', 'theses', $id, $old, null);
|
||||||
|
$deleted++;
|
||||||
|
error_log("[cleanup-drafts] Deleted orphaned draft thesis {$id} ({$row['identifier']}) — submitted {$row['submitted_at']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['deleted' => $deleted, 'candidates' => $candidates];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft-delete a single thesis (sets deleted_at).
|
* Soft-delete a single thesis (sets deleted_at).
|
||||||
*/
|
*/
|
||||||
|
|||||||
7
deploy/xamxam-cleanup.cron
Normal file
7
deploy/xamxam-cleanup.cron
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 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.
|
||||||
|
# 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
|
||||||
32
justfile
32
justfile
@@ -319,6 +319,27 @@ deploy-list-backups:
|
|||||||
# List all existing backups on the server (most recent last).
|
# List all existing backups on the server (most recent last).
|
||||||
ssh xamxam "ls -lth /var/backups/xamxam/ 2>/dev/null || echo 'No backups yet.'"
|
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')]
|
[group('deploy')]
|
||||||
test-restore remote_gz_path:
|
test-restore remote_gz_path:
|
||||||
# Test-restore a production backup snapshot to a local temp DB and verify.
|
# Test-restore a production backup snapshot to a local temp DB and verify.
|
||||||
@@ -351,8 +372,8 @@ 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-all-first: deploy deploy-backup deploy-cleanup-cron
|
||||||
# One-shot: full initial deploy including backup cron.
|
# One-shot: full initial deploy including backup and cleanup cron jobs.
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Testing
|
# Testing
|
||||||
@@ -434,3 +455,10 @@ clean:
|
|||||||
@rm -f app/error.log
|
@rm -f app/error.log
|
||||||
@rm -rf app/storage/cache/rate_limit/*
|
@rm -rf app/storage/cache/rate_limit/*
|
||||||
@rm -f /tmp/xamxam-*.log /tmp/xamxam-*.pid
|
@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";}'
|
||||||
|
|||||||
55
scripts/cleanup-drafts.php
Executable file
55
scripts/cleanup-drafts.php
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* cleanup-drafts.php — Delete orphaned draft theses older than 24h.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* php scripts/cleanup-drafts.php # dry-run (list candidates)
|
||||||
|
* php scripts/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');
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
|
||||||
|
$dryRun = !in_array('--no-dry-run', $argv, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = new Database();
|
||||||
|
$result = $db->cleanupOrphanedDrafts(24, $dryRun);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('[cleanup-drafts] Error: ' . $e->getMessage());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = count($result['candidates']);
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
exit(0); // nothing to do — quiet exit
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
foreach ($result['candidates'] as $row) {
|
||||||
|
printf(
|
||||||
|
"DRY-RUN → #%d %s \"%s\" (submitted %s)\n",
|
||||||
|
$row['id'],
|
||||||
|
$row['identifier'],
|
||||||
|
$row['title'],
|
||||||
|
$row['submitted_at']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo "Found {$count} orphaned draft(s). Re-run with --no-dry-run to delete.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Deleted %d orphaned draft(s).\n", $result['deleted']);
|
||||||
|
exit(0);
|
||||||
Reference in New Issue
Block a user