@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,164 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PROFILES_PATH = '~/.arela/profiles.json';
|
|
6
|
+
export const DEFAULT_AGENT_LOG_PATH = '~/.arela/arela-agent.log';
|
|
7
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Expand a leading '~' to the user's home directory
|
|
11
|
+
* @param {string} inputPath
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function expandHome(inputPath) {
|
|
15
|
+
if (inputPath && inputPath.startsWith('~')) {
|
|
16
|
+
return path.join(os.homedir(), inputPath.slice(1));
|
|
17
|
+
}
|
|
18
|
+
return inputPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Profile Manager
|
|
23
|
+
* Loads and validates the agent's profiles.json — the only local configuration
|
|
24
|
+
* the multi-profile agent needs: one machine serverId plus {name, url, token}
|
|
25
|
+
* per tenant API. Everything else (scan directories, tuning, schedule) lives
|
|
26
|
+
* server-side in pipeline_config.
|
|
27
|
+
*
|
|
28
|
+
* Minimal schema:
|
|
29
|
+
* {
|
|
30
|
+
* "version": 1,
|
|
31
|
+
* "serverId": "PALCO-01",
|
|
32
|
+
* "pollIntervalMs": 5000, // optional
|
|
33
|
+
* "logFile": "~/.arela/arela-agent.log", // optional
|
|
34
|
+
* "profiles": [
|
|
35
|
+
* { "name": "ktj", "url": "https://…", "token": "ak_…" },
|
|
36
|
+
* {
|
|
37
|
+
* "name": "cem", "url": "https://…", "token": "ak_…",
|
|
38
|
+
* "targets": { "agencia": { "baseUrl": "https://…", "token": "ak_…" } },
|
|
39
|
+
* "env": { "SCAN_DIRECTORY_LEVEL": "2" }
|
|
40
|
+
* }
|
|
41
|
+
* ]
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export class ProfileManager {
|
|
45
|
+
/**
|
|
46
|
+
* Load, validate and normalize the profiles file.
|
|
47
|
+
* @param {string} [configPath] - Path to profiles.json (supports '~')
|
|
48
|
+
* @returns {{serverId: string, pollIntervalMs: number, logFile: string, profiles: Object[]}}
|
|
49
|
+
*/
|
|
50
|
+
static loadProfiles(configPath = DEFAULT_PROFILES_PATH) {
|
|
51
|
+
const resolvedPath = path.resolve(expandHome(configPath));
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Profiles file not found: ${resolvedPath}. ` +
|
|
56
|
+
`Run 'arela agent init --from <dir...>' to generate one from existing .env folders.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Invalid JSON in ${resolvedPath}: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!raw.serverId || typeof raw.serverId !== 'string') {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${resolvedPath}: 'serverId' is required at the root level (one per machine, e.g. "PALCO-01")`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(raw.profiles) || raw.profiles.length === 0) {
|
|
74
|
+
throw new Error(`${resolvedPath}: 'profiles' must be a non-empty array`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const seenNames = new Set();
|
|
78
|
+
const profiles = [];
|
|
79
|
+
|
|
80
|
+
raw.profiles.forEach((profile, index) => {
|
|
81
|
+
const label = `Profile ${profile?.name ? `'${profile.name}'` : `#${index + 1}`}`;
|
|
82
|
+
|
|
83
|
+
if (!profile.name || typeof profile.name !== 'string') {
|
|
84
|
+
throw new Error(`Profile #${index + 1}: 'name' is required`);
|
|
85
|
+
}
|
|
86
|
+
const name = profile.name.toLowerCase();
|
|
87
|
+
if (seenNames.has(name)) {
|
|
88
|
+
throw new Error(`${label}: duplicate profile name`);
|
|
89
|
+
}
|
|
90
|
+
seenNames.add(name);
|
|
91
|
+
|
|
92
|
+
if (profile.enabled === false) return;
|
|
93
|
+
|
|
94
|
+
this.#validateEndpoint(label, profile.url, profile.token);
|
|
95
|
+
|
|
96
|
+
const targets = {
|
|
97
|
+
// Same tenant answers under every symbolic name jobs may reference
|
|
98
|
+
agencia: { baseUrl: profile.url, token: profile.token },
|
|
99
|
+
cliente: { baseUrl: profile.url, token: profile.token },
|
|
100
|
+
[name]: { baseUrl: profile.url, token: profile.token },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Explicit targets override symbolic names (real cross-tenant cases)
|
|
104
|
+
for (const [targetName, targetConfig] of Object.entries(
|
|
105
|
+
profile.targets || {},
|
|
106
|
+
)) {
|
|
107
|
+
this.#validateEndpoint(
|
|
108
|
+
`${label}: targets.${targetName}`,
|
|
109
|
+
targetConfig?.baseUrl,
|
|
110
|
+
targetConfig?.token,
|
|
111
|
+
);
|
|
112
|
+
targets[targetName.toLowerCase()] = {
|
|
113
|
+
baseUrl: targetConfig.baseUrl,
|
|
114
|
+
token: targetConfig.token,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (profile.env && typeof profile.env !== 'object') {
|
|
119
|
+
throw new Error(`${label}: 'env' must be an object of key/value pairs`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
profiles.push({
|
|
123
|
+
name,
|
|
124
|
+
url: profile.url,
|
|
125
|
+
token: profile.token,
|
|
126
|
+
serverId: raw.serverId,
|
|
127
|
+
targets,
|
|
128
|
+
env: profile.env || {},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (profiles.length === 0) {
|
|
133
|
+
throw new Error(`${resolvedPath}: all profiles are disabled`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
serverId: raw.serverId,
|
|
138
|
+
pollIntervalMs:
|
|
139
|
+
parseInt(raw.pollIntervalMs) > 0
|
|
140
|
+
? parseInt(raw.pollIntervalMs)
|
|
141
|
+
: DEFAULT_POLL_INTERVAL_MS,
|
|
142
|
+
logFile: path.resolve(expandHome(raw.logFile || DEFAULT_AGENT_LOG_PATH)),
|
|
143
|
+
profiles,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate a {url, token} pair with a per-field error message
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
static #validateEndpoint(label, url, token) {
|
|
152
|
+
if (!url || typeof url !== 'string') {
|
|
153
|
+
throw new Error(`${label}: 'url' is required`);
|
|
154
|
+
}
|
|
155
|
+
if (!/^https?:\/\//.test(url)) {
|
|
156
|
+
throw new Error(`${label}: 'url' must start with http:// or https://`);
|
|
157
|
+
}
|
|
158
|
+
if (!token || typeof token !== 'string') {
|
|
159
|
+
throw new Error(`${label}: 'token' is required`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default ProfileManager;
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReportingService.js
|
|
3
|
+
* Phase 7 - Task 2: Reporting & Analytics
|
|
4
|
+
*
|
|
5
|
+
* Provides comprehensive reporting and analytics including:
|
|
6
|
+
* - Session reports
|
|
7
|
+
* - Batch reports
|
|
8
|
+
* - Error analysis reports
|
|
9
|
+
* - Export functionality (JSON, CSV, PDF-ready)
|
|
10
|
+
* - Statistics aggregation
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
class ReportingService {
|
|
16
|
+
constructor(logger) {
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.reports = {};
|
|
19
|
+
this.sessions = [];
|
|
20
|
+
|
|
21
|
+
this.reportConfig = {
|
|
22
|
+
includeTimestamps: true,
|
|
23
|
+
includeMetrics: true,
|
|
24
|
+
includeErrors: true,
|
|
25
|
+
includeSummary: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate session report
|
|
31
|
+
*/
|
|
32
|
+
generateSessionReport(session) {
|
|
33
|
+
const report = {
|
|
34
|
+
type: 'session_report',
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
sessionId: session.sessionId,
|
|
37
|
+
duration: session.endTime
|
|
38
|
+
? session.endTime - session.startTime
|
|
39
|
+
: Date.now() - session.startTime,
|
|
40
|
+
startTime: new Date(session.startTime).toISOString(),
|
|
41
|
+
endTime: session.endTime ? new Date(session.endTime).toISOString() : null,
|
|
42
|
+
|
|
43
|
+
summary: {
|
|
44
|
+
totalFiles: session.fileCount || 0,
|
|
45
|
+
processedFiles: session.processedCount || 0,
|
|
46
|
+
successCount: session.successCount || 0,
|
|
47
|
+
errorCount: session.errorCount || 0,
|
|
48
|
+
skippedCount: session.skippedCount || 0,
|
|
49
|
+
successRate: this._calculateSuccessRate(session),
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
metrics: {
|
|
53
|
+
averageProcessingTime: this._calculateAvgTime(session),
|
|
54
|
+
filesPerSecond: this._calculateFilesPerSecond(session),
|
|
55
|
+
successRate: this._calculateSuccessRate(session),
|
|
56
|
+
errorRate: this._calculateErrorRate(session),
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
details: {
|
|
60
|
+
uploadedFiles: session.uploadedFiles || [],
|
|
61
|
+
failedFiles: session.failedFiles || [],
|
|
62
|
+
skippedFiles: session.skippedFiles || [],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
errors: this._extractErrors(session),
|
|
66
|
+
|
|
67
|
+
tags: session.tags || [],
|
|
68
|
+
notes: session.notes || '',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.reports[session.sessionId] = report;
|
|
72
|
+
this.sessions.push(report);
|
|
73
|
+
|
|
74
|
+
return report;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate batch report
|
|
79
|
+
*/
|
|
80
|
+
generateBatchReport(sessions) {
|
|
81
|
+
const report = {
|
|
82
|
+
type: 'batch_report',
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
batchId: `batch_${Date.now()}`,
|
|
85
|
+
|
|
86
|
+
summary: {
|
|
87
|
+
totalSessions: sessions.length,
|
|
88
|
+
totalFiles: 0,
|
|
89
|
+
totalProcessed: 0,
|
|
90
|
+
totalSuccess: 0,
|
|
91
|
+
totalErrors: 0,
|
|
92
|
+
totalSkipped: 0,
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
sessionMetrics: [],
|
|
96
|
+
aggregatedMetrics: {},
|
|
97
|
+
timelineData: [],
|
|
98
|
+
errorSummary: {},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Aggregate data from all sessions
|
|
102
|
+
for (const session of sessions) {
|
|
103
|
+
const sessionReport = this.generateSessionReport(session);
|
|
104
|
+
|
|
105
|
+
report.summary.totalFiles += sessionReport.summary.totalFiles;
|
|
106
|
+
report.summary.totalProcessed += sessionReport.summary.processedFiles;
|
|
107
|
+
report.summary.totalSuccess += sessionReport.summary.successCount;
|
|
108
|
+
report.summary.totalErrors += sessionReport.summary.errorCount;
|
|
109
|
+
report.summary.totalSkipped += sessionReport.summary.skippedCount;
|
|
110
|
+
|
|
111
|
+
report.sessionMetrics.push({
|
|
112
|
+
sessionId: session.sessionId,
|
|
113
|
+
duration: sessionReport.duration,
|
|
114
|
+
fileCount: sessionReport.summary.totalFiles,
|
|
115
|
+
successRate: sessionReport.metrics.successRate,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
report.timelineData.push({
|
|
119
|
+
timestamp: sessionReport.startTime,
|
|
120
|
+
filesProcessed: sessionReport.summary.processedFiles,
|
|
121
|
+
duration: sessionReport.duration,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Aggregate errors by type
|
|
125
|
+
for (const error of sessionReport.errors) {
|
|
126
|
+
const errorType = error.type || 'unknown';
|
|
127
|
+
if (!report.errorSummary[errorType]) {
|
|
128
|
+
report.errorSummary[errorType] = { count: 0, examples: [] };
|
|
129
|
+
}
|
|
130
|
+
report.errorSummary[errorType].count++;
|
|
131
|
+
if (report.errorSummary[errorType].examples.length < 3) {
|
|
132
|
+
report.errorSummary[errorType].examples.push(error.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Calculate aggregated metrics
|
|
138
|
+
report.aggregatedMetrics = {
|
|
139
|
+
totalDuration: report.sessionMetrics.reduce(
|
|
140
|
+
(sum, s) => sum + s.duration,
|
|
141
|
+
0,
|
|
142
|
+
),
|
|
143
|
+
averageSessionDuration:
|
|
144
|
+
report.sessionMetrics.length > 0
|
|
145
|
+
? report.sessionMetrics.reduce((sum, s) => sum + s.duration, 0) /
|
|
146
|
+
report.sessionMetrics.length
|
|
147
|
+
: 0,
|
|
148
|
+
overallSuccessRate:
|
|
149
|
+
report.summary.totalProcessed > 0
|
|
150
|
+
? (
|
|
151
|
+
(report.summary.totalSuccess / report.summary.totalProcessed) *
|
|
152
|
+
100
|
|
153
|
+
).toFixed(2) + '%'
|
|
154
|
+
: 'N/A',
|
|
155
|
+
overallErrorRate:
|
|
156
|
+
report.summary.totalProcessed > 0
|
|
157
|
+
? (
|
|
158
|
+
(report.summary.totalErrors / report.summary.totalProcessed) *
|
|
159
|
+
100
|
|
160
|
+
).toFixed(2) + '%'
|
|
161
|
+
: 'N/A',
|
|
162
|
+
averageFilesPerSession:
|
|
163
|
+
report.summary.totalFiles / Math.max(report.summary.totalSessions, 1),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate error analysis report
|
|
171
|
+
*/
|
|
172
|
+
generateErrorAnalysisReport(sessions) {
|
|
173
|
+
const report = {
|
|
174
|
+
type: 'error_analysis_report',
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
|
|
177
|
+
summary: {
|
|
178
|
+
totalErrors: 0,
|
|
179
|
+
uniqueErrorTypes: new Set(),
|
|
180
|
+
affectedSessions: new Set(),
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
errorBreakdown: {},
|
|
184
|
+
errorPatterns: [],
|
|
185
|
+
recommendations: [],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (const session of sessions) {
|
|
189
|
+
const sessionReport = this.generateSessionReport(session);
|
|
190
|
+
|
|
191
|
+
for (const error of sessionReport.errors) {
|
|
192
|
+
report.summary.totalErrors++;
|
|
193
|
+
report.summary.uniqueErrorTypes.add(error.type);
|
|
194
|
+
report.summary.affectedSessions.add(session.sessionId);
|
|
195
|
+
|
|
196
|
+
// Breakdown by type
|
|
197
|
+
const errorType = error.type || 'unknown';
|
|
198
|
+
if (!report.errorBreakdown[errorType]) {
|
|
199
|
+
report.errorBreakdown[errorType] = {
|
|
200
|
+
count: 0,
|
|
201
|
+
severity: error.severity || 'unknown',
|
|
202
|
+
examples: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
report.errorBreakdown[errorType].count++;
|
|
206
|
+
|
|
207
|
+
if (report.errorBreakdown[errorType].examples.length < 5) {
|
|
208
|
+
report.errorBreakdown[errorType].examples.push({
|
|
209
|
+
message: error.message,
|
|
210
|
+
file: error.file,
|
|
211
|
+
timestamp: error.timestamp,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Convert sets to arrays for JSON serialization
|
|
218
|
+
report.summary.uniqueErrorTypes = Array.from(
|
|
219
|
+
report.summary.uniqueErrorTypes,
|
|
220
|
+
);
|
|
221
|
+
report.summary.affectedSessions = Array.from(
|
|
222
|
+
report.summary.affectedSessions,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Identify patterns
|
|
226
|
+
const errorTypes = Object.keys(report.errorBreakdown);
|
|
227
|
+
if (errorTypes.length > 0) {
|
|
228
|
+
const topErrors = errorTypes
|
|
229
|
+
.sort(
|
|
230
|
+
(a, b) =>
|
|
231
|
+
report.errorBreakdown[b].count - report.errorBreakdown[a].count,
|
|
232
|
+
)
|
|
233
|
+
.slice(0, 3);
|
|
234
|
+
|
|
235
|
+
for (const errorType of topErrors) {
|
|
236
|
+
report.errorPatterns.push({
|
|
237
|
+
type: errorType,
|
|
238
|
+
frequency: report.errorBreakdown[errorType].count,
|
|
239
|
+
severity: report.errorBreakdown[errorType].severity,
|
|
240
|
+
occurrenceRate:
|
|
241
|
+
(
|
|
242
|
+
(report.errorBreakdown[errorType].count /
|
|
243
|
+
report.summary.totalErrors) *
|
|
244
|
+
100
|
|
245
|
+
).toFixed(2) + '%',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Generate recommendations
|
|
251
|
+
if (report.summary.totalErrors > 0) {
|
|
252
|
+
for (const pattern of report.errorPatterns) {
|
|
253
|
+
report.recommendations.push(
|
|
254
|
+
`${pattern.type}: Occurs in ${pattern.occurrenceRate} of errors. ` +
|
|
255
|
+
`Consider implementing retry logic or improving validation.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return report;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Export report to JSON
|
|
265
|
+
*/
|
|
266
|
+
async exportReportJson(report, outputPath) {
|
|
267
|
+
try {
|
|
268
|
+
const jsonContent = JSON.stringify(report, null, 2);
|
|
269
|
+
await fs.writeFile(outputPath, jsonContent, 'utf-8');
|
|
270
|
+
|
|
271
|
+
this.logger.info(`Report exported to JSON: ${outputPath}`);
|
|
272
|
+
return { success: true, path: outputPath };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.logger.error(`Failed to export report to JSON:`, error);
|
|
275
|
+
return { success: false, error: error.message };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Export report to CSV
|
|
281
|
+
*/
|
|
282
|
+
async exportReportCsv(report, outputPath) {
|
|
283
|
+
try {
|
|
284
|
+
let csvContent = '';
|
|
285
|
+
|
|
286
|
+
if (report.type === 'session_report') {
|
|
287
|
+
csvContent = this._generateSessionCsv(report);
|
|
288
|
+
} else if (report.type === 'batch_report') {
|
|
289
|
+
csvContent = this._generateBatchCsv(report);
|
|
290
|
+
} else if (report.type === 'error_analysis_report') {
|
|
291
|
+
csvContent = this._generateErrorAnalysisCsv(report);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await fs.writeFile(outputPath, csvContent, 'utf-8');
|
|
295
|
+
|
|
296
|
+
this.logger.info(`Report exported to CSV: ${outputPath}`);
|
|
297
|
+
return { success: true, path: outputPath };
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.logger.error(`Failed to export report to CSV:`, error);
|
|
300
|
+
return { success: false, error: error.message };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Generate summary statistics
|
|
306
|
+
*/
|
|
307
|
+
generateSummaryStatistics(sessions) {
|
|
308
|
+
const stats = {
|
|
309
|
+
timestamp: new Date().toISOString(),
|
|
310
|
+
periodAnalyzed: {
|
|
311
|
+
start: sessions.length > 0 ? sessions[0].startTime : null,
|
|
312
|
+
end: sessions.length > 0 ? sessions[sessions.length - 1].endTime : null,
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
totals: {
|
|
316
|
+
sessions: sessions.length,
|
|
317
|
+
files: 0,
|
|
318
|
+
processed: 0,
|
|
319
|
+
successful: 0,
|
|
320
|
+
failed: 0,
|
|
321
|
+
skipped: 0,
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
rates: {},
|
|
325
|
+
performance: {},
|
|
326
|
+
trending: {},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
for (const session of sessions) {
|
|
330
|
+
stats.totals.files += session.fileCount || 0;
|
|
331
|
+
stats.totals.processed += session.processedCount || 0;
|
|
332
|
+
stats.totals.successful += session.successCount || 0;
|
|
333
|
+
stats.totals.failed += session.errorCount || 0;
|
|
334
|
+
stats.totals.skipped += session.skippedCount || 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Calculate rates
|
|
338
|
+
if (stats.totals.processed > 0) {
|
|
339
|
+
stats.rates.successRate =
|
|
340
|
+
((stats.totals.successful / stats.totals.processed) * 100).toFixed(2) +
|
|
341
|
+
'%';
|
|
342
|
+
stats.rates.failureRate =
|
|
343
|
+
((stats.totals.failed / stats.totals.processed) * 100).toFixed(2) + '%';
|
|
344
|
+
stats.rates.skipRate =
|
|
345
|
+
((stats.totals.skipped / stats.totals.processed) * 100).toFixed(2) +
|
|
346
|
+
'%';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Performance metrics
|
|
350
|
+
if (sessions.length > 0) {
|
|
351
|
+
const totalDuration = sessions.reduce(
|
|
352
|
+
(sum, s) => sum + ((s.endTime || Date.now()) - s.startTime),
|
|
353
|
+
0,
|
|
354
|
+
);
|
|
355
|
+
stats.performance.averageSessionDuration =
|
|
356
|
+
(totalDuration / sessions.length).toFixed(0) + 'ms';
|
|
357
|
+
stats.performance.totalDuration = totalDuration + 'ms';
|
|
358
|
+
stats.performance.filesPerSecond = (
|
|
359
|
+
stats.totals.processed /
|
|
360
|
+
(totalDuration / 1000)
|
|
361
|
+
).toFixed(2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return stats;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get report history
|
|
369
|
+
*/
|
|
370
|
+
getReportHistory(limit = 10) {
|
|
371
|
+
return this.sessions
|
|
372
|
+
.slice(-limit)
|
|
373
|
+
.reverse()
|
|
374
|
+
.map((report) => ({
|
|
375
|
+
reportId: report.sessionId,
|
|
376
|
+
type: report.type,
|
|
377
|
+
timestamp: report.timestamp,
|
|
378
|
+
summary: report.summary,
|
|
379
|
+
successRate: report.metrics?.successRate,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear old reports
|
|
385
|
+
*/
|
|
386
|
+
clearOldReports(daysOld = 30) {
|
|
387
|
+
const cutoffTime = Date.now() - daysOld * 24 * 60 * 60 * 1000;
|
|
388
|
+
const beforeCount = this.sessions.length;
|
|
389
|
+
|
|
390
|
+
this.sessions = this.sessions.filter(
|
|
391
|
+
(report) => new Date(report.timestamp).getTime() > cutoffTime,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const removed = beforeCount - this.sessions.length;
|
|
395
|
+
this.logger.info(`Cleared ${removed} reports older than ${daysOld} days`);
|
|
396
|
+
|
|
397
|
+
return { removed, remaining: this.sessions.length };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Private helper methods
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
_calculateSuccessRate(session) {
|
|
405
|
+
if (!session.processedCount || session.processedCount === 0) return '0%';
|
|
406
|
+
return (
|
|
407
|
+
(((session.successCount || 0) / session.processedCount) * 100).toFixed(
|
|
408
|
+
2,
|
|
409
|
+
) + '%'
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_calculateErrorRate(session) {
|
|
414
|
+
if (!session.processedCount || session.processedCount === 0) return '0%';
|
|
415
|
+
return (
|
|
416
|
+
(((session.errorCount || 0) / session.processedCount) * 100).toFixed(2) +
|
|
417
|
+
'%'
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_calculateAvgTime(session) {
|
|
422
|
+
if (!session.processedCount || session.processedCount === 0) return 0;
|
|
423
|
+
const duration = session.endTime
|
|
424
|
+
? session.endTime - session.startTime
|
|
425
|
+
: Date.now() - session.startTime;
|
|
426
|
+
return Math.round(duration / session.processedCount) + 'ms';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_calculateFilesPerSecond(session) {
|
|
430
|
+
if (!session.processedCount) return 0;
|
|
431
|
+
const duration = session.endTime
|
|
432
|
+
? session.endTime - session.startTime
|
|
433
|
+
: Date.now() - session.startTime;
|
|
434
|
+
return (session.processedCount / (duration / 1000)).toFixed(2);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
_extractErrors(session) {
|
|
438
|
+
return (session.errors || []).map((error) => ({
|
|
439
|
+
type: error.type || 'unknown',
|
|
440
|
+
message: error.message,
|
|
441
|
+
file: error.file,
|
|
442
|
+
severity: error.severity || 'error',
|
|
443
|
+
timestamp: error.timestamp || new Date().toISOString(),
|
|
444
|
+
context: error.context,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_generateSessionCsv(report) {
|
|
449
|
+
const lines = [
|
|
450
|
+
'Session Report',
|
|
451
|
+
`Generated: ${report.timestamp}`,
|
|
452
|
+
`Session ID: ${report.sessionId}`,
|
|
453
|
+
'',
|
|
454
|
+
'Summary',
|
|
455
|
+
'Total Files,Processed,Success,Errors,Skipped',
|
|
456
|
+
`${report.summary.totalFiles},${report.summary.processedFiles},${report.summary.successCount},${report.summary.errorCount},${report.summary.skippedCount}`,
|
|
457
|
+
'',
|
|
458
|
+
'Metrics',
|
|
459
|
+
'Metric,Value',
|
|
460
|
+
`Success Rate,${report.metrics.successRate}`,
|
|
461
|
+
`Error Rate,${report.metrics.errorRate}`,
|
|
462
|
+
`Files Per Second,${report.metrics.filesPerSecond}`,
|
|
463
|
+
`Avg Processing Time,${report.metrics.averageProcessingTime}`,
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
return lines.join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_generateBatchCsv(report) {
|
|
470
|
+
const lines = [
|
|
471
|
+
'Batch Report',
|
|
472
|
+
`Generated: ${report.timestamp}`,
|
|
473
|
+
'',
|
|
474
|
+
'Session Metrics',
|
|
475
|
+
'Session ID,Duration (ms),File Count,Success Rate',
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
for (const metric of report.sessionMetrics) {
|
|
479
|
+
lines.push(
|
|
480
|
+
`${metric.sessionId},${metric.duration},${metric.fileCount},${metric.successRate}`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
lines.push('', 'Summary', 'Metric,Value');
|
|
485
|
+
for (const [key, value] of Object.entries(report.aggregatedMetrics)) {
|
|
486
|
+
lines.push(`${key},${value}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return lines.join('\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_generateErrorAnalysisCsv(report) {
|
|
493
|
+
const lines = [
|
|
494
|
+
'Error Analysis Report',
|
|
495
|
+
`Generated: ${report.timestamp}`,
|
|
496
|
+
'',
|
|
497
|
+
'Error Summary',
|
|
498
|
+
'Error Type,Count,Severity,Occurrence Rate',
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
for (const pattern of report.errorPatterns) {
|
|
502
|
+
lines.push(
|
|
503
|
+
`${pattern.type},${pattern.frequency},${pattern.severity},${pattern.occurrenceRate}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return lines.join('\n');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export default ReportingService;
|