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
209 lines
6.5 KiB
JavaScript
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);
|