@jagilber-org/index-server 1.26.11 → 1.27.2

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 (40) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/dist/config/dashboardConfig.d.ts +11 -4
  3. package/dist/config/dashboardConfig.js +1 -4
  4. package/dist/dashboard/client/admin.html +57 -27
  5. package/dist/dashboard/client/css/admin.css +54 -0
  6. package/dist/dashboard/client/js/admin.config.js +3 -6
  7. package/dist/dashboard/client/js/admin.embeddings.js +63 -4
  8. package/dist/dashboard/client/js/admin.events.js +256 -0
  9. package/dist/dashboard/client/js/admin.maintenance.js +75 -32
  10. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  11. package/dist/dashboard/server/AdminPanel.js +83 -6
  12. package/dist/dashboard/server/AdminPanelConfig.d.ts +12 -2
  13. package/dist/dashboard/server/AdminPanelConfig.js +48 -19
  14. package/dist/dashboard/server/ApiRoutes.d.ts +5 -4
  15. package/dist/dashboard/server/ApiRoutes.js +40 -35
  16. package/dist/dashboard/server/DashboardServer.js +13 -0
  17. package/dist/dashboard/server/routes/admin.routes.js +143 -17
  18. package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
  19. package/dist/dashboard/server/routes/index.js +11 -9
  20. package/dist/server/sdkServer.js +12 -4
  21. package/dist/services/embeddingService.d.ts +2 -0
  22. package/dist/services/embeddingService.js +16 -4
  23. package/dist/services/embeddingTrigger.d.ts +33 -0
  24. package/dist/services/embeddingTrigger.js +86 -0
  25. package/dist/services/eventBuffer.d.ts +45 -0
  26. package/dist/services/eventBuffer.js +110 -0
  27. package/dist/services/handlers/instructions.import.js +71 -13
  28. package/dist/services/handlers.dashboardConfig.js +82 -2
  29. package/dist/services/indexContext.d.ts +18 -0
  30. package/dist/services/indexContext.js +138 -30
  31. package/dist/services/logger.js +9 -0
  32. package/dist/services/manifestManager.js +11 -1
  33. package/dist/services/seedBootstrap.js +5 -1
  34. package/dist/services/storage/factory.d.ts +2 -0
  35. package/dist/services/storage/factory.js +12 -1
  36. package/dist/services/tracing.js +3 -1
  37. package/package.json +12 -2
  38. package/schemas/index-server.code-schema.json +7424 -1588
  39. package/schemas/manifest.json +3 -3
  40. package/server.json +3 -3
