/ src / reports / daily-progress-report-generator.js
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  };