@arela/uploader 1.1.4 → 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 (95) 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/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,240 @@
1
+ import FormData from 'form-data';
2
+ import fs from 'fs';
3
+ import { Agent } from 'http';
4
+ import { Agent as HttpsAgent } from 'https';
5
+ import fetch from 'node-fetch';
6
+ import path from 'path';
7
+
8
+ import appConfig from '../config/config.js';
9
+ import logger from './LoggingService.js';
10
+
11
+ /**
12
+ * Datastage API Service
13
+ * Handles API communication for the arela datastage command:
14
+ * - tracking endpoints under /api/uploader/datastage/*
15
+ * - zip upload endpoint POST /api/datastage (multipart, field: zipFile)
16
+ */
17
+ export class DatastageApiService {
18
+ /**
19
+ * @param {string|null} apiTarget - 'default'|'agencia'|'cliente'
20
+ */
21
+ constructor(apiTarget = null) {
22
+ this.apiTarget = apiTarget;
23
+ const apiConfig = appConfig.getApiConfig(apiTarget);
24
+ this.baseUrl = apiConfig.baseUrl;
25
+ this.token = apiConfig.token;
26
+
27
+ const maxApiConnections = parseInt(process.env.MAX_API_CONNECTIONS) || 10;
28
+ const connectionTimeout =
29
+ parseInt(process.env.API_CONNECTION_TIMEOUT) || 300000;
30
+
31
+ this.maxRetries = parseInt(process.env.API_MAX_RETRIES) || 3;
32
+ this.useExponentialBackoff =
33
+ process.env.API_RETRY_EXPONENTIAL_BACKOFF !== 'false';
34
+ this.fixedRetryDelay = parseInt(process.env.API_RETRY_DELAY) || 1000;
35
+
36
+ const agentOpts = {
37
+ keepAlive: true,
38
+ keepAliveMsecs: 30000,
39
+ maxSockets: maxApiConnections,
40
+ maxFreeSockets: Math.ceil(maxApiConnections / 2),
41
+ maxTotalSockets: maxApiConnections + 5,
42
+ timeout: connectionTimeout,
43
+ scheduling: 'fifo',
44
+ };
45
+ this.httpAgent = new Agent(agentOpts);
46
+ this.httpsAgent = new HttpsAgent(agentOpts);
47
+
48
+ logger.debug(
49
+ `🔗 Datastage API Service configured (target=${apiTarget || 'default'})`,
50
+ );
51
+ }
52
+
53
+ #getAgent(url) {
54
+ return url.startsWith('https://') ? this.httpsAgent : this.httpAgent;
55
+ }
56
+
57
+ #isRetryableError(error, response = null) {
58
+ if (
59
+ error?.code === 'ECONNRESET' ||
60
+ error?.code === 'ETIMEDOUT' ||
61
+ error?.code === 'ECONNREFUSED' ||
62
+ error?.code === 'ENOTFOUND' ||
63
+ error?.code === 'EAI_AGAIN'
64
+ ) {
65
+ return true;
66
+ }
67
+ if (response) {
68
+ const s = response.status;
69
+ if (s === 429 || (s >= 500 && s < 600)) return true;
70
+ }
71
+ if (error?.message && error.message.includes('timeout')) return true;
72
+ return false;
73
+ }
74
+
75
+ #calculateBackoff(attempt) {
76
+ if (!this.useExponentialBackoff) {
77
+ const jitter = this.fixedRetryDelay * 0.2 * (Math.random() * 2 - 1);
78
+ return Math.floor(this.fixedRetryDelay + jitter);
79
+ }
80
+ const baseDelay = 1000;
81
+ const maxDelay = 16000;
82
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
83
+ const jitter = delay * 0.2 * (Math.random() * 2 - 1);
84
+ return Math.floor(delay + jitter);
85
+ }
86
+
87
+ #sleep(ms) {
88
+ return new Promise((r) => setTimeout(r, ms));
89
+ }
90
+
91
+ async #requestJson(endpoint, method = 'GET', body = null, headers = {}) {
92
+ const url = `${this.baseUrl}${endpoint}`;
93
+ const options = {
94
+ method,
95
+ headers: {
96
+ 'x-api-key': this.token,
97
+ 'Content-Type': 'application/json',
98
+ ...headers,
99
+ },
100
+ agent: this.#getAgent(url),
101
+ };
102
+ if (body) options.body = JSON.stringify(body);
103
+
104
+ let lastError;
105
+ let lastResponse = null;
106
+ const retries = this.maxRetries;
107
+
108
+ for (let attempt = 1; attempt <= retries + 1; attempt++) {
109
+ try {
110
+ const response = await fetch(url, options);
111
+ lastResponse = response;
112
+ if (!response.ok) {
113
+ const errorText = await response.text();
114
+ let errorMessage = `API ${method} ${endpoint} failed: ${response.status} ${response.statusText}`;
115
+ try {
116
+ const j = JSON.parse(errorText);
117
+ errorMessage = j.message || errorMessage;
118
+ } catch {
119
+ errorMessage = errorText || errorMessage;
120
+ }
121
+ const err = new Error(errorMessage);
122
+ err.status = response.status;
123
+ if (this.#isRetryableError(err, response) && attempt <= retries) {
124
+ const d = this.#calculateBackoff(attempt);
125
+ logger.warn(
126
+ `Retrying ${method} ${endpoint} (attempt ${attempt}/${retries + 1}) in ${d}ms: ${errorMessage}`,
127
+ );
128
+ await this.#sleep(d);
129
+ continue;
130
+ }
131
+ throw err;
132
+ }
133
+ return await response.json();
134
+ } catch (error) {
135
+ lastError = error;
136
+ if (this.#isRetryableError(error, lastResponse) && attempt <= retries) {
137
+ const d = this.#calculateBackoff(attempt);
138
+ logger.warn(
139
+ `Retrying ${method} ${endpoint} (attempt ${attempt}/${retries + 1}) in ${d}ms: ${error.message}`,
140
+ );
141
+ await this.#sleep(d);
142
+ continue;
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ throw lastError;
148
+ }
149
+
150
+ // --- Tracking endpoints ---
151
+
152
+ async registerUpload({
153
+ absolutePath,
154
+ fileName,
155
+ sizeBytes,
156
+ fileModifiedAt,
157
+ sourceDirectory,
158
+ }) {
159
+ return this.#requestJson('/api/uploader/datastage/register', 'POST', {
160
+ absolutePath,
161
+ fileName,
162
+ sizeBytes,
163
+ fileModifiedAt,
164
+ sourceDirectory,
165
+ });
166
+ }
167
+
168
+ async getPending(sourceDirectory = null) {
169
+ const qs = sourceDirectory
170
+ ? `?sourceDirectory=${encodeURIComponent(sourceDirectory)}`
171
+ : '';
172
+ return this.#requestJson(`/api/uploader/datastage/pending${qs}`, 'GET');
173
+ }
174
+
175
+ async getStats(sourceDirectory = null) {
176
+ const qs = sourceDirectory
177
+ ? `?sourceDirectory=${encodeURIComponent(sourceDirectory)}`
178
+ : '';
179
+ return this.#requestJson(`/api/uploader/datastage/stats${qs}`, 'GET');
180
+ }
181
+
182
+ async markUploaded(id, { datastageId, folio }) {
183
+ return this.#requestJson(
184
+ `/api/uploader/datastage/${id}/mark-uploaded`,
185
+ 'PATCH',
186
+ { datastageId, folio },
187
+ );
188
+ }
189
+
190
+ async markFailed(id, error) {
191
+ return this.#requestJson(
192
+ `/api/uploader/datastage/${id}/mark-failed`,
193
+ 'PATCH',
194
+ { error: String(error || 'unknown') },
195
+ );
196
+ }
197
+
198
+ // --- Zip upload ---
199
+
200
+ /**
201
+ * Upload a single zip file to POST /api/datastage (multipart, field name 'zipFile').
202
+ * Returns the created Datastage row { id, folio, ... }.
203
+ */
204
+ async uploadZip(localPath) {
205
+ const url = `${this.baseUrl}/api/datastage`;
206
+ const form = new FormData();
207
+ const fileName = path.basename(localPath);
208
+ form.append('zipFile', fs.createReadStream(localPath), {
209
+ filename: fileName,
210
+ contentType: 'application/zip',
211
+ });
212
+
213
+ const response = await fetch(url, {
214
+ method: 'POST',
215
+ headers: {
216
+ 'x-api-key': this.token,
217
+ ...form.getHeaders(),
218
+ },
219
+ body: form,
220
+ agent: this.#getAgent(url),
221
+ });
222
+
223
+ if (!response.ok) {
224
+ const text = await response.text();
225
+ let msg = `Datastage upload failed: ${response.status} ${response.statusText}`;
226
+ try {
227
+ const j = JSON.parse(text);
228
+ msg = j.message || msg;
229
+ } catch {
230
+ msg = text || msg;
231
+ }
232
+ const err = new Error(msg);
233
+ err.status = response.status;
234
+ throw err;
235
+ }
236
+ return await response.json();
237
+ }
238
+ }
239
+
240
+ export default DatastageApiService;
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Error Monitor Service
3
+ * Tracks, logs, and monitors errors during signal handling and cleanup operations
4
+ * Provides detailed error reporting and failure recovery mechanisms
5
+ */
6
+ export class ErrorMonitor {
7
+ constructor(logger, cleanupManager, signalHandler) {
8
+ this.logger = logger;
9
+ this.cleanupManager = cleanupManager;
10
+ this.signalHandler = signalHandler;
11
+
12
+ /**
13
+ * Array of recorded errors
14
+ * @type {Array<Object>}
15
+ */
16
+ this.errors = [];
17
+
18
+ /**
19
+ * Error categories
20
+ * @type {Object}
21
+ */
22
+ this.errorCategories = {
23
+ SIGNAL: 'signal_error',
24
+ CLEANUP: 'cleanup_error',
25
+ DATABASE: 'database_error',
26
+ WATCH_SERVICE: 'watch_service_error',
27
+ LOGGING: 'logging_error',
28
+ GENERAL: 'general_error',
29
+ };
30
+
31
+ /**
32
+ * Error statistics
33
+ * @type {Object}
34
+ */
35
+ this.stats = {
36
+ totalErrors: 0,
37
+ errorsByCategory: {},
38
+ fatalErrors: 0,
39
+ recoveredErrors: 0,
40
+ lastErrorTime: null,
41
+ };
42
+
43
+ // Initialize error category stats
44
+ for (const category of Object.values(this.errorCategories)) {
45
+ this.stats.errorsByCategory[category] = 0;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Record an error occurrence
51
+ * @param {string} category - Error category
52
+ * @param {string} message - Error message
53
+ * @param {Error} error - Error object
54
+ * @param {Object} context - Additional context
55
+ * @param {boolean} isFatal - Whether error is fatal
56
+ * @returns {Object} Error record
57
+ */
58
+ recordError(category, message, error, context = {}, isFatal = false) {
59
+ const errorRecord = {
60
+ id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
61
+ timestamp: new Date().toISOString(),
62
+ category,
63
+ message,
64
+ isFatal,
65
+ stack: error?.stack || null,
66
+ context,
67
+ signal: this.signalHandler?.getSignalReceived() || null,
68
+ resolved: false,
69
+ };
70
+
71
+ this.errors.push(errorRecord);
72
+ this.stats.totalErrors++;
73
+ this.stats.errorsByCategory[category]++;
74
+ this.stats.lastErrorTime = errorRecord.timestamp;
75
+
76
+ if (isFatal) {
77
+ this.stats.fatalErrors++;
78
+ this.logger.error(
79
+ `🚨 FATAL ERROR [${category}]: ${message} - ${error?.message}`,
80
+ );
81
+ } else {
82
+ this.logger.warn(
83
+ `⚠️ ERROR [${category}]: ${message} - ${error?.message}`,
84
+ );
85
+ }
86
+
87
+ return errorRecord;
88
+ }
89
+
90
+ /**
91
+ * Mark an error as resolved
92
+ * @param {string} errorId - Error ID to resolve
93
+ * @returns {boolean} True if resolved
94
+ */
95
+ resolveError(errorId) {
96
+ const errorRecord = this.errors.find((e) => e.id === errorId);
97
+ if (errorRecord) {
98
+ errorRecord.resolved = true;
99
+ this.stats.recoveredErrors++;
100
+ this.logger.info(`✅ Error resolved: ${errorId}`);
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /**
107
+ * Get all unresolved errors
108
+ * @returns {Array<Object>} Unresolved error records
109
+ */
110
+ getUnresolvedErrors() {
111
+ return this.errors.filter((e) => !e.resolved);
112
+ }
113
+
114
+ /**
115
+ * Get errors by category
116
+ * @param {string} category - Error category
117
+ * @returns {Array<Object>} Errors in category
118
+ */
119
+ getErrorsByCategory(category) {
120
+ return this.errors.filter((e) => e.category === category);
121
+ }
122
+
123
+ /**
124
+ * Check if critical errors occurred
125
+ * @returns {boolean} True if there are fatal errors
126
+ */
127
+ hasCriticalErrors() {
128
+ return this.stats.fatalErrors > 0;
129
+ }
130
+
131
+ /**
132
+ * Get error statistics
133
+ * @returns {Object} Error statistics
134
+ */
135
+ getStatistics() {
136
+ return {
137
+ totalErrors: this.stats.totalErrors,
138
+ fatalErrors: this.stats.fatalErrors,
139
+ recoveredErrors: this.stats.recoveredErrors,
140
+ unresolvedErrors: this.getUnresolvedErrors().length,
141
+ errorsByCategory: { ...this.stats.errorsByCategory },
142
+ lastErrorTime: this.stats.lastErrorTime,
143
+ successRate:
144
+ this.stats.totalErrors > 0
145
+ ? (
146
+ (this.stats.recoveredErrors / this.stats.totalErrors) *
147
+ 100
148
+ ).toFixed(2)
149
+ : 100,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Generate detailed error report
155
+ * @returns {string} Formatted error report
156
+ */
157
+ generateErrorReport() {
158
+ const stats = this.getStatistics();
159
+ let report =
160
+ '\n═══════════════════════════════════════════════════════════\n';
161
+ report += 'ERROR MONITORING REPORT\n';
162
+ report += '═══════════════════════════════════════════════════════════\n\n';
163
+
164
+ report += 'STATISTICS\n';
165
+ report += `├─ Total Errors: ${stats.totalErrors}\n`;
166
+ report += `├─ Fatal Errors: ${stats.fatalErrors}\n`;
167
+ report += `├─ Recovered Errors: ${stats.recoveredErrors}\n`;
168
+ report += `├─ Unresolved Errors: ${stats.unresolvedErrors}\n`;
169
+ report += `├─ Recovery Rate: ${stats.successRate}%\n`;
170
+ report += `└─ Last Error: ${stats.lastErrorTime || 'None'}\n\n`;
171
+
172
+ if (this.errors.length > 0) {
173
+ report += 'ERROR DETAILS (Last 10)\n';
174
+ const recentErrors = this.errors.slice(-10);
175
+ recentErrors.forEach((err, idx) => {
176
+ const status = err.resolved ? '✅' : '❌';
177
+ const severity = err.isFatal ? '🚨' : '⚠️ ';
178
+ report += `${idx + 1}. ${status} ${severity} [${err.category}]\n`;
179
+ report += ` Message: ${err.message}\n`;
180
+ report += ` Time: ${err.timestamp}\n`;
181
+ if (err.signal) {
182
+ report += ` Signal: ${err.signal}\n`;
183
+ }
184
+ report += '\n';
185
+ });
186
+ }
187
+
188
+ report += '═══════════════════════════════════════════════════════════\n';
189
+ return report;
190
+ }
191
+
192
+ /**
193
+ * Clear error history (useful after processing)
194
+ * @returns {void}
195
+ */
196
+ clearErrors() {
197
+ const count = this.errors.length;
198
+ this.errors = [];
199
+ this.logger.debug(`ErrorMonitor: Cleared ${count} error records`);
200
+ }
201
+
202
+ /**
203
+ * Get error monitor status
204
+ * @returns {Object} Status information
205
+ */
206
+ getStatus() {
207
+ return {
208
+ isHealthy: this.stats.fatalErrors === 0,
209
+ hasErrors: this.stats.totalErrors > 0,
210
+ unresolvedCount: this.getUnresolvedErrors().length,
211
+ stats: this.getStatistics(),
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Validate cleanup operation for errors
217
+ * @param {Object} cleanupResult - Result from cleanup operation
218
+ * @returns {Object} Validation result
219
+ */
220
+ validateCleanupResult(cleanupResult) {
221
+ const validation = {
222
+ isValid: cleanupResult.failureCount === 0,
223
+ failureCount: cleanupResult.failureCount,
224
+ successCount: cleanupResult.successCount,
225
+ issues: [],
226
+ };
227
+
228
+ if (cleanupResult.results) {
229
+ for (const result of cleanupResult.results) {
230
+ if (!result.success) {
231
+ validation.issues.push({
232
+ resource: result.name,
233
+ error: result.error,
234
+ duration: result.duration,
235
+ });
236
+
237
+ this.recordError(
238
+ this.errorCategories.CLEANUP,
239
+ `Cleanup failed for ${result.name}`,
240
+ new Error(result.error),
241
+ { resource: result.name, duration: result.duration },
242
+ false,
243
+ );
244
+ }
245
+ }
246
+ }
247
+
248
+ return validation;
249
+ }
250
+
251
+ /**
252
+ * Reset monitor state (for testing)
253
+ * @returns {void}
254
+ */
255
+ reset() {
256
+ this.errors = [];
257
+ this.stats.totalErrors = 0;
258
+ this.stats.fatalErrors = 0;
259
+ this.stats.recoveredErrors = 0;
260
+ this.stats.lastErrorTime = null;
261
+
262
+ for (const category of Object.keys(this.stats.errorsByCategory)) {
263
+ this.stats.errorsByCategory[category] = 0;
264
+ }
265
+
266
+ this.logger.debug('ErrorMonitor: Reset to initial state');
267
+ }
268
+ }
269
+
270
+ // Export singleton
271
+ export function createErrorMonitor(logger, cleanupManager, signalHandler) {
272
+ return new ErrorMonitor(logger, cleanupManager, signalHandler);
273
+ }
274
+
275
+ export default ErrorMonitor;