@@ -0,0 +1,256 @@
1
+ /**
2
+ * admin.events.js — Recent events panel + nav-bubble polling.
3
+ *
4
+ * Polls /api/admin/events/counts every 10s for an unread bubble on the
5
+ * Monitoring nav button. When the Monitoring section is active, also fetches
6
+ * the full event list and renders into #events-panel.
7
+ *
8
+ * Client-side pagination + text search + per-row collapse-to-detail.
9
+ */
10
+ (function() {
11
+ var lastSeenId = 0;
12
+ var pollTimer = null;
13
+ var refreshTimer = null;
14
+ var allEvents = []; // most-recent-first cache
15
+ var filterText = '';
16
+ var filterLevel = '';
17
+ var pageIndex = 0;
18
+ var pageSize = 50;
19
+
20
+ function adminFetch(url, opts) {
21
+ if (window.adminAuth && typeof window.adminAuth.adminFetch === 'function') {
22
+ return window.adminAuth.adminFetch(url, opts);
23
+ }
24
+ return fetch(url, opts);
25
+ }
26
+
27
+ function escapeText(s) {
28
+ if (s == null) return '';
29
+ return String(s)
30
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
31
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
32
+ }
33
+
34
+ function highlight(text, term) {
35
+ var escaped = escapeText(text);
36
+ if (!term) return escaped;
37
+ try {
38
+ // Escape regex meta in user input.
39
+ var re = new RegExp(term.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
40
+ return escaped.replace(re, function(m) { return '<mark>' + m + '</mark>'; });
41
+ } catch (_err) { void _err; return escaped; }
42
+ }
43
+
44
+ function applyFilters() {
45
+ var t = filterText.trim().toLowerCase();
46
+ return allEvents.filter(function(e) {
47
+ if (filterLevel && e.level !== filterLevel) return false;
48
+ if (!t) return true;
49
+ var hay = (e.msg || '') + ' ' + (e.detail || '');
50
+ return hay.toLowerCase().indexOf(t) !== -1;
51
+ });
52
+ }
53
+
54
+ function renderEvents() {
55
+ var panel = document.getElementById('events-panel');
56
+ var pager = document.getElementById('events-pager');
57
+ var pageInfo = document.getElementById('events-page-info');
58
+ if (!panel) return;
59
+
60
+ var filtered = applyFilters();
61
+ var totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
62
+ if (pageIndex >= totalPages) pageIndex = totalPages - 1;
63
+ if (pageIndex < 0) pageIndex = 0;
64
+ var start = pageIndex * pageSize;
65
+ var slice = filtered.slice(start, start + pageSize);
66
+
67
+ panel.classList.remove('loading');
68
+ if (filtered.length === 0) {
69
+ panel.innerHTML = '<div class="muted">' + (allEvents.length === 0 ? 'No recent events.' : 'No events match the current filter.') + '</div>';
70
+ if (pager) pager.classList.add('hidden');
71
+ return;
72
+ }
73
+
74
+ var rows = slice.map(function(e) {
75
+ var lvlClass = e.level === 'ERROR' ? 'level-error' : 'level-warn';
76
+ var ts = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
77
+ var msgHtml = highlight(e.msg || '', filterText);
78
+ var detailHtml = e.detail ? '<div class="event-detail muted">' + highlight(e.detail, filterText) + '</div>' : '';
79
+ var caret = e.detail ? '<span class="toggle-caret">▶</span> ' : '';
80
+ return '<tr class="event-row ' + lvlClass + '" data-evt-id="' + e.id + '">'
81
+ + '<td class="event-ts">' + escapeText(ts) + '</td>'
82
+ + '<td class="event-level"><span class="event-badge ' + lvlClass + '">' + escapeText(e.level) + '</span></td>'
83
+ + '<td class="event-msg">' + caret + msgHtml + detailHtml + '</td>'
84
+ + '</tr>';
85
+ }).join('');
86
+
87
+ panel.innerHTML = '<table class="events-table">'
88
+ + '<thead><tr><th>Time</th><th>Level</th><th>Message</th></tr></thead>'
89
+ + '<tbody>' + rows + '</tbody></table>';
90
+
91
+ if (pager) {
92
+ pager.classList.remove('hidden');
93
+ if (pageInfo) {
94
+ pageInfo.textContent = 'Page ' + (pageIndex + 1) + ' of ' + totalPages
95
+ + ' · ' + filtered.length + ' event' + (filtered.length === 1 ? '' : 's')
96
+ + (filtered.length !== allEvents.length ? ' (filtered from ' + allEvents.length + ')' : '');
97
+ }
98
+ var prev = document.getElementById('events-prev');
99
+ var next = document.getElementById('events-next');
100
+ if (prev) prev.disabled = pageIndex === 0;
101
+ if (next) next.disabled = pageIndex >= totalPages - 1;
102
+ }
103
+ }
104
+
105
+ function loadEvents() {
106
+ var levelSel = document.getElementById('events-level-filter');
107
+ filterLevel = levelSel ? levelSel.value : '';
108
+ // Always fetch the buffer max — pagination/filter is client-side so
109
+ // searching can hit older events without round-trips.
110
+ adminFetch('/api/admin/events?limit=1000')
111
+ .then(function(r) { return r.json(); })
112
+ .then(function(data) {
113
+ if (!data || !data.success) return;
114
+ // listEvents returns oldest→newest; reverse for newest-first paging.
115
+ allEvents = (data.events || []).slice().reverse();
116
+ if (data.counts) {
117
+ lastSeenId = data.counts.latestId || lastSeenId;
118
+ updateBubble({ warn: 0, error: 0, total: 0 });
119
+ var summary = document.getElementById('events-counts-summary');
120
+ if (summary) summary.textContent = '(' + (data.counts.warn || 0) + ' warn / ' + (data.counts.error || 0) + ' error in buffer)';
121
+ }
122
+ renderEvents();
123
+ })
124
+ .catch(function() { /* ignore transient errors */ });
125
+ }
126
+
127
+ function clearEventsBuffer() {
128
+ adminFetch('/api/admin/events', { method: 'DELETE' })
129
+ .then(function(r) { return r.json(); })
130
+ .then(function() { allEvents = []; pageIndex = 0; loadEvents(); })
131
+ .catch(function() { /* ignore */ });
132
+ }
133
+
134
+ function updateBubble(counts) {
135
+ var bubble = document.getElementById('nav-events-bubble');
136
+ if (!bubble) return;
137
+ var total = (counts.warn || 0) + (counts.error || 0);
138
+ if (total > 0) {
139
+ bubble.textContent = total > 99 ? '99+' : String(total);
140
+ bubble.hidden = false;
141
+ bubble.classList.toggle('has-error', (counts.error || 0) > 0);
142
+ } else {
143
+ bubble.hidden = true;
144
+ bubble.classList.remove('has-error');
145
+ }
146
+ }
147
+
148
+ function pollCounts() {
149
+ adminFetch('/api/admin/events/counts?since=' + encodeURIComponent(lastSeenId))
150
+ .then(function(r) { return r.json(); })
151
+ .then(function(data) {
152
+ if (!data || !data.success || !data.counts) return;
153
+ updateBubble(data.counts);
154
+ })
155
+ .catch(function() { /* ignore */ });
156
+ }
157
+
158
+ function startPolling() {
159
+ stopPolling();
160
+ pollCounts();
161
+ pollTimer = setInterval(pollCounts, 10000);
162
+ }
163
+ function stopPolling() {
164
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
165
+ }
166
+
167
+ function startMonitoringRefresh() {
168
+ stopMonitoringRefresh();
169
+ refreshTimer = setInterval(function() {
170
+ var section = document.getElementById('monitoring-section');
171
+ if (section && !section.classList.contains('hidden')) loadEvents();
172
+ }, 15000);
173
+ }
174
+ function stopMonitoringRefresh() {
175
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
176
+ }
177
+
178
+ // When user navigates to Monitoring, load events; mark-as-read by adopting latestId.
179
+ document.addEventListener('click', function(ev) {
180
+ var t = ev.target;
181
+ if (t && t.getAttribute && t.getAttribute('data-section') === 'monitoring') {
182
+ setTimeout(loadEvents, 50);
183
+ }
184
+ }, true);
185
+
186
+ // Row expand/collapse (delegated; only inside events-panel).
187
+ function attachPanelHandlers() {
188
+ var panel = document.getElementById('events-panel');
189
+ if (!panel || panel._evtBound) return;
190
+ panel._evtBound = true;
191
+ panel.addEventListener('click', function(ev) {
192
+ var row = ev.target.closest && ev.target.closest('tr.event-row');
193
+ if (!row) return;
194
+ // Don't toggle on selection of text within already-expanded detail.
195
+ var sel = window.getSelection && window.getSelection();
196
+ if (sel && sel.toString().length > 0) return;
197
+ row.classList.toggle('expanded');
198
+ });
199
+ }
200
+
201
+ function attachControlHandlers() {
202
+ var search = document.getElementById('events-search');
203
+ if (search && !search._evtBound) {
204
+ search._evtBound = true;
205
+ var debounce;
206
+ search.addEventListener('input', function() {
207
+ clearTimeout(debounce);
208
+ debounce = setTimeout(function() {
209
+ filterText = search.value || '';
210
+ pageIndex = 0;
211
+ renderEvents();
212
+ }, 120);
213
+ });
214
+ }
215
+ var pageSel = document.getElementById('events-page-size');
216
+ if (pageSel && !pageSel._evtBound) {
217
+ pageSel._evtBound = true;
218
+ pageSel.addEventListener('change', function() {
219
+ var n = parseInt(pageSel.value, 10);
220
+ if (Number.isFinite(n) && n > 0) { pageSize = n; pageIndex = 0; renderEvents(); }
221
+ });
222
+ }
223
+ var prev = document.getElementById('events-prev');
224
+ if (prev && !prev._evtBound) {
225
+ prev._evtBound = true;
226
+ prev.addEventListener('click', function() { if (pageIndex > 0) { pageIndex--; renderEvents(); } });
227
+ }
228
+ var next = document.getElementById('events-next');
229
+ if (next && !next._evtBound) {
230
+ next._evtBound = true;
231
+ next.addEventListener('click', function() { pageIndex++; renderEvents(); });
232
+ }
233
+ }
234
+
235
+ // Init after DOM ready (script is `defer`).
236
+ function init() {
237
+ startPolling();
238
+ startMonitoringRefresh();
239
+ attachPanelHandlers();
240
+ attachControlHandlers();
241
+ var levelFilter = document.getElementById('events-level-filter');
242
+ if (levelFilter && !levelFilter._evtBound) {
243
+ levelFilter._evtBound = true;
244
+ levelFilter.addEventListener('change', function() { pageIndex = 0; loadEvents(); });
245
+ }
246
+ }
247
+
248
+ if (document.readyState === 'loading') {
249
+ document.addEventListener('DOMContentLoaded', init);
250
+ } else {
251
+ init();
252
+ }
253
+
254
+ window.loadEvents = loadEvents;
255
+ window.clearEvents = clearEventsBuffer;
256
+ })();
@@ -76,25 +76,43 @@
76
76
  }
