@arela/uploader 1.1.3 → 1.2.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 (99) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/document-type-shared.js +1 -1
  92. package/src/document-types/pedimento-completo.js +8 -1
  93. package/src/file-detection.js +9 -0
  94. package/src/index.js +55 -0
  95. package/src/scoring/scoring-engine.js +1 -1
  96. package/src/services/LoggingService.js +35 -0
  97. package/src/services/PipelineApiService.js +7 -1
  98. package/src/services/PipelineJobRunner.js +389 -0
  99. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,649 @@
1
+ import { randomUUID } from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ import appConfig from '../config/config.js';
6
+ import FileOperations from '../utils/FileOperations.js';
7
+
8
+ /**
9
+ * Logging Service
10
+ * Handles application logging with buffering, file output, console output, and event tracking
11
+ */
12
+ export class LoggingService {
13
+ constructor() {
14
+ this.logFilePath = appConfig.logging.logFilePath;
15
+ this.isVerbose = appConfig.logging.verbose;
16
+ this.buffer = [];
17
+ this.bufferSize = appConfig.performance.logBufferSize;
18
+ this.flushInterval = appConfig.performance.logFlushInterval;
19
+ this.lastFlushTime = Date.now();
20
+ this.context = null;
21
+
22
+ // Event tracking (new in Fase 5)
23
+ this.uploadEvents = [];
24
+ this.retryEvents = [];
25
+ this.sessionStartTime = null;
26
+ this.sessionId = null;
27
+
28
+ this.#setupProcessHandlers();
29
+ }
30
+
31
+ /**
32
+ * Setup process handlers for graceful shutdown
33
+ * @private
34
+ */
35
+ #setupProcessHandlers() {
36
+ const flushAndExit = (exitCode = 0) => {
37
+ this.flush();
38
+ process.exit(exitCode);
39
+ };
40
+
41
+ process.on('exit', () => this.flush());
42
+ process.on('SIGINT', () => flushAndExit(0));
43
+ process.on('SIGTERM', () => flushAndExit(0));
44
+ process.on('uncaughtException', (error) => {
45
+ this.error(`Uncaught Exception: ${error.message}`);
46
+ flushAndExit(1);
47
+ });
48
+ process.on('unhandledRejection', (reason, promise) => {
49
+ this.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
50
+ // Don't kill the process — allow it to continue processing remaining files
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Prefix a message with the active context (agent profile name), so both the
56
+ * console echo and the file entry stay grep-able per profile
57
+ * @private
58
+ */
59
+ #withContext(message) {
60
+ return this.context ? `[${this.context}] ${message}` : message;
61
+ }
62
+
63
+ /**
64
+ * Write a log message
65
+ * @param {string} message - Log message
66
+ * @param {string} level - Log level (info, warn, error, debug)
67
+ */
68
+ writeLog(message, level = 'info') {
69
+ try {
70
+ const timestamp = new Date().toISOString();
71
+ const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
72
+
73
+ this.buffer.push(logEntry);
74
+
75
+ // Flush if buffer is full or enough time has passed
76
+ const now = Date.now();
77
+ if (
78
+ this.buffer.length >= this.bufferSize ||
79
+ now - this.lastFlushTime >= this.flushInterval
80
+ ) {
81
+ this.flush();
82
+ }
83
+ } catch (error) {
84
+ console.error(`❌ Error buffering log message: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Log info message
90
+ * @param {string} message - Message to log
91
+ */
92
+ info(message) {
93
+ message = this.#withContext(message);
94
+ this.writeLog(message, 'info');
95
+ // Echo to console: run banners, stats and the 🧩 observability lines were
96
+ // file-only, so operators watching the terminal never saw them (the v1
97
+ // canary summary was invisible until someone grepped arela-upload.log).
98
+ console.log(message);
99
+ }
100
+
101
+ /**
102
+ * Log warning message
103
+ * @param {string} message - Message to log
104
+ */
105
+ warn(message) {
106
+ message = this.#withContext(message);
107
+ this.writeLog(message, 'warn');
108
+ console.warn(`⚠️ ${message}`);
109
+ }
110
+
111
+ /**
112
+ * Log error message
113
+ * @param {string} message - Message to log
114
+ */
115
+ error(message) {
116
+ message = this.#withContext(message);
117
+ this.writeLog(message, 'error');
118
+ console.error(`❌ ${message}`);
119
+ }
120
+
121
+ /**
122
+ * Log debug message (only if verbose mode is enabled)
123
+ * @param {string} message - Message to log
124
+ */
125
+ debug(message) {
126
+ if (this.isVerbose) {
127
+ message = this.#withContext(message);
128
+ this.writeLog(message, 'debug');
129
+ console.log(`🔍 ${message}`);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Log verbose message (only if verbose mode is enabled)
135
+ * @param {string} message - Message to log
136
+ */
137
+ verbose(message) {
138
+ if (this.isVerbose) {
139
+ message = this.#withContext(message);
140
+ this.writeLog(message, 'verbose');
141
+ console.log(message);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Log success message
147
+ * @param {string} message - Message to log
148
+ */
149
+ success(message) {
150
+ message = this.#withContext(message);
151
+ this.writeLog(message, 'success');
152
+ console.log(`✅ ${message}`);
153
+ }
154
+
155
+ /**
156
+ * Flush log buffer to file
157
+ */
158
+ flush() {
159
+ if (this.buffer.length === 0) return;
160
+
161
+ try {
162
+ const logContent = this.buffer.join('\n') + '\n';
163
+ FileOperations.appendToFile(this.logFilePath, logContent);
164
+ this.buffer = [];
165
+ this.lastFlushTime = Date.now();
166
+ } catch (error) {
167
+ console.error(`❌ Error writing to log file: ${error.message}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Clear the log file
173
+ */
174
+ clearLogFile() {
175
+ try {
176
+ if (FileOperations.fileExists(this.logFilePath)) {
177
+ fs.unlinkSync(this.logFilePath);
178
+ }
179
+ } catch (error) {
180
+ console.error(`❌ Error clearing log file: ${error.message}`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get log file path
186
+ * @returns {string} Path to log file
187
+ */
188
+ getLogFilePath() {
189
+ return this.logFilePath;
190
+ }
191
+
192
+ /**
193
+ * Redirect logging to a different file (agent mode uses ~/.arela/arela-agent.log
194
+ * instead of the cwd-bound arela-upload.log). Flushes pending entries first.
195
+ * @param {string} filePath - Absolute path to the new log file
196
+ */
197
+ setLogFilePath(filePath) {
198
+ this.flush();
199
+ this.logFilePath = filePath;
200
+ }
201
+
202
+ /**
203
+ * Set a context prefix added to every file log entry (e.g. the active agent
204
+ * profile name, so one shared log stays grep-able per profile).
205
+ * @param {string|null} prefix - Context name, or null to clear
206
+ */
207
+ setContext(prefix) {
208
+ this.context = prefix || null;
209
+ }
210
+
211
+ /**
212
+ * Check if verbose logging is enabled
213
+ * @returns {boolean} True if verbose logging is enabled
214
+ */
215
+ isVerboseEnabled() {
216
+ return this.isVerbose;
217
+ }
218
+
219
+ /**
220
+ * Set verbose mode
221
+ * @param {boolean} verbose - Enable/disable verbose mode
222
+ */
223
+ setVerbose(verbose) {
224
+ this.isVerbose = verbose;
225
+ }
226
+
227
+ /**
228
+ * Get current buffer size
229
+ * @returns {number} Number of buffered log entries
230
+ */
231
+ getBufferSize() {
232
+ return this.buffer.length;
233
+ }
234
+
235
+ /**
236
+ * Initialize session tracking (Fase 5)
237
+ * @param {string} sessionId - Unique session identifier
238
+ */
239
+ initializeSession(sessionId) {
240
+ this.sessionId = sessionId;
241
+ this.sessionStartTime = Date.now();
242
+ this.uploadEvents = [];
243
+ this.retryEvents = [];
244
+ this.info(`Session initialized: ${sessionId}`);
245
+ }
246
+
247
+ /**
248
+ * Record an upload event
249
+ * @param {Object} uploadEvent - Upload event details
250
+ */
251
+ recordUploadEvent(uploadEvent) {
252
+ const event = {
253
+ id: randomUUID(),
254
+ timestamp: new Date().toISOString(),
255
+ ...uploadEvent,
256
+ };
257
+
258
+ this.uploadEvents.push(event);
259
+ this.info(
260
+ `Upload event recorded: ${event.strategy} (${event.fileCount} files, ` +
261
+ `${event.successCount} success, ${event.failureCount} failed)`,
262
+ );
263
+
264
+ return event.id;
265
+ }
266
+
267
+ /**
268
+ * Record a retry event
269
+ * @param {string} uploadEventId - ID of the original upload event
270
+ * @param {number} attemptNumber - Retry attempt number
271
+ * @param {string} error - Error message
272
+ * @param {number} backoffMs - Backoff time in milliseconds
273
+ */
274
+ recordRetryEvent(uploadEventId, attemptNumber, error, backoffMs) {
275
+ const event = {
276
+ id: randomUUID(),
277
+ timestamp: new Date().toISOString(),
278
+ uploadEventId,
279
+ attemptNumber,
280
+ error,
281
+ backoffMs,
282
+ };
283
+
284
+ this.retryEvents.push(event);
285
+ this.warn(
286
+ `Retry attempt ${attemptNumber} for upload ${uploadEventId.substring(0, 8)}: ${error}`,
287
+ );
288
+
289
+ return event.id;
290
+ }
291
+
292
+ /**
293
+ * Get upload history
294
+ * @returns {Array} Array of upload events
295
+ */
296
+ getUploadHistory() {
297
+ return [...this.uploadEvents];
298
+ }
299
+
300
+ /**
301
+ * Get retry history
302
+ * @returns {Array} Array of retry events
303
+ */
304
+ getRetryHistory() {
305
+ return [...this.retryEvents];
306
+ }
307
+
308
+ /**
309
+ * Get session statistics
310
+ * @returns {Object} Session statistics
311
+ */
312
+ getSessionStatistics() {
313
+ const stats = {
314
+ sessionId: this.sessionId,
315
+ startTime: new Date(this.sessionStartTime).toISOString(),
316
+ duration: Date.now() - this.sessionStartTime,
317
+ totalUploadEvents: this.uploadEvents.length,
318
+ totalRetryEvents: this.retryEvents.length,
319
+ totalFileCount: this.uploadEvents.reduce(
320
+ (sum, e) => sum + e.fileCount,
321
+ 0,
322
+ ),
323
+ totalSuccessCount: this.uploadEvents.reduce(
324
+ (sum, e) => sum + e.successCount,
325
+ 0,
326
+ ),
327
+ totalFailureCount: this.uploadEvents.reduce(
328
+ (sum, e) => sum + e.failureCount,
329
+ 0,
330
+ ),
331
+ successRate: 0,
332
+ byStrategy: {
333
+ individual: this.#getStrategyStats('individual'),
334
+ batch: this.#getStrategyStats('batch'),
335
+ fullStructure: this.#getStrategyStats('full-structure'),
336
+ },
337
+ retryStats: {
338
+ totalRetries: this.retryEvents.length,
339
+ uniqueUploadsWithRetries: new Set(
340
+ this.retryEvents.map((e) => e.uploadEventId),
341
+ ).size,
342
+ totalRetryDuration: this.retryEvents.reduce(
343
+ (sum, e) => sum + e.backoffMs,
344
+ 0,
345
+ ),
346
+ },
347
+ };
348
+
349
+ // Calculate success rate
350
+ if (stats.totalFileCount > 0) {
351
+ stats.successRate =
352
+ (stats.totalSuccessCount / stats.totalFileCount) * 100;
353
+ }
354
+
355
+ return stats;
356
+ }
357
+
358
+ /**
359
+ * Get statistics for a specific strategy (private)
360
+ * @private
361
+ * @param {string} strategy - Strategy name
362
+ * @returns {Object} Strategy statistics
363
+ */
364
+ #getStrategyStats(strategy) {
365
+ const strategyEvents = this.uploadEvents.filter(
366
+ (e) => e.strategy === strategy,
367
+ );
368
+
369
+ return {
370
+ uploadCount: strategyEvents.length,
371
+ totalFiles: strategyEvents.reduce((sum, e) => sum + e.fileCount, 0),
372
+ totalSuccess: strategyEvents.reduce((sum, e) => sum + e.successCount, 0),
373
+ totalFailure: strategyEvents.reduce((sum, e) => sum + e.failureCount, 0),
374
+ totalDuration: strategyEvents.reduce((sum, e) => sum + e.duration, 0),
375
+ averageDuration:
376
+ strategyEvents.length > 0
377
+ ? strategyEvents.reduce((sum, e) => sum + e.duration, 0) /
378
+ strategyEvents.length
379
+ : 0,
380
+ successRate:
381
+ strategyEvents.length > 0
382
+ ? (strategyEvents.reduce((sum, e) => sum + e.successCount, 0) /
383
+ strategyEvents.reduce((sum, e) => sum + e.fileCount, 0)) *
384
+ 100
385
+ : 0,
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Format session report
391
+ * @returns {string} Formatted session report
392
+ */
393
+ formatSessionReport() {
394
+ const stats = this.getSessionStatistics();
395
+ const endTime = new Date();
396
+ const startTime = new Date(stats.startTime);
397
+ const durationMin = Math.floor(stats.duration / 60000);
398
+ const durationSec = Math.floor((stats.duration % 60000) / 1000);
399
+
400
+ let report = '';
401
+
402
+ // Header with visual separators
403
+ report +=
404
+ '\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n';
405
+ report +=
406
+ '┃ 📊 SESSION SUMMARY REPORT ┃\n';
407
+ report +=
408
+ '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n\n';
409
+
410
+ // Session Information
411
+ report += '🕐 SESSION INFORMATION\n';
412
+ report +=
413
+ '─────────────────────────────────────────────────────────────────\n';
414
+ report += `│ Session ID: ${this.#padRight(stats.sessionId, 49)}\n`;
415
+ report += `│ Duration: ${this.#padRight(`${durationMin}m ${durationSec}s`, 49)}\n`;
416
+ report += `│ Start Time: ${this.#padRight(stats.startTime, 49)}\n`;
417
+ report += `│ End Time: ${this.#padRight(endTime.toISOString(), 49)}\n`;
418
+ report += `│ Total Uploads: ${this.#padRight(`${stats.totalUploadEvents}`, 49)}\n`;
419
+ report += `│ Total Retries: ${this.#padRight(`${stats.totalRetryEvents}`, 49)}\n\n`;
420
+
421
+ // Overall Statistics
422
+ report += '� OVERALL STATISTICS\n';
423
+ report +=
424
+ '─────────────────────────────────────────────────────────────────\n';
425
+ report += `│ Total Files Processed: ${this.#padRight(`${stats.totalFileCount}`, 41)}\n`;
426
+ report += `│ Successful Files: ${this.#padRight(`${stats.totalSuccessCount} (${stats.successRate.toFixed(1)}%)`, 41)}\n`;
427
+ report += `│ Failed Files: ${this.#padRight(`${stats.totalFailureCount}`, 41)}\n`;
428
+ report += `│ Session Success Rate: ${this.#padRight(`${stats.successRate.toFixed(1)}%`, 41)}\n\n`;
429
+
430
+ // Strategy breakdown
431
+ report += '🎯 BY STRATEGY BREAKDOWN\n';
432
+ report +=
433
+ '─────────────────────────────────────────────────────────────────\n';
434
+ report += this.#formatStrategyStatsDetailed(stats.byStrategy);
435
+
436
+ // Performance metrics
437
+ if (stats.totalFileCount > 0) {
438
+ const avgTimePerFile = stats.duration / stats.totalFileCount;
439
+ const filesPerMin = (
440
+ stats.totalFileCount /
441
+ (stats.duration / 60000)
442
+ ).toFixed(1);
443
+
444
+ report += '⚡ PERFORMANCE METRICS\n';
445
+ report +=
446
+ '─────────────────────────────────────────────────────────────────\n';
447
+ report += `│ Average Time per File: ${this.#padRight(`${avgTimePerFile.toFixed(1)}ms`, 41)}\n`;
448
+ report += `│ Processing Speed: ${this.#padRight(`${filesPerMin} files/min`, 41)}\n`;
449
+ report += `│ Total Session Duration: ${this.#padRight(`${durationMin}m ${durationSec}s`, 41)}\n\n`;
450
+ }
451
+
452
+ // Retry statistics
453
+ if (stats.retryStats.totalRetries > 0) {
454
+ const avgRetryWait =
455
+ stats.retryStats.totalRetryDuration /
456
+ stats.retryStats.totalRetries /
457
+ 1000;
458
+ report += '🔄 RETRY STATISTICS\n';
459
+ report +=
460
+ '─────────────────────────────────────────────────────────────────\n';
461
+ report += `│ Total Retries: ${this.#padRight(`${stats.retryStats.totalRetries}`, 41)}\n`;
462
+ report += `│ Uploads with Retries: ${this.#padRight(`${stats.retryStats.uniqueUploadsWithRetries}`, 41)}\n`;
463
+ report += `│ Total Retry Duration: ${this.#padRight(`${(stats.retryStats.totalRetryDuration / 1000).toFixed(2)}s`, 41)}\n`;
464
+ report += `│ Retry Success Rate: ${this.#padRight(`100%`, 41)}\n\n`;
465
+ }
466
+
467
+ // Intelligent recommendations
468
+ report += '💡 RECOMMENDATIONS\n';
469
+ report +=
470
+ '─────────────────────────────────────────────────────────────────\n';
471
+ report += this.#generateRecommendations(stats);
472
+
473
+ // Footer
474
+ report +=
475
+ '\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n';
476
+
477
+ return report;
478
+ }
479
+
480
+ /**
481
+ * Pad string to the right with spaces (private)
482
+ * @private
483
+ * @param {string} str - String to pad
484
+ * @param {number} length - Target length
485
+ * @returns {string} Padded string
486
+ */
487
+ #padRight(str, length) {
488
+ const padding = Math.max(0, length - String(str).length);
489
+ return String(str) + ' '.repeat(padding);
490
+ }
491
+
492
+ /**
493
+ * Format detailed strategy statistics (private)
494
+ * @private
495
+ * @param {Object} strategies - Strategy statistics
496
+ * @returns {string} Formatted statistics
497
+ */
498
+ #formatStrategyStatsDetailed(strategies) {
499
+ let report = '';
500
+ const strategyEmojis = {
501
+ individual: '📁',
502
+ batch: '📦',
503
+ 'full-structure': '🗂️',
504
+ };
505
+
506
+ for (const [strategyName, stats] of Object.entries(strategies)) {
507
+ if (stats.uploadCount > 0) {
508
+ const emoji = strategyEmojis[strategyName] || '📋';
509
+ const durationSec = (stats.totalDuration / 1000).toFixed(1);
510
+ const filesPerMin =
511
+ stats.totalDuration > 0
512
+ ? (stats.totalFiles / (stats.totalDuration / 60000)).toFixed(1)
513
+ : '0.0';
514
+
515
+ report += `${emoji} ${strategyName.toUpperCase()}\n`;
516
+ report += `│ ├─ Uploads: ${this.#padRight(`${stats.uploadCount}`, 47)}\n`;
517
+ report += `│ ├─ Files: ${this.#padRight(`${stats.totalFiles}`, 47)}\n`;
518
+ report += `│ ├─ Success: ${this.#padRight(`${stats.totalSuccess} (${stats.successRate.toFixed(1)}%)`, 47)}\n`;
519
+ report += `│ ├─ Failed: ${this.#padRight(`${stats.totalFiles - stats.totalSuccess}`, 47)}\n`;
520
+ report += `│ ├─ Duration: ${this.#padRight(`${durationSec}s`, 47)}\n`;
521
+ report += `│ └─ Speed: ${this.#padRight(`${filesPerMin} files/min`, 47)}\n`;
522
+ report += `│\n`;
523
+ }
524
+ }
525
+
526
+ return report;
527
+ }
528
+
529
+ /**
530
+ * Generate intelligent recommendations based on statistics (private)
531
+ * @private
532
+ * @param {Object} stats - Session statistics
533
+ * @returns {string} Formatted recommendations
534
+ */
535
+ #generateRecommendations(stats) {
536
+ const recommendations = [];
537
+
538
+ // Analyze success rate
539
+ if (stats.successRate >= 95) {
540
+ recommendations.push(
541
+ '✅ Excellent performance! Success rate is outstanding.',
542
+ );
543
+ } else if (stats.successRate >= 80) {
544
+ recommendations.push(
545
+ '⚠️ Good performance but consider investigating failures.',
546
+ );
547
+ } else {
548
+ recommendations.push(
549
+ '🚨 Low success rate - review error logs for issues.',
550
+ );
551
+ }
552
+
553
+ // Analyze retries
554
+ if (stats.retryStats.totalRetries === 0) {
555
+ recommendations.push(
556
+ '✅ No retries needed - upload system is very stable.',
557
+ );
558
+ } else if (stats.retryStats.totalRetries <= 5) {
559
+ recommendations.push(
560
+ '✅ Retries were minimal and successful - system handled errors well.',
561
+ );
562
+ } else {
563
+ recommendations.push(
564
+ '⚠️ High retry count - consider improving connection stability.',
565
+ );
566
+ }
567
+
568
+ // Analyze strategies
569
+ const byStrategy = stats.byStrategy;
570
+ const strategies = Object.entries(byStrategy)
571
+ .filter(([_, s]) => s.uploadCount > 0)
572
+ .sort((a, b) => {
573
+ const aSpeed =
574
+ a[1].totalDuration > 0
575
+ ? a[1].totalFiles / (a[1].totalDuration / 60000)
576
+ : 0;
577
+ const bSpeed =
578
+ b[1].totalDuration > 0
579
+ ? b[1].totalFiles / (b[1].totalDuration / 60000)
580
+ : 0;
581
+ return bSpeed - aSpeed;
582
+ });
583
+
584
+ if (strategies.length > 0) {
585
+ const fastest = strategies[0];
586
+ recommendations.push(
587
+ `💡 Fastest strategy: ${fastest[0]} (${(fastest[1].totalFiles / (fastest[1].totalDuration / 60000)).toFixed(1)} files/min)`,
588
+ );
589
+ }
590
+
591
+ if (stats.totalFileCount > 100) {
592
+ recommendations.push(
593
+ '💡 Large session detected - monitor memory usage for future sessions.',
594
+ );
595
+ }
596
+
597
+ // Format recommendations
598
+ let report = '';
599
+ recommendations.forEach((rec, idx) => {
600
+ if (idx < recommendations.length - 1) {
601
+ report += `│ ${rec}\n`;
602
+ } else {
603
+ report += `│ ${rec}\n`;
604
+ }
605
+ });
606
+
607
+ return report;
608
+ }
609
+
610
+ /**
611
+ * Format strategy statistics (private) - Legacy method
612
+ * @private
613
+ * @param {Object} strategies - Strategy statistics
614
+ * @returns {string} Formatted statistics
615
+ */
616
+ #formatStrategyStats(strategies) {
617
+ let report = '';
618
+
619
+ for (const [strategyName, stats] of Object.entries(strategies)) {
620
+ if (stats.uploadCount > 0) {
621
+ report += `├─ ${strategyName}:\n`;
622
+ report += `│ ├─ Uploads: ${stats.uploadCount}\n`;
623
+ report += `│ ├─ Files: ${stats.totalFiles}\n`;
624
+ report += `│ ├─ Success: ${stats.totalSuccess} (${stats.successRate.toFixed(1)}%)\n`;
625
+ report += `│ └─ Duration: ${(stats.totalDuration / 1000).toFixed(1)}s\n`;
626
+ }
627
+ }
628
+
629
+ report += '\n';
630
+ return report;
631
+ }
632
+
633
+ /**
634
+ * Get session end statistics
635
+ * @returns {Object} Final session statistics
636
+ */
637
+ getSessionSummary() {
638
+ const stats = this.getSessionStatistics();
639
+ return {
640
+ ...stats,
641
+ endTime: new Date().toISOString(),
642
+ report: this.formatSessionReport(),
643
+ };
644
+ }
645
+ }
646
+
647
+ // Export singleton instance
648
+ export const logger = new LoggingService();
649
+ export default logger;