daily-progress-report-generator.js
1 #!/usr/bin/env node 2 3 /** 4 * Daily Progress Report Generator 5 * Generates PDF reports from daily git, database, and system activity 6 * Requires: pdfkit (already installed) 7 */ 8 9 import PDFDocument from 'pdfkit'; 10 import { createWriteStream, existsSync, mkdirSync } from 'fs'; 11 import { join, dirname } from 'path'; 12 import { fileURLToPath } from 'url'; 13 import Logger from '../utils/logger.js'; 14 15 const __filename = fileURLToPath(import.meta.url); 16 const __dirname = dirname(__filename); 17 const projectRoot = join(__dirname, '../..'); 18 19 const logger = new Logger('DailyReport'); 20 21 // Report output directory 22 const REPORTS_DIR = join(projectRoot, 'reports'); 23 24 // Ensure reports directory exists 25 if (!existsSync(REPORTS_DIR)) { 26 mkdirSync(REPORTS_DIR, { recursive: true }); 27 } 28 29 /** 30 * Generate daily progress report PDF 31 * @param {Object} reportData - Consolidated report data 32 * @param {Object} reportData.gitActivity - Git commits, files changed, branch activity 33 * @param {Object} reportData.dbChanges - Database statistics 34 * @param {Object} reportData.codeQuality - Test coverage, TODOs 35 * @param {Object} reportData.systemHealth - Errors, warnings 36 * @param {string} [outputPath] - Custom output path (optional) 37 * @returns {Promise<string>} Path to generated PDF 38 */ 39 export async function generateDailyReport(reportData, outputPath = null) { 40 logger.info('Generating daily progress report PDF'); 41 42 // Generate PDF filename 43 const dateString = new Date().toISOString().split('T')[0]; // YYYY-MM-DD 44 const filename = `daily-progress-${dateString}.pdf`; 45 const filepath = outputPath || join(REPORTS_DIR, filename); 46 47 // Create PDF 48 await createReportPDF(reportData, filepath); 49 50 logger.success(`Generated report: ${filename}`); 51 52 return filepath; 53 } 54 55 /** 56 * Create PDF document with daily progress data 57 */ 58 // eslint-disable-next-line require-await -- Wraps Promise-based PDF generation 59 async function createReportPDF(data, filepath) { 60 return new Promise((resolve, reject) => { 61 try { 62 const doc = new PDFDocument({ size: 'A4', margin: 50 }); 63 // eslint-disable-next-line security/detect-non-literal-fs-filename -- Dynamic report path 64 const stream = createWriteStream(filepath); 65 66 doc.pipe(stream); 67 68 // Build report sections 69 addHeaderSection(doc); 70 addExecutiveSummary(doc, data); 71 addMetricsSection(doc, data); 72 addCommitsSection(doc, data); 73 addHealthSection(doc, data); 74 addNextStepsSection(doc, data); 75 addFooterSection(doc); 76 77 // Finalize PDF 78 doc.end(); 79 80 stream.on('finish', () => { 81 logger.info(`PDF written to ${filepath}`); 82 resolve(filepath); 83 }); 84 85 stream.on('error', reject); 86 } catch (error) { 87 reject(error); 88 } 89 }); 90 } 91 92 /** 93 * Add report header (title and date) 94 */ 95 function addHeaderSection(doc) { 96 const today = new Date().toLocaleDateString('en-US', { 97 year: 'numeric', 98 month: 'long', 99 day: 'numeric', 100 }); 101 102 doc 103 .fontSize(24) 104 .font('Helvetica-Bold') 105 .fillColor('#333') 106 .text('Daily Progress Report', { align: 'center' }); 107 108 doc.moveDown(0.3); 109 110 doc 111 .fontSize(12) 112 .font('Helvetica') 113 .fillColor('#666') 114 .text(`${today} (Last 24 Hours)`, { align: 'center' }); 115 116 doc.moveDown(1.5); 117 } 118 119 /** 120 * Add executive summary section 121 */ 122 function addExecutiveSummary(doc, data) { 123 addSection(doc, '📋 What Shipped'); 124 addBulletList(doc, data.summary?.shipped || []); 125 doc.moveDown(0.5); 126 } 127 128 /** 129 * Add metrics section (database, git, code quality) 130 */ 131 function addMetricsSection(doc, data) { 132 addSection(doc, '📊 System Metrics'); 133 addDatabaseMetrics(doc, data); 134 addGitMetrics(doc, data); 135 addCodeQualityMetrics(doc, data); 136 doc.moveDown(1); 137 } 138 139 /** 140 * Add database metrics box 141 */ 142 function addDatabaseMetrics(doc, data) { 143 if (data.dbChanges) { 144 addMetricBox(doc, 'Database', [ 145 { label: 'Total Sites', value: data.dbChanges.totalSites?.toLocaleString() || '0' }, 146 { label: 'New Sites (24h)', value: data.dbChanges.newSites?.toLocaleString() || '0' }, 147 { 148 label: 'Assets Captured', 149 value: data.dbChanges.assetsCaptured?.toLocaleString() || '0', 150 }, 151 { label: 'Sites Failing', value: data.dbChanges.failing?.toLocaleString() || '0' }, 152 ]); 153 doc.moveDown(0.5); 154 } 155 } 156 157 /** 158 * Add git activity metrics box 159 */ 160 function addGitMetrics(doc, data) { 161 if (data.gitActivity?.commits?.length > 0) { 162 addMetricBox(doc, 'Git Activity', [ 163 { label: 'Commits', value: data.gitActivity.commits.length.toString() }, 164 { label: 'Files Changed', value: data.gitActivity.filesChanged?.toString() || '0' }, 165 { 166 label: 'Lines Added', 167 value: `+${data.gitActivity.linesAdded?.toLocaleString() || 0}`, 168 color: '#4caf50', 169 }, 170 { 171 label: 'Lines Removed', 172 value: `-${data.gitActivity.linesRemoved?.toLocaleString() || 0}`, 173 color: '#f44336', 174 }, 175 ]); 176 doc.moveDown(0.5); 177 } 178 } 179 180 /** 181 * Add code quality metrics box 182 */ 183 function addCodeQualityMetrics(doc, data) { 184 if (data.codeQuality) { 185 const { coverage, coverageChange } = data.codeQuality; 186 187 addMetricBox(doc, 'Code Quality', [ 188 { 189 label: 'Test Coverage', 190 value: coverage ? `${coverage}%` : 'N/A', 191 color: coverage >= 80 ? '#4caf50' : coverage >= 70 ? '#ffc107' : '#f44336', 192 }, 193 { 194 label: 'Coverage Change', 195 value: 196 coverageChange !== undefined 197 ? `${coverageChange > 0 ? '+' : ''}${coverageChange}%` 198 : 'N/A', 199 color: coverageChange > 0 ? '#4caf50' : coverageChange < 0 ? '#f44336' : '#666', 200 }, 201 { label: 'Failing Tests', value: data.codeQuality.failingTests?.toString() || '0' }, 202 { label: 'New TODOs', value: data.codeQuality.newTodos?.toString() || '0' }, 203 ]); 204 doc.moveDown(0.5); 205 } 206 } 207 208 /** 209 * Add recent commits section 210 */ 211 function addCommitsSection(doc, data) { 212 if (data.gitActivity?.commits?.length > 0) { 213 addSection(doc, '💻 Recent Commits'); 214 215 const commits = data.gitActivity.commits.slice(0, 10); 216 217 doc.fontSize(10).font('Helvetica').fillColor('#000'); 218 219 commits.forEach(commit => { 220 doc.text(`• ${commit.hash} - ${commit.message}`, { indent: 15 }); 221 doc.fontSize(9).fillColor('#666').text(` ${commit.timeAgo}`, { indent: 20 }); 222 doc.fontSize(10).fillColor('#000'); 223 }); 224 225 doc.moveDown(0.5); 226 } 227 } 228 229 /** 230 * Add system health section 231 */ 232 function addHealthSection(doc, data) { 233 addSection(doc, '🏥 System Health'); 234 235 if (data.systemHealth?.errors?.length > 0) { 236 doc.fontSize(11).font('Helvetica-Bold').fillColor('#f44336').text('⚠️ Recent Errors:'); 237 238 doc.fontSize(10).font('Helvetica').fillColor('#000'); 239 240 const errors = data.systemHealth.errors.slice(0, 5); 241 242 errors.forEach(error => { 243 doc.text(`• ${error.message || error}`, { indent: 15 }); 244 if (error.count) { 245 doc.fontSize(9).fillColor('#666').text(` Occurrences: ${error.count}`, { indent: 20 }); 246 doc.fontSize(10).fillColor('#000'); 247 } 248 }); 249 250 doc.moveDown(0.5); 251 } else { 252 doc 253 .fontSize(11) 254 .font('Helvetica') 255 .fillColor('#4caf50') 256 .text('✅ No critical errors in last 24 hours'); 257 258 doc.moveDown(0.5); 259 } 260 } 261 262 /** 263 * Add next steps section 264 */ 265 function addNextStepsSection(doc, data) { 266 if (data.summary?.nextSteps?.length > 0) { 267 addSection(doc, '🎯 Recommended Next Steps'); 268 addBulletList(doc, data.summary.nextSteps); 269 } 270 } 271 272 /** 273 * Add footer with generation timestamp 274 */ 275 function addFooterSection(doc) { 276 const generatedAt = new Date().toLocaleString('en-US', { 277 year: 'numeric', 278 month: 'long', 279 day: 'numeric', 280 hour: '2-digit', 281 minute: '2-digit', 282 }); 283 284 doc 285 .fontSize(9) 286 .fillColor('#999') 287 .text(`Report generated on ${generatedAt} | Confidential`, 50, doc.page.height - 50, { 288 align: 'center', 289 }); 290 } 291 292 /** 293 * Add section header to PDF 294 */ 295 function addSection(doc, title) { 296 doc.fontSize(16).font('Helvetica-Bold').fillColor('#333').text(title); 297 298 doc.moveDown(0.3); 299 doc.strokeColor('#ddd').lineWidth(1.5).moveTo(50, doc.y).lineTo(545, doc.y).stroke(); 300 doc.moveDown(0.5); 301 } 302 303 /** 304 * Add bullet list to PDF 305 */ 306 function addBulletList(doc, items) { 307 if (!items || items.length === 0) { 308 doc.fontSize(11).font('Helvetica').fillColor('#666').text('No items to report'); 309 return; 310 } 311 312 doc.fontSize(11).font('Helvetica').fillColor('#000'); 313 314 items.forEach(item => { 315 doc.text(`• ${item}`, { indent: 15 }); 316 }); 317 } 318 319 /** 320 * Add metric box with label/value pairs 321 */ 322 function addMetricBox(doc, title, metrics) { 323 // Box background 324 const boxHeight = 20 + metrics.length * 18; 325 doc.rect(50, doc.y, 495, boxHeight).fillAndStroke('#f8f8f8', '#ddd'); 326 327 // Title 328 const titleY = doc.y - boxHeight + 8; 329 doc.fontSize(12).font('Helvetica-Bold').fillColor('#333').text(title, 60, titleY, { width: 200 }); 330 331 // Metrics 332 let metricY = titleY + 18; 333 334 metrics.forEach(metric => { 335 const color = metric.color || '#000'; 336 337 doc.fontSize(10).font('Helvetica').fillColor('#666').text(`${metric.label}:`, 70, metricY); 338 339 doc.font('Helvetica-Bold').fillColor(color).text(metric.value, 250, metricY); 340 341 metricY += 18; 342 }); 343 344 doc.moveDown(boxHeight / 12); // Adjust vertical position 345 } 346 347 export default { 348 generateDailyReport, 349 };