77
77
 
78
78
  async function restoreSelectedBackup() {
79
+ const statusEl = document.getElementById('backup-restore-status');
80
+ const sel = document.getElementById('backup-select');
79
81
  try {
80
- const sel = document.getElementById('backup-select');
81
- const statusEl = document.getElementById('backup-restore-status');
82
- if (!sel || !sel.value) { if (statusEl) statusEl.textContent = 'Select a backup first'; return; }
82
+ if (!sel || !sel.value) {
83
+ if (statusEl) statusEl.textContent = 'Select a backup first';
84
+ console.warn('[restore] aborted: no backup selected');
85
+ return;
86
+ }
83
87
  const choice = sel.value;
84
- if (!confirm(`Restore backup ${choice}? Current instructions will be safety-backed up first.`)) return;
88
+ if (!confirm(`Restore backup ${choice}? Current instructions will be safety-backed up first.`)) {
89
+ console.info('[restore] aborted: user cancelled confirm dialog for', choice);
90
+ if (statusEl) statusEl.textContent = 'Restore cancelled';
91
+ return;
92
+ }
85
93
  if (statusEl) statusEl.textContent = 'Restoring...';
94
+ console.info('[restore] POST /api/admin/maintenance/restore', { backupId: choice });
86
95
  const res = await adminAuth.adminFetch('/api/admin/maintenance/restore', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backupId: choice }) });
