laminark 0.1.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/README.md +147 -0
- package/package.json +65 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +10 -0
- package/plugin/commands/recall.md +55 -0
- package/plugin/commands/remember.md +34 -0
- package/plugin/commands/resume.md +45 -0
- package/plugin/commands/stash.md +34 -0
- package/plugin/commands/status.md +33 -0
- package/plugin/dist/analysis/worker.d.ts +1 -0
- package/plugin/dist/analysis/worker.js +233 -0
- package/plugin/dist/analysis/worker.js.map +1 -0
- package/plugin/dist/config-t8LZeB-u.mjs +90 -0
- package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
- package/plugin/dist/hooks/handler.d.ts +286 -0
- package/plugin/dist/hooks/handler.d.ts.map +1 -0
- package/plugin/dist/hooks/handler.js +2413 -0
- package/plugin/dist/hooks/handler.js.map +1 -0
- package/plugin/dist/index.d.ts +447 -0
- package/plugin/dist/index.d.ts.map +1 -0
- package/plugin/dist/index.js +7334 -0
- package/plugin/dist/index.js.map +1 -0
- package/plugin/dist/observations-CorAAc1A.d.mts +192 -0
- package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
- package/plugin/dist/tool-registry-e710BvXq.mjs +3574 -0
- package/plugin/dist/tool-registry-e710BvXq.mjs.map +1 -0
- package/plugin/hooks/hooks.json +78 -0
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +17 -0
- package/plugin/scripts/README.md +65 -0
- package/plugin/scripts/bump-version.sh +42 -0
- package/plugin/scripts/dev-sync.sh +58 -0
- package/plugin/scripts/ensure-deps.sh +15 -0
- package/plugin/scripts/install.sh +139 -0
- package/plugin/scripts/local-install.sh +138 -0
- package/plugin/scripts/uninstall.sh +133 -0
- package/plugin/scripts/update.sh +39 -0
- package/plugin/scripts/verify-install.sh +87 -0
- package/plugin/skills/status/SKILL.md +6 -0
- package/plugin/ui/activity.js +197 -0
- package/plugin/ui/app.js +1612 -0
- package/plugin/ui/graph.js +2560 -0
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/help.js +932 -0
- package/plugin/ui/index.html +756 -0
- package/plugin/ui/settings.js +1414 -0
- package/plugin/ui/styles.css +3856 -0
- package/plugin/ui/timeline.js +652 -0
- package/plugin/ui/tools.js +826 -0
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laminark Settings tab — database statistics, config sections, and reset operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
var currentStats = null;
|
|
7
|
+
|
|
8
|
+
// Preset-to-multiplier mapping
|
|
9
|
+
var PRESET_MULTIPLIERS = { sensitive: 1.0, balanced: 1.5, relaxed: 2.5 };
|
|
10
|
+
|
|
11
|
+
function getProjectHash() {
|
|
12
|
+
return window.laminarkState.currentProject || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// =========================================================================
|
|
16
|
+
// Stats
|
|
17
|
+
// =========================================================================
|
|
18
|
+
|
|
19
|
+
async function fetchStats(projectHash) {
|
|
20
|
+
var params = new URLSearchParams();
|
|
21
|
+
if (projectHash) params.set('project', projectHash);
|
|
22
|
+
var url = '/api/admin/stats' + (params.toString() ? '?' + params.toString() : '');
|
|
23
|
+
try {
|
|
24
|
+
var res = await fetch(url);
|
|
25
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
26
|
+
return await res.json();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('[laminark] Failed to fetch stats:', err);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderStats(stats) {
|
|
34
|
+
currentStats = stats;
|
|
35
|
+
var grid = document.getElementById('db-stats-grid');
|
|
36
|
+
if (!grid || !stats) return;
|
|
37
|
+
|
|
38
|
+
var cards = [
|
|
39
|
+
{ label: 'Observations', value: stats.observations, id: 'stat-observations' },
|
|
40
|
+
{ label: 'Embeddings', value: stats.observationEmbeddings, id: 'stat-embeddings' },
|
|
41
|
+
{ label: 'Staleness Flags', value: stats.stalenessFlags || 0, id: 'stat-staleness' },
|
|
42
|
+
{ label: 'Graph Nodes', value: stats.graphNodes, id: 'stat-nodes' },
|
|
43
|
+
{ label: 'Graph Edges', value: stats.graphEdges, id: 'stat-edges' },
|
|
44
|
+
{ label: 'Sessions', value: stats.sessions, id: 'stat-sessions' },
|
|
45
|
+
{ label: 'Context Stashes', value: stats.contextStashes, id: 'stat-stashes' },
|
|
46
|
+
{ label: 'Shift Decisions', value: stats.shiftDecisions, id: 'stat-shifts' },
|
|
47
|
+
{ label: 'Notifications', value: stats.pendingNotifications || 0, id: 'stat-notifications' },
|
|
48
|
+
{ label: 'Projects', value: stats.projects, id: 'stat-projects' },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
grid.innerHTML = '';
|
|
52
|
+
cards.forEach(function (card) {
|
|
53
|
+
var el = document.createElement('div');
|
|
54
|
+
el.className = 'stat-card';
|
|
55
|
+
el.id = card.id;
|
|
56
|
+
|
|
57
|
+
var valueEl = document.createElement('div');
|
|
58
|
+
valueEl.className = 'stat-value';
|
|
59
|
+
valueEl.textContent = card.value.toLocaleString();
|
|
60
|
+
|
|
61
|
+
var labelEl = document.createElement('div');
|
|
62
|
+
labelEl.className = 'stat-label';
|
|
63
|
+
labelEl.textContent = card.label;
|
|
64
|
+
|
|
65
|
+
el.appendChild(valueEl);
|
|
66
|
+
el.appendChild(labelEl);
|
|
67
|
+
grid.appendChild(el);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =========================================================================
|
|
72
|
+
// Reset
|
|
73
|
+
// =========================================================================
|
|
74
|
+
|
|
75
|
+
async function resetData(type, scope) {
|
|
76
|
+
var projectHash = scope === 'current' ? getProjectHash() : undefined;
|
|
77
|
+
try {
|
|
78
|
+
var res = await fetch('/api/admin/reset', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ type: type, scope: scope, projectHash: projectHash }),
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
84
|
+
return await res.json();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('[laminark] Reset failed:', err);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getResetDescription(type) {
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'observations':
|
|
94
|
+
return 'This will permanently delete all observations, full-text search indexes, and vector embeddings.';
|
|
95
|
+
case 'graph':
|
|
96
|
+
return 'This will permanently delete all knowledge graph nodes and edges.';
|
|
97
|
+
case 'sessions':
|
|
98
|
+
return 'This will permanently delete all sessions, context stashes, threshold history, and shift decisions.';
|
|
99
|
+
case 'all':
|
|
100
|
+
return 'This will permanently delete ALL data: observations, graph, sessions, and intelligence data.';
|
|
101
|
+
default:
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAffectedCount(type) {
|
|
107
|
+
if (!currentStats) return 0;
|
|
108
|
+
var s = currentStats;
|
|
109
|
+
switch (type) {
|
|
110
|
+
case 'observations':
|
|
111
|
+
return s.observations + s.observationEmbeddings + (s.stalenessFlags || 0);
|
|
112
|
+
case 'graph':
|
|
113
|
+
return s.graphNodes + s.graphEdges;
|
|
114
|
+
case 'sessions':
|
|
115
|
+
return s.sessions + s.contextStashes + s.shiftDecisions + s.thresholdHistory + (s.pendingNotifications || 0);
|
|
116
|
+
case 'all':
|
|
117
|
+
return s.observations + s.observationEmbeddings + (s.stalenessFlags || 0) +
|
|
118
|
+
s.graphNodes + s.graphEdges +
|
|
119
|
+
s.sessions + s.contextStashes +
|
|
120
|
+
s.shiftDecisions + s.thresholdHistory + (s.pendingNotifications || 0) +
|
|
121
|
+
s.projects;
|
|
122
|
+
default:
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function showConfirmDialog(type) {
|
|
128
|
+
var overlay = document.getElementById('confirm-overlay');
|
|
129
|
+
if (!overlay) return;
|
|
130
|
+
|
|
131
|
+
var scope = getSelectedScope();
|
|
132
|
+
var count = getAffectedCount(type);
|
|
133
|
+
var projectName = '';
|
|
134
|
+
if (scope === 'current') {
|
|
135
|
+
var select = document.getElementById('project-selector');
|
|
136
|
+
if (select && select.selectedOptions && select.selectedOptions[0]) {
|
|
137
|
+
projectName = select.selectedOptions[0].textContent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
var scopeLabel = scope === 'current' ? 'project "' + (projectName || 'unknown') + '"' : 'ALL projects';
|
|
141
|
+
|
|
142
|
+
var title = document.getElementById('confirm-title');
|
|
143
|
+
var desc = document.getElementById('confirm-desc');
|
|
144
|
+
var countEl = document.getElementById('confirm-count');
|
|
145
|
+
var input = document.getElementById('confirm-input');
|
|
146
|
+
var confirmBtn = document.getElementById('confirm-btn');
|
|
147
|
+
|
|
148
|
+
if (title) title.textContent = 'Reset ' + type;
|
|
149
|
+
if (desc) desc.textContent = getResetDescription(type) + ' Scope: ' + scopeLabel + '.';
|
|
150
|
+
if (countEl) countEl.textContent = count.toLocaleString() + ' rows will be deleted';
|
|
151
|
+
if (input) {
|
|
152
|
+
input.value = '';
|
|
153
|
+
input.placeholder = 'Type DELETE to confirm';
|
|
154
|
+
}
|
|
155
|
+
if (confirmBtn) {
|
|
156
|
+
confirmBtn.disabled = true;
|
|
157
|
+
confirmBtn.onclick = async function () {
|
|
158
|
+
confirmBtn.disabled = true;
|
|
159
|
+
confirmBtn.textContent = 'Resetting...';
|
|
160
|
+
var result = await resetData(type, scope);
|
|
161
|
+
hideConfirmDialog();
|
|
162
|
+
if (result && result.ok) {
|
|
163
|
+
showSuccessMessage('Successfully reset ' + type + ' data.');
|
|
164
|
+
await refreshStats();
|
|
165
|
+
if (window.laminarkGraph && window.laminarkState.graphInitialized) {
|
|
166
|
+
window.laminarkGraph.loadGraphData();
|
|
167
|
+
}
|
|
168
|
+
if (window.laminarkTimeline && window.laminarkState.timelineInitialized) {
|
|
169
|
+
window.laminarkTimeline.loadTimelineData();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
confirmBtn.textContent = 'Reset';
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (input && confirmBtn) {
|
|
177
|
+
input.oninput = function () {
|
|
178
|
+
confirmBtn.disabled = input.value !== 'DELETE';
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
overlay.classList.remove('hidden');
|
|
183
|
+
if (input) input.focus();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function hideConfirmDialog() {
|
|
187
|
+
var overlay = document.getElementById('confirm-overlay');
|
|
188
|
+
if (overlay) overlay.classList.add('hidden');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function showSuccessMessage(text) {
|
|
192
|
+
var msg = document.getElementById('settings-success');
|
|
193
|
+
if (!msg) return;
|
|
194
|
+
msg.textContent = text;
|
|
195
|
+
msg.classList.remove('hidden');
|
|
196
|
+
setTimeout(function () {
|
|
197
|
+
msg.classList.add('hidden');
|
|
198
|
+
}, 4000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getSelectedScope() {
|
|
202
|
+
var radio = document.querySelector('input[name="reset-scope"]:checked');
|
|
203
|
+
return radio ? radio.value : 'current';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function refreshStats() {
|
|
207
|
+
var scope = getSelectedScope();
|
|
208
|
+
var projectHash = scope === 'current' ? getProjectHash() : null;
|
|
209
|
+
var stats = await fetchStats(projectHash);
|
|
210
|
+
if (stats) renderStats(stats);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =========================================================================
|
|
214
|
+
// Config helpers
|
|
215
|
+
// =========================================================================
|
|
216
|
+
|
|
217
|
+
function bindSlider(sliderId, valueId) {
|
|
218
|
+
var slider = document.getElementById(sliderId);
|
|
219
|
+
var label = document.getElementById(valueId);
|
|
220
|
+
if (!slider || !label) return;
|
|
221
|
+
slider.addEventListener('input', function () {
|
|
222
|
+
label.textContent = parseFloat(slider.value).toFixed(2);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function updateStatusBadge(badgeId, enabled) {
|
|
227
|
+
var badge = document.getElementById(badgeId);
|
|
228
|
+
if (!badge) return;
|
|
229
|
+
badge.textContent = enabled ? 'Enabled' : 'Disabled';
|
|
230
|
+
badge.className = 'config-section-status ' + (enabled ? 'enabled' : 'disabled');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setFieldsDisabled(containerId, disabled) {
|
|
234
|
+
var el = document.getElementById(containerId);
|
|
235
|
+
if (!el) return;
|
|
236
|
+
if (disabled) {
|
|
237
|
+
el.classList.add('disabled-fields');
|
|
238
|
+
} else {
|
|
239
|
+
el.classList.remove('disabled-fields');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// =========================================================================
|
|
244
|
+
// Topic Detection Config
|
|
245
|
+
// =========================================================================
|
|
246
|
+
|
|
247
|
+
function populateTopicDetection(config) {
|
|
248
|
+
var enabled = document.getElementById('td-enabled');
|
|
249
|
+
if (enabled) enabled.checked = config.enabled;
|
|
250
|
+
updateStatusBadge('td-status', config.enabled);
|
|
251
|
+
setFieldsDisabled('td-fields', !config.enabled);
|
|
252
|
+
|
|
253
|
+
// Preset radio
|
|
254
|
+
var presetBtns = document.querySelectorAll('#td-preset .config-radio');
|
|
255
|
+
presetBtns.forEach(function (btn) {
|
|
256
|
+
btn.classList.toggle('active', btn.getAttribute('data-value') === config.sensitivityPreset);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
var multiplier = document.getElementById('td-multiplier');
|
|
260
|
+
if (multiplier) multiplier.value = config.sensitivityMultiplier;
|
|
261
|
+
|
|
262
|
+
var manualEnabled = document.getElementById('td-manual-enabled');
|
|
263
|
+
var manualValue = document.getElementById('td-manual-value');
|
|
264
|
+
if (manualEnabled) manualEnabled.checked = config.manualThreshold !== null;
|
|
265
|
+
if (manualValue) {
|
|
266
|
+
manualValue.disabled = config.manualThreshold === null;
|
|
267
|
+
manualValue.value = config.manualThreshold !== null ? config.manualThreshold : 0.3;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
var ewma = document.getElementById('td-ewma');
|
|
271
|
+
var ewmaVal = document.getElementById('td-ewma-val');
|
|
272
|
+
if (ewma) ewma.value = config.ewmaAlpha;
|
|
273
|
+
if (ewmaVal) ewmaVal.textContent = config.ewmaAlpha.toFixed(2);
|
|
274
|
+
|
|
275
|
+
var boundsMin = document.getElementById('td-bounds-min');
|
|
276
|
+
var boundsMax = document.getElementById('td-bounds-max');
|
|
277
|
+
if (boundsMin) boundsMin.value = config.thresholdBounds.min;
|
|
278
|
+
if (boundsMax) boundsMax.value = config.thresholdBounds.max;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function gatherTopicDetection() {
|
|
282
|
+
var manualEnabled = document.getElementById('td-manual-enabled');
|
|
283
|
+
var manualValue = document.getElementById('td-manual-value');
|
|
284
|
+
var activePreset = document.querySelector('#td-preset .config-radio.active');
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
enabled: document.getElementById('td-enabled').checked,
|
|
288
|
+
sensitivityPreset: activePreset ? activePreset.getAttribute('data-value') : 'balanced',
|
|
289
|
+
sensitivityMultiplier: parseFloat(document.getElementById('td-multiplier').value) || 1.5,
|
|
290
|
+
manualThreshold: manualEnabled && manualEnabled.checked ? (parseFloat(manualValue.value) || 0.3) : null,
|
|
291
|
+
ewmaAlpha: parseFloat(document.getElementById('td-ewma').value) || 0.3,
|
|
292
|
+
thresholdBounds: {
|
|
293
|
+
min: parseFloat(document.getElementById('td-bounds-min').value) || 0.15,
|
|
294
|
+
max: parseFloat(document.getElementById('td-bounds-max').value) || 0.6,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function loadTopicDetectionConfig() {
|
|
300
|
+
try {
|
|
301
|
+
var res = await fetch('/api/admin/config/topic-detection');
|
|
302
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
303
|
+
var config = await res.json();
|
|
304
|
+
populateTopicDetection(config);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error('[laminark] Failed to load topic detection config:', err);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function saveTopicDetectionConfig(data) {
|
|
311
|
+
try {
|
|
312
|
+
var res = await fetch('/api/admin/config/topic-detection', {
|
|
313
|
+
method: 'PUT',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify(data),
|
|
316
|
+
});
|
|
317
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
318
|
+
var config = await res.json();
|
|
319
|
+
populateTopicDetection(config);
|
|
320
|
+
return config;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error('[laminark] Failed to save topic detection config:', err);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function initTopicDetection() {
|
|
328
|
+
// Enabled toggle
|
|
329
|
+
var enabled = document.getElementById('td-enabled');
|
|
330
|
+
if (enabled) {
|
|
331
|
+
enabled.addEventListener('change', function () {
|
|
332
|
+
updateStatusBadge('td-status', enabled.checked);
|
|
333
|
+
setFieldsDisabled('td-fields', !enabled.checked);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Preset buttons
|
|
338
|
+
var presetBtns = document.querySelectorAll('#td-preset .config-radio');
|
|
339
|
+
presetBtns.forEach(function (btn) {
|
|
340
|
+
btn.addEventListener('click', function () {
|
|
341
|
+
presetBtns.forEach(function (b) { b.classList.remove('active'); });
|
|
342
|
+
btn.classList.add('active');
|
|
343
|
+
var preset = btn.getAttribute('data-value');
|
|
344
|
+
var multiplier = document.getElementById('td-multiplier');
|
|
345
|
+
if (multiplier && PRESET_MULTIPLIERS[preset] !== undefined) {
|
|
346
|
+
multiplier.value = PRESET_MULTIPLIERS[preset];
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Manual threshold toggle
|
|
352
|
+
var manualEnabled = document.getElementById('td-manual-enabled');
|
|
353
|
+
var manualValue = document.getElementById('td-manual-value');
|
|
354
|
+
if (manualEnabled && manualValue) {
|
|
355
|
+
manualEnabled.addEventListener('change', function () {
|
|
356
|
+
manualValue.disabled = !manualEnabled.checked;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// EWMA slider
|
|
361
|
+
bindSlider('td-ewma', 'td-ewma-val');
|
|
362
|
+
|
|
363
|
+
// Save button
|
|
364
|
+
var saveBtn = document.getElementById('td-save');
|
|
365
|
+
if (saveBtn) {
|
|
366
|
+
saveBtn.addEventListener('click', async function () {
|
|
367
|
+
var data = gatherTopicDetection();
|
|
368
|
+
var result = await saveTopicDetectionConfig(data);
|
|
369
|
+
if (result) showSuccessMessage('Topic detection settings saved.');
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Reset to defaults
|
|
374
|
+
var defaultsBtn = document.getElementById('td-defaults');
|
|
375
|
+
if (defaultsBtn) {
|
|
376
|
+
var tdResetTimer = null;
|
|
377
|
+
defaultsBtn.addEventListener('click', async function () {
|
|
378
|
+
if (defaultsBtn.classList.contains('confirming')) {
|
|
379
|
+
clearTimeout(tdResetTimer);
|
|
380
|
+
defaultsBtn.classList.remove('confirming');
|
|
381
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
382
|
+
var result = await saveTopicDetectionConfig({ __reset: true });
|
|
383
|
+
if (result) showSuccessMessage('Topic detection reset to defaults.');
|
|
384
|
+
} else {
|
|
385
|
+
defaultsBtn.classList.add('confirming');
|
|
386
|
+
defaultsBtn.textContent = 'Confirm?';
|
|
387
|
+
tdResetTimer = setTimeout(function () {
|
|
388
|
+
defaultsBtn.classList.remove('confirming');
|
|
389
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
390
|
+
}, 3000);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
loadTopicDetectionConfig();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// =========================================================================
|
|
399
|
+
// Graph Extraction Config
|
|
400
|
+
// =========================================================================
|
|
401
|
+
|
|
402
|
+
function populateGraphExtraction(config) {
|
|
403
|
+
var enabled = document.getElementById('ge-enabled');
|
|
404
|
+
if (enabled) enabled.checked = config.enabled;
|
|
405
|
+
updateStatusBadge('ge-status', config.enabled);
|
|
406
|
+
setFieldsDisabled('ge-fields', !config.enabled);
|
|
407
|
+
|
|
408
|
+
// Temporal decay
|
|
409
|
+
setVal('ge-halflife', config.temporalDecay.halfLifeDays);
|
|
410
|
+
setSlider('ge-minfloor', 'ge-minfloor-val', config.temporalDecay.minFloor);
|
|
411
|
+
setSlider('ge-delthresh', 'ge-delthresh-val', config.temporalDecay.deletionThreshold);
|
|
412
|
+
setVal('ge-maxage', config.temporalDecay.maxAgeDays);
|
|
413
|
+
|
|
414
|
+
// Fuzzy dedup
|
|
415
|
+
setVal('ge-levenshtein', config.fuzzyDedup.maxLevenshteinDistance);
|
|
416
|
+
setSlider('ge-jaccard', 'ge-jaccard-val', config.fuzzyDedup.jaccardThreshold);
|
|
417
|
+
|
|
418
|
+
// Quality gate
|
|
419
|
+
setVal('ge-maxfiles', config.qualityGate.maxFilesPerObservation);
|
|
420
|
+
setSlider('ge-filenonchange', 'ge-filenonchange-val', config.qualityGate.fileNonChangeMultiplier);
|
|
421
|
+
setVal('ge-minname', config.qualityGate.minNameLength);
|
|
422
|
+
setVal('ge-maxname', config.qualityGate.maxNameLength);
|
|
423
|
+
|
|
424
|
+
// Type confidence thresholds
|
|
425
|
+
var thresholds = config.qualityGate.typeConfidenceThresholds || {};
|
|
426
|
+
var grid = document.getElementById('ge-thresholds');
|
|
427
|
+
if (grid) {
|
|
428
|
+
var rows = grid.querySelectorAll('.config-threshold-row');
|
|
429
|
+
rows.forEach(function (row) {
|
|
430
|
+
var slider = row.querySelector('.config-slider');
|
|
431
|
+
var label = row.querySelector('.config-slider-value');
|
|
432
|
+
var type = slider.getAttribute('data-type');
|
|
433
|
+
if (type && thresholds[type] !== undefined) {
|
|
434
|
+
slider.value = thresholds[type];
|
|
435
|
+
if (label) label.textContent = thresholds[type].toFixed(2);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Relationship detector
|
|
441
|
+
setSlider('ge-minedge', 'ge-minedge-val', config.relationshipDetector.minEdgeConfidence);
|
|
442
|
+
|
|
443
|
+
// Signal classifier
|
|
444
|
+
setVal('ge-mincontent', config.signalClassifier.minContentLength);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function setVal(id, value) {
|
|
448
|
+
var el = document.getElementById(id);
|
|
449
|
+
if (el) el.value = value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function setSlider(sliderId, labelId, value) {
|
|
453
|
+
var slider = document.getElementById(sliderId);
|
|
454
|
+
var label = document.getElementById(labelId);
|
|
455
|
+
if (slider) slider.value = value;
|
|
456
|
+
if (label) label.textContent = parseFloat(value).toFixed(2);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function gatherGraphExtraction() {
|
|
460
|
+
var thresholds = {};
|
|
461
|
+
var grid = document.getElementById('ge-thresholds');
|
|
462
|
+
if (grid) {
|
|
463
|
+
var sliders = grid.querySelectorAll('.config-slider');
|
|
464
|
+
sliders.forEach(function (slider) {
|
|
465
|
+
var type = slider.getAttribute('data-type');
|
|
466
|
+
if (type) thresholds[type] = parseFloat(slider.value);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
enabled: document.getElementById('ge-enabled').checked,
|
|
472
|
+
temporalDecay: {
|
|
473
|
+
halfLifeDays: parseInt(document.getElementById('ge-halflife').value, 10) || 30,
|
|
474
|
+
minFloor: parseFloat(document.getElementById('ge-minfloor').value) || 0.05,
|
|
475
|
+
deletionThreshold: parseFloat(document.getElementById('ge-delthresh').value) || 0.08,
|
|
476
|
+
maxAgeDays: parseInt(document.getElementById('ge-maxage').value, 10) || 180,
|
|
477
|
+
},
|
|
478
|
+
fuzzyDedup: {
|
|
479
|
+
maxLevenshteinDistance: parseInt(document.getElementById('ge-levenshtein').value, 10) || 2,
|
|
480
|
+
jaccardThreshold: parseFloat(document.getElementById('ge-jaccard').value) || 0.7,
|
|
481
|
+
},
|
|
482
|
+
qualityGate: {
|
|
483
|
+
maxFilesPerObservation: parseInt(document.getElementById('ge-maxfiles').value, 10) || 5,
|
|
484
|
+
typeConfidenceThresholds: thresholds,
|
|
485
|
+
fileNonChangeMultiplier: parseFloat(document.getElementById('ge-filenonchange').value) || 0.74,
|
|
486
|
+
minNameLength: parseInt(document.getElementById('ge-minname').value, 10) || 3,
|
|
487
|
+
maxNameLength: parseInt(document.getElementById('ge-maxname').value, 10) || 200,
|
|
488
|
+
},
|
|
489
|
+
relationshipDetector: {
|
|
490
|
+
minEdgeConfidence: parseFloat(document.getElementById('ge-minedge').value) || 0.45,
|
|
491
|
+
},
|
|
492
|
+
signalClassifier: {
|
|
493
|
+
minContentLength: parseInt(document.getElementById('ge-mincontent').value, 10) || 30,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function loadGraphExtractionConfig() {
|
|
499
|
+
try {
|
|
500
|
+
var res = await fetch('/api/admin/config/graph-extraction');
|
|
501
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
502
|
+
var config = await res.json();
|
|
503
|
+
populateGraphExtraction(config);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error('[laminark] Failed to load graph extraction config:', err);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function saveGraphExtractionConfig(data) {
|
|
510
|
+
try {
|
|
511
|
+
var res = await fetch('/api/admin/config/graph-extraction', {
|
|
512
|
+
method: 'PUT',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify(data),
|
|
515
|
+
});
|
|
516
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
517
|
+
var config = await res.json();
|
|
518
|
+
populateGraphExtraction(config);
|
|
519
|
+
return config;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error('[laminark] Failed to save graph extraction config:', err);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function initGraphExtraction() {
|
|
527
|
+
// Enabled toggle
|
|
528
|
+
var enabled = document.getElementById('ge-enabled');
|
|
529
|
+
if (enabled) {
|
|
530
|
+
enabled.addEventListener('change', function () {
|
|
531
|
+
updateStatusBadge('ge-status', enabled.checked);
|
|
532
|
+
setFieldsDisabled('ge-fields', !enabled.checked);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Bind all sliders
|
|
537
|
+
bindSlider('ge-minfloor', 'ge-minfloor-val');
|
|
538
|
+
bindSlider('ge-delthresh', 'ge-delthresh-val');
|
|
539
|
+
bindSlider('ge-jaccard', 'ge-jaccard-val');
|
|
540
|
+
bindSlider('ge-filenonchange', 'ge-filenonchange-val');
|
|
541
|
+
bindSlider('ge-minedge', 'ge-minedge-val');
|
|
542
|
+
|
|
543
|
+
// Threshold grid sliders
|
|
544
|
+
var grid = document.getElementById('ge-thresholds');
|
|
545
|
+
if (grid) {
|
|
546
|
+
var rows = grid.querySelectorAll('.config-threshold-row');
|
|
547
|
+
rows.forEach(function (row) {
|
|
548
|
+
var slider = row.querySelector('.config-slider');
|
|
549
|
+
var label = row.querySelector('.config-slider-value');
|
|
550
|
+
if (slider && label) {
|
|
551
|
+
slider.addEventListener('input', function () {
|
|
552
|
+
label.textContent = parseFloat(slider.value).toFixed(2);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Save button
|
|
559
|
+
var saveBtn = document.getElementById('ge-save');
|
|
560
|
+
if (saveBtn) {
|
|
561
|
+
saveBtn.addEventListener('click', async function () {
|
|
562
|
+
var data = gatherGraphExtraction();
|
|
563
|
+
var result = await saveGraphExtractionConfig(data);
|
|
564
|
+
if (result) showSuccessMessage('Graph extraction settings saved.');
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Reset to defaults
|
|
569
|
+
var defaultsBtn = document.getElementById('ge-defaults');
|
|
570
|
+
if (defaultsBtn) {
|
|
571
|
+
var geResetTimer = null;
|
|
572
|
+
defaultsBtn.addEventListener('click', async function () {
|
|
573
|
+
if (defaultsBtn.classList.contains('confirming')) {
|
|
574
|
+
clearTimeout(geResetTimer);
|
|
575
|
+
defaultsBtn.classList.remove('confirming');
|
|
576
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
577
|
+
var result = await saveGraphExtractionConfig({ __reset: true });
|
|
578
|
+
if (result) showSuccessMessage('Graph extraction reset to defaults.');
|
|
579
|
+
} else {
|
|
580
|
+
defaultsBtn.classList.add('confirming');
|
|
581
|
+
defaultsBtn.textContent = 'Confirm?';
|
|
582
|
+
geResetTimer = setTimeout(function () {
|
|
583
|
+
defaultsBtn.classList.remove('confirming');
|
|
584
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
585
|
+
}, 3000);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
loadGraphExtractionConfig();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// =========================================================================
|
|
594
|
+
// Cross-Project Access Config
|
|
595
|
+
// =========================================================================
|
|
596
|
+
|
|
597
|
+
var caReadable = []; // current readable project hashes
|
|
598
|
+
|
|
599
|
+
async function loadCrossAccessConfig() {
|
|
600
|
+
var project = getProjectHash();
|
|
601
|
+
if (!project) return;
|
|
602
|
+
try {
|
|
603
|
+
var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project));
|
|
604
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
605
|
+
var config = await res.json();
|
|
606
|
+
caReadable = config.readableProjects || [];
|
|
607
|
+
populateCrossAccessLists();
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error('[laminark] Failed to load cross-access config:', err);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getAllProjects() {
|
|
614
|
+
var select = document.getElementById('project-selector');
|
|
615
|
+
if (!select) return [];
|
|
616
|
+
var projects = [];
|
|
617
|
+
for (var i = 0; i < select.options.length; i++) {
|
|
618
|
+
projects.push({
|
|
619
|
+
hash: select.options[i].value,
|
|
620
|
+
name: select.options[i].textContent,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return projects;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function populateCrossAccessLists() {
|
|
627
|
+
var currentProject = getProjectHash();
|
|
628
|
+
var allProjects = getAllProjects();
|
|
629
|
+
var availableList = document.getElementById('ca-available');
|
|
630
|
+
var readableList = document.getElementById('ca-readable');
|
|
631
|
+
if (!availableList || !readableList) return;
|
|
632
|
+
|
|
633
|
+
availableList.innerHTML = '';
|
|
634
|
+
readableList.innerHTML = '';
|
|
635
|
+
|
|
636
|
+
var readableSet = new Set(caReadable);
|
|
637
|
+
|
|
638
|
+
allProjects.forEach(function (p) {
|
|
639
|
+
if (p.hash === currentProject) return; // skip self
|
|
640
|
+
if (readableSet.has(p.hash)) {
|
|
641
|
+
readableList.appendChild(createCrossAccessItem(p, 'remove'));
|
|
642
|
+
} else {
|
|
643
|
+
availableList.appendChild(createCrossAccessItem(p, 'add'));
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Update status badge
|
|
648
|
+
var badge = document.getElementById('ca-status');
|
|
649
|
+
if (badge) {
|
|
650
|
+
var count = caReadable.length;
|
|
651
|
+
badge.textContent = count + ' project' + (count !== 1 ? 's' : '');
|
|
652
|
+
badge.className = 'config-section-status ' + (count > 0 ? 'enabled' : 'disabled');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function createCrossAccessItem(project, action) {
|
|
657
|
+
var li = document.createElement('li');
|
|
658
|
+
li.className = 'cross-access-item';
|
|
659
|
+
li.setAttribute('data-hash', project.hash);
|
|
660
|
+
|
|
661
|
+
var nameSpan = document.createElement('span');
|
|
662
|
+
nameSpan.className = 'cross-access-item-name';
|
|
663
|
+
nameSpan.textContent = project.name;
|
|
664
|
+
nameSpan.title = project.hash;
|
|
665
|
+
li.appendChild(nameSpan);
|
|
666
|
+
|
|
667
|
+
var btn = document.createElement('button');
|
|
668
|
+
if (action === 'add') {
|
|
669
|
+
btn.className = 'cross-access-add-btn';
|
|
670
|
+
btn.innerHTML = '▶'; // right arrow
|
|
671
|
+
btn.title = 'Add to readable projects';
|
|
672
|
+
btn.addEventListener('click', function () {
|
|
673
|
+
caReadable.push(project.hash);
|
|
674
|
+
populateCrossAccessLists();
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
btn.className = 'cross-access-remove-btn';
|
|
678
|
+
btn.innerHTML = '×';
|
|
679
|
+
btn.title = 'Remove from readable projects';
|
|
680
|
+
btn.addEventListener('click', function () {
|
|
681
|
+
caReadable = caReadable.filter(function (h) { return h !== project.hash; });
|
|
682
|
+
populateCrossAccessLists();
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
li.appendChild(btn);
|
|
686
|
+
|
|
687
|
+
return li;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function saveCrossAccessConfig() {
|
|
691
|
+
var project = getProjectHash();
|
|
692
|
+
if (!project) return null;
|
|
693
|
+
try {
|
|
694
|
+
var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project), {
|
|
695
|
+
method: 'PUT',
|
|
696
|
+
headers: { 'Content-Type': 'application/json' },
|
|
697
|
+
body: JSON.stringify({ readableProjects: caReadable }),
|
|
698
|
+
});
|
|
699
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
700
|
+
var config = await res.json();
|
|
701
|
+
caReadable = config.readableProjects || [];
|
|
702
|
+
populateCrossAccessLists();
|
|
703
|
+
return config;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.error('[laminark] Failed to save cross-access config:', err);
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function initCrossAccess() {
|
|
711
|
+
// Save button
|
|
712
|
+
var saveBtn = document.getElementById('ca-save');
|
|
713
|
+
if (saveBtn) {
|
|
714
|
+
saveBtn.addEventListener('click', async function () {
|
|
715
|
+
var result = await saveCrossAccessConfig();
|
|
716
|
+
if (result) showSuccessMessage('Cross-project access settings saved.');
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Reset to defaults
|
|
721
|
+
var defaultsBtn = document.getElementById('ca-defaults');
|
|
722
|
+
if (defaultsBtn) {
|
|
723
|
+
var caResetTimer = null;
|
|
724
|
+
defaultsBtn.addEventListener('click', async function () {
|
|
725
|
+
if (defaultsBtn.classList.contains('confirming')) {
|
|
726
|
+
clearTimeout(caResetTimer);
|
|
727
|
+
defaultsBtn.classList.remove('confirming');
|
|
728
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
729
|
+
var project = getProjectHash();
|
|
730
|
+
if (!project) return;
|
|
731
|
+
try {
|
|
732
|
+
var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project), {
|
|
733
|
+
method: 'PUT',
|
|
734
|
+
headers: { 'Content-Type': 'application/json' },
|
|
735
|
+
body: JSON.stringify({ __reset: true }),
|
|
736
|
+
});
|
|
737
|
+
if (res.ok) {
|
|
738
|
+
var config = await res.json();
|
|
739
|
+
caReadable = config.readableProjects || [];
|
|
740
|
+
populateCrossAccessLists();
|
|
741
|
+
showSuccessMessage('Cross-project access reset to defaults.');
|
|
742
|
+
}
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.error('[laminark] Failed to reset cross-access config:', err);
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
defaultsBtn.classList.add('confirming');
|
|
748
|
+
defaultsBtn.textContent = 'Confirm?';
|
|
749
|
+
caResetTimer = setTimeout(function () {
|
|
750
|
+
defaultsBtn.classList.remove('confirming');
|
|
751
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
752
|
+
}, 3000);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
loadCrossAccessConfig();
|
|
758
|
+
|
|
759
|
+
// Reload when project changes
|
|
760
|
+
var projectSelector = document.getElementById('project-selector');
|
|
761
|
+
if (projectSelector) {
|
|
762
|
+
projectSelector.addEventListener('change', function () {
|
|
763
|
+
loadCrossAccessConfig();
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// =========================================================================
|
|
769
|
+
// Database Hygiene
|
|
770
|
+
// =========================================================================
|
|
771
|
+
|
|
772
|
+
var lastHygieneReport = null;
|
|
773
|
+
|
|
774
|
+
function getSelectedTier() {
|
|
775
|
+
var activeBtn = document.querySelector('#hy-tier .config-radio.active');
|
|
776
|
+
return activeBtn ? activeBtn.getAttribute('data-value') : 'high';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function runHygieneScan() {
|
|
780
|
+
var project = getProjectHash();
|
|
781
|
+
if (!project) return;
|
|
782
|
+
|
|
783
|
+
var tier = getSelectedTier();
|
|
784
|
+
var params = new URLSearchParams({ project: project, tier: tier, limit: '100' });
|
|
785
|
+
|
|
786
|
+
var badge = document.getElementById('hy-status');
|
|
787
|
+
if (badge) { badge.textContent = 'Scanning...'; badge.className = 'config-section-status disabled'; }
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
var res = await fetch('/api/admin/hygiene?' + params.toString());
|
|
791
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
792
|
+
var report = await res.json();
|
|
793
|
+
lastHygieneReport = report;
|
|
794
|
+
renderHygieneReport(report, tier);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error('[laminark] Hygiene scan failed:', err);
|
|
797
|
+
if (badge) { badge.textContent = 'Error'; badge.className = 'config-section-status disabled'; }
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderHygieneReport(report, tier) {
|
|
802
|
+
var badge = document.getElementById('hy-status');
|
|
803
|
+
var total = report.summary.high + report.summary.medium;
|
|
804
|
+
if (badge) {
|
|
805
|
+
badge.textContent = total + ' candidate' + (total !== 1 ? 's' : '');
|
|
806
|
+
badge.className = 'config-section-status ' + (total > 0 ? 'enabled' : 'disabled');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
var results = document.getElementById('hy-results');
|
|
810
|
+
if (results) results.classList.remove('hidden');
|
|
811
|
+
|
|
812
|
+
// Summary
|
|
813
|
+
var summary = document.getElementById('hy-summary');
|
|
814
|
+
if (summary) {
|
|
815
|
+
summary.innerHTML =
|
|
816
|
+
'<div class="hygiene-summary-row">' +
|
|
817
|
+
'<span class="hygiene-stat"><strong>' + report.totalObservations.toLocaleString() + '</strong> analyzed</span>' +
|
|
818
|
+
'<span class="hygiene-stat hy-high"><strong>' + report.summary.high + '</strong> high</span>' +
|
|
819
|
+
'<span class="hygiene-stat hy-medium"><strong>' + report.summary.medium + '</strong> medium</span>' +
|
|
820
|
+
'<span class="hygiene-stat"><strong>' + report.summary.orphanNodeCount + '</strong> orphan nodes</span>' +
|
|
821
|
+
'</div>';
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Table
|
|
825
|
+
var tbody = document.getElementById('hy-table-body');
|
|
826
|
+
if (!tbody) return;
|
|
827
|
+
tbody.innerHTML = '';
|
|
828
|
+
|
|
829
|
+
if (report.candidates.length === 0) {
|
|
830
|
+
var tr = document.createElement('tr');
|
|
831
|
+
tr.innerHTML = '<td colspan="6" style="text-align:center;opacity:0.5;">No candidates at this tier</td>';
|
|
832
|
+
tbody.appendChild(tr);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
report.candidates.forEach(function (c) {
|
|
837
|
+
var signals = [];
|
|
838
|
+
if (c.signals.orphaned) signals.push('orphaned');
|
|
839
|
+
if (c.signals.islandNode) signals.push('island');
|
|
840
|
+
if (c.signals.noiseClassified) signals.push('noise');
|
|
841
|
+
if (c.signals.shortContent) signals.push('short');
|
|
842
|
+
if (c.signals.autoCaptured) signals.push('auto');
|
|
843
|
+
if (c.signals.stale) signals.push('stale');
|
|
844
|
+
|
|
845
|
+
var tr = document.createElement('tr');
|
|
846
|
+
tr.className = c.tier === 'high' ? 'hy-row-high' : c.tier === 'medium' ? 'hy-row-medium' : '';
|
|
847
|
+
tr.innerHTML =
|
|
848
|
+
'<td class="mono">' + c.shortId + '</td>' +
|
|
849
|
+
'<td>' + c.kind + '</td>' +
|
|
850
|
+
'<td>' + c.source + '</td>' +
|
|
851
|
+
'<td>' + c.confidence.toFixed(2) + '</td>' +
|
|
852
|
+
'<td class="hygiene-signals">' + signals.map(function (s) { return '<span class="hy-signal">' + s + '</span>'; }).join(' ') + '</td>' +
|
|
853
|
+
'<td class="hygiene-preview">' + escapeHtml(c.contentPreview) + '</td>';
|
|
854
|
+
tbody.appendChild(tr);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function escapeHtml(text) {
|
|
859
|
+
var div = document.createElement('div');
|
|
860
|
+
div.textContent = text;
|
|
861
|
+
return div.innerHTML;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function runHygienePurge() {
|
|
865
|
+
var project = getProjectHash();
|
|
866
|
+
if (!project) return;
|
|
867
|
+
|
|
868
|
+
var tier = getSelectedTier();
|
|
869
|
+
var purgeBtn = document.getElementById('hy-purge');
|
|
870
|
+
|
|
871
|
+
if (purgeBtn && !purgeBtn.classList.contains('confirming')) {
|
|
872
|
+
purgeBtn.classList.add('confirming');
|
|
873
|
+
purgeBtn.textContent = 'Confirm Purge?';
|
|
874
|
+
setTimeout(function () {
|
|
875
|
+
if (purgeBtn.classList.contains('confirming')) {
|
|
876
|
+
purgeBtn.classList.remove('confirming');
|
|
877
|
+
purgeBtn.textContent = 'Purge Selected Tier';
|
|
878
|
+
}
|
|
879
|
+
}, 3000);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (purgeBtn) {
|
|
884
|
+
purgeBtn.classList.remove('confirming');
|
|
885
|
+
purgeBtn.textContent = 'Purging...';
|
|
886
|
+
purgeBtn.disabled = true;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
var res = await fetch('/api/admin/hygiene/purge', {
|
|
891
|
+
method: 'POST',
|
|
892
|
+
headers: { 'Content-Type': 'application/json' },
|
|
893
|
+
body: JSON.stringify({ tier: tier }),
|
|
894
|
+
});
|
|
895
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
896
|
+
var result = await res.json();
|
|
897
|
+
|
|
898
|
+
showSuccessMessage(
|
|
899
|
+
'Purged ' + result.observationsPurged + ' observations, ' +
|
|
900
|
+
result.orphanNodesRemoved + ' orphan nodes removed.'
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Refresh stats and re-scan
|
|
904
|
+
await refreshStats();
|
|
905
|
+
await runHygieneScan();
|
|
906
|
+
} catch (err) {
|
|
907
|
+
console.error('[laminark] Hygiene purge failed:', err);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (purgeBtn) {
|
|
911
|
+
purgeBtn.disabled = false;
|
|
912
|
+
purgeBtn.textContent = 'Purge Selected Tier';
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function initHygiene() {
|
|
917
|
+
// Tier radio buttons
|
|
918
|
+
var tierBtns = document.querySelectorAll('#hy-tier .config-radio');
|
|
919
|
+
tierBtns.forEach(function (btn) {
|
|
920
|
+
btn.addEventListener('click', function () {
|
|
921
|
+
tierBtns.forEach(function (b) { b.classList.remove('active'); });
|
|
922
|
+
btn.classList.add('active');
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// Scan button
|
|
927
|
+
var scanBtn = document.getElementById('hy-scan');
|
|
928
|
+
if (scanBtn) {
|
|
929
|
+
scanBtn.addEventListener('click', runHygieneScan);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Purge button
|
|
933
|
+
var purgeBtn = document.getElementById('hy-purge');
|
|
934
|
+
if (purgeBtn) {
|
|
935
|
+
purgeBtn.addEventListener('click', runHygienePurge);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// =========================================================================
|
|
940
|
+
// Hygiene Config
|
|
941
|
+
// =========================================================================
|
|
942
|
+
|
|
943
|
+
var HC_WEIGHT_KEYS = ['orphaned', 'islandNode', 'noiseClassified', 'shortContent', 'autoCaptured', 'stale'];
|
|
944
|
+
|
|
945
|
+
function populateHygieneConfig(config) {
|
|
946
|
+
HC_WEIGHT_KEYS.forEach(function (key) {
|
|
947
|
+
var slider = document.getElementById('hc-w-' + key);
|
|
948
|
+
var label = document.getElementById('hc-w-' + key + '-val');
|
|
949
|
+
if (slider) slider.value = config.signalWeights[key];
|
|
950
|
+
if (label) label.textContent = config.signalWeights[key].toFixed(2);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
setSlider('hc-t-high', 'hc-t-high-val', config.tierThresholds.high);
|
|
954
|
+
setSlider('hc-t-medium', 'hc-t-medium-val', config.tierThresholds.medium);
|
|
955
|
+
setVal('hc-short-threshold', config.shortContentThreshold);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function gatherHygieneConfig() {
|
|
959
|
+
var weights = {};
|
|
960
|
+
HC_WEIGHT_KEYS.forEach(function (key) {
|
|
961
|
+
var slider = document.getElementById('hc-w-' + key);
|
|
962
|
+
weights[key] = slider ? parseFloat(slider.value) : 0;
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
signalWeights: weights,
|
|
967
|
+
tierThresholds: {
|
|
968
|
+
high: parseFloat(document.getElementById('hc-t-high').value) || 0.70,
|
|
969
|
+
medium: parseFloat(document.getElementById('hc-t-medium').value) || 0.50,
|
|
970
|
+
},
|
|
971
|
+
shortContentThreshold: parseInt(document.getElementById('hc-short-threshold').value, 10) || 50,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function loadHygieneConfig() {
|
|
976
|
+
try {
|
|
977
|
+
var res = await fetch('/api/admin/config/hygiene');
|
|
978
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
979
|
+
var config = await res.json();
|
|
980
|
+
populateHygieneConfig(config);
|
|
981
|
+
} catch (err) {
|
|
982
|
+
console.error('[laminark] Failed to load hygiene config:', err);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function saveHygieneConfig(data) {
|
|
987
|
+
try {
|
|
988
|
+
var res = await fetch('/api/admin/config/hygiene', {
|
|
989
|
+
method: 'PUT',
|
|
990
|
+
headers: { 'Content-Type': 'application/json' },
|
|
991
|
+
body: JSON.stringify(data),
|
|
992
|
+
});
|
|
993
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
994
|
+
var config = await res.json();
|
|
995
|
+
populateHygieneConfig(config);
|
|
996
|
+
return config;
|
|
997
|
+
} catch (err) {
|
|
998
|
+
console.error('[laminark] Failed to save hygiene config:', err);
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function runFindAnalysis() {
|
|
1004
|
+
var project = getProjectHash();
|
|
1005
|
+
if (!project) return;
|
|
1006
|
+
|
|
1007
|
+
var findBtn = document.getElementById('hc-find');
|
|
1008
|
+
if (findBtn) { findBtn.textContent = 'Analyzing...'; findBtn.disabled = true; }
|
|
1009
|
+
|
|
1010
|
+
try {
|
|
1011
|
+
var res = await fetch('/api/admin/hygiene/find?project=' + encodeURIComponent(project));
|
|
1012
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1013
|
+
var report = await res.json();
|
|
1014
|
+
renderFindResults(report);
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
console.error('[laminark] Find analysis failed:', err);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (findBtn) { findBtn.textContent = 'Find'; findBtn.disabled = false; }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function renderFindResults(report) {
|
|
1023
|
+
var container = document.getElementById('hc-find-results');
|
|
1024
|
+
if (container) container.classList.remove('hidden');
|
|
1025
|
+
|
|
1026
|
+
// Summary
|
|
1027
|
+
var summary = document.getElementById('hc-find-summary');
|
|
1028
|
+
if (summary) {
|
|
1029
|
+
var signals = report.bySignal;
|
|
1030
|
+
summary.innerHTML =
|
|
1031
|
+
'<div class="hygiene-summary-row">' +
|
|
1032
|
+
'<span class="hygiene-stat"><strong>' + report.total.toLocaleString() + '</strong> total</span>' +
|
|
1033
|
+
'<span class="hygiene-stat"><strong>' + signals.orphaned + '</strong> orphaned</span>' +
|
|
1034
|
+
'<span class="hygiene-stat"><strong>' + signals.islandNode + '</strong> island</span>' +
|
|
1035
|
+
'<span class="hygiene-stat"><strong>' + signals.noiseClassified + '</strong> noise</span>' +
|
|
1036
|
+
'<span class="hygiene-stat"><strong>' + signals.shortContent + '</strong> short</span>' +
|
|
1037
|
+
'<span class="hygiene-stat"><strong>' + signals.autoCaptured + '</strong> auto</span>' +
|
|
1038
|
+
'<span class="hygiene-stat"><strong>' + signals.stale + '</strong> stale</span>' +
|
|
1039
|
+
'</div>';
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Histogram
|
|
1043
|
+
var histogram = document.getElementById('hc-find-histogram');
|
|
1044
|
+
if (histogram) {
|
|
1045
|
+
var maxCount = 0;
|
|
1046
|
+
report.distribution.forEach(function (d) { if (d.count > maxCount) maxCount = d.count; });
|
|
1047
|
+
|
|
1048
|
+
var html = '<div class="hygiene-histogram">';
|
|
1049
|
+
report.distribution.forEach(function (d) {
|
|
1050
|
+
var pct = maxCount > 0 ? (d.count / maxCount * 100) : 0;
|
|
1051
|
+
html += '<div class="hygiene-histogram-bar-wrap">' +
|
|
1052
|
+
'<div class="hygiene-histogram-bar" style="height:' + Math.max(pct, 2) + '%;" title="' + d.range + ': ' + d.count + '"></div>' +
|
|
1053
|
+
'<div class="hygiene-histogram-label">' + d.range.split('-')[0] + '</div>' +
|
|
1054
|
+
'</div>';
|
|
1055
|
+
});
|
|
1056
|
+
html += '</div>';
|
|
1057
|
+
histogram.innerHTML = html;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Island nodes
|
|
1061
|
+
var islands = document.getElementById('hc-find-islands');
|
|
1062
|
+
if (islands && report.islandNodes) {
|
|
1063
|
+
var isl = report.islandNodes;
|
|
1064
|
+
var cap = isl.capturedAtCurrentThresholds;
|
|
1065
|
+
islands.innerHTML =
|
|
1066
|
+
'<div class="hygiene-summary-row">' +
|
|
1067
|
+
'<span class="hygiene-stat"><strong>' + isl.total + '</strong> island obs</span>' +
|
|
1068
|
+
'<span class="hygiene-stat">conf: ' + isl.minConfidence.toFixed(2) + ' – ' + isl.maxConfidence.toFixed(2) + '</span>' +
|
|
1069
|
+
'<span class="hygiene-stat">median: <strong>' + isl.medianConfidence.toFixed(2) + '</strong></span>' +
|
|
1070
|
+
'<span class="hygiene-stat hy-high">high: <strong>' + cap.high + '</strong></span>' +
|
|
1071
|
+
'<span class="hygiene-stat hy-medium">medium+: <strong>' + cap.medium + '</strong></span>' +
|
|
1072
|
+
'<span class="hygiene-stat">all: <strong>' + cap.all + '</strong></span>' +
|
|
1073
|
+
'</div>';
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function initHygieneConfig() {
|
|
1078
|
+
// Bind sliders
|
|
1079
|
+
HC_WEIGHT_KEYS.forEach(function (key) {
|
|
1080
|
+
bindSlider('hc-w-' + key, 'hc-w-' + key + '-val');
|
|
1081
|
+
});
|
|
1082
|
+
bindSlider('hc-t-high', 'hc-t-high-val');
|
|
1083
|
+
bindSlider('hc-t-medium', 'hc-t-medium-val');
|
|
1084
|
+
|
|
1085
|
+
// Save button
|
|
1086
|
+
var saveBtn = document.getElementById('hc-save');
|
|
1087
|
+
if (saveBtn) {
|
|
1088
|
+
saveBtn.addEventListener('click', async function () {
|
|
1089
|
+
var data = gatherHygieneConfig();
|
|
1090
|
+
var result = await saveHygieneConfig(data);
|
|
1091
|
+
if (result) showSuccessMessage('Hygiene settings saved.');
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Reset to defaults
|
|
1096
|
+
var defaultsBtn = document.getElementById('hc-defaults');
|
|
1097
|
+
if (defaultsBtn) {
|
|
1098
|
+
var hcResetTimer = null;
|
|
1099
|
+
defaultsBtn.addEventListener('click', async function () {
|
|
1100
|
+
if (defaultsBtn.classList.contains('confirming')) {
|
|
1101
|
+
clearTimeout(hcResetTimer);
|
|
1102
|
+
defaultsBtn.classList.remove('confirming');
|
|
1103
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
1104
|
+
var result = await saveHygieneConfig({ __reset: true });
|
|
1105
|
+
if (result) showSuccessMessage('Hygiene settings reset to defaults.');
|
|
1106
|
+
} else {
|
|
1107
|
+
defaultsBtn.classList.add('confirming');
|
|
1108
|
+
defaultsBtn.textContent = 'Confirm?';
|
|
1109
|
+
hcResetTimer = setTimeout(function () {
|
|
1110
|
+
defaultsBtn.classList.remove('confirming');
|
|
1111
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
1112
|
+
}, 3000);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Find button
|
|
1118
|
+
var findBtn = document.getElementById('hc-find');
|
|
1119
|
+
if (findBtn) {
|
|
1120
|
+
findBtn.addEventListener('click', runFindAnalysis);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
loadHygieneConfig();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// =========================================================================
|
|
1127
|
+
// Tool Response Verbosity Config
|
|
1128
|
+
// =========================================================================
|
|
1129
|
+
|
|
1130
|
+
var LEVEL_LABELS = { 1: 'Minimal', 2: 'Standard', 3: 'Verbose' };
|
|
1131
|
+
|
|
1132
|
+
function populateToolVerbosity(config) {
|
|
1133
|
+
var levelBtns = document.querySelectorAll('#tv-level .config-radio');
|
|
1134
|
+
levelBtns.forEach(function (btn) {
|
|
1135
|
+
btn.classList.toggle('active', btn.getAttribute('data-value') === String(config.level));
|
|
1136
|
+
});
|
|
1137
|
+
var badge = document.getElementById('tv-status');
|
|
1138
|
+
if (badge) {
|
|
1139
|
+
badge.textContent = LEVEL_LABELS[config.level] || 'Standard';
|
|
1140
|
+
badge.className = 'config-section-status enabled';
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function loadToolVerbosityConfig() {
|
|
1145
|
+
try {
|
|
1146
|
+
var res = await fetch('/api/admin/config/tool-verbosity');
|
|
1147
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1148
|
+
var config = await res.json();
|
|
1149
|
+
populateToolVerbosity(config);
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
console.error('[laminark] Failed to load tool verbosity config:', err);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function saveToolVerbosityConfig(data) {
|
|
1156
|
+
try {
|
|
1157
|
+
var res = await fetch('/api/admin/config/tool-verbosity', {
|
|
1158
|
+
method: 'PUT',
|
|
1159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1160
|
+
body: JSON.stringify(data),
|
|
1161
|
+
});
|
|
1162
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1163
|
+
var config = await res.json();
|
|
1164
|
+
populateToolVerbosity(config);
|
|
1165
|
+
return config;
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.error('[laminark] Failed to save tool verbosity config:', err);
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function initToolVerbosity() {
|
|
1173
|
+
// Level radio buttons
|
|
1174
|
+
var levelBtns = document.querySelectorAll('#tv-level .config-radio');
|
|
1175
|
+
levelBtns.forEach(function (btn) {
|
|
1176
|
+
btn.addEventListener('click', function () {
|
|
1177
|
+
levelBtns.forEach(function (b) { b.classList.remove('active'); });
|
|
1178
|
+
btn.classList.add('active');
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// Save button
|
|
1183
|
+
var saveBtn = document.getElementById('tv-save');
|
|
1184
|
+
if (saveBtn) {
|
|
1185
|
+
saveBtn.addEventListener('click', async function () {
|
|
1186
|
+
var activeBtn = document.querySelector('#tv-level .config-radio.active');
|
|
1187
|
+
var level = activeBtn ? parseInt(activeBtn.getAttribute('data-value'), 10) : 2;
|
|
1188
|
+
var result = await saveToolVerbosityConfig({ level: level });
|
|
1189
|
+
if (result) showSuccessMessage('Tool verbosity settings saved.');
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Reset to defaults
|
|
1194
|
+
var defaultsBtn = document.getElementById('tv-defaults');
|
|
1195
|
+
if (defaultsBtn) {
|
|
1196
|
+
var tvResetTimer = null;
|
|
1197
|
+
defaultsBtn.addEventListener('click', async function () {
|
|
1198
|
+
if (defaultsBtn.classList.contains('confirming')) {
|
|
1199
|
+
clearTimeout(tvResetTimer);
|
|
1200
|
+
defaultsBtn.classList.remove('confirming');
|
|
1201
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
1202
|
+
var result = await saveToolVerbosityConfig({ __reset: true });
|
|
1203
|
+
if (result) showSuccessMessage('Tool verbosity reset to defaults.');
|
|
1204
|
+
} else {
|
|
1205
|
+
defaultsBtn.classList.add('confirming');
|
|
1206
|
+
defaultsBtn.textContent = 'Confirm?';
|
|
1207
|
+
tvResetTimer = setTimeout(function () {
|
|
1208
|
+
defaultsBtn.classList.remove('confirming');
|
|
1209
|
+
defaultsBtn.textContent = 'Reset to Defaults';
|
|
1210
|
+
}, 3000);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
loadToolVerbosityConfig();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// =========================================================================
|
|
1219
|
+
// System Info
|
|
1220
|
+
// =========================================================================
|
|
1221
|
+
|
|
1222
|
+
function formatBytes(bytes) {
|
|
1223
|
+
if (bytes === 0) return '0 B';
|
|
1224
|
+
var units = ['B', 'KB', 'MB', 'GB'];
|
|
1225
|
+
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1226
|
+
if (i >= units.length) i = units.length - 1;
|
|
1227
|
+
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function formatUptime(seconds) {
|
|
1231
|
+
var d = Math.floor(seconds / 86400);
|
|
1232
|
+
var h = Math.floor((seconds % 86400) / 3600);
|
|
1233
|
+
var m = Math.floor((seconds % 3600) / 60);
|
|
1234
|
+
if (d > 0) return d + 'd ' + h + 'h';
|
|
1235
|
+
if (h > 0) return h + 'h ' + m + 'm';
|
|
1236
|
+
return m + 'm';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function makeStatCard(id, value, label) {
|
|
1240
|
+
var el = document.createElement('div');
|
|
1241
|
+
el.className = 'stat-card';
|
|
1242
|
+
if (id) el.id = id;
|
|
1243
|
+
|
|
1244
|
+
var valueEl = document.createElement('div');
|
|
1245
|
+
valueEl.className = 'stat-value';
|
|
1246
|
+
valueEl.textContent = value;
|
|
1247
|
+
|
|
1248
|
+
var labelEl = document.createElement('div');
|
|
1249
|
+
labelEl.className = 'stat-label';
|
|
1250
|
+
labelEl.textContent = label;
|
|
1251
|
+
|
|
1252
|
+
el.appendChild(valueEl);
|
|
1253
|
+
el.appendChild(labelEl);
|
|
1254
|
+
return el;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function fetchSystemInfo() {
|
|
1258
|
+
try {
|
|
1259
|
+
var res = await fetch('/api/admin/system');
|
|
1260
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1261
|
+
return await res.json();
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
console.error('[laminark] Failed to fetch system info:', err);
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function renderSystemInfo(info) {
|
|
1269
|
+
if (!info) return;
|
|
1270
|
+
|
|
1271
|
+
// System Info grid
|
|
1272
|
+
var sysGrid = document.getElementById('system-info-grid');
|
|
1273
|
+
if (sysGrid) {
|
|
1274
|
+
sysGrid.innerHTML = '';
|
|
1275
|
+
sysGrid.appendChild(makeStatCard(null, info.laminarkVersion, 'Laminark'));
|
|
1276
|
+
sysGrid.appendChild(makeStatCard(null, info.nodeVersion, 'Node.js'));
|
|
1277
|
+
sysGrid.appendChild(makeStatCard(null, info.platform + ' ' + info.arch, 'Platform'));
|
|
1278
|
+
sysGrid.appendChild(makeStatCard(null, formatUptime(info.uptimeSeconds), 'Uptime'));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Database Storage grid
|
|
1282
|
+
var dbGrid = document.getElementById('db-storage-grid');
|
|
1283
|
+
if (dbGrid) {
|
|
1284
|
+
dbGrid.innerHTML = '';
|
|
1285
|
+
dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.sizeBytes), 'DB Size'));
|
|
1286
|
+
dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.walSizeBytes), 'WAL Size'));
|
|
1287
|
+
dbGrid.appendChild(makeStatCard(null, info.database.pageCount.toLocaleString(), 'Page Count'));
|
|
1288
|
+
dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.pageSize), 'Page Size'));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Process Memory grid
|
|
1292
|
+
var memGrid = document.getElementById('process-memory-grid');
|
|
1293
|
+
if (memGrid) {
|
|
1294
|
+
memGrid.innerHTML = '';
|
|
1295
|
+
memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.rssBytes), 'RSS'));
|
|
1296
|
+
memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.heapUsedBytes), 'Heap Used'));
|
|
1297
|
+
memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.heapTotalBytes), 'Heap Total'));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
async function refreshSystemInfo() {
|
|
1302
|
+
var info = await fetchSystemInfo();
|
|
1303
|
+
renderSystemInfo(info);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// =========================================================================
|
|
1307
|
+
// Sidebar Navigation
|
|
1308
|
+
// =========================================================================
|
|
1309
|
+
|
|
1310
|
+
function initSettingsSidebar() {
|
|
1311
|
+
var items = document.querySelectorAll('.settings-sidebar-item');
|
|
1312
|
+
var panels = document.querySelectorAll('.settings-panel');
|
|
1313
|
+
|
|
1314
|
+
function activateTab(tabName) {
|
|
1315
|
+
items.forEach(function (item) {
|
|
1316
|
+
item.classList.toggle('active', item.getAttribute('data-settings-tab') === tabName);
|
|
1317
|
+
});
|
|
1318
|
+
panels.forEach(function (panel) {
|
|
1319
|
+
panel.classList.toggle('active', panel.getAttribute('data-settings-panel') === tabName);
|
|
1320
|
+
});
|
|
1321
|
+
try { localStorage.setItem('laminark-settings-tab', tabName); } catch (e) {}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
items.forEach(function (item) {
|
|
1325
|
+
item.addEventListener('click', function () {
|
|
1326
|
+
activateTab(item.getAttribute('data-settings-tab'));
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// Restore last-selected tab
|
|
1331
|
+
var saved = null;
|
|
1332
|
+
try { saved = localStorage.getItem('laminark-settings-tab'); } catch (e) {}
|
|
1333
|
+
if (saved && document.querySelector('[data-settings-panel="' + saved + '"]')) {
|
|
1334
|
+
activateTab(saved);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// =========================================================================
|
|
1339
|
+
// Init
|
|
1340
|
+
// =========================================================================
|
|
1341
|
+
|
|
1342
|
+
function initSettings() {
|
|
1343
|
+
initSettingsSidebar();
|
|
1344
|
+
// Reset action buttons
|
|
1345
|
+
var resetBtns = document.querySelectorAll('.reset-action-btn');
|
|
1346
|
+
resetBtns.forEach(function (btn) {
|
|
1347
|
+
btn.addEventListener('click', function () {
|
|
1348
|
+
var type = btn.getAttribute('data-reset-type');
|
|
1349
|
+
if (type) showConfirmDialog(type);
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// Cancel button in confirm dialog
|
|
1354
|
+
var cancelBtn = document.getElementById('confirm-cancel');
|
|
1355
|
+
if (cancelBtn) {
|
|
1356
|
+
cancelBtn.addEventListener('click', hideConfirmDialog);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Close on overlay click
|
|
1360
|
+
var overlay = document.getElementById('confirm-overlay');
|
|
1361
|
+
if (overlay) {
|
|
1362
|
+
overlay.addEventListener('click', function (e) {
|
|
1363
|
+
if (e.target === overlay) hideConfirmDialog();
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Escape key closes dialog
|
|
1368
|
+
document.addEventListener('keydown', function (e) {
|
|
1369
|
+
if (e.key === 'Escape') hideConfirmDialog();
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
// Scope radio change refreshes stats
|
|
1373
|
+
var radios = document.querySelectorAll('input[name="reset-scope"]');
|
|
1374
|
+
radios.forEach(function (radio) {
|
|
1375
|
+
radio.addEventListener('change', refreshStats);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// Show current project name in danger zone scope label
|
|
1379
|
+
updateResetScopeProjectName();
|
|
1380
|
+
var projectSelector = document.getElementById('project-selector');
|
|
1381
|
+
if (projectSelector) {
|
|
1382
|
+
projectSelector.addEventListener('change', updateResetScopeProjectName);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// System info (fetched once, not re-fetched on project change)
|
|
1386
|
+
refreshSystemInfo();
|
|
1387
|
+
|
|
1388
|
+
// Config sections
|
|
1389
|
+
initHygiene();
|
|
1390
|
+
initHygieneConfig();
|
|
1391
|
+
initToolVerbosity();
|
|
1392
|
+
initTopicDetection();
|
|
1393
|
+
initGraphExtraction();
|
|
1394
|
+
initCrossAccess();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function updateResetScopeProjectName() {
|
|
1398
|
+
var el = document.getElementById('reset-scope-project-name');
|
|
1399
|
+
if (!el) return;
|
|
1400
|
+
var select = document.getElementById('project-selector');
|
|
1401
|
+
if (select && select.selectedOptions && select.selectedOptions[0]) {
|
|
1402
|
+
el.textContent = select.selectedOptions[0].textContent;
|
|
1403
|
+
} else if (window.laminarkState && window.laminarkState.currentProject) {
|
|
1404
|
+
el.textContent = window.laminarkState.currentProject.substring(0, 8) + '...';
|
|
1405
|
+
} else {
|
|
1406
|
+
el.textContent = 'unknown';
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
window.laminarkSettings = {
|
|
1411
|
+
initSettings: initSettings,
|
|
1412
|
+
refreshStats: refreshStats,
|
|
1413
|
+
};
|
|
1414
|
+
})();
|