gdc_cmod/node_spooler/cleanup.js
mahdahar 2843ddd392 Migrate PDF generation from legacy spooler_db to CI4 + node_spooler
BREAKING CHANGE: Remove public/spooler_db/ legacy system

Changes:
- Migrate validation preview from http://glenlis/spooler_db/main_dev.php to CI4 /report/{accessnumber}
- Add ReportController::preview() for HTML preview in validation dialog
- Add ReportController::generatePdf() to queue PDF generation via node_spooler at http://glenlis:3030
- Add ReportController::checkPdfStatus() to poll spooler job status
- Add ReportController::postToSpooler() helper for curl requests to spooler API
- Add routes: GET /report/(:num)/preview, GET /report/(:num)/pdf, GET /report/status/(:any)
- Delete public/spooler_db/ directory (40+ legacy files)
- Compact node_spooler/README.md from 577 to 342 lines

Technical Details:
- New architecture: CI4 Controller -> node_spooler (port 3030) -> Chrome CDP (port 42020)
- API endpoints: POST /api/pdf/generate, GET /api/pdf/status/:jobId, GET /api/queue/stats
- Features: Max 5 concurrent jobs, max 100 in queue, auto-cleanup after 60 min
- Error handling: Chrome crash detection, manual error review in data/error/
- PDF infrastructure ready, frontend PDF buttons to be updated later in production

Migration verified:
- No external code references spooler_db
- All assets duplicated in public/assets/report/
- Syntax checks passed for ReportController.php and Routes.php

Refs: node_spooler/README.md
2026-02-03 11:33:55 +07:00

209 lines
6.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const LOGS_DIR = path.join(__dirname, 'logs');
const LOG_FILE = path.join(LOGS_DIR, 'cleanup.log');
const CONFIG = {
PDF_DIR: path.join(__dirname, 'data/pdfs'),
ARCHIVE_DIR: path.join(__dirname, 'data/archive'),
ERROR_DIR: path.join(__dirname, 'data/error'),
PDF_RETENTION_DAYS: 7,
ARCHIVE_RETENTION_DAYS: 45,
LOG_COMPRESS_DAYS: 7,
LOG_DELETE_DAYS: 30,
DISK_USAGE_THRESHOLD: 80
};
function logInfo(message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [INFO] ${message}${data ? ' ' + JSON.stringify(data) : ''}\n`;
fs.appendFileSync(LOG_FILE, logEntry);
console.log(`[INFO] ${message}`, data || '');
}
function logWarn(message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [WARN] ${message}${data ? ' ' + JSON.stringify(data) : ''}\n`;
fs.appendFileSync(LOG_FILE, logEntry);
console.warn(`[WARN] ${message}`, data || '');
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
async function runCleanup(dryRun = false) {
logInfo('Cleanup started', { dryRun });
const startTime = Date.now();
try {
await archiveOldPDFs(dryRun);
await deleteOldArchives(dryRun);
await rotateLogs(dryRun);
const diskInfo = await checkDiskSpace();
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
logInfo('Cleanup completed', { elapsedSeconds: elapsed, diskInfo });
console.log('\nCleanup completed in', elapsed + 's');
console.log('Disk space:', diskInfo.total, 'total,', diskInfo.used, 'used,', diskInfo.free, 'free');
console.log('Disk usage:', diskInfo.percentage + '%');
if (diskInfo.percentage > CONFIG.DISK_USAGE_THRESHOLD) {
logWarn('Disk usage above threshold', { current: diskInfo.percentage, threshold: CONFIG.DISK_USAGE_THRESHOLD });
console.warn('WARNING: Disk usage above', CONFIG.DISK_USAGE_THRESHOLD + '%');
}
} catch (error) {
logError('Cleanup failed', error);
console.error('Cleanup failed:', error.message);
process.exit(1);
}
}
async function archiveOldPDFs(dryRun) {
logInfo('Archiving old PDFs...');
const files = fs.readdirSync(CONFIG.PDF_DIR);
const now = Date.now();
const oneDayMs = 24 * 60 * 60 * 1000;
const sevenDaysMs = 7 * oneDayMs;
let archivedCount = 0;
files.forEach(file => {
if (!file.endsWith('.pdf')) return;
const filePath = path.join(CONFIG.PDF_DIR, file);
const stats = fs.statSync(filePath);
const age = now - stats.mtimeMs;
if (age > sevenDaysMs) {
const month = new Date(stats.mtimeMs).toISOString().slice(0, 7);
const archivePath = path.join(CONFIG.ARCHIVE_DIR, month);
if (!fs.existsSync(archivePath)) {
fs.mkdirSync(archivePath, { recursive: true });
}
if (!dryRun) {
fs.renameSync(filePath, path.join(archivePath, file));
archivedCount++;
logInfo('Archived PDF', { file, month });
} else {
logInfo('[DRY-RUN] Would archive', { file, month });
}
}
});
logInfo('Archived PDFs', { count: archivedCount, dryRun });
}
async function deleteOldArchives(dryRun) {
logInfo('Deleting old archives...');
const now = Date.now();
const fortyFiveDaysMs = 45 * 24 * 60 * 60 * 1000;
const months = fs.readdirSync(CONFIG.ARCHIVE_DIR);
let deletedCount = 0;
months.forEach(month => {
const monthPath = path.join(CONFIG.ARCHIVE_DIR, month);
const stats = fs.statSync(monthPath);
const age = now - stats.mtimeMs;
if (age > fortyFiveDaysMs) {
if (!dryRun) {
fs.rmSync(monthPath, { recursive: true, force: true });
deletedCount++;
logInfo('Deleted old archive', { month });
} else {
logInfo('[DRY-RUN] Would delete archive', { month });
}
}
});
logInfo('Deleted old archives', { count: deletedCount, dryRun });
}
async function rotateLogs(dryRun) {
logInfo('Rotating logs...');
const files = fs.readdirSync(LOGS_DIR);
const now = Date.now();
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
let compressedCount = 0;
let deletedCount = 0;
files.forEach(file => {
const filePath = path.join(LOGS_DIR, file);
const stats = fs.statSync(filePath);
const age = now - stats.mtimeMs;
if (age > sevenDaysMs && !file.endsWith('.gz')) {
if (!dryRun) {
try {
fs.copyFileSync(filePath, filePath + '.gz');
fs.unlinkSync(filePath);
compressedCount++;
logInfo('Compressed log', { file });
} catch (error) {
logWarn('Failed to compress log', { file, error: error.message });
}
} else {
logInfo('[DRY-RUN] Would compress', { file });
}
}
if (age > thirtyDaysMs) {
if (!dryRun) {
fs.unlinkSync(filePath);
deletedCount++;
logInfo('Deleted old log', { file });
} else {
logInfo('[DRY-RUN] Would delete', { file });
}
}
});
logInfo('Rotated logs', { compressed: compressedCount, deleted: deletedCount, dryRun });
}
async function checkDiskSpace() {
try {
const stats = fs.statfsSync(CONFIG.PDF_DIR);
const total = stats.bavail * stats.frsize;
const free = stats.bfree * stats.frsize;
const used = total - free;
const usedPercent = (used / total) * 100;
return {
total: formatBytes(total),
used: formatBytes(used),
free: formatBytes(free),
percentage: usedPercent.toFixed(1)
};
} catch (error) {
logError('Failed to check disk space', error);
return {
total: 'Unknown',
used: 'Unknown',
free: 'Unknown',
percentage: 0
};
}
}
const dryRun = process.argv.includes('--dry-run');
runCleanup(dryRun);