87
- const data = await res.json();
88
- if (data.success) {
96
+ console.info('[restore] response status', res.status);
97
+ const data = await res.json().catch(function(e){ console.error('[restore] failed to parse JSON', e); return { success: false, error: 'invalid JSON response' }; });
98
+ console.info('[restore] response body', data);
99
+ if (res.ok && data.success) {
89
100
  if (statusEl) statusEl.textContent = `Restored ${choice} (${data.restored || 0} files)`;
101
+ if (typeof showSuccess === 'function') showSuccess(`Restored ${choice} (${data.restored || 0} files)`);
90
102
  // Re-load stats & instructions to reflect changes
91
103
  if (typeof loadOverviewData === 'function') loadOverviewData();
92
104
  if (typeof currentSection !== 'undefined' && currentSection === 'instructions' && typeof loadInstructions === 'function') loadInstructions();
105
+ loadBackups();
93
106
  } else {
94
- if (statusEl) statusEl.textContent = `Restore failed: ${data.error || data.message || 'unknown'}`;
107
+ const msg = `Restore failed: ${data.error || data.message || ('HTTP ' + res.status)}`;
108
+ if (statusEl) statusEl.textContent = msg;
109
+ console.error('[restore]', msg);
110
+ alert(msg);
95
111
  }
96
112
  } catch (err) {
97
- if (statusEl) statusEl.textContent = 'Error restoring backup';
113
+ console.error('[restore] exception', err);
114
+ if (statusEl) statusEl.textContent = 'Error restoring backup: ' + (err && err.message || err);
115
+ alert('Error restoring backup: ' + (err && err.message || err));
98
116
  }
99
117
  }
100
118
 
@@ -104,7 +122,8 @@
104
122
  const data = await response.json();
105
123
 
106
124
  if (data.success) {
107
- if (typeof displayMaintenanceStatus === 'function') displayMaintenanceStatus(data.maintenance); // lgtm[js/unneeded-defensive-code] global may load asynchronously across dashboard panels
125
+ // displayMaintenanceStatus is declared in this same module (function declaration, hoisted).
126
+ displayMaintenanceStatus(data.maintenance);
108
127
  } else {
109
128
  if (typeof showError === 'function') showError('Failed to load maintenance status');
110
129
  }
@@ -350,18 +369,36 @@
350
369
 
