@jagilber-org/index-server 1.22.1 → 1.26.1
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/CHANGELOG.md +87 -2
- package/CODE_OF_CONDUCT.md +2 -0
- package/CONTRIBUTING.md +32 -2
- package/README.md +82 -19
- package/SECURITY.md +17 -5
- package/dist/config/dashboardConfig.d.ts +3 -0
- package/dist/config/dashboardConfig.js +3 -0
- package/dist/config/defaultValues.d.ts +1 -1
- package/dist/config/defaultValues.js +1 -1
- package/dist/config/featureConfig.d.ts +2 -0
- package/dist/config/featureConfig.js +6 -1
- package/dist/config/runtimeConfig.d.ts +1 -1
- package/dist/config/runtimeConfig.js +8 -9
- package/dist/dashboard/client/admin.html +170 -53
- package/dist/dashboard/client/css/admin.css +132 -0
- package/dist/dashboard/client/js/admin.auth.js +25 -11
- package/dist/dashboard/client/js/admin.config.js +1 -1
- package/dist/dashboard/client/js/admin.feedback.js +328 -0
- package/dist/dashboard/client/js/admin.graph.js +120 -18
- package/dist/dashboard/client/js/admin.instructions.js +27 -13
- package/dist/dashboard/client/js/admin.logs.js +1 -5
- package/dist/dashboard/client/js/admin.maintenance.js +53 -8
- package/dist/dashboard/client/js/admin.messaging.js +1 -4
- package/dist/dashboard/client/js/admin.overview.js +5 -1
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/client/js/admin.utils.js +43 -1
- package/dist/dashboard/client/js/mermaid.min.js +813 -537
- package/dist/dashboard/export/DataExporter.js +2 -1
- package/dist/dashboard/server/AdminPanel.d.ts +3 -0
- package/dist/dashboard/server/AdminPanel.js +132 -35
- package/dist/dashboard/server/ApiRoutes.js +40 -9
- package/dist/dashboard/server/DashboardServer.js +1 -1
- package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
- package/dist/dashboard/server/FileMetricsStorage.js +52 -5
- package/dist/dashboard/server/HttpTransport.js +6 -0
- package/dist/dashboard/server/InstanceManager.js +7 -2
- package/dist/dashboard/server/KnowledgeStore.js +7 -2
- package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
- package/dist/dashboard/server/MetricsCollector.js +113 -17
- package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
- package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
- package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
- package/dist/dashboard/server/routes/admin.routes.js +35 -27
- package/dist/dashboard/server/routes/alerts.routes.js +4 -3
- package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
- package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
- package/dist/dashboard/server/routes/graph.routes.js +10 -13
- package/dist/dashboard/server/routes/index.d.ts +1 -0
- package/dist/dashboard/server/routes/index.js +74 -39
- package/dist/dashboard/server/routes/instances.routes.js +2 -1
- package/dist/dashboard/server/routes/instructions.routes.js +46 -27
- package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
- package/dist/dashboard/server/routes/logs.routes.js +5 -4
- package/dist/dashboard/server/routes/messaging.routes.js +15 -14
- package/dist/dashboard/server/routes/metrics.routes.js +14 -13
- package/dist/dashboard/server/routes/scripts.routes.js +6 -3
- package/dist/dashboard/server/routes/status.routes.js +5 -4
- package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
- package/dist/dashboard/server/routes/usage.routes.js +2 -1
- package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
- package/dist/dashboard/server/utils/escapeHtml.js +11 -0
- package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
- package/dist/dashboard/server/utils/pathContainment.js +15 -0
- package/dist/dashboard/server/wsInit.js +2 -2
- package/dist/lib/mcpStdioLogging.d.ts +165 -0
- package/dist/lib/mcpStdioLogging.js +287 -0
- package/dist/schemas/index.d.ts +37 -2
- package/dist/schemas/index.js +27 -3
- package/dist/server/backgroundServicesStartup.d.ts +7 -1
- package/dist/server/backgroundServicesStartup.js +25 -8
- package/dist/server/certInit.d.ts +97 -0
- package/dist/server/certInit.js +359 -0
- package/dist/server/certInit.types.d.ts +92 -0
- package/dist/server/certInit.types.js +34 -0
- package/dist/server/handshake/fallbackFrames.d.ts +31 -0
- package/dist/server/handshake/fallbackFrames.js +38 -0
- package/dist/server/handshake/initializeDetector.d.ts +31 -0
- package/dist/server/handshake/initializeDetector.js +88 -0
- package/dist/server/handshake/protocol.d.ts +15 -0
- package/dist/server/handshake/protocol.js +37 -0
- package/dist/server/handshake/readyEmitter.d.ts +6 -0
- package/dist/server/handshake/readyEmitter.js +88 -0
- package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
- package/dist/server/handshake/safetyFallbacks.js +134 -0
- package/dist/server/handshake/stdinSniffer.d.ts +1 -0
- package/dist/server/handshake/stdinSniffer.js +260 -0
- package/dist/server/handshake/tracing.d.ts +16 -0
- package/dist/server/handshake/tracing.js +95 -0
- package/dist/server/handshakeManager.d.ts +23 -23
- package/dist/server/handshakeManager.js +36 -466
- package/dist/server/index-server.d.ts +23 -0
- package/dist/server/index-server.js +194 -9
- package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
- package/dist/server/mcpReadOnlySurfaces.js +297 -0
- package/dist/server/sdkServer.js +69 -7
- package/dist/server/transport.d.ts +5 -6
- package/dist/server/transport.js +46 -64
- package/dist/server/transportFactory.d.ts +3 -9
- package/dist/server/transportFactory.js +18 -380
- package/dist/services/atomicFs.d.ts +3 -0
- package/dist/services/atomicFs.js +171 -13
- package/dist/services/auditLog.d.ts +17 -2
- package/dist/services/auditLog.js +75 -14
- package/dist/services/bootstrapGating.js +1 -1
- package/dist/services/categoryRules.d.ts +10 -0
- package/dist/services/categoryRules.js +17 -0
- package/dist/services/classificationService.js +7 -5
- package/dist/services/embeddingService.d.ts +27 -11
- package/dist/services/embeddingService.js +51 -14
- package/dist/services/feedbackStorage.d.ts +39 -0
- package/dist/services/feedbackStorage.js +88 -0
- package/dist/services/handlers/instructions.add.js +429 -317
- package/dist/services/handlers/instructions.groom.js +128 -31
- package/dist/services/handlers/instructions.import.js +56 -23
- package/dist/services/handlers/instructions.patch.js +43 -32
- package/dist/services/handlers/instructions.query.js +20 -29
- package/dist/services/handlers/instructions.shared.d.ts +54 -0
- package/dist/services/handlers/instructions.shared.js +126 -1
- package/dist/services/handlers.activation.js +83 -81
- package/dist/services/handlers.dashboardConfig.d.ts +2 -2
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/handlers.diagnostics.js +75 -54
- package/dist/services/handlers.feedback.d.ts +4 -11
- package/dist/services/handlers.feedback.js +11 -333
- package/dist/services/handlers.gates.js +69 -37
- package/dist/services/handlers.graph.js +2 -2
- package/dist/services/handlers.help.js +2 -2
- package/dist/services/handlers.instructionSchema.js +4 -2
- package/dist/services/handlers.integrity.js +42 -22
- package/dist/services/handlers.messaging.js +1 -1
- package/dist/services/handlers.metrics.js +51 -6
- package/dist/services/handlers.prompt.js +10 -2
- package/dist/services/handlers.search.js +94 -44
- package/dist/services/handlers.trace.js +1 -1
- package/dist/services/handlers.usage.js +38 -7
- package/dist/services/indexContext.d.ts +21 -1
- package/dist/services/indexContext.js +263 -78
- package/dist/services/indexLoader.d.ts +1 -0
- package/dist/services/indexLoader.js +28 -8
- package/dist/services/instructionRecordValidation.d.ts +39 -0
- package/dist/services/instructionRecordValidation.js +388 -0
- package/dist/services/instructions.dispatcher.js +4 -4
- package/dist/services/loaderSchemaValidator.d.ts +15 -0
- package/dist/services/loaderSchemaValidator.js +69 -0
- package/dist/services/logger.js +11 -2
- package/dist/services/mcpLogBridge.d.ts +49 -0
- package/dist/services/mcpLogBridge.js +83 -0
- package/dist/services/ownershipService.js +18 -8
- package/dist/services/performanceBaseline.js +23 -22
- package/dist/services/promptReviewService.d.ts +3 -1
- package/dist/services/promptReviewService.js +41 -13
- package/dist/services/regexSafety.d.ts +6 -0
- package/dist/services/regexSafety.js +46 -0
- package/dist/services/seedBootstrap.js +1 -1
- package/dist/services/storage/factory.d.ts +14 -1
- package/dist/services/storage/factory.js +61 -1
- package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
- package/dist/services/storage/jsonEmbeddingStore.js +83 -0
- package/dist/services/storage/jsonFileStore.d.ts +3 -1
- package/dist/services/storage/jsonFileStore.js +8 -6
- package/dist/services/storage/migrationEngine.d.ts +13 -0
- package/dist/services/storage/migrationEngine.js +31 -0
- package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
- package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
- package/dist/services/storage/sqliteStore.d.ts +3 -1
- package/dist/services/storage/sqliteStore.js +2 -2
- package/dist/services/storage/types.d.ts +48 -1
- package/dist/services/toolRegistry.js +77 -67
- package/dist/services/toolRegistry.zod.js +89 -86
- package/dist/services/tracing.js +5 -4
- package/dist/utils/envUtils.d.ts +4 -0
- package/dist/utils/envUtils.js +7 -0
- package/dist/utils/memoryMonitor.js +11 -10
- package/package.json +11 -4
- package/schemas/instruction.schema.json +38 -1
- package/scripts/copy-dashboard-assets.mjs +1 -1
- package/scripts/dist/README.md +1 -1
- package/scripts/setup-wizard.mjs +781 -0
- package/server.json +1 -0
- package/dist/externalClientLib.d.ts +0 -1
- package/dist/externalClientLib.js +0 -2
- package/dist/portableClientWrapper.d.ts +0 -1
- package/dist/portableClientWrapper.js +0 -2
- package/dist/services/indexingService.d.ts +0 -1
- package/dist/services/indexingService.js +0 -2
|
@@ -14,6 +14,7 @@ exports.DataExporter = void 0;
|
|
|
14
14
|
const jsonExporter_js_1 = require("./exporters/jsonExporter.js");
|
|
15
15
|
const csvExporter_js_1 = require("./exporters/csvExporter.js");
|
|
16
16
|
const xmlExporter_js_1 = require("./exporters/xmlExporter.js");
|
|
17
|
+
const logger_js_1 = require("../../services/logger.js");
|
|
17
18
|
class DataExporter {
|
|
18
19
|
exportConfigs = new Map();
|
|
19
20
|
exportJobs = new Map();
|
|
@@ -613,7 +614,7 @@ class DataExporter {
|
|
|
613
614
|
callback(job);
|
|
614
615
|
}
|
|
615
616
|
catch (error) {
|
|
616
|
-
|
|
617
|
+
(0, logger_js_1.logError)('Error in export job callback:', error);
|
|
617
618
|
}
|
|
618
619
|
});
|
|
619
620
|
}
|
|
@@ -101,6 +101,7 @@ export declare class AdminPanel {
|
|
|
101
101
|
instructionCount: number;
|
|
102
102
|
schemaVersion?: string;
|
|
103
103
|
sizeBytes: number;
|
|
104
|
+
warnings?: string[];
|
|
104
105
|
}[];
|
|
105
106
|
restoreBackup(backupId: string): {
|
|
106
107
|
success: boolean;
|
|
@@ -118,6 +119,7 @@ export declare class AdminPanel {
|
|
|
118
119
|
success: boolean;
|
|
119
120
|
message: string;
|
|
120
121
|
pruned?: number;
|
|
122
|
+
errors?: string[];
|
|
121
123
|
};
|
|
122
124
|
/** Export a backup — returns the zip file path for streaming, or falls back to JSON bundle for legacy dirs */
|
|
123
125
|
exportBackup(backupId: string): {
|
|
@@ -128,6 +130,7 @@ export declare class AdminPanel {
|
|
|
128
130
|
manifest: Record<string, unknown>;
|
|
129
131
|
files: Record<string, unknown>;
|
|
130
132
|
};
|
|
133
|
+
warnings?: string[];
|
|
131
134
|
};
|
|
132
135
|
/** Import a backup from a JSON bundle uploaded by the client — creates a zip */
|
|
133
136
|
importBackup(bundle: {
|
|
@@ -24,6 +24,7 @@ const indexContext_1 = require("../../services/indexContext");
|
|
|
24
24
|
const AdminPanelConfig_1 = require("./AdminPanelConfig");
|
|
25
25
|
const AdminPanelState_1 = require("./AdminPanelState");
|
|
26
26
|
const backupZip_1 = require("../../services/backupZip");
|
|
27
|
+
const auditLog_1 = require("../../services/auditLog");
|
|
27
28
|
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
28
29
|
class AdminPanel {
|
|
29
30
|
panelConfig;
|
|
@@ -154,9 +155,11 @@ class AdminPanel {
|
|
|
154
155
|
return { success: true, message: 'System backup completed successfully', backupId, files: fileCount };
|
|
155
156
|
}
|
|
156
157
|
catch (error) {
|
|
158
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
159
|
+
(0, auditLog_1.logAudit)('admin/backup/perform_failed', undefined, { error: errMsg }, 'mutation');
|
|
157
160
|
return {
|
|
158
161
|
success: false,
|
|
159
|
-
message: `Backup failed: ${
|
|
162
|
+
message: `Backup failed: ${errMsg}`
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -192,6 +195,7 @@ class AdminPanel {
|
|
|
192
195
|
let createdAt = new Date(stat.mtime).toISOString();
|
|
193
196
|
let instructionCount = 0;
|
|
194
197
|
let schemaVersion;
|
|
198
|
+
const entryWarnings = [];
|
|
195
199
|
if (fs_1.default.existsSync(manifestPath)) {
|
|
196
200
|
try {
|
|
197
201
|
const mf = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
@@ -199,7 +203,13 @@ class AdminPanel {
|
|
|
199
203
|
instructionCount = mf.instructionCount || 0;
|
|
200
204
|
schemaVersion = mf.schemaVersion;
|
|
201
205
|
}
|
|
202
|
-
catch {
|
|
206
|
+
catch (err) {
|
|
207
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
const msg = `Failed to parse manifest for backup '${entry}': ${errMsg}`;
|
|
209
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
210
|
+
entryWarnings.push(msg);
|
|
211
|
+
(0, auditLog_1.logAudit)('admin/backup/list_warning', [entry], { error: errMsg, phase: 'manifest_parse' }, 'read');
|
|
212
|
+
}
|
|
203
213
|
}
|
|
204
214
|
else {
|
|
205
215
|
instructionCount = fs_1.default.readdirSync(full).filter(f => f.toLowerCase().endsWith('.json')).length;
|
|
@@ -212,10 +222,18 @@ class AdminPanel {
|
|
|
212
222
|
return sum;
|
|
213
223
|
}
|
|
214
224
|
}, 0);
|
|
215
|
-
|
|
225
|
+
const result = { id: entry, createdAt, instructionCount, schemaVersion, sizeBytes };
|
|
226
|
+
if (entryWarnings.length > 0)
|
|
227
|
+
result.warnings = entryWarnings;
|
|
228
|
+
results.push(result);
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
|
-
catch {
|
|
231
|
+
catch (err) {
|
|
232
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
233
|
+
const msg = `Failed to read backup entry '${entry}': ${errMsg}`;
|
|
234
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
235
|
+
(0, auditLog_1.logAudit)('admin/backup/list_warning', [entry], { error: errMsg, phase: 'read_entry' }, 'read');
|
|
236
|
+
}
|
|
219
237
|
}
|
|
220
238
|
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
221
239
|
return results;
|
|
@@ -228,8 +246,11 @@ class AdminPanel {
|
|
|
228
246
|
const backupDir = path_1.default.join(backupRoot, safeId);
|
|
229
247
|
const isZip = fs_1.default.existsSync(zipPath);
|
|
230
248
|
const isDir = fs_1.default.existsSync(backupDir) && fs_1.default.statSync(backupDir).isDirectory();
|
|
231
|
-
if (!isZip && !isDir)
|
|
232
|
-
|
|
249
|
+
if (!isZip && !isDir) {
|
|
250
|
+
const msg = `Backup not found: ${safeId}`;
|
|
251
|
+
(0, auditLog_1.logAudit)('admin/backup/restore_failed', [safeId], { error: msg }, 'mutation');
|
|
252
|
+
return { success: false, message: msg };
|
|
253
|
+
}
|
|
233
254
|
const instructionsDir = this.instructionsRoot;
|
|
234
255
|
if (!fs_1.default.existsSync(instructionsDir))
|
|
235
256
|
fs_1.default.mkdirSync(instructionsDir, { recursive: true });
|
|
@@ -265,7 +286,9 @@ class AdminPanel {
|
|
|
265
286
|
return { success: true, message: `Backup ${safeId} restored`, restored };
|
|
266
287
|
}
|
|
267
288
|
catch (error) {
|
|
268
|
-
|
|
289
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
290
|
+
(0, auditLog_1.logAudit)('admin/backup/restore_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'mutation');
|
|
291
|
+
return { success: false, message: `Restore failed: ${errMsg}` };
|
|
269
292
|
}
|
|
270
293
|
}
|
|
271
294
|
/** Delete a backup zip or directory (safety checks on name) */
|
|
@@ -278,10 +301,15 @@ class AdminPanel {
|
|
|
278
301
|
const dirPath = path_1.default.join(backupRoot, safeId);
|
|
279
302
|
const hasZip = fs_1.default.existsSync(zipPath);
|
|
280
303
|
const hasDir = fs_1.default.existsSync(dirPath) && fs_1.default.statSync(dirPath).isDirectory();
|
|
281
|
-
if (!hasZip && !hasDir)
|
|
282
|
-
|
|
304
|
+
if (!hasZip && !hasDir) {
|
|
305
|
+
const msg = `Backup not found: ${safeId}`;
|
|
306
|
+
(0, auditLog_1.logAudit)('admin/backup/delete_failed', [safeId], { error: msg }, 'mutation');
|
|
307
|
+
return { success: false, message: msg };
|
|
308
|
+
}
|
|
283
309
|
if (!/^backup_|^instructions-|^pre_restore_|^auto-backup-/.test(safeId)) {
|
|
284
|
-
|
|
310
|
+
const msg = 'Refusing to delete unexpected backup name';
|
|
311
|
+
(0, auditLog_1.logAudit)('admin/backup/delete_failed', [safeId], { error: msg }, 'mutation');
|
|
312
|
+
return { success: false, message: msg };
|
|
285
313
|
}
|
|
286
314
|
if (hasZip)
|
|
287
315
|
fs_1.default.unlinkSync(zipPath);
|
|
@@ -291,17 +319,23 @@ class AdminPanel {
|
|
|
291
319
|
return { success: true, message: `Backup ${safeId} deleted`, removed: true };
|
|
292
320
|
}
|
|
293
321
|
catch (error) {
|
|
294
|
-
|
|
322
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
323
|
+
(0, auditLog_1.logAudit)('admin/backup/delete_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'mutation');
|
|
324
|
+
return { success: false, message: `Delete failed: ${errMsg}` };
|
|
295
325
|
}
|
|
296
326
|
}
|
|
297
327
|
/** Prune backups keeping newest N (by createdAt / mtime). Returns count pruned. */
|
|
298
328
|
pruneBackups(retain) {
|
|
299
329
|
try {
|
|
300
|
-
if (retain < 0)
|
|
301
|
-
|
|
330
|
+
if (retain < 0) {
|
|
331
|
+
const msg = 'retain must be >= 0';
|
|
332
|
+
(0, auditLog_1.logAudit)('admin/backup/prune_failed', undefined, { error: msg, retain }, 'mutation');
|
|
333
|
+
return { success: false, message: msg };
|
|
334
|
+
}
|
|
302
335
|
const backupRoot = this.backupRoot;
|
|
303
336
|
if (!fs_1.default.existsSync(backupRoot))
|
|
304
337
|
return { success: true, message: 'No backups to prune', pruned: 0 };
|
|
338
|
+
const pruneErrors = [];
|
|
305
339
|
const entries = fs_1.default.readdirSync(backupRoot)
|
|
306
340
|
.map(name => {
|
|
307
341
|
const full = path_1.default.join(backupRoot, name);
|
|
@@ -341,10 +375,21 @@ class AdminPanel {
|
|
|
341
375
|
fs_1.default.rmSync(d.full, { recursive: true, force: true });
|
|
342
376
|
prunedAll++;
|
|
343
377
|
}
|
|
344
|
-
catch {
|
|
378
|
+
catch (err) {
|
|
379
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
380
|
+
const msg = `Failed to delete backup '${d.id}': ${errMsg}`;
|
|
381
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
382
|
+
pruneErrors.push(msg);
|
|
383
|
+
(0, auditLog_1.logAudit)('admin/backup/prune_warning', [d.id], { error: errMsg, phase: 'delete_all' }, 'mutation');
|
|
384
|
+
}
|
|
345
385
|
}
|
|
346
386
|
process.stderr.write(`[admin] Pruned all backups (${prunedAll})\n`);
|
|
347
|
-
|
|
387
|
+
const result = { success: true, message: `Pruned ${prunedAll} backups`, pruned: prunedAll };
|
|
388
|
+
if (pruneErrors.length > 0) {
|
|
389
|
+
result.message += ` (${pruneErrors.length} error(s))`;
|
|
390
|
+
result.errors = pruneErrors;
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
348
393
|
}
|
|
349
394
|
const toDelete = entries.slice(retain);
|
|
350
395
|
let pruned = 0;
|
|
@@ -356,13 +401,26 @@ class AdminPanel {
|
|
|
356
401
|
fs_1.default.rmSync(d.full, { recursive: true, force: true });
|
|
357
402
|
pruned++;
|
|
358
403
|
}
|
|
359
|
-
catch {
|
|
404
|
+
catch (err) {
|
|
405
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
406
|
+
const msg = `Failed to delete backup '${d.id}': ${errMsg}`;
|
|
407
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
408
|
+
pruneErrors.push(msg);
|
|
409
|
+
(0, auditLog_1.logAudit)('admin/backup/prune_warning', [d.id], { error: errMsg, phase: 'delete_retain' }, 'mutation');
|
|
410
|
+
}
|
|
360
411
|
}
|
|
361
412
|
process.stderr.write(`[admin] Pruned ${pruned} backup(s); retained ${entries.length - pruned}\n`);
|
|
362
|
-
|
|
413
|
+
const result = { success: true, message: `Pruned ${pruned} backups (retained ${entries.length - pruned})`, pruned };
|
|
414
|
+
if (pruneErrors.length > 0) {
|
|
415
|
+
result.message += ` (${pruneErrors.length} error(s))`;
|
|
416
|
+
result.errors = pruneErrors;
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
363
419
|
}
|
|
364
420
|
catch (error) {
|
|
365
|
-
|
|
421
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
422
|
+
(0, auditLog_1.logAudit)('admin/backup/prune_failed', undefined, { error: errMsg, retain }, 'mutation');
|
|
423
|
+
return { success: false, message: `Prune failed: ${errMsg}` };
|
|
366
424
|
}
|
|
367
425
|
}
|
|
368
426
|
/** Export a backup — returns the zip file path for streaming, or falls back to JSON bundle for legacy dirs */
|
|
@@ -375,15 +433,25 @@ class AdminPanel {
|
|
|
375
433
|
}
|
|
376
434
|
// Legacy directory fallback
|
|
377
435
|
const backupDir = path_1.default.join(this.backupRoot, safeId);
|
|
378
|
-
if (!fs_1.default.existsSync(backupDir) || !fs_1.default.statSync(backupDir).isDirectory())
|
|
379
|
-
|
|
436
|
+
if (!fs_1.default.existsSync(backupDir) || !fs_1.default.statSync(backupDir).isDirectory()) {
|
|
437
|
+
const msg = `Backup not found: ${safeId}`;
|
|
438
|
+
(0, auditLog_1.logAudit)('admin/backup/export_failed', [safeId], { error: msg }, 'read');
|
|
439
|
+
return { success: false, message: msg };
|
|
440
|
+
}
|
|
380
441
|
let manifest = {};
|
|
442
|
+
const exportWarnings = [];
|
|
381
443
|
const manifestPath = path_1.default.join(backupDir, 'manifest.json');
|
|
382
444
|
if (fs_1.default.existsSync(manifestPath)) {
|
|
383
445
|
try {
|
|
384
446
|
manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
385
447
|
}
|
|
386
|
-
catch {
|
|
448
|
+
catch (err) {
|
|
449
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
450
|
+
const msg = `Failed to parse manifest.json for backup '${safeId}': ${errMsg}`;
|
|
451
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
452
|
+
exportWarnings.push(msg);
|
|
453
|
+
(0, auditLog_1.logAudit)('admin/backup/export_warning', [safeId], { error: errMsg, phase: 'manifest_parse' }, 'read');
|
|
454
|
+
}
|
|
387
455
|
}
|
|
388
456
|
const files = {};
|
|
389
457
|
for (const f of fs_1.default.readdirSync(backupDir)) {
|
|
@@ -391,20 +459,35 @@ class AdminPanel {
|
|
|
391
459
|
try {
|
|
392
460
|
files[f] = JSON.parse(fs_1.default.readFileSync(path_1.default.join(backupDir, f), 'utf-8'));
|
|
393
461
|
}
|
|
394
|
-
catch {
|
|
462
|
+
catch (err) {
|
|
463
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
464
|
+
const msg = `Skipped corrupt file '${f}' in backup '${safeId}': ${errMsg}`;
|
|
465
|
+
process.stderr.write(`[admin] ${msg}\n`);
|
|
466
|
+
exportWarnings.push(msg);
|
|
467
|
+
(0, auditLog_1.logAudit)('admin/backup/export_warning', [safeId], { error: errMsg, phase: 'file_parse', file: f }, 'read');
|
|
468
|
+
}
|
|
395
469
|
}
|
|
396
470
|
}
|
|
397
|
-
|
|
471
|
+
const result = { success: true, message: 'Export ready', bundle: { manifest, files } };
|
|
472
|
+
if (exportWarnings.length > 0) {
|
|
473
|
+
result.warnings = exportWarnings;
|
|
474
|
+
result.message = `Export ready (${exportWarnings.length} warning(s))`;
|
|
475
|
+
}
|
|
476
|
+
return result;
|
|
398
477
|
}
|
|
399
478
|
catch (error) {
|
|
400
|
-
|
|
479
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
480
|
+
(0, auditLog_1.logAudit)('admin/backup/export_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'read');
|
|
481
|
+
return { success: false, message: `Export failed: ${errMsg}` };
|
|
401
482
|
}
|
|
402
483
|
}
|
|
403
484
|
/** Import a backup from a JSON bundle uploaded by the client — creates a zip */
|
|
404
485
|
importBackup(bundle) {
|
|
405
486
|
try {
|
|
406
487
|
if (!bundle || typeof bundle !== 'object' || !bundle.files || typeof bundle.files !== 'object') {
|
|
407
|
-
|
|
488
|
+
const msg = 'Invalid bundle: must contain a "files" object';
|
|
489
|
+
(0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg }, 'mutation');
|
|
490
|
+
return { success: false, message: msg };
|
|
408
491
|
}
|
|
409
492
|
const now = new Date();
|
|
410
493
|
const baseTs = now.toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
|
|
@@ -437,21 +520,27 @@ class AdminPanel {
|
|
|
437
520
|
return { success: true, message: `Imported ${written} files as ${backupId}`, backupId, files: written };
|
|
438
521
|
}
|
|
439
522
|
catch (error) {
|
|
440
|
-
|
|
523
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
524
|
+
(0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'json' }, 'mutation');
|
|
525
|
+
return { success: false, message: `Import failed: ${errMsg}` };
|
|
441
526
|
}
|
|
442
527
|
}
|
|
443
528
|
/** Import a zip backup uploaded by the client without rewriting its contents. */
|
|
444
529
|
importZipBackup(zipBuffer, sourceName) {
|
|
445
530
|
try {
|
|
446
531
|
if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
|
|
447
|
-
|
|
532
|
+
const msg = 'Invalid zip backup: upload was empty';
|
|
533
|
+
(0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
|
|
534
|
+
return { success: false, message: msg };
|
|
448
535
|
}
|
|
449
536
|
const zip = new adm_zip_1.default(zipBuffer);
|
|
450
537
|
const instructionFiles = zip.getEntries()
|
|
451
538
|
.map(entry => path_1.default.basename(entry.entryName))
|
|
452
539
|
.filter(name => name.toLowerCase().endsWith('.json') && name === path_1.default.basename(name) && name !== 'manifest.json');
|
|
453
540
|
if (!instructionFiles.length) {
|
|
454
|
-
|
|
541
|
+
const msg = 'Invalid zip backup: contains no instruction files';
|
|
542
|
+
(0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
|
|
543
|
+
return { success: false, message: msg };
|
|
455
544
|
}
|
|
456
545
|
const now = new Date();
|
|
457
546
|
const baseTs = now.toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
|
|
@@ -460,13 +549,15 @@ class AdminPanel {
|
|
|
460
549
|
const zipPath = path_1.default.join(this.backupRoot, `${backupId}.zip`);
|
|
461
550
|
if (!fs_1.default.existsSync(this.backupRoot))
|
|
462
551
|
fs_1.default.mkdirSync(this.backupRoot, { recursive: true });
|
|
463
|
-
fs_1.default.writeFileSync(zipPath, zipBuffer);
|
|
552
|
+
fs_1.default.writeFileSync(zipPath, zipBuffer); // lgtm[js/http-to-file-access] — zipPath is generated under controlled backupRoot; admin endpoint behind dashboardAdminAuth
|
|
464
553
|
const safeSourceName = sourceName ? path_1.default.basename(sourceName) : undefined;
|
|
465
554
|
process.stderr.write(`[admin] Imported zip backup from file: ${backupId}.zip (${instructionFiles.length} files${safeSourceName ? `, source=${safeSourceName}` : ''})\n`);
|
|
466
555
|
return { success: true, message: `Imported ${instructionFiles.length} files as ${backupId}`, backupId, files: instructionFiles.length };
|
|
467
556
|
}
|
|
468
557
|
catch (error) {
|
|
469
|
-
|
|
558
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
559
|
+
(0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'zip' }, 'mutation');
|
|
560
|
+
return { success: false, message: `Import failed: ${errMsg}` };
|
|
470
561
|
}
|
|
471
562
|
}
|
|
472
563
|
/**
|
|
@@ -582,8 +673,8 @@ class AdminPanel {
|
|
|
582
673
|
skipped = Math.max(0, scanned - accepted);
|
|
583
674
|
}
|
|
584
675
|
}
|
|
585
|
-
catch {
|
|
586
|
-
|
|
676
|
+
catch (err) {
|
|
677
|
+
process.stderr.write(`[admin] getAdminStats: failed to read index state: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
587
678
|
}
|
|
588
679
|
// Count instructions from the store (raw count uses store, with disk fallback for transparency)
|
|
589
680
|
const indexDir = this.instructionsRoot;
|
|
@@ -598,7 +689,9 @@ class AdminPanel {
|
|
|
598
689
|
rawFileCount = fs_1.default.readdirSync(indexDir).filter(f => f.toLowerCase().endsWith('.json')).length;
|
|
599
690
|
}
|
|
600
691
|
}
|
|
601
|
-
catch {
|
|
692
|
+
catch (err) {
|
|
693
|
+
process.stderr.write(`[admin] getAdminStats: failed to count instruction files on disk: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
694
|
+
}
|
|
602
695
|
}
|
|
603
696
|
// Recompute schema version snapshot only when any of these counts change
|
|
604
697
|
const cacheNeedsUpdate = !this.indexStatsCache || this.indexStatsCache.acceptedInstructions !== accepted || this.indexStatsCache.rawFileCount !== rawFileCount || this.indexStatsCache.skippedInstructions !== skipped;
|
|
@@ -623,11 +716,15 @@ class AdminPanel {
|
|
|
623
716
|
if (typeof json.schemaVersion === 'string')
|
|
624
717
|
schemaVersions.add(json.schemaVersion);
|
|
625
718
|
}
|
|
626
|
-
catch {
|
|
719
|
+
catch (err) {
|
|
720
|
+
process.stderr.write(`[admin] getAdminStats: failed to parse schema version from '${f}': ${err instanceof Error ? err.message : String(err)}\n`);
|
|
721
|
+
}
|
|
627
722
|
}
|
|
628
723
|
}
|
|
629
724
|
}
|
|
630
|
-
catch {
|
|
725
|
+
catch (err) {
|
|
726
|
+
process.stderr.write(`[admin] getAdminStats: failed to read instruction files for schema version: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
727
|
+
}
|
|
631
728
|
}
|
|
632
729
|
const schemaVersion = schemaVersions.size === 0 ? 'unknown' : (schemaVersions.size === 1 ? Array.from(schemaVersions)[0] : `mixed(${Array.from(schemaVersions).join(',')})`);
|
|
633
730
|
this.indexStatsCache = {
|
|
@@ -39,22 +39,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
return result;
|
|
40
40
|
};
|
|
41
41
|
})();
|
|
42
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
43
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
44
|
-
};
|
|
45
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
43
|
exports.createApiRoutes = createApiRoutes;
|
|
47
44
|
const express_1 = __importStar(require("express"));
|
|
48
|
-
const express_rate_limit_1 =
|
|
45
|
+
const express_rate_limit_1 = __importStar(require("express-rate-limit"));
|
|
49
46
|
const MetricsCollector_js_1 = require("./MetricsCollector.js");
|
|
50
47
|
const auditLog_1 = require("../../services/auditLog");
|
|
51
48
|
const runtimeConfig_js_1 = require("../../config/runtimeConfig.js");
|
|
52
49
|
const index_js_1 = require("./routes/index.js");
|
|
53
50
|
const ensureLoadedMiddleware_js_1 = require("./middleware/ensureLoadedMiddleware.js");
|
|
51
|
+
const logger_js_1 = require("../../services/logger.js");
|
|
54
52
|
function createApiRoutes(options = {}) {
|
|
55
53
|
const router = (0, express_1.Router)();
|
|
56
54
|
const metricsCollector = (0, MetricsCollector_js_1.getMetricsCollector)();
|
|
57
|
-
const
|
|
55
|
+
const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
|
|
56
|
+
const rateLimitOpts = options.rateLimit ?? { windowMs: httpCfg.rateLimitWindowMs, max: httpCfg.rateLimitMax };
|
|
58
57
|
// CORS middleware (if enabled)
|
|
59
58
|
// Security: only allow loopback origins (localhost, 127.0.0.1, [::1]) to prevent
|
|
60
59
|
// cross-origin attacks. No wildcard (*) origins; credentials are not exposed.
|
|
@@ -63,7 +62,7 @@ function createApiRoutes(options = {}) {
|
|
|
63
62
|
const origin = req.headers.origin;
|
|
64
63
|
// nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration -- origin is validated against loopback-only regex; not user-controlled
|
|
65
64
|
if (origin && /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin)) {
|
|
66
|
-
res.header('Access-Control-Allow-Origin', origin);
|
|
65
|
+
res.header('Access-Control-Allow-Origin', origin); // lgtm[js/cors-misconfiguration] — origin validated against loopback-only regex above
|
|
67
66
|
}
|
|
68
67
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
69
68
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
@@ -72,8 +71,9 @@ function createApiRoutes(options = {}) {
|
|
|
72
71
|
}
|
|
73
72
|
// JSON middleware
|
|
74
73
|
router.use(express_1.default.json());
|
|
75
|
-
const rateLimitEnabled =
|
|
74
|
+
const rateLimitEnabled = httpCfg.rateLimitEnabled;
|
|
76
75
|
if (rateLimitEnabled && rateLimitOpts.max > 0 && rateLimitOpts.windowMs > 0) {
|
|
76
|
+
// Global rate limit for all routes (reads + mutations)
|
|
77
77
|
router.use((0, express_rate_limit_1.default)({
|
|
78
78
|
windowMs: rateLimitOpts.windowMs,
|
|
79
79
|
max: rateLimitOpts.max,
|
|
@@ -81,13 +81,43 @@ function createApiRoutes(options = {}) {
|
|
|
81
81
|
legacyHeaders: true,
|
|
82
82
|
validate: { ip: false },
|
|
83
83
|
skip: (req) => req.method === 'OPTIONS',
|
|
84
|
-
keyGenerator: (req) =>
|
|
84
|
+
keyGenerator: (req) => {
|
|
85
|
+
const clientIp = req.ip || req.socket.remoteAddress;
|
|
86
|
+
return clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown';
|
|
87
|
+
},
|
|
85
88
|
handler: (_req, res) => {
|
|
86
89
|
const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
|
|
87
90
|
res.status(429).json({
|
|
88
91
|
error: 'Too Many Requests',
|
|
89
92
|
message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
|
|
90
93
|
retryAfterSeconds: retryAfter,
|
|
94
|
+
tier: 'global',
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
// Stricter rate limit for mutation endpoints (POST/PUT/PATCH/DELETE).
|
|
100
|
+
// Defaults to runtimeConfig httpCfg.rateLimitMutationMax (env-configurable),
|
|
101
|
+
// bounded by the global per-window cap so mutation-tier never exceeds global.
|
|
102
|
+
const mutationMax = Math.max(1, Math.min(httpCfg.rateLimitMutationMax, rateLimitOpts.max));
|
|
103
|
+
router.use((0, express_rate_limit_1.default)({
|
|
104
|
+
windowMs: rateLimitOpts.windowMs,
|
|
105
|
+
max: mutationMax,
|
|
106
|
+
standardHeaders: true,
|
|
107
|
+
legacyHeaders: false,
|
|
108
|
+
validate: { ip: false },
|
|
109
|
+
skip: (req) => req.method === 'OPTIONS' || req.method === 'GET' || req.method === 'HEAD',
|
|
110
|
+
keyGenerator: (req) => {
|
|
111
|
+
const clientIp = req.ip || req.socket.remoteAddress;
|
|
112
|
+
return `mutation:${clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown'}`;
|
|
113
|
+
},
|
|
114
|
+
handler: (_req, res) => {
|
|
115
|
+
const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
|
|
116
|
+
res.status(429).json({
|
|
117
|
+
error: 'Too Many Requests',
|
|
118
|
+
message: `Mutation rate limit exceeded. Try again in ${retryAfter} second(s).`,
|
|
119
|
+
retryAfterSeconds: retryAfter,
|
|
120
|
+
tier: 'mutation',
|
|
91
121
|
timestamp: Date.now(),
|
|
92
122
|
});
|
|
93
123
|
},
|
|
@@ -169,9 +199,10 @@ function createApiRoutes(options = {}) {
|
|
|
169
199
|
router.use((0, index_js_1.createScriptsRoutes)());
|
|
170
200
|
router.use((0, index_js_1.createMessagingRoutes)());
|
|
171
201
|
router.use((0, index_js_1.createSqliteRoutes)());
|
|
202
|
+
router.use((0, index_js_1.createAdminFeedbackRoutes)());
|
|
172
203
|
// Error handling middleware
|
|
173
204
|
router.use((error, _req, res, _next) => {
|
|
174
|
-
|
|
205
|
+
(0, logger_js_1.logError)('[API] Unhandled error:', error);
|
|
175
206
|
const exposeDetails = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http.verboseLogging;
|
|
176
207
|
res.status(500).json({
|
|
177
208
|
error: 'Internal server error',
|
|
@@ -118,7 +118,7 @@ class DashboardServer {
|
|
|
118
118
|
const origin = req.headers.origin;
|
|
119
119
|
// nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration -- origin is validated against loopback-only regex; not user-controlled
|
|
120
120
|
if (origin && /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin)) {
|
|
121
|
-
res.header('Access-Control-Allow-Origin', origin);
|
|
121
|
+
res.header('Access-Control-Allow-Origin', origin); // lgtm[js/cors-misconfiguration] — origin validated against loopback-only regex above
|
|
122
122
|
}
|
|
123
123
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
124
124
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
@@ -7,6 +7,7 @@ export declare class FileMetricsStorage {
|
|
|
7
7
|
private storageDir;
|
|
8
8
|
private maxFiles;
|
|
9
9
|
private retentionMinutes;
|
|
10
|
+
private cleanupHealth;
|
|
10
11
|
constructor(options?: {
|
|
11
12
|
storageDir?: string;
|
|
12
13
|
maxFiles?: number;
|
|
@@ -32,15 +33,33 @@ export declare class FileMetricsStorage {
|
|
|
32
33
|
totalSizeKB: number;
|
|
33
34
|
oldestTimestamp?: number;
|
|
34
35
|
newestTimestamp?: number;
|
|
36
|
+
cleanup: {
|
|
37
|
+
degraded: boolean;
|
|
38
|
+
deletionFailures: number;
|
|
39
|
+
lastDeletionError?: string;
|
|
40
|
+
lastDeletionFailureAt?: number;
|
|
41
|
+
lastDeletionRecoveredAt?: number;
|
|
42
|
+
};
|
|
35
43
|
}>;
|
|
36
44
|
/**
|
|
37
45
|
* Clear all stored metrics
|
|
38
46
|
*/
|
|
39
47
|
clearAll(): Promise<void>;
|
|
48
|
+
getCleanupHealth(): {
|
|
49
|
+
degraded: boolean;
|
|
50
|
+
deletionFailures: number;
|
|
51
|
+
lastDeletionError?: string;
|
|
52
|
+
lastDeletionFailureAt?: number;
|
|
53
|
+
lastDeletionRecoveredAt?: number;
|
|
54
|
+
};
|
|
40
55
|
private ensureStorageDir;
|
|
41
56
|
private getSnapshotFiles;
|
|
42
57
|
private extractTimestampFromFilename;
|
|
43
58
|
private cleanupOldFiles;
|
|
59
|
+
private formatCleanupError;
|
|
60
|
+
private recordDeletionFailure;
|
|
61
|
+
private recordDeletionRecovery;
|
|
62
|
+
private deleteSnapshotFile;
|
|
44
63
|
}
|
|
45
64
|
export declare function getFileMetricsStorage(options?: {
|
|
46
65
|
storageDir?: string;
|
|
@@ -16,6 +16,13 @@ class FileMetricsStorage {
|
|
|
16
16
|
storageDir;
|
|
17
17
|
maxFiles;
|
|
18
18
|
retentionMinutes;
|
|
19
|
+
cleanupHealth = {
|
|
20
|
+
degraded: false,
|
|
21
|
+
deletionFailures: 0,
|
|
22
|
+
lastDeletionError: undefined,
|
|
23
|
+
lastDeletionFailureAt: undefined,
|
|
24
|
+
lastDeletionRecoveredAt: undefined,
|
|
25
|
+
};
|
|
19
26
|
constructor(options = {}) {
|
|
20
27
|
this.storageDir = options.storageDir || path_1.default.join(process.cwd(), 'metrics');
|
|
21
28
|
this.maxFiles = options.maxFiles || 720; // 12 hours at 1-minute intervals
|
|
@@ -96,7 +103,7 @@ class FileMetricsStorage {
|
|
|
96
103
|
try {
|
|
97
104
|
const files = await this.getSnapshotFiles();
|
|
98
105
|
if (files.length === 0) {
|
|
99
|
-
return { fileCount: 0, totalSizeKB: 0 };
|
|
106
|
+
return { fileCount: 0, totalSizeKB: 0, cleanup: this.getCleanupHealth() };
|
|
100
107
|
}
|
|
101
108
|
let totalSize = 0;
|
|
102
109
|
for (const file of files) {
|
|
@@ -114,11 +121,12 @@ class FileMetricsStorage {
|
|
|
114
121
|
totalSizeKB: Math.round(totalSize / 1024),
|
|
115
122
|
oldestTimestamp: timestamps.length > 0 ? Math.min(...timestamps) : undefined,
|
|
116
123
|
newestTimestamp: timestamps.length > 0 ? Math.max(...timestamps) : undefined,
|
|
124
|
+
cleanup: this.getCleanupHealth(),
|
|
117
125
|
};
|
|
118
126
|
}
|
|
119
127
|
catch (error) {
|
|
120
128
|
(0, logger_js_1.logError)('[FileMetricsStorage] Failed to get storage stats', error);
|
|
121
|
-
return { fileCount: 0, totalSizeKB: 0 };
|
|
129
|
+
return { fileCount: 0, totalSizeKB: 0, cleanup: this.getCleanupHealth() };
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
/**
|
|
@@ -127,12 +135,19 @@ class FileMetricsStorage {
|
|
|
127
135
|
async clearAll() {
|
|
128
136
|
try {
|
|
129
137
|
const files = await this.getSnapshotFiles();
|
|
130
|
-
await Promise.all(files.map(file =>
|
|
138
|
+
const results = await Promise.all(files.map(file => this.deleteSnapshotFile(file)));
|
|
139
|
+
const failedCount = results.filter(result => !result).length;
|
|
140
|
+
if (failedCount > 0) {
|
|
141
|
+
(0, logger_js_1.logWarn)('[FileMetricsStorage] Failed to delete one or more metrics files during clear', { failedCount });
|
|
142
|
+
}
|
|
131
143
|
}
|
|
132
144
|
catch (error) {
|
|
133
145
|
(0, logger_js_1.logError)('[FileMetricsStorage] Failed to clear metrics storage', error);
|
|
134
146
|
}
|
|
135
147
|
}
|
|
148
|
+
getCleanupHealth() {
|
|
149
|
+
return { ...this.cleanupHealth };
|
|
150
|
+
}
|
|
136
151
|
async ensureStorageDir() {
|
|
137
152
|
try {
|
|
138
153
|
await fs_1.default.promises.mkdir(this.storageDir, { recursive: true });
|
|
@@ -174,14 +189,46 @@ class FileMetricsStorage {
|
|
|
174
189
|
filesToDelete = [...new Set([...expiredFiles, ...oldestFiles])];
|
|
175
190
|
}
|
|
176
191
|
if (filesToDelete.length > 0) {
|
|
177
|
-
await Promise.all(filesToDelete.map(file =>
|
|
178
|
-
|
|
192
|
+
const results = await Promise.all(filesToDelete.map(file => this.deleteSnapshotFile(file)));
|
|
193
|
+
const deletedCount = results.filter(Boolean).length;
|
|
194
|
+
const failedCount = filesToDelete.length - deletedCount;
|
|
195
|
+
(0, logger_js_1.logInfo)('[FileMetricsStorage] Cleaned up old metrics files', { count: deletedCount, failedCount });
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
198
|
catch (error) {
|
|
182
199
|
(0, logger_js_1.logError)('[FileMetricsStorage] Failed to cleanup old metrics files', error);
|
|
183
200
|
}
|
|
184
201
|
}
|
|
202
|
+
formatCleanupError(error) {
|
|
203
|
+
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
204
|
+
}
|
|
205
|
+
recordDeletionFailure(file, error) {
|
|
206
|
+
this.cleanupHealth.degraded = true;
|
|
207
|
+
this.cleanupHealth.deletionFailures += 1;
|
|
208
|
+
this.cleanupHealth.lastDeletionError = this.formatCleanupError(error);
|
|
209
|
+
this.cleanupHealth.lastDeletionFailureAt = Date.now();
|
|
210
|
+
this.cleanupHealth.lastDeletionRecoveredAt = undefined;
|
|
211
|
+
(0, logger_js_1.logWarn)(`[FileMetricsStorage] Failed to delete metrics file ${file}`, error);
|
|
212
|
+
}
|
|
213
|
+
recordDeletionRecovery(file) {
|
|
214
|
+
if (!this.cleanupHealth.degraded)
|
|
215
|
+
return;
|
|
216
|
+
this.cleanupHealth.degraded = false;
|
|
217
|
+
this.cleanupHealth.lastDeletionError = undefined;
|
|
218
|
+
this.cleanupHealth.lastDeletionRecoveredAt = Date.now();
|
|
219
|
+
(0, logger_js_1.logInfo)('[FileMetricsStorage] Metrics file cleanup recovered', { file, deletionFailures: this.cleanupHealth.deletionFailures });
|
|
220
|
+
}
|
|
221
|
+
async deleteSnapshotFile(file) {
|
|
222
|
+
try {
|
|
223
|
+
await fs_1.default.promises.unlink(path_1.default.join(this.storageDir, file));
|
|
224
|
+
this.recordDeletionRecovery(file);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
this.recordDeletionFailure(file, error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
185
232
|
}
|
|
186
233
|
exports.FileMetricsStorage = FileMetricsStorage;
|
|
187
234
|
/**
|