@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.
- 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/index.js +55 -0
- 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,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;
|