@arela/uploader 0.2.12 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.env.template +66 -0
  2. package/.vscode/settings.json +1 -0
  3. package/README.md +134 -58
  4. package/SUPABASE_UPLOAD_FIX.md +157 -0
  5. package/package.json +3 -2
  6. package/scripts/cleanup-ds-store.js +109 -0
  7. package/scripts/cleanup-system-files.js +69 -0
  8. package/scripts/tests/phase-7-features.test.js +415 -0
  9. package/scripts/tests/signal-handling.test.js +275 -0
  10. package/scripts/tests/smart-watch-integration.test.js +554 -0
  11. package/scripts/tests/watch-service-integration.test.js +584 -0
  12. package/src/commands/UploadCommand.js +36 -2
  13. package/src/commands/WatchCommand.js +1305 -0
  14. package/src/config/config.js +113 -0
  15. package/src/document-type-shared.js +2 -0
  16. package/src/document-types/support-document.js +201 -0
  17. package/src/file-detection.js +2 -1
  18. package/src/index.js +44 -0
  19. package/src/services/AdvancedFilterService.js +505 -0
  20. package/src/services/AutoProcessingService.js +639 -0
  21. package/src/services/BenchmarkingService.js +381 -0
  22. package/src/services/DatabaseService.js +723 -170
  23. package/src/services/ErrorMonitor.js +275 -0
  24. package/src/services/LoggingService.js +419 -1
  25. package/src/services/MonitoringService.js +401 -0
  26. package/src/services/PerformanceOptimizer.js +511 -0
  27. package/src/services/ReportingService.js +511 -0
  28. package/src/services/SignalHandler.js +255 -0
  29. package/src/services/SmartWatchDatabaseService.js +527 -0
  30. package/src/services/WatchService.js +783 -0
  31. package/src/services/upload/ApiUploadService.js +30 -4
  32. package/src/services/upload/SupabaseUploadService.js +28 -6
  33. package/src/utils/CleanupManager.js +262 -0
  34. package/src/utils/FileOperations.js +41 -0
  35. package/src/utils/WatchEventHandler.js +517 -0
  36. package/supabase/migrations/001_create_initial_schema.sql +366 -0
  37. package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  38. package/commands.md +0 -6
