@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
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
+
})();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Human-operator surface for browsing, creating, editing, and deleting
|
|
5
5
|
* persisted feedback entries, plus a client-side GitHub issue handoff.
|
|
6
6
|
*
|
|
7
|
-
* Design constraints (
|
|
7
|
+
* Design constraints (architecture review / decisions.md G-1..G-8):
|
|
8
8
|
* - CRUD against /api/admin/feedback (operator-tier, NOT the MCP surface)
|
|
9
9
|
* - GitHub handoff is client-side ONLY — window.open with pre-filled URL
|
|
10
10
|
* - No server-side GitHub API calls, no token handling, no OAuth
|
|
@@ -448,7 +448,7 @@
|
|
|
448
448
|
|
|
449
449
|
function formatInstructionJson(){ const ta = document.getElementById('instruction-content'); if(!ta) return; try{ const parsed = JSON.parse(ta.value); ta.value = JSON.stringify(parsed, null, 2); updateInstructionEditorDiagnostics(); } catch { globals.showError && globals.showError('Cannot format: invalid JSON'); } }
|
|
450
450
|
|
|
451
|
-
function applyInstructionTemplate(){ const ta = document.getElementById('instruction-content'); if(!ta) return; if(ta.value.trim() && !confirm('Replace current content with template?')) return; const now = new Date().toISOString(); const template = { id:'sample-instruction', title:'Sample Instruction', body:'Detailed instruction content here.\nAdd multi-line guidance and steps.', contentType:'instruction', priority:50, audience:'all', requirement:'optional', categories:['general'], primaryCategory:'general', schemaVersion:'
|
|
451
|
+
function applyInstructionTemplate(){ const ta = document.getElementById('instruction-content'); if(!ta) return; if(ta.value.trim() && !confirm('Replace current content with template?')) return; const now = new Date().toISOString(); const template = { id:'sample-instruction', title:'Sample Instruction', body:'Detailed instruction content here.\nAdd multi-line guidance and steps.', contentType:'instruction', priority:50, audience:'all', requirement:'optional', categories:['general'], primaryCategory:'general', schemaVersion:'5', status:'draft', owner:'you@example.com', version:'1.0.0', reviewIntervalDays:180, semanticSummary:'Brief summary of what this instruction covers.', createdAt: now, updatedAt: now }; ta.value = JSON.stringify(template, null, 2); updateInstructionEditorDiagnostics(); }
|
|
452
452
|
|
|
453
453
|
async function deleteInstruction(name) {
|
|
454
454
|
if (!confirm('Delete instruction ' + name + '?')) return;
|
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.`))
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
if(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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){
|
|
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 {
|
|
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();
|
|
@@ -473,11 +473,11 @@ class SecurityMonitor {
|
|
|
473
473
|
// Utility methods for simulated monitoring
|
|
474
474
|
getCPUUsage() {
|
|
475
475
|
// Simulate CPU usage between 10-80%
|
|
476
|
-
return Math.random() * 70 + 10;
|
|
476
|
+
return Math.random() * 70 + 10; // nosemgrep: insecure-randomness — simulated metric, not security context
|
|
477
477
|
}
|
|
478
478
|
getAverageAPILatency() {
|
|
479
479
|
// Simulate API latency between 50-300ms
|
|
480
|
-
return Math.random() * 250 + 50;
|
|
480
|
+
return Math.random() * 250 + 50; // nosemgrep: insecure-randomness — simulated metric, not security context
|
|
481
481
|
}
|
|
482
482
|
checkForSuspiciousPatterns(patterns, _caseSensitive) {
|
|
483
483
|
// Simulate 1% chance of detecting suspicious pattern
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
@@ -30,11 +34,18 @@ export declare class AdminPanelConfig {
|
|
|
30
34
|
private config;
|
|
31
35
|
constructor();
|
|
32
36
|
private loadDefaultConfig;
|
|
37
|
+
/** Re-read from runtime config so callers always see the current authoritative values. */
|
|
33
38
|
getAdminConfig(): AdminConfig;
|
|
34
39
|
updateAdminConfig(updates: Partial<AdminConfig>): {
|
|
35
40
|
success: boolean;
|
|
36
41
|
message: string;
|
|
42
|
+
appliedFields?: string[];
|
|
37
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
|
+
*/
|
|
38
49
|
private applyConfigChanges;
|
|
39
50
|
/** Session timeout in milliseconds — consumed by state management. */
|
|
40
51
|
get sessionTimeout(): number;
|