@jagilber-org/index-server 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/CONTRIBUTING.md +3 -3
  3. package/dist/dashboard/client/admin.html +58 -28
  4. package/dist/dashboard/client/css/admin.css +54 -0
  5. package/dist/dashboard/client/js/admin.config.js +3 -6
  6. package/dist/dashboard/client/js/admin.embeddings.js +63 -4
  7. package/dist/dashboard/client/js/admin.events.js +256 -0
  8. package/dist/dashboard/client/js/admin.feedback.js +1 -1
  9. package/dist/dashboard/client/js/admin.instructions.js +1 -1
  10. package/dist/dashboard/client/js/admin.maintenance.js +75 -32
  11. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  12. package/dist/dashboard/security/SecurityMonitor.js +2 -2
  13. package/dist/dashboard/server/AdminPanel.js +83 -6
  14. package/dist/dashboard/server/AdminPanelConfig.d.ts +11 -0
  15. package/dist/dashboard/server/AdminPanelConfig.js +47 -17
  16. package/dist/dashboard/server/AdminPanelState.js +5 -1
  17. package/dist/dashboard/server/ApiRoutes.js +2 -1
  18. package/dist/dashboard/server/DashboardServer.js +13 -0
  19. package/dist/dashboard/server/MetricsCollector.js +3 -2
  20. package/dist/dashboard/server/WebSocketManager.js +2 -2
  21. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  22. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +1 -1
  23. package/dist/dashboard/server/routes/admin.routes.js +143 -17
  24. package/dist/dashboard/server/routes/api.usage.routes.js +5 -1
  25. package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
  26. package/dist/dashboard/server/routes/instructions.routes.js +142 -12
  27. package/dist/dashboard/server/routes/scripts.routes.js +1 -1
  28. package/dist/dashboard/server/routes/sqlite.routes.js +74 -0
  29. package/dist/models/instruction.d.ts +1 -1
  30. package/dist/schemas/index.d.ts +1 -1
  31. package/dist/schemas/index.js +1 -1
  32. package/dist/server/sdkServer.js +12 -4
  33. package/dist/services/auditLog.d.ts +1 -1
  34. package/dist/services/auditLog.js +1 -1
  35. package/dist/services/embeddingService.d.ts +2 -0
  36. package/dist/services/embeddingService.js +16 -4
  37. package/dist/services/embeddingTrigger.d.ts +33 -0
  38. package/dist/services/embeddingTrigger.js +86 -0
  39. package/dist/services/eventBuffer.d.ts +45 -0
  40. package/dist/services/eventBuffer.js +110 -0
  41. package/dist/services/feedbackStorage.js +1 -1
  42. package/dist/services/handlers/instructions.add.js +36 -3
  43. package/dist/services/handlers/instructions.import.js +71 -13
  44. package/dist/services/handlers.dashboardConfig.js +81 -0
  45. package/dist/services/handlers.feedback.js +1 -1
  46. package/dist/services/handlers.instructionSchema.js +4 -4
  47. package/dist/services/handlers.search.js +3 -3
  48. package/dist/services/indexContext.d.ts +18 -0
  49. package/dist/services/indexContext.js +133 -24
  50. package/dist/services/instructionRecordValidation.d.ts +3 -0
  51. package/dist/services/instructionRecordValidation.js +64 -4
  52. package/dist/services/logger.js +9 -0
  53. package/dist/services/manifestManager.js +11 -1
  54. package/dist/services/seedBootstrap.js +7 -3
  55. package/dist/services/storage/factory.d.ts +2 -0
  56. package/dist/services/storage/factory.js +12 -1
  57. package/dist/services/toolRegistry.js +8 -8
  58. package/dist/services/toolRegistry.zod.js +3 -3
  59. package/dist/services/tracing.js +3 -1
  60. package/dist/versioning/schemaVersion.d.ts +1 -1
  61. package/dist/versioning/schemaVersion.js +47 -2
  62. package/package.json +54 -40
  63. package/schemas/index-server.code-schema.json +1 -1
  64. package/schemas/instruction.schema.json +3 -3
  65. package/schemas/json-schema/instruction-content-type.schema.json +1 -1
  66. package/schemas/json-schema/instruction-instruction-entry.schema.json +1 -1
  67. package/scripts/README.md +48 -0
  68. package/scripts/{generate-certs.mjs → build/generate-certs.mjs} +1 -1
  69. package/scripts/{setup-wizard.mjs → build/setup-wizard.mjs} +1 -1
  70. package/scripts/{setup-hooks.cjs → hooks/setup-hooks.cjs} +3 -3
  71. package/server.json +3 -3
  72. /package/scripts/{copy-dashboard-assets.mjs → build/copy-dashboard-assets.mjs} +0 -0