351
370
  async function handleBackupFileSelected(ev){
352
371
  const file = ev.target && ev.target.files && ev.target.files[0];
353
- if(!file) return;
372
+ if(!file) {
373
+ console.warn('[restore-from-file] no file selected');
374
+ return;
375
+ }
376
+ const statusEl = document.getElementById('backup-restore-status');
354
377
  try {
355
378
  const lowerName = file.name.toLowerCase();
356
379
  const arrayBuffer = await file.arrayBuffer();
357
380
  const bytes = new Uint8Array(arrayBuffer);
358
381
  const zipBackup = looksLikeZip(bytes) || lowerName.endsWith('.zip');
382
+ console.info('[restore-from-file] selected', { name: file.name, size: file.size, zip: zipBackup });
359
383
 
360
384
  if(!zipBackup && !lowerName.endsWith('.json')){ alert('Please select a .json or .zip backup file'); return; }
361
385
 
386
+ // "Restore from File" performs import + restore in one shot via ?restore=1.
387
+ // The current instructions/ are safety-backed up by the server before overwrite.
388
+ const fileCount = zipBackup ? null : (function(){ try { return Object.keys(JSON.parse(new TextDecoder('utf-8').decode(bytes)).files || {}).length; } catch { return null; } })();
389
+ const promptMsg = 'Restore from ' + file.name + '?\n\nThis will overwrite the live instructions. A safety backup of the current state will be taken first.' + (fileCount != null ? '\n\nFile count in bundle: ' + fileCount : '');
390
+ if(!confirm(promptMsg)) {
391
+ console.info('[restore-from-file] cancelled by user');
392
+ if (statusEl) statusEl.textContent = 'Restore cancelled';
393
+ return;
394
+ }
395
+
396
+ if (statusEl) statusEl.textContent = 'Importing & restoring ' + file.name + '...';
397
+
398
+ let res;
362
399
  if(zipBackup){
363
- if(!confirm('Import backup from ' + file.name + '? (zip archive)')) return;
364
- const res = await adminAuth.adminFetch('/api/admin/maintenance/backup/import', {
400
+ console.info('[restore-from-file] POST /api/admin/maintenance/backup/import?restore=1 (zip)', { bytes: arrayBuffer.byteLength });
401
+ res = await adminAuth.adminFetch('/api/admin/maintenance/backup/import?restore=1', {
365
402
  method:'POST',
366
403
  headers:{
367
404
  'Content-Type':'application/zip',
@@ -369,29 +406,35 @@
369
406
  },
370
407
  body: arrayBuffer,
371
408
  });
372
- const data = await res.json();
373
- if(data.success){
374
- if(typeof showSuccess === 'function') showSuccess(data.message || 'Imported');
375
- loadMaintenanceStatus(); loadBackups();
376
- } else {
377
- alert('Import failed: '+(data.error||'unknown'));
378
- }
379
- return;
409
+ } else {
410
+ const text = new TextDecoder('utf-8').decode(bytes);
411
+ const bundle = JSON.parse(text);
412
+ if(!bundle.files || typeof bundle.files !== 'object'){ alert('Invalid backup file: must contain a "files" object'); return; }
413
+ console.info('[restore-from-file] POST /api/admin/maintenance/backup/import?restore=1 (json)', { files: Object.keys(bundle.files).length });
414
+ res = await adminAuth.adminFetch('/api/admin/maintenance/backup/import?restore=1', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(bundle) });
380
415
  }
381
-
382
- const text = new TextDecoder('utf-8').decode(bytes);
383
- const bundle = JSON.parse(text);
384
- if(!bundle.files || typeof bundle.files !== 'object'){ alert('Invalid backup file: must contain a "files" object'); return; }
385
- if(!confirm('Import backup from ' + file.name + '? (' + Object.keys(bundle.files).length + ' files)')) return;
386
- const res = await adminAuth.adminFetch('/api/admin/maintenance/backup/import', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(bundle) });
387
- const data = await res.json();
388
- if(data.success){
389
- if(typeof showSuccess === 'function') showSuccess(data.message || 'Imported');
390
- loadMaintenanceStatus(); loadBackups();
416
+ console.info('[restore-from-file] response status', res.status);
417
+ const data = await res.json().catch(function(e){ console.error('[restore-from-file] failed to parse JSON', e); return { success: false, error: 'invalid JSON response' }; });
418
+ console.info('[restore-from-file] response body', data);
419
+ if(res.ok && data.success){
420
+ const msg = data.message || ('Imported and restored ' + (data.backupId || ''));
421
+ if(typeof showSuccess === 'function') showSuccess(msg);
422
+ if (statusEl) statusEl.textContent = msg;
423
+ loadMaintenanceStatus();
424
+ loadBackups();
425
+ if (typeof loadOverviewData === 'function') loadOverviewData();
426
+ if (typeof currentSection !== 'undefined' && currentSection === 'instructions' && typeof loadInstructions === 'function') loadInstructions();
391
427
  } else {
392
- alert('Import failed: '+(data.error||'unknown'));
428
+ const errMsg = 'Restore failed: ' + (data.error || data.message || ('HTTP ' + res.status));
429
+ console.error('[restore-from-file]', errMsg, data);
430
+ if (statusEl) statusEl.textContent = errMsg;
431
+ alert(errMsg);
393
432
  }
394
- } catch(e){ alert('Import error: '+(e.message||e)); }
433
+ } catch(e){
434
+ console.error('[restore-from-file] exception', e);
435
+ if (statusEl) statusEl.textContent = 'Error: ' + (e.message||e);
436
+ alert('Restore-from-file error: '+(e.message||e));
437
+ }
395
438
  }
