@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.
- package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- 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;
|