@@ -4,6 +4,10 @@
4
4
  *
5
5
  * Owns the AdminConfig data structure and provides CRUD methods for
6
6
  * reading and updating admin panel configuration.
7
+ *
8
+ * Updates here mutate `process.env.INDEX_SERVER_*` and call `reloadRuntimeConfig()`
9
+ * so the dashboard "Server Configuration" panel reflects (and applies to) the
10
+ * single runtimeConfig source of truth.
7
11
  */
8
12
  Object.defineProperty(exports, "__esModule", { value: true });
9
13
  exports.AdminPanelConfig = void 0;
@@ -39,14 +43,18 @@ class AdminPanelConfig {
39
43
  }
40
44
  };
41
45
  }
46
+ /** Re-read from runtime config so callers always see the current authoritative values. */
42
47
  getAdminConfig() {
48
+ this.config = this.loadDefaultConfig();
43
49
  return JSON.parse(JSON.stringify(this.config));
44
50
  }
45
51
  updateAdminConfig(updates) {
46
52
  try {
47
- this.config = { ...this.config, ...updates };
48
- this.applyConfigChanges(updates);
49
- return { success: true, message: 'Configuration updated successfully' };
53
+ const applied = [];
54
+ this.applyConfigChanges(updates, applied);
55
+ // Refresh in-memory snapshot from runtime config (post-reload).
56
+ this.config = this.loadDefaultConfig();
57
+ return { success: true, message: 'Configuration updated successfully', appliedFields: applied };
50
58
  }
51
59
  catch (error) {
52
60
  return {
@@ -55,21 +63,43 @@ class AdminPanelConfig {
55
63
  };
56
64
  }
57
65
  }
58
- applyConfigChanges(updates) {
59
- if (updates.serverSettings) {
60
- let runtimeReloadNeeded = false;
61
- if (updates.serverSettings.enableVerboseLogging !== undefined) {
62
- process.env.INDEX_SERVER_VERBOSE_LOGGING = updates.serverSettings.enableVerboseLogging ? '1' : '0';
63
- runtimeReloadNeeded = true;
64
- }
65
- if (updates.serverSettings.enableMutation !== undefined) {
66
- process.env.INDEX_SERVER_MUTATION = updates.serverSettings.enableMutation ? '1' : '0';
67
- runtimeReloadNeeded = true;
68
- }
69
- if (runtimeReloadNeeded) {
70
- (0, runtimeConfig_1.reloadRuntimeConfig)();
71
- }
66
+ /**
67
+ * Apply incoming serverSettings to `process.env.INDEX_SERVER_*` and reload runtime config
68
+ * so the dashboard form actually drives behavior. Bind every editable field to its env var
69
+ * (per constitution S-4: "All environment configuration must flow through runtimeConfig.ts").
70
+ */
71
+ applyConfigChanges(updates, applied = []) {
72
+ if (!updates.serverSettings)
73
+ return;
74
+ const s = updates.serverSettings;
75
+ let needsReload = false;
76
+ if (s.enableVerboseLogging !== undefined) {
77
+ process.env.INDEX_SERVER_VERBOSE_LOGGING = s.enableVerboseLogging ? '1' : '0';
78
+ applied.push('verboseLogging');
79
+ needsReload = true;
80
+ }
81
+ if (s.enableMutation !== undefined) {
82
+ process.env.INDEX_SERVER_MUTATION = s.enableMutation ? '1' : '0';
83
+ applied.push('mutation');
84
+ needsReload = true;
85
+ }
86
+ if (s.maxConnections !== undefined && Number.isFinite(s.maxConnections) && s.maxConnections > 0) {
87
+ process.env.INDEX_SERVER_MAX_CONNECTIONS = String(Math.floor(s.maxConnections));
88
+ applied.push('maxConnections');
89
+ needsReload = true;
90
+ }
91
+ if (s.requestTimeout !== undefined && Number.isFinite(s.requestTimeout) && s.requestTimeout > 0) {
92
+ process.env.INDEX_SERVER_REQUEST_TIMEOUT = String(Math.floor(s.requestTimeout));
93
+ applied.push('requestTimeout');
94
+ needsReload = true;
95
+ }
96
+ if (s.rateLimit && s.rateLimit.perMinute !== undefined && Number.isFinite(s.rateLimit.perMinute) && s.rateLimit.perMinute >= 0) {
97
+ process.env.INDEX_SERVER_RATE_LIMIT = String(Math.floor(s.rateLimit.perMinute));
98
+ applied.push('rateLimitPerMinute');
99
+ needsReload = true;
72
100
  }
101
+ if (needsReload)
102
+ (0, runtimeConfig_1.reloadRuntimeConfig)();
73
103
  }
74
104
  /** Session timeout in milliseconds — consumed by state management. */
75
105
  get sessionTimeout() {
@@ -5,8 +5,12 @@
5
5
  * Manages active admin sessions and session history, including
6
6
  * persistence, creation, termination, and cleanup of expired sessions.
7
7
  */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.AdminPanelState = void 0;
13
+ const crypto_1 = __importDefault(require("crypto"));
10
14
  const runtimeConfig_1 = require("../../config/runtimeConfig");
11
15
  const SessionPersistenceManager_1 = require("./SessionPersistenceManager");
12
16
  const logger_js_1 = require("../../services/logger.js");
@@ -178,7 +182,7 @@ class AdminPanelState {
178
182
  }
179
183
  }
180
184
  generateSessionId() {
181
- return `admin_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
185
+ return `admin_${crypto_1.default.randomUUID()}`;
182
186
  }
183
187
  getSessionHistory(limit) {
184
188
  const slice = typeof limit === 'number' ? this.sessionHistory.slice(0, Math.max(0, limit)) : this.sessionHistory;
@@ -148,6 +148,7 @@ function createApiRoutes(options = {}) {
148
148
  const success = res.statusCode < 500;
149
149
  const route = normalizeRoute(req);
150
150
  const toolId = `http/${req.method} ${route}`;
151
+ metricsCollector.recordToolCall('http/request', success, ms, success ? undefined : `http_${res.statusCode}`);
151
152
  metricsCollector.recordToolCall(toolId, success, ms, success ? undefined : `http_${res.statusCode}`);
152
153
  }
153
154
  catch { /* never block response path */ }
@@ -185,7 +186,7 @@ function createApiRoutes(options = {}) {
185
186
  // -------------------------------------------------------------------------
186
187
  // Pre-load instruction index once per request so route handlers can use
187
188
  // res.locals.indexState instead of calling ensureLoaded() repeatedly.
188
- // See: https://github.com/jagilber-dev/index-server/issues/45
189
+ // See internal tracker #45.
189
190
  router.use(ensureLoadedMiddleware_js_1.ensureLoadedMiddleware);
190
191
  // Mount route modules
191
192
  router.use((0, index_js_1.createStatusRoutes)(metricsCollector));
@@ -146,6 +146,19 @@ class DashboardServer {
146
146
  }
147
147
  next();
148
148
  });
149
+ // Structured request logger — every dashboard HTTP hit lands in mcp-server.log.
150
+ // Critical for diagnosing import/restore failures where stderr output is lost
151
+ // when the MCP server runs under stdio transport without an attached TTY.
152
+ this.app.use((req, res, next) => {
153
+ const start = Date.now();
154
+ const ctype = req.header('content-type') || '';
155
+ const clen = req.header('content-length') || '';
156
+ (0, logger_js_1.logInfo)('[http] request', { method: req.method, url: req.originalUrl || req.url, ctype, clen });
157
+ res.on('finish', () => {
158
+ (0, logger_js_1.logInfo)('[http] response', { method: req.method, url: req.originalUrl || req.url, status: res.statusCode, ms: Date.now() - start });
159
+ });
160
+ next();
161
+ });
149
162
  this.app.use(express_1.default.json());
150
163
  this.app.use(express_1.default.static(path_1.default.join(__dirname, '..', 'client'), {
151
164
  etag: true,
@@ -14,6 +14,7 @@ exports.MetricsCollector = void 0;
14
14
  exports.getMetricsCollector = getMetricsCollector;
15
15
  exports.setMetricsCollector = setMetricsCollector;
16
16
  const fs_1 = __importDefault(require("fs"));
17
+ const crypto_1 = __importDefault(require("crypto"));
17
18
  const path_1 = __importDefault(require("path"));
18
19
  const v8_1 = __importDefault(require("v8"));
19
20
  const FileMetricsStorage_js_1 = require("./FileMetricsStorage.js");
@@ -690,7 +691,7 @@ class MetricsCollector {
690
691
  recentActivity: (0, metricsSerializer_js_1.buildRecentActivity)(this.tools),
691
692
  streamingStats: {
692
693
  totalStreamingConnections: this.activeConnections,
693
- dataTransferRate: this.connections.size * 0.1 + Math.random() * 0.5,
694
+ dataTransferRate: this.connections.size * 0.1 + Math.random() * 0.5, // nosemgrep: insecure-randomness — simulated metric jitter
694
695
  latency,
695
696
  compressionRatio: 0.7,
696
697
  },
@@ -735,7 +736,7 @@ class MetricsCollector {
735
736
  */
736
737
  generateRealTimeAlert(type, severity, message, value, threshold) {
737
738
  const alert = {
738
- id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
739
+ id: `alert_${Date.now()}_${crypto_1.default.randomBytes(6).toString('hex')}`,
739
740
  type: type,
740
741
  severity: severity,
741
742
  message,
@@ -148,7 +148,7 @@ class WebSocketManager {
148
148
  }
149
149
  catch {
150
150
  // Fallback simple id
151
- ws.clientId = `client-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
151
+ ws.clientId = `client-${Date.now()}-${Math.floor(Math.random() * 1000)}`; // lgtm[js/insecure-randomness] — fallback when safeGenerateClientId() throws; loopback-only dashboard
152
152
  }
153
153
  ws.connectedAt = Date.now();
154
154
  ws.lastActivity = Date.now();
@@ -327,7 +327,7 @@ class WebSocketManager {
327
327
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
328
328
  return crypto.randomUUID();
329
329
  }
330
- return 'client-' + Math.random().toString(36).slice(2);
330
+ return 'client-' + Math.random().toString(36).slice(2); // lgtm[js/insecure-randomness] — fallback when crypto.randomUUID unavailable
331
331
  }
332
332
  /** Send current metrics snapshot to a specific client */
333
333
  sendCurrentMetrics(client) {
@@ -8,7 +8,7 @@
8
8
  * Mutation handlers that call invalidate() must still call ensureLoaded()
9
9
  * explicitly after invalidation to pick up their own changes.
10
10
  *
11
- * See: https://github.com/jagilber-dev/index-server/issues/45
11
+ * See internal tracker #45.
12
12
  */
13
13
  import { Request, Response, NextFunction } from 'express';
14
14
  import { IndexState } from '../../../services/indexContext.js';
@@ -9,7 +9,7 @@
9
9
  * Mutation handlers that call invalidate() must still call ensureLoaded()
10
10
  * explicitly after invalidation to pick up their own changes.
11
11
  *
12
- * See: https://github.com/jagilber-dev/index-server/issues/45
12
+ * See internal tracker #45.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.ensureLoadedMiddleware = ensureLoadedMiddleware;
@@ -23,6 +23,8 @@ const AdminPanel_js_1 = require("../AdminPanel.js");
23
23
  const WebSocketManager_js_1 = require("../WebSocketManager.js");
24
24
  const featureFlags_js_1 = require("../../../services/featureFlags.js");
25
25
  const handlers_dashboardConfig_js_1 = require("../../../services/handlers.dashboardConfig.js");
26
+ const runtimeConfig_js_1 = require("../../../config/runtimeConfig.js");
27
+ const eventBuffer_js_1 = require("../../../services/eventBuffer.js");
26
28
  const registry_js_1 = require("../../../server/registry.js");
27
29
  const adminAuth_js_1 = require("./adminAuth.js");
28
30
  const logger_js_1 = require("../../../services/logger.js");
@@ -91,10 +93,35 @@ function createAdminRoutes(metricsCollector) {
91
93
  try {
92
94
  const updates = req.body;
93
95
  const result = adminPanel.updateAdminConfig(updates);
94
- // Feature flag persistence (optional field featureFlags { name:boolean })
96
+ // Feature flag persistence: split incoming flags into
97
+ // (a) namespace flags (e.g. response_envelope_v1) — persisted to flags.json
98
+ // (b) INDEX_SERVER_* boolean toggles — routed to process.env + reloadRuntimeConfig()
99
+ // so dashboard toggles actually take effect at runtime (issue #282 / fix #4).
95
100
  if (updates.featureFlags && typeof updates.featureFlags === 'object') {
96
101
  try {
97
- (0, featureFlags_js_1.updateFlags)(updates.featureFlags);
102
+ const knownEnv = new Set((0, handlers_dashboardConfig_js_1.getFlagRegistrySnapshot)().filter(f => f.type === 'boolean').map(f => f.name));
103
+ const namespaceFlags = {};
104
+ let envChanged = 0;
105
+ const appliedEnv = [];
106
+ for (const [k, v] of Object.entries(updates.featureFlags)) {
107
+ if (typeof v !== 'boolean')
108
+ continue;
109
+ const upper = String(k).toUpperCase();
110
+ if (upper.startsWith('INDEX_SERVER_') && knownEnv.has(upper)) {
111
+ process.env[upper] = v ? '1' : '0';
112
+ appliedEnv.push(upper);
113
+ envChanged++;
114
+ }
115
+ else {
116
+ namespaceFlags[k] = v;
117
+ }
118
+ }
119
+ if (envChanged > 0) {
120
+ (0, runtimeConfig_js_1.reloadRuntimeConfig)();
121
+ (0, logger_js_1.logInfo)(`[admin] applied ${envChanged} INDEX_SERVER_* toggle(s): ${appliedEnv.join(', ')}`);
122
+ }
123
+ if (Object.keys(namespaceFlags).length > 0)
124
+ (0, featureFlags_js_1.updateFlags)(namespaceFlags);
98
125
  }
99
126
  catch (e) {
100
127
  (0, logger_js_1.logWarn)('[API] feature flag update failed:', e instanceof Error ? e.message : e);
@@ -340,7 +367,9 @@ function createAdminRoutes(metricsCollector) {
340
367
  router.post('/admin/maintenance/restore', (req, res) => {
341
368
  try {
342
369
  const { backupId } = req.body || {};
370
+ (0, logger_js_1.logInfo)('[admin] restore requested', { backupId });
343
371
  const result = adminPanel.restoreBackup(backupId);
372
+ (0, logger_js_1.logInfo)('[admin] restore result', { backupId, success: result.success, restored: result.restored ?? 0, message: result.message });
344
373
  if (result.success) {
345
374
  res.json({ success: true, message: result.message, restored: result.restored, timestamp: Date.now() });
346
375
  }
@@ -428,28 +457,75 @@ function createAdminRoutes(metricsCollector) {
428
457
  /**
429
458
  * POST /api/admin/maintenance/backup/import - Import backup from uploaded JSON bundle or zip archive
430
459
  * body: { manifest?: object, files: { [filename]: content } } or raw zip bytes
460
+ * query: ?restore=1 - if set, immediately restore the imported backup (one-click "Restore from File")
431
461
  */
432
462
  router.post('/admin/maintenance/backup/import', (0, express_1.raw)({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
463
+ // Narrow req.query.restore safely — Express may return string | string[] | ParsedQs | ParsedQs[].
464
+ // Only treat a literal scalar string '1' / 'true' (case-insensitive) as opt-in; arrays/objects are rejected.
465
+ const restoreParam = req.query?.restore;
466
+ const wantRestore = typeof restoreParam === 'string' && (restoreParam === '1' || restoreParam.toLowerCase() === 'true');
467
+ const ctype = req.header('content-type') || '';
468
+ const rawBody = req.body;
469
+ // Refine req.body through typed locals so downstream uses don't rely on `as Buffer` casts.
470
+ // CodeQL js/type-confusion-through-parameter-tampering does NOT recognize Buffer.isBuffer
471
+ // as a sanitizer; use `instanceof Buffer` (native JS predicate, modeled by CodeQL).
472
+ // Array.isArray IS recognized. Avoid `.length` interpolation on tainted values.
473
+ const bodyBuffer = rawBody instanceof Buffer ? rawBody : null;
474
+ const bodyArray = Array.isArray(rawBody) ? rawBody : null;
475
+ const bodyObject = rawBody !== null
476
+ && typeof rawBody === 'object'
477
+ && !Array.isArray(rawBody)
478
+ && !(rawBody instanceof Buffer)
479
+ ? rawBody
480
+ : null;
481
+ const bodyKind = bodyBuffer !== null
482
+ ? 'buffer'
483
+ : bodyArray !== null
484
+ ? 'array'
485
+ : bodyObject !== null
486
+ ? 'json'
487
+ : typeof rawBody;
488
+ (0, logger_js_1.logInfo)('[admin] backup/import received', { ctype, body: bodyKind, restore: wantRestore });
433
489
  try {
434
- if (Buffer.isBuffer(req.body) && req.body.length > 0) {
435
- const sourceName = req.header('x-backup-filename') || req.header('x-file-name') || undefined;
436
- const result = adminPanel.importZipBackup(req.body, sourceName);
437
- if (result.success) {
438
- return res.json({ success: true, message: result.message, backupId: result.backupId, files: result.files, timestamp: Date.now() });
439
- }
440
- return res.status(400).json({ success: false, error: result.message, timestamp: Date.now() });
490
+ let importResult;
491
+ if (bodyBuffer !== null && bodyBuffer.length > 0) {
492
+ const filenameHeader = req.header('x-backup-filename') || req.header('x-file-name');
493
+ const sourceName = typeof filenameHeader === 'string' ? filenameHeader : undefined;
494
+ importResult = adminPanel.importZipBackup(bodyBuffer, sourceName);
441
495
  }
442
- const bundle = req.body;
443
- if (!bundle || typeof bundle !== 'object' || !bundle.files) {
444
- return res.status(400).json({ success: false, error: 'Request body must contain a "files" object', timestamp: Date.now() });
496
+ else {
497
+ if (bodyObject === null) {
498
+ (0, logger_js_1.logWarn)('[admin] backup/import rejected', { reason: 'body-not-json-object', ctype });
499
+ return res.status(400).json({ success: false, error: 'Request body must be a JSON object containing a "files" object', timestamp: Date.now() });
500
+ }
501
+ const bundle = bodyObject;
502
+ const files = bundle.files;
503
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
504
+ (0, logger_js_1.logWarn)('[admin] backup/import rejected', { reason: 'missing-or-invalid-files-object' });
505
+ return res.status(400).json({ success: false, error: 'Request body must contain a "files" object', timestamp: Date.now() });
506
+ }
507
+ importResult = adminPanel.importBackup({
508
+ manifest: typeof bundle.manifest === 'object' && bundle.manifest !== null && !Array.isArray(bundle.manifest)
509
+ ? bundle.manifest
510
+ : undefined,
511
+ files: files,
512
+ });
445
513
  }
446
- const result = adminPanel.importBackup(bundle);
447
- if (result.success) {
448
- res.json({ success: true, message: result.message, backupId: result.backupId, files: result.files, timestamp: Date.now() });
514
+ if (!importResult.success) {
515
+ (0, logger_js_1.logWarn)('[admin] backup/import failed', { message: importResult.message });
516
+ return res.status(400).json({ success: false, error: importResult.message, timestamp: Date.now() });
449
517
  }
450
- else {
451
- res.status(400).json({ success: false, error: result.message, timestamp: Date.now() });
518
+ (0, logger_js_1.logInfo)('[admin] backup/import ok', { backupId: importResult.backupId, files: importResult.files });
519
+ if (wantRestore && importResult.backupId) {
520
+ (0, logger_js_1.logInfo)('[admin] backup/import auto-restore start', { backupId: importResult.backupId });
521
+ const restoreResult = adminPanel.restoreBackup(importResult.backupId);
522
+ (0, logger_js_1.logInfo)('[admin] backup/import auto-restore result', { backupId: importResult.backupId, success: restoreResult.success, restored: restoreResult.restored ?? 0, message: restoreResult.message });
523
+ if (!restoreResult.success) {
524
+ return res.status(500).json({ success: false, error: `Imported as ${importResult.backupId} but restore failed: ${restoreResult.message}`, backupId: importResult.backupId, files: importResult.files, restored: 0, timestamp: Date.now() });
525
+ }
526
+ return res.json({ success: true, message: `Imported and restored ${importResult.backupId} (${restoreResult.restored ?? 0} files)`, backupId: importResult.backupId, files: importResult.files, restored: restoreResult.restored ?? 0, restored_applied: true, timestamp: Date.now() });
452
527
  }
528
+ return res.json({ success: true, message: importResult.message, backupId: importResult.backupId, files: importResult.files, timestamp: Date.now() });
453
529
  }
454
530
  catch (error) {
455
531
  (0, logger_js_1.logError)('[API] Import backup error:', error);
@@ -578,5 +654,55 @@ function createAdminRoutes(metricsCollector) {
578
654
  });
579
655
  }
580
656
  });
657
+ /**
658
+ * GET /api/admin/events - Recent WARN/ERROR events from the in-memory ring buffer
659
+ * Query: ?since=<id>&level=WARN|ERROR&limit=<n>
660
+ */
661
+ router.get('/admin/events', (req, res) => {
662
+ try {
663
+ const since = req.query.since !== undefined ? parseInt(String(req.query.since), 10) : undefined;
664
+ const limit = req.query.limit !== undefined ? parseInt(String(req.query.limit), 10) : undefined;
665
+ const levelRaw = String(req.query.level || '').toUpperCase();
666
+ const level = (levelRaw === 'WARN' || levelRaw === 'ERROR') ? levelRaw : undefined;
667
+ const events = (0, eventBuffer_js_1.listEvents)({
668
+ sinceId: Number.isFinite(since) ? since : undefined,
669
+ limit: Number.isFinite(limit) ? limit : undefined,
670
+ level,
671
+ });
672
+ const counts = (0, eventBuffer_js_1.eventCounts)(0);
673
+ res.json({ success: true, events, counts, timestamp: Date.now() });
674
+ }
675
+ catch (error) {
676
+ (0, logger_js_1.logError)('[API] Get events error:', error);
677
+ res.status(500).json({ success: false, error: 'Failed to read events' });
678
+ }
679
+ });
680
+ /**
681
+ * GET /api/admin/events/counts - Lightweight counts for nav-bubble polling
682
+ */
683
+ router.get('/admin/events/counts', (req, res) => {
684
+ try {
685
+ const since = req.query.since !== undefined ? parseInt(String(req.query.since), 10) : 0;
686
+ const counts = (0, eventBuffer_js_1.eventCounts)(Number.isFinite(since) ? since : 0);
687
+ res.json({ success: true, counts, timestamp: Date.now() });
688
+ }
689
+ catch (error) {
690
+ (0, logger_js_1.logError)('[API] Get event counts error:', error);
691
+ res.status(500).json({ success: false, error: 'Failed to read event counts' });
692
+ }
693
+ });
694
+ /**
695
+ * DELETE /api/admin/events - Clear the buffer (also acts as "mark all read")
696
+ */
697
+ router.delete('/admin/events', (_req, res) => {
698
+ try {
699
+ (0, eventBuffer_js_1.clearEvents)();
700
+ res.json({ success: true, message: 'Events cleared', timestamp: Date.now() });
701
+ }
702
+ catch (error) {
703
+ (0, logger_js_1.logError)('[API] Clear events error:', error);
704
+ res.status(500).json({ success: false, error: 'Failed to clear events' });
705
+ }
706
+ });
581
707
  return router;
582
708
  }
@@ -5,8 +5,12 @@
5
5
  * Manages API request execution (retry, rate-limit enforcement, auth, transforms)
6
6
  * and the monitoring event bus. Depends on EndpointManager for endpoint config.
7
7
  */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.UsageManager = void 0;
13
+ const crypto_1 = __importDefault(require("crypto"));
10
14
  const logger_js_1 = require("../../../services/logger.js");
11
15
  // ── UsageManager ─────────────────────────────────────────────────────────────────────
12
16
  class UsageManager {
@@ -22,7 +26,7 @@ class UsageManager {
22
26
  if (!this.endpointMgr.checkRateLimit(endpointId)) {
23
27
  throw new Error(`Rate limit exceeded for endpoint: ${endpointId}`);
24
28
  }
25
- const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
29
+ const requestId = `req_${Date.now()}_${crypto_1.default.randomBytes(6).toString('hex')}`;
26
30
  const request = {
27
31
  id: requestId,
28
32
  endpointId,
@@ -211,6 +211,85 @@ function createEmbeddingsRoutes(embeddingPathOverride, embeddingStore) {
211
211
  });
212
212
  }
213
213
  });
214
+ // GET /embeddings/status — surface model + cache state for the Embeddings tab
215
+ router.get('/embeddings/status', (_req, res) => {
216
+ try {
217
+ const config = (0, runtimeConfig_js_1.getRuntimeConfig)();
218
+ const sem = config.semantic;
219
+ if (!sem.enabled) {
220
+ return res.json({
221
+ success: true,
222
+ enabled: false,
223
+ ready: false,
224
+ state: 'disabled',
225
+ message: 'Semantic embeddings are disabled. Set INDEX_SERVER_SEMANTIC_ENABLED=1 and restart to enable.',
226
+ });
227
+ }
228
+ const readiness = (0, embeddingService_js_1.checkModelReadiness)(sem.model, sem.cacheDir, sem.localOnly);
229
+ let embeddingsFileExists = false;
230
+ let embeddingsCount = 0;
231
+ let embeddingsModelName;
232
+ try {
233
+ if (embeddingStore) {
234
+ const loaded = embeddingStore.load();
235
+ if (loaded) {
236
+ embeddingsFileExists = true;
237
+ embeddingsCount = Object.keys(loaded.embeddings || {}).length;
238
+ embeddingsModelName = loaded.modelName ?? undefined;
239
+ }
240
+ }
241
+ else {
242
+ const file = embeddingPathOverride ?? sem.embeddingPath;
243
+ if (file && node_fs_1.default.existsSync(file)) {
244
+ embeddingsFileExists = true;
245
+ const raw = node_fs_1.default.readFileSync(file, 'utf8');
246
+ const parsed = JSON.parse(raw);
247
+ embeddingsCount = parsed.embeddings ? Object.keys(parsed.embeddings).length : 0;
248
+ embeddingsModelName = parsed.modelName ?? undefined;
249
+ }
250
+ }
251
+ }
252
+ catch { /* tolerate parse / read failures — surface as not present */ }
253
+ // State machine for the dashboard banner.
254
+ let state;
255
+ if (!readiness.ready) {
256
+ state = 'missing'; // localOnly=true and model not cached
257
+ }
258
+ else if (!readiness.cached) {
259
+ state = 'will-download';
260
+ }
261
+ else if (!embeddingsFileExists || embeddingsCount === 0) {
262
+ state = 'no-embeddings';
263
+ }
264
+ else {
265
+ state = 'ready';
266
+ }
267
+ return res.json({
268
+ success: true,
269
+ enabled: true,
270
+ ready: readiness.ready && embeddingsFileExists && embeddingsCount > 0,
271
+ state,
272
+ model: sem.model,
273
+ device: sem.device,
274
+ cacheDir: sem.cacheDir,
275
+ localOnly: sem.localOnly,
276
+ modelCached: readiness.cached,
277
+ modelPath: readiness.modelPath,
278
+ embeddingPath: sem.embeddingPath,
279
+ embeddingsFileExists,
280
+ embeddingsCount,
281
+ embeddingsModelName,
282
+ message: readiness.message,
283
+ });
284
+ }
285
+ catch (err) {
286
+ return res.status(500).json({
287
+ success: false,
288
+ error: 'Failed to compute embeddings status',
289
+ message: err.message,
290
+ });
291
+ }
292
+ });
214
293
  // POST /embeddings/compute — trigger embedding computation for all instructions
215
294
  router.post('/embeddings/compute', adminAuth_js_1.dashboardAdminAuth, async (_req, res) => {
216
295
  try {
@@ -244,10 +323,21 @@ function createEmbeddingsRoutes(embeddingPathOverride, embeddingStore) {
244
323
  });
245
324
  }
246
325
  catch (err) {
326
+ const msg = err.message ?? String(err);
327
+ // Detect the canonical "model not cached locally" signature from
328
+ // @huggingface/transformers and translate to an actionable hint so
329
+ // the dashboard can surface a useful banner.
330
+ const isLocalOnlyMiss = /local_files_only=true/i.test(msg) ||
331
+ /allowRemoteModels=false/i.test(msg) ||
332
+ /file was not found locally/i.test(msg);
247
333
  return res.status(500).json({
248
334
  success: false,
249
335
  error: 'Failed to compute embeddings',
250
- message: err.message,
336
+ message: msg,
337
+ hint: isLocalOnlyMiss
338
+ ? 'The embedding model is not cached locally and remote downloads are disabled. ' +
339
+ 'Set INDEX_SERVER_SEMANTIC_LOCAL_ONLY=0 and restart, or pre-stage the model in INDEX_SERVER_SEMANTIC_CACHE_DIR.'
340
+ : undefined,
251
341
  });
252
342
  }
253
343
  });