396
439
 
397
440
  // Signal Groom: apply usage signals to instruction priority/requirement
@@ -147,7 +147,7 @@
147
147
  if (connEl) connEl.innerHTML = '<div class="error-message">Error loading active connections</div>';
148
148
  }
149
149
 
150
- try { if (typeof loadSessionHistory === 'function') loadSessionHistory(parseInt(document.getElementById('session-history-limit')?.value || '50', 10)); } catch {} // lgtm[js/unneeded-defensive-code] — global may load asynchronously across dashboard panels
150
+ try { loadSessionHistory(parseInt(document.getElementById('session-history-limit')?.value || '50', 10)); } catch {}
151
151
  window.__lastSessionsCount = sessionsCount;
152
152
  window.__lastConnectionsCount = connectionsCount;
153
153
  updateSessionsNavBadge();
@@ -25,6 +25,14 @@ const AdminPanelConfig_1 = require("./AdminPanelConfig");
25
25
  const AdminPanelState_1 = require("./AdminPanelState");
26
26
  const backupZip_1 = require("../../services/backupZip");
27
27
  const auditLog_1 = require("../../services/auditLog");
28
+ const logger_1 = require("../../services/logger");
29
+ // Stackless WARN: the log-hygiene gate (scripts/crawl-logs.mjs --strict)
30
+ // treats WARN-with-stack as a budget violation (max-stack-warn=5). logWarn()
31
+ // auto-captures a JS stack via captureCallStack(), so for routine admin
32
+ // rejection paths use log('WARN', ...) directly with a serialized detail.
33
+ const warnStruct = (msg, detail) => (0, logger_1.log)('WARN', msg, { detail: detail === undefined ? undefined : typeof detail === 'string' ? detail : JSON.stringify(detail) });
34
+ const embeddingTrigger_1 = require("../../services/embeddingTrigger");
35
+ const migrationEngine_1 = require("../../services/storage/migrationEngine");
28
36
  const adm_zip_1 = __importDefault(require("adm-zip"));
29
37
  class AdminPanel {
30
38
  panelConfig;
@@ -265,7 +273,7 @@ class AdminPanel {
265
273
  source: safeId,
266
274
  originalCount: existing.length,
267
275
  });
268
- process.stderr.write(`[admin] Pre-restore safety backup created: ${safetyId}.zip\n`);
276
+ (0, logger_1.logInfo)('[admin] pre-restore safety backup created', { safetyId, originalCount: existing.length });
269
277
  }
270
278
  let restored = 0;
271
279
  if (isZip) {
@@ -282,11 +290,58 @@ class AdminPanel {
282
290
  }
283
291
  }
284
292
  this.indexStatsCache = null;