@@ -0,0 +1,511 @@
1
+ /**
2
+ * ReportingService.js
3
+ * Phase 7 - Task 2: Reporting & Analytics
4
+ *
5
+ * Provides comprehensive reporting and analytics including:
6
+ * - Session reports
7
+ * - Batch reports
8
+ * - Error analysis reports
9
+ * - Export functionality (JSON, CSV, PDF-ready)
10
+ * - Statistics aggregation
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+
15
+ class ReportingService {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+ this.reports = {};
19
+ this.sessions = [];
20
+
21
+ this.reportConfig = {
22
+ includeTimestamps: true,
23
+ includeMetrics: true,
24
+ includeErrors: true,
25
+ includeSummary: true,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Generate session report
31
+ */
32
+ generateSessionReport(session) {
33
+ const report = {
34
+ type: 'session_report',
35
+ timestamp: new Date().toISOString(),
36
+ sessionId: session.sessionId,
37
+ duration: session.endTime
38
+ ? session.endTime - session.startTime
39
+ : Date.now() - session.startTime,
40
+ startTime: new Date(session.startTime).toISOString(),
41
+ endTime: session.endTime ? new Date(session.endTime).toISOString() : null,
42
+
43
+ summary: {
44
+ totalFiles: session.fileCount || 0,
45
+ processedFiles: session.processedCount || 0,
46
+ successCount: session.successCount || 0,
47
+ errorCount: session.errorCount || 0,
48
+ skippedCount: session.skippedCount || 0,
49
+ successRate: this._calculateSuccessRate(session),
50
+ },
51
+
52
+ metrics: {
53
+ averageProcessingTime: this._calculateAvgTime(session),
54
+ filesPerSecond: this._calculateFilesPerSecond(session),
55
+ successRate: this._calculateSuccessRate(session),
56
+ errorRate: this._calculateErrorRate(session),
57
+ },
58
+
59
+ details: {
60
+ uploadedFiles: session.uploadedFiles || [],
61
+ failedFiles: session.failedFiles || [],
62
+ skippedFiles: session.skippedFiles || [],
63
+ },
64
+
65
+ errors: this._extractErrors(session),
66
+
67
+ tags: session.tags || [],
68
+ notes: session.notes || '',
69
+ };
70
+
71
+ this.reports[session.sessionId] = report;
72
+ this.sessions.push(report);
73
+
74
+ return report;
75
+ }
76
+
77
+ /**
78
+ * Generate batch report
79
+ */
80
+ generateBatchReport(sessions) {
81
+ const report = {
82
+ type: 'batch_report',
83
+ timestamp: new Date().toISOString(),
84
+ batchId: `batch_${Date.now()}`,
85
+
86
+ summary: {
87
+ totalSessions: sessions.length,
88
+ totalFiles: 0,
89
+ totalProcessed: 0,
90
+ totalSuccess: 0,
91
+ totalErrors: 0,
92
+ totalSkipped: 0,
93
+ },
94
+
95
+ sessionMetrics: [],
96
+ aggregatedMetrics: {},
97
+ timelineData: [],
98
+ errorSummary: {},
99
+ };
100
+
101
+ // Aggregate data from all sessions
102
+ for (const session of sessions) {
103
+ const sessionReport = this.generateSessionReport(session);
104
+
105
+ report.summary.totalFiles += sessionReport.summary.totalFiles;
106
+ report.summary.totalProcessed += sessionReport.summary.processedFiles;
107
+ report.summary.totalSuccess += sessionReport.summary.successCount;
108
+ report.summary.totalErrors += sessionReport.summary.errorCount;
109
+ report.summary.totalSkipped += sessionReport.summary.skippedCount;
110
+
111
+ report.sessionMetrics.push({
112
+ sessionId: session.sessionId,
113
+ duration: sessionReport.duration,
114
+ fileCount: sessionReport.summary.totalFiles,
115
+ successRate: sessionReport.metrics.successRate,
116
+ });
117
+
118
+ report.timelineData.push({
119
+ timestamp: sessionReport.startTime,
120
+ filesProcessed: sessionReport.summary.processedFiles,
121
+ duration: sessionReport.duration,
122
+ });
123
+
124
+ // Aggregate errors by type
125
+ for (const error of sessionReport.errors) {
126
+ const errorType = error.type || 'unknown';
127
+ if (!report.errorSummary[errorType]) {
128
+ report.errorSummary[errorType] = { count: 0, examples: [] };
129
+ }
130
+ report.errorSummary[errorType].count++;
131
+ if (report.errorSummary[errorType].examples.length < 3) {
132
+ report.errorSummary[errorType].examples.push(error.message);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Calculate aggregated metrics
138
+ report.aggregatedMetrics = {
139
+ totalDuration: report.sessionMetrics.reduce(
140
+ (sum, s) => sum + s.duration,
141
+ 0,
142
+ ),
143
+ averageSessionDuration:
144
+ report.sessionMetrics.length > 0
145
+ ? report.sessionMetrics.reduce((sum, s) => sum + s.duration, 0) /
146
+ report.sessionMetrics.length
147
+ : 0,
148
+ overallSuccessRate:
149
+ report.summary.totalProcessed > 0
150
+ ? (
151
+ (report.summary.totalSuccess / report.summary.totalProcessed) *
152
+ 100
153
+ ).toFixed(2) + '%'
154
+ : 'N/A',
155
+ overallErrorRate:
156
+ report.summary.totalProcessed > 0
157
+ ? (
158
+ (report.summary.totalErrors / report.summary.totalProcessed) *
159
+ 100
160
+ ).toFixed(2) + '%'
161
+ : 'N/A',
162
+ averageFilesPerSession:
163
+ report.summary.totalFiles / Math.max(report.summary.totalSessions, 1),
164
+ };
165
+
166
+ return report;
167
+ }
168
+
169
+ /**
170
+ * Generate error analysis report
171
+ */
172
+ generateErrorAnalysisReport(sessions) {
173
+ const report = {
174
+ type: 'error_analysis_report',
175
+ timestamp: new Date().toISOString(),
176
+
177
+ summary: {
178
+ totalErrors: 0,
179
+ uniqueErrorTypes: new Set(),
180
+ affectedSessions: new Set(),
181
+ },
182
+
183
+ errorBreakdown: {},
184
+ errorPatterns: [],
185
+ recommendations: [],
186
+ };
187
+
188
+ for (const session of sessions) {
189
+ const sessionReport = this.generateSessionReport(session);
190
+
191
+ for (const error of sessionReport.errors) {
192
+ report.summary.totalErrors++;
193
+ report.summary.uniqueErrorTypes.add(error.type);
194
+ report.summary.affectedSessions.add(session.sessionId);
195
+
196
+ // Breakdown by type
197
+ const errorType = error.type || 'unknown';
198
+ if (!report.errorBreakdown[errorType]) {
199
+ report.errorBreakdown[errorType] = {
200
+ count: 0,
201
+ severity: error.severity || 'unknown',
202
+ examples: [],
203
+ };
204
+ }
205
+ report.errorBreakdown[errorType].count++;
206
+
207
+ if (report.errorBreakdown[errorType].examples.length < 5) {
208
+ report.errorBreakdown[errorType].examples.push({
209
+ message: error.message,
210
+ file: error.file,
211
+ timestamp: error.timestamp,
212
+ });
213
+ }
214
+ }
215
+ }
216
+
217
+ // Convert sets to arrays for JSON serialization
218
+ report.summary.uniqueErrorTypes = Array.from(
219
+ report.summary.uniqueErrorTypes,
220
+ );
221
+ report.summary.affectedSessions = Array.from(
222
+ report.summary.affectedSessions,
223
+ );
224
+
225
+ // Identify patterns
226
+ const errorTypes = Object.keys(report.errorBreakdown);
227
+ if (errorTypes.length > 0) {
228
+ const topErrors = errorTypes
229
+ .sort(
230
+ (a, b) =>
231
+ report.errorBreakdown[b].count - report.errorBreakdown[a].count,
232
+ )
233
+ .slice(0, 3);
234
+
235
+ for (const errorType of topErrors) {
236
+ report.errorPatterns.push({
237
+ type: errorType,
238
+ frequency: report.errorBreakdown[errorType].count,
239
+ severity: report.errorBreakdown[errorType].severity,
240
+ occurrenceRate:
241
+ (
242
+ (report.errorBreakdown[errorType].count /
243
+ report.summary.totalErrors) *
244
+ 100
245
+ ).toFixed(2) + '%',
246
+ });
247
+ }
248
+ }
249
+
250
+ // Generate recommendations
251
+ if (report.summary.totalErrors > 0) {
252
+ for (const pattern of report.errorPatterns) {
253
+ report.recommendations.push(
254
+ `${pattern.type}: Occurs in ${pattern.occurrenceRate} of errors. ` +
255
+ `Consider implementing retry logic or improving validation.`,
256
+ );
257
+ }
258
+ }
259
+
260
+ return report;
261
+ }
262
+
263
+ /**
264
+ * Export report to JSON
265
+ */
266
+ async exportReportJson(report, outputPath) {
267
+ try {
268
+ const jsonContent = JSON.stringify(report, null, 2);
269
+ await fs.writeFile(outputPath, jsonContent, 'utf-8');
270
+
271
+ this.logger.info(`Report exported to JSON: ${outputPath}`);
272
+ return { success: true, path: outputPath };
273
+ } catch (error) {
274
+ this.logger.error(`Failed to export report to JSON:`, error);
275
+ return { success: false, error: error.message };
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Export report to CSV
281
+ */
282
+ async exportReportCsv(report, outputPath) {
283
+ try {
284
+ let csvContent = '';
285
+
286
+ if (report.type === 'session_report') {
287
+ csvContent = this._generateSessionCsv(report);
288
+ } else if (report.type === 'batch_report') {
289
+ csvContent = this._generateBatchCsv(report);
290
+ } else if (report.type === 'error_analysis_report') {
291
+ csvContent = this._generateErrorAnalysisCsv(report);
292
+ }
293
+
294
+ await fs.writeFile(outputPath, csvContent, 'utf-8');
295
+
296
+ this.logger.info(`Report exported to CSV: ${outputPath}`);
297
+ return { success: true, path: outputPath };
298
+ } catch (error) {
299
+ this.logger.error(`Failed to export report to CSV:`, error);
300
+ return { success: false, error: error.message };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Generate summary statistics
306
+ */
307
+ generateSummaryStatistics(sessions) {
308
+ const stats = {
309
+ timestamp: new Date().toISOString(),
310
+ periodAnalyzed: {
311
+ start: sessions.length > 0 ? sessions[0].startTime : null,
312
+ end: sessions.length > 0 ? sessions[sessions.length - 1].endTime : null,
313
+ },
314
+
315
+ totals: {
316
+ sessions: sessions.length,
317
+ files: 0,
318
+ processed: 0,
319
+ successful: 0,
320
+ failed: 0,
321
+ skipped: 0,
322
+ },
323
+
324
+ rates: {},
325
+ performance: {},
326
+ trending: {},
327
+ };
328
+
329
+ for (const session of sessions) {
330
+ stats.totals.files += session.fileCount || 0;
331
+ stats.totals.processed += session.processedCount || 0;
332
+ stats.totals.successful += session.successCount || 0;
333
+ stats.totals.failed += session.errorCount || 0;
334
+ stats.totals.skipped += session.skippedCount || 0;
335
+ }
336
+
337
+ // Calculate rates
338
+ if (stats.totals.processed > 0) {
339
+ stats.rates.successRate =
340
+ ((stats.totals.successful / stats.totals.processed) * 100).toFixed(2) +
341
+ '%';
342
+ stats.rates.failureRate =
343
+ ((stats.totals.failed / stats.totals.processed) * 100).toFixed(2) + '%';
344
+ stats.rates.skipRate =
345
+ ((stats.totals.skipped / stats.totals.processed) * 100).toFixed(2) +
346
+ '%';
347
+ }
348
+
349
+ // Performance metrics
350
+ if (sessions.length > 0) {
351
+ const totalDuration = sessions.reduce(
352
+ (sum, s) => sum + ((s.endTime || Date.now()) - s.startTime),
353
+ 0,
354
+ );
355
+ stats.performance.averageSessionDuration =
356
+ (totalDuration / sessions.length).toFixed(0) + 'ms';
357
+ stats.performance.totalDuration = totalDuration + 'ms';
358
+ stats.performance.filesPerSecond = (
359
+ stats.totals.processed /
360
+ (totalDuration / 1000)
361
+ ).toFixed(2);
362
+ }
363
+
364
+ return stats;
365
+ }
366
+
367
+ /**
368
+ * Get report history
369
+ */
370
+ getReportHistory(limit = 10) {
371
+ return this.sessions
372
+ .slice(-limit)
373
+ .reverse()
374
+ .map((report) => ({
375
+ reportId: report.sessionId,
376
+ type: report.type,
377
+ timestamp: report.timestamp,
378
+ summary: report.summary,
379
+ successRate: report.metrics?.successRate,
380
+ }));
381
+ }
382
+
383
+ /**
384
+ * Clear old reports
385
+ */
386
+ clearOldReports(daysOld = 30) {
387
+ const cutoffTime = Date.now() - daysOld * 24 * 60 * 60 * 1000;
388
+ const beforeCount = this.sessions.length;
389
+
390
+ this.sessions = this.sessions.filter(
391
+ (report) => new Date(report.timestamp).getTime() > cutoffTime,
392
+ );
393
+
394
+ const removed = beforeCount - this.sessions.length;
395
+ this.logger.info(`Cleared ${removed} reports older than ${daysOld} days`);
396
+
397
+ return { removed, remaining: this.sessions.length };
398
+ }
399
+
400
+ /**
401
+ * Private helper methods
402
+ */
403
+
404
+ _calculateSuccessRate(session) {
405
+ if (!session.processedCount || session.processedCount === 0) return '0%';
406
+ return (
407
+ (((session.successCount || 0) / session.processedCount) * 100).toFixed(
408
+ 2,
409
+ ) + '%'
410
+ );
411
+ }
412
+
413
+ _calculateErrorRate(session) {
414
+ if (!session.processedCount || session.processedCount === 0) return '0%';
415
+ return (
416
+ (((session.errorCount || 0) / session.processedCount) * 100).toFixed(2) +
417
+ '%'
418
+ );
419
+ }
420
+
421
+ _calculateAvgTime(session) {
422
+ if (!session.processedCount || session.processedCount === 0) return 0;
423
+ const duration = session.endTime
424
+ ? session.endTime - session.startTime
425
+ : Date.now() - session.startTime;
426
+ return Math.round(duration / session.processedCount) + 'ms';
427
+ }
428
+
429
+ _calculateFilesPerSecond(session) {
430
+ if (!session.processedCount) return 0;
431
+ const duration = session.endTime
432
+ ? session.endTime - session.startTime
433
+ : Date.now() - session.startTime;
434
+ return (session.processedCount / (duration / 1000)).toFixed(2);
435
+ }
436
+
437
+ _extractErrors(session) {
438
+ return (session.errors || []).map((error) => ({
439
+ type: error.type || 'unknown',
440
+ message: error.message,
441
+ file: error.file,
442
+ severity: error.severity || 'error',
443
+ timestamp: error.timestamp || new Date().toISOString(),
444
+ context: error.context,
445
+ }));
446
+ }
447
+
448
+ _generateSessionCsv(report) {
449
+ const lines = [
450
+ 'Session Report',
451
+ `Generated: ${report.timestamp}`,
452
+ `Session ID: ${report.sessionId}`,
453
+ '',
454
+ 'Summary',
455
+ 'Total Files,Processed,Success,Errors,Skipped',
456
+ `${report.summary.totalFiles},${report.summary.processedFiles},${report.summary.successCount},${report.summary.errorCount},${report.summary.skippedCount}`,
457
+ '',
458
+ 'Metrics',
459
+ 'Metric,Value',
460
+ `Success Rate,${report.metrics.successRate}`,
461
+ `Error Rate,${report.metrics.errorRate}`,
462
+ `Files Per Second,${report.metrics.filesPerSecond}`,
463
+ `Avg Processing Time,${report.metrics.averageProcessingTime}`,
464
+ ];
465
+
466
+ return lines.join('\n');
467
+ }
468
+
469
+ _generateBatchCsv(report) {
470
+ const lines = [
471
+ 'Batch Report',
472
+ `Generated: ${report.timestamp}`,
473
+ '',
474
+ 'Session Metrics',
475
+ 'Session ID,Duration (ms),File Count,Success Rate',
476
+ ];
477
+
478
+ for (const metric of report.sessionMetrics) {
479
+ lines.push(
480
+ `${metric.sessionId},${metric.duration},${metric.fileCount},${metric.successRate}`,
481
+ );
482
+ }
483
+
484
+ lines.push('', 'Summary', 'Metric,Value');
485
+ for (const [key, value] of Object.entries(report.aggregatedMetrics)) {
486
+ lines.push(`${key},${value}`);
487
+ }
488
+
489
+ return lines.join('\n');
490
+ }
491
+
492
+ _generateErrorAnalysisCsv(report) {
493
+ const lines = [
494
+ 'Error Analysis Report',
495
+ `Generated: ${report.timestamp}`,
496
+ '',
497
+ 'Error Summary',
498
+ 'Error Type,Count,Severity,Occurrence Rate',
499
+ ];
500
+
501
+ for (const pattern of report.errorPatterns) {
502
+ lines.push(
503
+ `${pattern.type},${pattern.frequency},${pattern.severity},${pattern.occurrenceRate}`,
504
+ );
505
+ }
506
+
507
+ return lines.join('\n');
508
+ }
509
+ }
510
+
511
+ export default ReportingService;