development-engine-vector 0.3.0__py3-none-any.whl
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.
- dev/__init__.py +3 -0
- dev/__main__.py +5 -0
- dev/cli/__init__.py +0 -0
- dev/cli/cli.py +674 -0
- dev/kernel/__init__.py +0 -0
- dev/kernel/config.py +46 -0
- dev/kernel/db.py +129 -0
- dev/kernel/paths.py +85 -0
- dev/kernel/pmf_kernel.py +432 -0
- dev/kernel/selfcheck.py +137 -0
- dev/ui/__init__.py +0 -0
- dev/ui/actions.py +47 -0
- dev/ui/db/__init__.py +26 -0
- dev/ui/db/base.py +75 -0
- dev/ui/db/checks.py +16 -0
- dev/ui/db/events.py +18 -0
- dev/ui/db/health.py +34 -0
- dev/ui/db/identity.py +12 -0
- dev/ui/db/manifest.py +35 -0
- dev/ui/db/overview.py +47 -0
- dev/ui/db/runs.py +47 -0
- dev/ui/routes.py +114 -0
- dev/ui/static/__init__.py +0 -0
- dev/ui/static/web.css +722 -0
- dev/ui/static/web.js +528 -0
- dev/ui/templates.py +231 -0
- dev/ui/web.py +28 -0
- dev/utils.py +93 -0
- dev/vcs/__init__.py +0 -0
- dev/vcs/git.py +107 -0
- dev/vcs/github.py +77 -0
- dev/workflow/__init__.py +0 -0
- dev/workflow/cda.py +143 -0
- dev/workflow/changelog.py +102 -0
- dev/workflow/preflight.py +307 -0
- dev/workflow/release.py +217 -0
- dev/workflow/versioning.py +87 -0
- development_engine_vector-0.3.0.dist-info/METADATA +252 -0
- development_engine_vector-0.3.0.dist-info/RECORD +41 -0
- development_engine_vector-0.3.0.dist-info/WHEEL +4 -0
- development_engine_vector-0.3.0.dist-info/entry_points.txt +2 -0
dev/ui/static/web.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
const safeArray = arr => Array.isArray(arr) ? arr : [];
|
|
2
|
+
|
|
3
|
+
// Navigation
|
|
4
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
5
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
6
|
+
item.addEventListener('click', () => showPage(item.dataset.page));
|
|
7
|
+
});
|
|
8
|
+
showPage('dashboard');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function showPage(page) {
|
|
12
|
+
if (!PAGE_REGISTRY[page]) page = 'dashboard';
|
|
13
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
14
|
+
document.querySelector(`[data-page="${page}"]`).classList.add('active');
|
|
15
|
+
document.getElementById('main-content').innerHTML = PAGE_REGISTRY[page]();
|
|
16
|
+
initializePage(page);
|
|
17
|
+
window.scrollTo(0, 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function initializePage(page) {
|
|
21
|
+
switch (page) {
|
|
22
|
+
case 'dashboard': initDashboard(); break;
|
|
23
|
+
case 'runs': initRuns(); break;
|
|
24
|
+
case 'health': initHealth(); break;
|
|
25
|
+
case 'checks': initChecks(); break;
|
|
26
|
+
case 'manifest': initManifest(); break;
|
|
27
|
+
case 'identity': initIdentity(); break;
|
|
28
|
+
case 'events': initEvents(); break;
|
|
29
|
+
case 'query': initQuery(); break;
|
|
30
|
+
default: break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Dashboard ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function initDashboard() {
|
|
37
|
+
const container = document.getElementById('dashboard-content');
|
|
38
|
+
if (!container) return;
|
|
39
|
+
container.innerHTML = '<div class="spinner"></div> Loading overview...';
|
|
40
|
+
fetch('/api/overview').then(r => r.json()).then(data => {
|
|
41
|
+
if (data.error) {
|
|
42
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const s = data.stats || {};
|
|
46
|
+
const recentRuns = safeArray(data.recent_runs);
|
|
47
|
+
const passRates = safeArray(data.check_pass_rates);
|
|
48
|
+
|
|
49
|
+
const lastStatus = s.last_exit_code === 0 ? '<span class="badge badge-success">PASS</span>'
|
|
50
|
+
: s.last_exit_code === null ? '<span class="badge badge-warning">N/A</span>'
|
|
51
|
+
: '<span class="badge badge-danger">FAIL</span>';
|
|
52
|
+
|
|
53
|
+
const html = `
|
|
54
|
+
<div class="grid-4">
|
|
55
|
+
<div class="card">
|
|
56
|
+
<div class="card-header">Total Runs</div>
|
|
57
|
+
<div class="card-value">${s.total_runs || 0}</div>
|
|
58
|
+
<div class="card-label">Vet executions</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="card">
|
|
61
|
+
<div class="card-header">Last Run</div>
|
|
62
|
+
<div class="card-value" style="font-size:20px;">${lastStatus}</div>
|
|
63
|
+
<div class="card-label">${s.last_notes || 'No runs yet'}</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="card">
|
|
66
|
+
<div class="card-header">Clean Runs</div>
|
|
67
|
+
<div class="card-value">${s.clean_runs || 0}</div>
|
|
68
|
+
<div class="card-label">Exit code 0</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="card">
|
|
71
|
+
<div class="card-header">Checks Active</div>
|
|
72
|
+
<div class="card-value">${s.enabled_checks || 0}</div>
|
|
73
|
+
<div class="card-label">Manifest files: ${(s.manifest_files || 0).toLocaleString()}</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
${passRates.length ? `
|
|
77
|
+
<div class="card mb-20">
|
|
78
|
+
<div class="card-header">Check Pass Rates</div>
|
|
79
|
+
<table class="table">
|
|
80
|
+
<thead><tr><th>Check</th><th>Total Runs</th><th>Passed</th><th>Pass Rate</th></tr></thead>
|
|
81
|
+
<tbody>
|
|
82
|
+
${passRates.map(r => `
|
|
83
|
+
<tr>
|
|
84
|
+
<td><strong>${r.check_name}</strong></td>
|
|
85
|
+
<td>${r.total}</td>
|
|
86
|
+
<td>${r.passed}</td>
|
|
87
|
+
<td><span class="badge badge-${r.pass_rate >= 90 ? 'success' : r.pass_rate >= 70 ? 'warning' : 'danger'}">${r.pass_rate}%</span></td>
|
|
88
|
+
</tr>
|
|
89
|
+
`).join('')}
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</div>` : ''}
|
|
93
|
+
<div class="card">
|
|
94
|
+
<div class="card-header">Recent Runs</div>
|
|
95
|
+
<table class="table">
|
|
96
|
+
<thead><tr><th>#</th><th>Started</th><th>Duration</th><th>Status</th><th>Notes</th></tr></thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
${recentRuns.map(r => {
|
|
99
|
+
const started = r.started_at ? new Date(r.started_at).toLocaleString() : '—';
|
|
100
|
+
const dur = (r.started_at && r.finished_at)
|
|
101
|
+
? ((new Date(r.finished_at) - new Date(r.started_at)) / 1000).toFixed(1) + 's'
|
|
102
|
+
: '—';
|
|
103
|
+
const badge = r.exit_code === 0 ? 'badge-success' : r.exit_code === null ? 'badge-warning' : 'badge-danger';
|
|
104
|
+
const label = r.exit_code === 0 ? 'PASS' : r.exit_code === null ? 'RUNNING' : 'FAIL';
|
|
105
|
+
return `
|
|
106
|
+
<tr class="clickable run-row" data-run-id="${r.id}">
|
|
107
|
+
<td>${r.id}</td>
|
|
108
|
+
<td>${started}</td>
|
|
109
|
+
<td>${dur}</td>
|
|
110
|
+
<td><span class="badge ${badge}">${label}</span></td>
|
|
111
|
+
<td>${r.notes || '—'}</td>
|
|
112
|
+
</tr>
|
|
113
|
+
`;
|
|
114
|
+
}).join('')}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
container.innerHTML = html;
|
|
120
|
+
document.querySelectorAll('#dashboard-content .run-row').forEach(row => {
|
|
121
|
+
row.addEventListener('click', () => openRunDrawer(+row.dataset.runId));
|
|
122
|
+
});
|
|
123
|
+
}).catch(err => {
|
|
124
|
+
container.innerHTML = '<div class="alert alert-danger">Failed to load overview.</div>';
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Runs ─────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function initRuns() {
|
|
131
|
+
const container = document.getElementById('runs-content');
|
|
132
|
+
if (!container) return;
|
|
133
|
+
container.innerHTML = '<div class="spinner"></div> Loading runs...';
|
|
134
|
+
fetch('/api/runs').then(r => r.json()).then(data => {
|
|
135
|
+
if (data.error) {
|
|
136
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const runs = safeArray(data.runs);
|
|
140
|
+
const html = `
|
|
141
|
+
<div class="card">
|
|
142
|
+
<div class="card-header">All Runs (${data.total || 0})</div>
|
|
143
|
+
<table class="table">
|
|
144
|
+
<thead><tr><th>#</th><th>Started</th><th>Finished</th><th>Trigger</th><th>Status</th><th>Notes</th></tr></thead>
|
|
145
|
+
<tbody>
|
|
146
|
+
${runs.map(r => {
|
|
147
|
+
const badge = r.exit_code === 0 ? 'badge-success' : r.exit_code === null ? 'badge-warning' : 'badge-danger';
|
|
148
|
+
const label = r.exit_code === 0 ? 'PASS' : r.exit_code === null ? 'RUNNING' : 'FAIL';
|
|
149
|
+
return `
|
|
150
|
+
<tr class="clickable run-row" data-run-id="${r.id}">
|
|
151
|
+
<td><strong>${r.id}</strong></td>
|
|
152
|
+
<td>${r.started_at ? new Date(r.started_at).toLocaleString() : '—'}</td>
|
|
153
|
+
<td>${r.finished_at ? new Date(r.finished_at).toLocaleString() : '—'}</td>
|
|
154
|
+
<td>${r.trigger || '—'}</td>
|
|
155
|
+
<td><span class="badge ${badge}">${label}</span></td>
|
|
156
|
+
<td>${r.notes || '—'}</td>
|
|
157
|
+
</tr>
|
|
158
|
+
`;
|
|
159
|
+
}).join('')}
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
container.innerHTML = html;
|
|
165
|
+
document.querySelectorAll('#runs-content .run-row').forEach(row => {
|
|
166
|
+
row.addEventListener('click', () => openRunDrawer(+row.dataset.runId));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Run detail drawer ────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function openRunDrawer(runId) {
|
|
174
|
+
const drawer = document.getElementById('detail-drawer');
|
|
175
|
+
const titleEl = document.getElementById('drawer-run-title');
|
|
176
|
+
const subtitleEl = document.getElementById('drawer-run-subtitle');
|
|
177
|
+
const body = document.getElementById('drawer-body');
|
|
178
|
+
const tabButtons = document.querySelectorAll('.drawer-tab');
|
|
179
|
+
|
|
180
|
+
titleEl.textContent = `Run #${runId}`;
|
|
181
|
+
subtitleEl.textContent = 'Loading...';
|
|
182
|
+
body.innerHTML = '<div class="spinner"></div> Loading run details...';
|
|
183
|
+
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
184
|
+
document.querySelector('.drawer-tab[data-tab="overview"]').classList.add('active');
|
|
185
|
+
drawer.classList.add('open');
|
|
186
|
+
|
|
187
|
+
fetch('/api/run?run_id=' + runId).then(r => r.json()).then(data => {
|
|
188
|
+
if (data.error) {
|
|
189
|
+
body.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
window.currentRunDetail = data;
|
|
193
|
+
const run = data.run || {};
|
|
194
|
+
titleEl.textContent = `Run #${run.id}`;
|
|
195
|
+
subtitleEl.textContent = `${run.trigger || 'manual'} · ${run.started_at ? new Date(run.started_at).toLocaleString() : ''}`;
|
|
196
|
+
body.innerHTML = renderRunTabContent('overview', data);
|
|
197
|
+
}).catch(() => {
|
|
198
|
+
body.innerHTML = '<div class="alert alert-danger">Error loading run details.</div>';
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function switchRunTab(tab) {
|
|
203
|
+
document.querySelectorAll('.drawer-tab').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
|
|
204
|
+
const body = document.getElementById('drawer-body');
|
|
205
|
+
if (!window.currentRunDetail) {
|
|
206
|
+
body.innerHTML = '<div class="spinner"></div>';
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
body.innerHTML = renderRunTabContent(tab, window.currentRunDetail);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderRunTabContent(tab, data) {
|
|
213
|
+
const run = data.run || {};
|
|
214
|
+
const health = safeArray(data.health);
|
|
215
|
+
|
|
216
|
+
const badge = run.exit_code === 0 ? 'badge-success' : run.exit_code === null ? 'badge-warning' : 'badge-danger';
|
|
217
|
+
const label = run.exit_code === 0 ? 'PASS' : run.exit_code === null ? 'RUNNING' : 'FAIL';
|
|
218
|
+
const dur = (run.started_at && run.finished_at)
|
|
219
|
+
? ((new Date(run.finished_at) - new Date(run.started_at)) / 1000).toFixed(2) + 's'
|
|
220
|
+
: '—';
|
|
221
|
+
|
|
222
|
+
const metaSection = `
|
|
223
|
+
<div class="drawer-section">
|
|
224
|
+
<h3>Run Metadata</h3>
|
|
225
|
+
<div class="session-panel">
|
|
226
|
+
${[
|
|
227
|
+
['Run ID', run.id],
|
|
228
|
+
['Trigger', run.trigger || '—'],
|
|
229
|
+
['Started', run.started_at ? new Date(run.started_at).toLocaleString() : '—'],
|
|
230
|
+
['Finished', run.finished_at ? new Date(run.finished_at).toLocaleString() : '—'],
|
|
231
|
+
['Duration', dur],
|
|
232
|
+
['Status', `<span class="badge ${badge}">${label}</span>`],
|
|
233
|
+
['Notes', run.notes || '—'],
|
|
234
|
+
].map(([lbl, val]) => `
|
|
235
|
+
<div class="data-row">
|
|
236
|
+
<div class="data-label">${lbl}</div>
|
|
237
|
+
<div class="data-value">${val}</div>
|
|
238
|
+
</div>
|
|
239
|
+
`).join('')}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
if (tab === 'overview') {
|
|
245
|
+
const checksHtml = health.length
|
|
246
|
+
? `<table class="table">
|
|
247
|
+
<thead><tr><th>Check</th><th>Result</th></tr></thead>
|
|
248
|
+
<tbody>
|
|
249
|
+
${health.map(h => `
|
|
250
|
+
<tr>
|
|
251
|
+
<td><strong>${h.check_name}</strong></td>
|
|
252
|
+
<td><span class="badge ${h.passed ? 'badge-success' : 'badge-danger'}">${h.passed ? 'PASS' : 'FAIL'}</span></td>
|
|
253
|
+
</tr>
|
|
254
|
+
`).join('')}
|
|
255
|
+
</tbody>
|
|
256
|
+
</table>`
|
|
257
|
+
: '<div class="alert alert-info">No health records for this run.</div>';
|
|
258
|
+
return `
|
|
259
|
+
${metaSection}
|
|
260
|
+
<div class="drawer-section">
|
|
261
|
+
<h3>Check Results (${health.length})</h3>
|
|
262
|
+
${checksHtml}
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (tab === 'output') {
|
|
268
|
+
const outputHtml = health.length
|
|
269
|
+
? health.map(h => `
|
|
270
|
+
<div class="drawer-section">
|
|
271
|
+
<h3>${h.check_name} — <span class="badge ${h.passed ? 'badge-success' : 'badge-danger'}">${h.passed ? 'PASS' : 'FAIL'}</span></h3>
|
|
272
|
+
<pre class="code-block" style="white-space:pre-wrap;word-break:break-word;">${(h.message || 'ok').replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
273
|
+
</div>
|
|
274
|
+
`).join('')
|
|
275
|
+
: '<div class="alert alert-info">No output recorded.</div>';
|
|
276
|
+
return outputHtml;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (tab === 'raw') {
|
|
280
|
+
return `
|
|
281
|
+
<div class="drawer-section">
|
|
282
|
+
<h3>Raw Payload</h3>
|
|
283
|
+
<pre class="code-block" style="white-space:pre-wrap;word-break:break-word;">${JSON.stringify(data, null, 2).replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return '<p>Tab content unavailable.</p>';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function closeRunDrawer() {
|
|
292
|
+
document.getElementById('detail-drawer').classList.remove('open');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Health ───────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
function initHealth() {
|
|
298
|
+
const container = document.getElementById('health-content');
|
|
299
|
+
if (!container) return;
|
|
300
|
+
container.innerHTML = '<div class="spinner"></div> Loading health records...';
|
|
301
|
+
fetch('/api/health').then(r => r.json()).then(data => {
|
|
302
|
+
if (data.error) {
|
|
303
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const rows = safeArray(data.health);
|
|
307
|
+
const html = `
|
|
308
|
+
<div class="card">
|
|
309
|
+
<div class="card-header">Health Records (${data.total || 0})</div>
|
|
310
|
+
<table class="table">
|
|
311
|
+
<thead><tr><th>Run</th><th>Check</th><th>Result</th><th>Run At</th><th>Message</th></tr></thead>
|
|
312
|
+
<tbody>
|
|
313
|
+
${rows.map(h => `
|
|
314
|
+
<tr>
|
|
315
|
+
<td>${h.run_id || '—'}</td>
|
|
316
|
+
<td><strong>${h.check_name}</strong></td>
|
|
317
|
+
<td><span class="badge ${h.passed ? 'badge-success' : 'badge-danger'}">${h.passed ? 'PASS' : 'FAIL'}</span></td>
|
|
318
|
+
<td>${h.run_at ? new Date(h.run_at).toLocaleString() : '—'}</td>
|
|
319
|
+
<td class="truncate">${(h.message || '').replace(/</g, '<').replace(/>/g, '>')}</td>
|
|
320
|
+
</tr>
|
|
321
|
+
`).join('')}
|
|
322
|
+
</tbody>
|
|
323
|
+
</table>
|
|
324
|
+
</div>
|
|
325
|
+
`;
|
|
326
|
+
container.innerHTML = html;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Checks ───────────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function initChecks() {
|
|
333
|
+
const container = document.getElementById('checks-content');
|
|
334
|
+
if (!container) return;
|
|
335
|
+
container.innerHTML = '<div class="spinner"></div> Loading checks...';
|
|
336
|
+
fetch('/api/checks').then(r => r.json()).then(data => {
|
|
337
|
+
if (data.error) {
|
|
338
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const rows = safeArray(data.checks);
|
|
342
|
+
const html = `
|
|
343
|
+
<div class="card">
|
|
344
|
+
<div class="card-header">Check Definitions (${data.total || 0})</div>
|
|
345
|
+
<table class="table">
|
|
346
|
+
<thead><tr><th>Name</th><th>Kind</th><th>Command</th><th>Timeout</th><th>Enabled</th></tr></thead>
|
|
347
|
+
<tbody>
|
|
348
|
+
${rows.map(c => `
|
|
349
|
+
<tr>
|
|
350
|
+
<td><strong>${c.name}</strong></td>
|
|
351
|
+
<td>${c.kind}</td>
|
|
352
|
+
<td class="truncate"><code>${c.command}</code></td>
|
|
353
|
+
<td>${c.timeout}s</td>
|
|
354
|
+
<td><span class="badge ${c.enabled ? 'badge-success' : 'badge-warning'}">${c.enabled ? 'ON' : 'OFF'}</span></td>
|
|
355
|
+
</tr>
|
|
356
|
+
`).join('')}
|
|
357
|
+
</tbody>
|
|
358
|
+
</table>
|
|
359
|
+
</div>
|
|
360
|
+
`;
|
|
361
|
+
container.innerHTML = html;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Manifest ─────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
function initManifest() {
|
|
368
|
+
loadManifest('');
|
|
369
|
+
const input = document.getElementById('manifest-search');
|
|
370
|
+
if (input) {
|
|
371
|
+
input.addEventListener('keypress', e => { if (e.key === 'Enter') loadManifest(input.value); });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function loadManifest(q) {
|
|
376
|
+
const container = document.getElementById('manifest-content');
|
|
377
|
+
if (!container) return;
|
|
378
|
+
container.innerHTML = '<div class="spinner"></div> Loading manifest...';
|
|
379
|
+
fetch('/api/manifest?q=' + encodeURIComponent(q)).then(r => r.json()).then(data => {
|
|
380
|
+
if (data.error) {
|
|
381
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const files = safeArray(data.files);
|
|
385
|
+
const html = `
|
|
386
|
+
<div class="card">
|
|
387
|
+
<div class="card-header">Files (showing ${files.length} of ${data.total || 0})</div>
|
|
388
|
+
<table class="table">
|
|
389
|
+
<thead><tr><th>Path</th><th>Type</th><th>Size</th><th>Tracked</th></tr></thead>
|
|
390
|
+
<tbody>
|
|
391
|
+
${files.map(f => `
|
|
392
|
+
<tr>
|
|
393
|
+
<td class="truncate">${f.relative_path}</td>
|
|
394
|
+
<td>${f.file_type || f.extension || '—'}</td>
|
|
395
|
+
<td>${f.size_bytes ? (f.size_bytes / 1024).toFixed(1) + ' KB' : '—'}</td>
|
|
396
|
+
<td><span class="badge ${f.is_tracked ? 'badge-success' : 'badge-warning'}">${f.is_tracked ? 'YES' : 'NO'}</span></td>
|
|
397
|
+
</tr>
|
|
398
|
+
`).join('')}
|
|
399
|
+
</tbody>
|
|
400
|
+
</table>
|
|
401
|
+
</div>
|
|
402
|
+
`;
|
|
403
|
+
container.innerHTML = html;
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Identity ─────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
function initIdentity() {
|
|
410
|
+
const container = document.getElementById('identity-content');
|
|
411
|
+
if (!container) return;
|
|
412
|
+
container.innerHTML = '<div class="spinner"></div> Loading identity...';
|
|
413
|
+
fetch('/api/identity').then(r => r.json()).then(data => {
|
|
414
|
+
if (data.error) {
|
|
415
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const rows = safeArray(data.identity);
|
|
419
|
+
const html = `
|
|
420
|
+
<div class="card">
|
|
421
|
+
<div class="card-header">Identity Records (${data.total || 0})</div>
|
|
422
|
+
<table class="table">
|
|
423
|
+
<thead><tr><th>Key</th><th>Value</th><th>Updated</th></tr></thead>
|
|
424
|
+
<tbody>
|
|
425
|
+
${rows.map(r => `
|
|
426
|
+
<tr>
|
|
427
|
+
<td><strong>${r.key}</strong></td>
|
|
428
|
+
<td class="truncate">${r.value !== null && r.value !== undefined ? String(r.value).replace(/</g, '<') : '—'}</td>
|
|
429
|
+
<td>${r.updated_at ? new Date(r.updated_at).toLocaleDateString() : '—'}</td>
|
|
430
|
+
</tr>
|
|
431
|
+
`).join('')}
|
|
432
|
+
</tbody>
|
|
433
|
+
</table>
|
|
434
|
+
</div>
|
|
435
|
+
`;
|
|
436
|
+
container.innerHTML = html;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Events ───────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
function initEvents() {
|
|
443
|
+
const container = document.getElementById('events-content');
|
|
444
|
+
if (!container) return;
|
|
445
|
+
container.innerHTML = '<div class="spinner"></div> Loading events...';
|
|
446
|
+
fetch('/api/events').then(r => r.json()).then(data => {
|
|
447
|
+
if (data.error) {
|
|
448
|
+
container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const rows = safeArray(data.events);
|
|
452
|
+
const html = rows.length
|
|
453
|
+
? `<div class="card">
|
|
454
|
+
<div class="card-header">Events (${data.total || 0})</div>
|
|
455
|
+
<table class="table">
|
|
456
|
+
<thead><tr><th>When</th><th>Kind</th><th>Actor</th><th>Subject</th><th>Detail</th></tr></thead>
|
|
457
|
+
<tbody>
|
|
458
|
+
${rows.map(e => `
|
|
459
|
+
<tr>
|
|
460
|
+
<td>${e.occurred_at ? new Date(e.occurred_at).toLocaleString() : '—'}</td>
|
|
461
|
+
<td><span class="badge badge-info">${e.kind}</span></td>
|
|
462
|
+
<td>${e.actor || '—'}</td>
|
|
463
|
+
<td class="truncate">${e.subject || '—'}</td>
|
|
464
|
+
<td class="truncate">${(e.detail || '').replace(/</g, '<')}</td>
|
|
465
|
+
</tr>
|
|
466
|
+
`).join('')}
|
|
467
|
+
</tbody>
|
|
468
|
+
</table>
|
|
469
|
+
</div>`
|
|
470
|
+
: '<div class="card"><p class="text-muted">No events recorded yet.</p></div>';
|
|
471
|
+
container.innerHTML = html;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Query ─────────────────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
function initQuery() {
|
|
478
|
+
const results = document.getElementById('query-results');
|
|
479
|
+
if (results) results.classList.add('hidden');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function executeQuery() {
|
|
483
|
+
const sql = document.getElementById('query-input').value;
|
|
484
|
+
if (!sql) return alert('Enter a SQL query');
|
|
485
|
+
const results = document.getElementById('query-results');
|
|
486
|
+
if (!results) return;
|
|
487
|
+
results.classList.remove('hidden');
|
|
488
|
+
results.innerHTML = '<div class="spinner"></div> Running query...';
|
|
489
|
+
fetch('/api/query', {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
headers: {'Content-Type': 'application/json'},
|
|
492
|
+
body: JSON.stringify({sql}),
|
|
493
|
+
}).then(r => r.json()).then(data => {
|
|
494
|
+
if (data.error) {
|
|
495
|
+
results.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const rows = data.rows || [];
|
|
499
|
+
if (!rows.length) {
|
|
500
|
+
results.innerHTML = '<div class="card"><p class="text-muted">No rows returned.</p></div>';
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
let html = '<div class="card"><table class="table"><thead><tr>';
|
|
504
|
+
Object.keys(rows[0]).forEach(k => { html += '<th>' + k + '</th>'; });
|
|
505
|
+
html += '</tr></thead><tbody>';
|
|
506
|
+
rows.forEach(row => {
|
|
507
|
+
html += '<tr>';
|
|
508
|
+
Object.values(row).forEach(v => { html += '<td class="truncate">' + (v !== null && v !== undefined ? String(v).replace(/</g, '<') : '') + '</td>'; });
|
|
509
|
+
html += '</tr>';
|
|
510
|
+
});
|
|
511
|
+
html += '</tbody></table></div>';
|
|
512
|
+
results.innerHTML = html;
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Vet action ────────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
function runVet() {
|
|
519
|
+
const status = document.getElementById('vet-status');
|
|
520
|
+
if (status) { status.classList.remove('hidden'); document.getElementById('vet-text').textContent = 'Running vet...'; }
|
|
521
|
+
fetch('/api/action', {
|
|
522
|
+
method: 'POST',
|
|
523
|
+
headers: {'Content-Type': 'application/json'},
|
|
524
|
+
body: JSON.stringify({action: 'vet'}),
|
|
525
|
+
}).then(r => r.json()).then(data => {
|
|
526
|
+
if (status) document.getElementById('vet-text').textContent = data.message || 'Vet started.';
|
|
527
|
+
});
|
|
528
|
+
}
|