285
- process.stderr.write(`[admin] Restored backup ${safeId} (${restored} instruction files)\n`);
293
+ // Restore wrote JSON files to instructionsDir, but the live index may
294
+ // not be sourcing from disk:
295
+ // - JSON backend: IndexContext re-reads on next ensureLoaded() because
296
+ // instructionsDir mtime changed (no .index-version file in dev).
297
+ // - SQLite backend: source of truth is the .db at storage.sqlitePath,
298
+ // NOT the JSON files. Without re-ingesting, the dashboard keeps
299
+ // showing the pre-restore row count (RCA 2026-05-01: 702-file zip
300
+ // restored, Overview kept showing 2 seed-bootstrap entries).
301
+ // Therefore: re-ingest into SQLite when applicable, then invalidate the
302
+ // in-memory cache so the next read reflects the restored set.
303
+ const backend = (0, runtimeConfig_1.getRuntimeConfig)().storage?.backend ?? 'json';
304
+ let sqliteIngest;
305
+ if (backend === 'sqlite') {
306
+ try {
307
+ const dbPath = (0, runtimeConfig_1.getRuntimeConfig)().storage?.sqlitePath
308
+ ?? path_1.default.join(process.cwd(), 'data', 'index.db');
309
+ const mr = (0, migrationEngine_1.migrateJsonToSqlite)(instructionsDir, dbPath);
310
+ sqliteIngest = { migrated: mr.migrated, errors: mr.errors.length };
311
+ (0, logger_1.logInfo)('[admin] post-restore sqlite re-ingest', { backupId: safeId, ...sqliteIngest });
312
+ }
313
+ catch (err) {
314
+ (0, logger_1.logError)('[admin] post-restore sqlite re-ingest failed', {
315
+ backupId: safeId,
316
+ error: err instanceof Error ? err.message : String(err),
317
+ });
318
+ }
319
+ }
320
+ (0, indexContext_1.touchIndexVersion)();
321
+ (0, indexContext_1.invalidate)();
322
+ let afterCount = -1;
323
+ try {
324
+ afterCount = Object.keys((0, indexContext_1.ensureLoaded)().byId || {}).length;
325
+ }
326
+ catch (reloadErr) {
327
+ (0, logger_1.logError)('[admin] post-restore reload failed', {
328
+ backupId: safeId,
329
+ error: reloadErr instanceof Error ? reloadErr.message : String(reloadErr),
330
+ });
331
+ }
332
+ (0, logger_1.logInfo)('[admin] backup restored', { backupId: safeId, restored, source: isZip ? 'zip' : 'dir', backend, afterCount, ...(sqliteIngest ? { sqliteIngest } : {}) });
333
+ // Fire-and-forget embedding compute when semantic is enabled — addresses
334
+ // post-import gap where embeddings file is missing until manually triggered.
335
+ try {
336
+ (0, embeddingTrigger_1.scheduleEmbeddingComputeAfterImport)(`restore:${safeId}`);
337
+ }
338
+ catch { /* never block restore */ }
286
339
  return { success: true, message: `Backup ${safeId} restored`, restored };
287
340
  }
288
341
  catch (error) {
289
342
  const errMsg = error instanceof Error ? error.message : String(error);
343
+ const stack = error instanceof Error ? error.stack : undefined;
344
+ (0, logger_1.logError)('[admin] backup restore failed', { backupId, error: errMsg, stack });
290
345
  (0, auditLog_1.logAudit)('admin/backup/restore_failed', backupId ? [String(backupId)] : undefined, { error: errMsg }, 'mutation');
291
346
  return { success: false, message: `Restore failed: ${errMsg}` };
292
347
  }
@@ -516,29 +571,49 @@ class AdminPanel {
516
571
  };
517
572
  zip.addFile('manifest.json', Buffer.from(JSON.stringify(manifest, null, 2)));
518
573
  zip.writeZip(zipPath);
519
- process.stderr.write(`[admin] Imported backup from file: ${backupId}.zip (${written} files)\n`);
574
+ (0, logger_1.logInfo)('[admin] importBackup complete', { backupId, files: written, mode: 'json' });
520
575
  return { success: true, message: `Imported ${written} files as ${backupId}`, backupId, files: written };
521
576
  }
522
577
  catch (error) {
523
578
  const errMsg = error instanceof Error ? error.message : String(error);
579
+ const stack = error instanceof Error ? error.stack : undefined;
580
+ (0, logger_1.logError)('[admin] importBackup failed', { error: errMsg, stack, mode: 'json' });
524
581
  (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'json' }, 'mutation');
525
582
  return { success: false, message: `Import failed: ${errMsg}` };
526
583
  }
527
584
  }
528
585
  /** Import a zip backup uploaded by the client without rewriting its contents. */
