@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.
- package/CHANGELOG.md +63 -1
- package/CONTRIBUTING.md +3 -3
- package/dist/dashboard/client/admin.html +58 -28
- package/dist/dashboard/client/css/admin.css +54 -0
- package/dist/dashboard/client/js/admin.config.js +3 -6
- package/dist/dashboard/client/js/admin.embeddings.js +63 -4
- package/dist/dashboard/client/js/admin.events.js +256 -0
- package/dist/dashboard/client/js/admin.feedback.js +1 -1
- package/dist/dashboard/client/js/admin.instructions.js +1 -1
- package/dist/dashboard/client/js/admin.maintenance.js +75 -32
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/security/SecurityMonitor.js +2 -2
- package/dist/dashboard/server/AdminPanel.js +83 -6
- package/dist/dashboard/server/AdminPanelConfig.d.ts +11 -0
- package/dist/dashboard/server/AdminPanelConfig.js +47 -17
- package/dist/dashboard/server/AdminPanelState.js +5 -1
- package/dist/dashboard/server/ApiRoutes.js +2 -1
- package/dist/dashboard/server/DashboardServer.js +13 -0
- package/dist/dashboard/server/MetricsCollector.js +3 -2
- package/dist/dashboard/server/WebSocketManager.js +2 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +1 -1
- package/dist/dashboard/server/routes/admin.routes.js +143 -17
- package/dist/dashboard/server/routes/api.usage.routes.js +5 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
- package/dist/dashboard/server/routes/instructions.routes.js +142 -12
- package/dist/dashboard/server/routes/scripts.routes.js +1 -1
- package/dist/dashboard/server/routes/sqlite.routes.js +74 -0
- package/dist/models/instruction.d.ts +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/server/sdkServer.js +12 -4
- package/dist/services/auditLog.d.ts +1 -1
- package/dist/services/auditLog.js +1 -1
- package/dist/services/embeddingService.d.ts +2 -0
- package/dist/services/embeddingService.js +16 -4
- package/dist/services/embeddingTrigger.d.ts +33 -0
- package/dist/services/embeddingTrigger.js +86 -0
- package/dist/services/eventBuffer.d.ts +45 -0
- package/dist/services/eventBuffer.js +110 -0
- package/dist/services/feedbackStorage.js +1 -1
- package/dist/services/handlers/instructions.add.js +36 -3
- package/dist/services/handlers/instructions.import.js +71 -13
- package/dist/services/handlers.dashboardConfig.js +81 -0
- package/dist/services/handlers.feedback.js +1 -1
- package/dist/services/handlers.instructionSchema.js +4 -4
- package/dist/services/handlers.search.js +3 -3
- package/dist/services/indexContext.d.ts +18 -0
- package/dist/services/indexContext.js +133 -24
- package/dist/services/instructionRecordValidation.d.ts +3 -0
- package/dist/services/instructionRecordValidation.js +64 -4
- package/dist/services/logger.js +9 -0
- package/dist/services/manifestManager.js +11 -1
- package/dist/services/seedBootstrap.js +7 -3
- package/dist/services/storage/factory.d.ts +2 -0
- package/dist/services/storage/factory.js +12 -1
- package/dist/services/toolRegistry.js +8 -8
- package/dist/services/toolRegistry.zod.js +3 -3
- package/dist/services/tracing.js +3 -1
- package/dist/versioning/schemaVersion.d.ts +1 -1
- package/dist/versioning/schemaVersion.js +47 -2
- package/package.json +54 -40
- package/schemas/index-server.code-schema.json +1 -1
- package/schemas/instruction.schema.json +3 -3
- package/schemas/json-schema/instruction-content-type.schema.json +1 -1
- package/schemas/json-schema/instruction-instruction-entry.schema.json +1 -1
- package/scripts/README.md +48 -0
- package/scripts/{generate-certs.mjs → build/generate-certs.mjs} +1 -1
- package/scripts/{setup-wizard.mjs → build/setup-wizard.mjs} +1 -1
- package/scripts/{setup-hooks.cjs → hooks/setup-hooks.cjs} +3 -3
- package/server.json +3 -3
- /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
|
-
|
|
48
|
-
this.applyConfigChanges(updates);
|
|
49
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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_${
|
|
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
|
|
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()}_${
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
res.json({ success:
|
|
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
|
-
|
|
451
|
-
|
|
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()}_${
|
|
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:
|
|
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
|
});
|