529
586
  importZipBackup(zipBuffer, sourceName) {
587
+ // CodeQL: Array.isArray is the negative pattern recognized by the
588
+ // js/type-confusion-through-parameter-tampering query. Buffer.isBuffer
589
+ // alone is not treated as narrowing, so guard before any .length read.
590
+ const sizeBytes = !Array.isArray(zipBuffer) && Buffer.isBuffer(zipBuffer) ? zipBuffer.length : 0;
591
+ const safeSource = typeof sourceName === 'string' && !Array.isArray(sourceName) ? sourceName : undefined;
592
+ (0, logger_1.logInfo)('[admin] importZipBackup start', { sourceName: safeSource ? path_1.default.basename(safeSource) : undefined, sizeBytes });
530
593
  try {
531
- if (!Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
594
+ if (Array.isArray(zipBuffer) || !Buffer.isBuffer(zipBuffer) || zipBuffer.length === 0) {
532
595
  const msg = 'Invalid zip backup: upload was empty';
596
+ warnStruct('[admin] importZipBackup rejected', { reason: 'empty-buffer', sizeBytes });
533
597
  (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
534
598
  return { success: false, message: msg };
535
599
  }
536
- const zip = new adm_zip_1.default(zipBuffer);
600
+ let zip;
601
+ try {
602
+ zip = new adm_zip_1.default(zipBuffer);
603
+ }
604
+ catch (e) {
605
+ const errMsg = e instanceof Error ? e.message : String(e);
606
+ (0, logger_1.logError)('[admin] importZipBackup zip-parse-error', { error: errMsg, sizeBytes });
607
+ (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'zip' }, 'mutation');
608
+ return { success: false, message: `Import failed: ${errMsg}` };
609
+ }
610
+ const allEntries = zip.getEntries().map(e => e.entryName);
537
611
  const instructionFiles = zip.getEntries()
538
612
  .map(entry => path_1.default.basename(entry.entryName))
539
613
  .filter(name => name.toLowerCase().endsWith('.json') && name === path_1.default.basename(name) && name !== 'manifest.json');
540
614
  if (!instructionFiles.length) {
541
615
  const msg = 'Invalid zip backup: contains no instruction files';
616
+ warnStruct('[admin] importZipBackup rejected', { reason: 'no-instruction-files', entryCount: allEntries.length, sample: allEntries.slice(0, 10) });
542
617
  (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: msg, mode: 'zip' }, 'mutation');
543
618
  return { success: false, message: msg };
544
619
  }
@@ -551,11 +626,13 @@ class AdminPanel {
551
626
  fs_1.default.mkdirSync(this.backupRoot, { recursive: true });
552
627
  fs_1.default.writeFileSync(zipPath, zipBuffer); // lgtm[js/http-to-file-access] — zipPath is generated under controlled backupRoot; admin endpoint behind dashboardAdminAuth
553
628
  const safeSourceName = sourceName ? path_1.default.basename(sourceName) : undefined;
554
- process.stderr.write(`[admin] Imported zip backup from file: ${backupId}.zip (${instructionFiles.length} files${safeSourceName ? `, source=${safeSourceName}` : ''})\n`);
629
+ (0, logger_1.logInfo)('[admin] importZipBackup complete', { backupId, files: instructionFiles.length, source: safeSourceName, zipPath });
555
630
  return { success: true, message: `Imported ${instructionFiles.length} files as ${backupId}`, backupId, files: instructionFiles.length };
556
631
  }
557
632
  catch (error) {
558
633
  const errMsg = error instanceof Error ? error.message : String(error);
634
+ const stack = error instanceof Error ? error.stack : undefined;
635
+ (0, logger_1.logError)('[admin] importZipBackup failed', { error: errMsg, stack, sourceName: sourceName ? path_1.default.basename(sourceName) : undefined, sizeBytes });
559
636
  (0, auditLog_1.logAudit)('admin/backup/import_failed', undefined, { error: errMsg, mode: 'zip' }, 'mutation');
560
637
  return { success: false, message: `Import failed: ${errMsg}` };
561
638
  }
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Owns the AdminConfig data structure and provides CRUD methods for
5
5
  * reading and updating admin panel configuration.
6
+ *
7
+ * Updates here mutate `process.env.INDEX_SERVER_*` and call `reloadRuntimeConfig()`
8
+ * so the dashboard "Server Configuration" panel reflects (and applies to) the
9
+ * single runtimeConfig source of truth.
6
10
  */
7
11
  export interface AdminConfig {
8
12
  serverSettings: {
@@ -11,8 +15,7 @@ export interface AdminConfig {
11
15
  enableVerboseLogging: boolean;
12
16
  enableMutation: boolean;
13
17
  rateLimit: {
14
- windowMs: number;
15
- maxRequests: number;
18
+ perMinute: number;
16
19
  };
17
20
  };
18
21
  indexSettings: {
@@ -31,11 +34,18 @@ export declare class AdminPanelConfig {
31
34
  private config;
32
35
  constructor();
33
36
  private loadDefaultConfig;
37
+ /** Re-read from runtime config so callers always see the current authoritative values. */
34
38
  getAdminConfig(): AdminConfig;
35
39
  updateAdminConfig(updates: Partial<AdminConfig>): {
36
40
  success: boolean;
37
41
  message: string;
42
+ appliedFields?: string[];
38
43
  };
44
+ /**
45
+ * Apply incoming serverSettings to `process.env.INDEX_SERVER_*` and reload runtime config
46
+ * so the dashboard form actually drives behavior. Bind every editable field to its env var
47
+ * (per constitution S-4: "All environment configuration must flow through runtimeConfig.ts").
48
+ */
39
49
  private applyConfigChanges;
40
50
  /** Session timeout in milliseconds — consumed by state management. */
41
51
  get sessionTimeout(): number;