@balpal4495/quorum 3.3.3 → 3.4.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 +73 -0
- package/bin/__tests__/ingest.test.js +220 -0
- package/bin/__tests__/mcp-server.test.js +739 -0
- package/bin/__tests__/mcp-tools.test.js +525 -0
- package/bin/commands/bootstrap.js +65 -0
- package/bin/commands/ingest-git.js +192 -0
- package/bin/commands/ingest-url.js +224 -0
- package/bin/commands/ingest.js +212 -0
- package/bin/commands/serve.js +52 -0
- package/bin/mcp/server.js +301 -0
- package/bin/mcp/tools.js +454 -0
- package/bin/quorum.js +51 -0
- package/bin/shared/chronicle.js +40 -0
- package/bin/ui/app.html +676 -0
- package/package.json +1 -1
package/bin/ui/app.html
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Quorum</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d0d0f;
|
|
12
|
+
--surface: #16161a;
|
|
13
|
+
--border: #2a2a30;
|
|
14
|
+
--text: #e8e8ec;
|
|
15
|
+
--muted: #6e6e7e;
|
|
16
|
+
--accent: #7c6eff;
|
|
17
|
+
--green: #34c97a;
|
|
18
|
+
--red: #e05252;
|
|
19
|
+
--yellow: #e0b952;
|
|
20
|
+
--blue: #52a8e0;
|
|
21
|
+
--radius: 8px;
|
|
22
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
23
|
+
--mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
font-family: var(--font);
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Layout ── */
|
|
36
|
+
header {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 24px;
|
|
40
|
+
padding: 0 24px;
|
|
41
|
+
height: 52px;
|
|
42
|
+
background: var(--surface);
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
position: sticky;
|
|
45
|
+
top: 0;
|
|
46
|
+
z-index: 100;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.logo {
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
font-size: 16px;
|
|
52
|
+
color: var(--accent);
|
|
53
|
+
letter-spacing: -.3px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
nav {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: 2px;
|
|
59
|
+
flex: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
nav button {
|
|
63
|
+
padding: 6px 14px;
|
|
64
|
+
border: none;
|
|
65
|
+
background: none;
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
font: inherit;
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
border-radius: var(--radius);
|
|
71
|
+
transition: background .15s, color .15s;
|
|
72
|
+
}
|
|
73
|
+
nav button:hover { background: rgba(255,255,255,.05); color: var(--text); }
|
|
74
|
+
nav button.active { background: rgba(124,110,255,.15); color: var(--accent); }
|
|
75
|
+
|
|
76
|
+
.badge {
|
|
77
|
+
display: inline-block;
|
|
78
|
+
padding: 1px 7px;
|
|
79
|
+
border-radius: 99px;
|
|
80
|
+
font-size: 11px;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
margin-left: 4px;
|
|
83
|
+
background: rgba(224,82,82,.2);
|
|
84
|
+
color: var(--red);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main {
|
|
88
|
+
max-width: 960px;
|
|
89
|
+
margin: 0 auto;
|
|
90
|
+
padding: 28px 24px 60px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ── Tabs ── */
|
|
94
|
+
.tab { display: none; }
|
|
95
|
+
.tab.active { display: block; }
|
|
96
|
+
|
|
97
|
+
/* ── Search / toolbar ── */
|
|
98
|
+
.toolbar {
|
|
99
|
+
display: flex;
|
|
100
|
+
gap: 10px;
|
|
101
|
+
margin-bottom: 20px;
|
|
102
|
+
align-items: center;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
input[type="search"], input[type="text"] {
|
|
106
|
+
flex: 1;
|
|
107
|
+
padding: 9px 14px;
|
|
108
|
+
background: var(--surface);
|
|
109
|
+
border: 1px solid var(--border);
|
|
110
|
+
border-radius: var(--radius);
|
|
111
|
+
color: var(--text);
|
|
112
|
+
font: inherit;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
outline: none;
|
|
115
|
+
transition: border-color .15s;
|
|
116
|
+
}
|
|
117
|
+
input:focus { border-color: var(--accent); }
|
|
118
|
+
input::placeholder { color: var(--muted); }
|
|
119
|
+
|
|
120
|
+
/* ── Cards ── */
|
|
121
|
+
.card {
|
|
122
|
+
background: var(--surface);
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
border-radius: var(--radius);
|
|
125
|
+
padding: 16px 18px;
|
|
126
|
+
margin-bottom: 10px;
|
|
127
|
+
transition: border-color .15s;
|
|
128
|
+
}
|
|
129
|
+
.card:hover { border-color: rgba(124,110,255,.35); }
|
|
130
|
+
|
|
131
|
+
.card-header {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: flex-start;
|
|
134
|
+
gap: 10px;
|
|
135
|
+
margin-bottom: 8px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.card-title {
|
|
139
|
+
flex: 1;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
line-height: 1.4;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.card-meta {
|
|
146
|
+
font-size: 12px;
|
|
147
|
+
color: var(--muted);
|
|
148
|
+
display: flex;
|
|
149
|
+
gap: 14px;
|
|
150
|
+
flex-wrap: wrap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.card-body {
|
|
154
|
+
font-size: 13px;
|
|
155
|
+
color: var(--muted);
|
|
156
|
+
margin-top: 6px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.areas {
|
|
160
|
+
margin-top: 8px;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 4px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.area-tag {
|
|
167
|
+
font-family: var(--mono);
|
|
168
|
+
font-size: 11px;
|
|
169
|
+
padding: 2px 7px;
|
|
170
|
+
background: rgba(255,255,255,.05);
|
|
171
|
+
border-radius: 4px;
|
|
172
|
+
color: var(--muted);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ── Status badges ── */
|
|
176
|
+
.status {
|
|
177
|
+
font-size: 11px;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
padding: 2px 8px;
|
|
180
|
+
border-radius: 99px;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
}
|
|
183
|
+
.status-validated { background: rgba(52,201,122,.15); color: var(--green); }
|
|
184
|
+
.status-open { background: rgba(82,168,224,.15); color: var(--blue); }
|
|
185
|
+
.status-refuted { background: rgba(224,82,82,.15); color: var(--red); }
|
|
186
|
+
.status-pending { background: rgba(224,185,82,.15); color: var(--yellow); }
|
|
187
|
+
|
|
188
|
+
/* ── Confidence ── */
|
|
189
|
+
.confidence {
|
|
190
|
+
display: inline-flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 6px;
|
|
193
|
+
}
|
|
194
|
+
.conf-bar {
|
|
195
|
+
width: 48px;
|
|
196
|
+
height: 4px;
|
|
197
|
+
border-radius: 2px;
|
|
198
|
+
background: var(--border);
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
}
|
|
201
|
+
.conf-fill { height: 100%; border-radius: 2px; background: var(--accent); }
|
|
202
|
+
|
|
203
|
+
/* ── Actions ── */
|
|
204
|
+
.actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
205
|
+
|
|
206
|
+
button.btn {
|
|
207
|
+
padding: 7px 14px;
|
|
208
|
+
border-radius: var(--radius);
|
|
209
|
+
border: 1px solid var(--border);
|
|
210
|
+
background: none;
|
|
211
|
+
color: var(--text);
|
|
212
|
+
font: inherit;
|
|
213
|
+
font-size: 12px;
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
transition: background .15s, border-color .15s;
|
|
217
|
+
}
|
|
218
|
+
button.btn:hover { background: rgba(255,255,255,.06); }
|
|
219
|
+
button.btn-approve {
|
|
220
|
+
background: rgba(52,201,122,.12);
|
|
221
|
+
border-color: rgba(52,201,122,.3);
|
|
222
|
+
color: var(--green);
|
|
223
|
+
}
|
|
224
|
+
button.btn-approve:hover { background: rgba(52,201,122,.22); }
|
|
225
|
+
button.btn-reject {
|
|
226
|
+
background: rgba(224,82,82,.08);
|
|
227
|
+
border-color: rgba(224,82,82,.25);
|
|
228
|
+
color: var(--red);
|
|
229
|
+
}
|
|
230
|
+
button.btn-reject:hover { background: rgba(224,82,82,.16); }
|
|
231
|
+
button.btn:disabled { opacity: .4; cursor: not-allowed; }
|
|
232
|
+
|
|
233
|
+
/* ── Coverage ── */
|
|
234
|
+
.coverage-header {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 16px;
|
|
238
|
+
margin-bottom: 24px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.pct-ring {
|
|
242
|
+
width: 72px;
|
|
243
|
+
height: 72px;
|
|
244
|
+
flex-shrink: 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.coverage-bar-wrap {
|
|
248
|
+
flex: 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cov-bar {
|
|
252
|
+
height: 10px;
|
|
253
|
+
border-radius: 5px;
|
|
254
|
+
background: var(--border);
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
margin-bottom: 8px;
|
|
257
|
+
}
|
|
258
|
+
.cov-fill {
|
|
259
|
+
height: 100%;
|
|
260
|
+
border-radius: 5px;
|
|
261
|
+
background: linear-gradient(90deg, var(--accent), var(--green));
|
|
262
|
+
transition: width .6s ease;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.cov-stats {
|
|
266
|
+
display: flex;
|
|
267
|
+
gap: 20px;
|
|
268
|
+
font-size: 13px;
|
|
269
|
+
color: var(--muted);
|
|
270
|
+
}
|
|
271
|
+
.cov-stats strong { color: var(--text); }
|
|
272
|
+
|
|
273
|
+
.cov-section { margin-bottom: 20px; }
|
|
274
|
+
.cov-section-title {
|
|
275
|
+
font-size: 12px;
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
text-transform: uppercase;
|
|
278
|
+
letter-spacing: .06em;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
margin-bottom: 8px;
|
|
281
|
+
padding-bottom: 6px;
|
|
282
|
+
border-bottom: 1px solid var(--border);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.file-row {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 10px;
|
|
289
|
+
padding: 6px 0;
|
|
290
|
+
border-bottom: 1px solid rgba(255,255,255,.03);
|
|
291
|
+
font-family: var(--mono);
|
|
292
|
+
font-size: 12px;
|
|
293
|
+
}
|
|
294
|
+
.file-row:last-child { border-bottom: none; }
|
|
295
|
+
.file-dot {
|
|
296
|
+
width: 7px;
|
|
297
|
+
height: 7px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
flex-shrink: 0;
|
|
300
|
+
}
|
|
301
|
+
.dot-green { background: var(--green); }
|
|
302
|
+
.dot-red { background: var(--red); }
|
|
303
|
+
.file-name { flex: 1; color: var(--text); }
|
|
304
|
+
.file-entries { color: var(--muted); font-size: 11px; }
|
|
305
|
+
|
|
306
|
+
/* ── Empty / loading states ── */
|
|
307
|
+
.empty {
|
|
308
|
+
text-align: center;
|
|
309
|
+
color: var(--muted);
|
|
310
|
+
padding: 48px 0;
|
|
311
|
+
font-size: 14px;
|
|
312
|
+
}
|
|
313
|
+
.empty small { display: block; margin-top: 6px; font-size: 12px; }
|
|
314
|
+
|
|
315
|
+
.loading { color: var(--muted); padding: 32px 0; text-align: center; }
|
|
316
|
+
|
|
317
|
+
/* ── Section heading ── */
|
|
318
|
+
.section-heading {
|
|
319
|
+
font-size: 18px;
|
|
320
|
+
font-weight: 700;
|
|
321
|
+
margin-bottom: 20px;
|
|
322
|
+
}
|
|
323
|
+
.section-sub {
|
|
324
|
+
font-size: 13px;
|
|
325
|
+
color: var(--muted);
|
|
326
|
+
margin-top: -14px;
|
|
327
|
+
margin-bottom: 20px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ── Toast ── */
|
|
331
|
+
#toast {
|
|
332
|
+
position: fixed;
|
|
333
|
+
bottom: 24px;
|
|
334
|
+
right: 24px;
|
|
335
|
+
padding: 10px 18px;
|
|
336
|
+
background: var(--surface);
|
|
337
|
+
border: 1px solid var(--border);
|
|
338
|
+
border-radius: var(--radius);
|
|
339
|
+
font-size: 13px;
|
|
340
|
+
transform: translateY(80px);
|
|
341
|
+
opacity: 0;
|
|
342
|
+
transition: transform .25s, opacity .25s;
|
|
343
|
+
z-index: 999;
|
|
344
|
+
max-width: 320px;
|
|
345
|
+
}
|
|
346
|
+
#toast.show { transform: translateY(0); opacity: 1; }
|
|
347
|
+
#toast.ok { border-color: rgba(52,201,122,.4); color: var(--green); }
|
|
348
|
+
#toast.err { border-color: rgba(224,82,82,.4); color: var(--red); }
|
|
349
|
+
|
|
350
|
+
/* ── Scrollbar ── */
|
|
351
|
+
::-webkit-scrollbar { width: 6px; }
|
|
352
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
353
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
354
|
+
</style>
|
|
355
|
+
</head>
|
|
356
|
+
<body>
|
|
357
|
+
|
|
358
|
+
<header>
|
|
359
|
+
<span class="logo">Quorum</span>
|
|
360
|
+
<nav>
|
|
361
|
+
<button class="active" onclick="showTab('chronicle')">Chronicle</button>
|
|
362
|
+
<button onclick="showTab('proposals')">Proposals <span class="badge" id="proposalCount" style="display:none"></span></button>
|
|
363
|
+
<button onclick="showTab('coverage')">Coverage</button>
|
|
364
|
+
</nav>
|
|
365
|
+
</header>
|
|
366
|
+
|
|
367
|
+
<main>
|
|
368
|
+
<!-- ── Chronicle tab ────────────────────────────────────────────── -->
|
|
369
|
+
<div id="tab-chronicle" class="tab active">
|
|
370
|
+
<div class="toolbar">
|
|
371
|
+
<input type="search" id="chronicleSearch" placeholder="Search entries…" oninput="onSearch(this.value)" autocomplete="off">
|
|
372
|
+
</div>
|
|
373
|
+
<div id="chronicleList"><div class="loading">Loading…</div></div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<!-- ── Proposals tab ────────────────────────────────────────────── -->
|
|
377
|
+
<div id="tab-proposals" class="tab">
|
|
378
|
+
<h2 class="section-heading">Proposals</h2>
|
|
379
|
+
<p class="section-sub">Review and approve Chronicle entries staged by AI agents.</p>
|
|
380
|
+
<div id="proposalList"><div class="loading">Loading…</div></div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<!-- ── Coverage tab ─────────────────────────────────────────────── -->
|
|
384
|
+
<div id="tab-coverage" class="tab">
|
|
385
|
+
<h2 class="section-heading">Coverage</h2>
|
|
386
|
+
<p class="section-sub">Source files with Chronicle entries referencing them.</p>
|
|
387
|
+
<div id="coverageView"><div class="loading">Loading…</div></div>
|
|
388
|
+
</div>
|
|
389
|
+
</main>
|
|
390
|
+
|
|
391
|
+
<div id="toast"></div>
|
|
392
|
+
|
|
393
|
+
<script>
|
|
394
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
let allEntries = []
|
|
397
|
+
let allProposals = []
|
|
398
|
+
let coverageData = null
|
|
399
|
+
let searchTimer = null
|
|
400
|
+
let activeTab = "chronicle"
|
|
401
|
+
|
|
402
|
+
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
window.addEventListener("DOMContentLoaded", () => {
|
|
405
|
+
loadChronicle()
|
|
406
|
+
loadProposals()
|
|
407
|
+
// Lazy-load coverage when tab is first opened
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// ── Tab switching ──────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function showTab(name) {
|
|
413
|
+
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"))
|
|
414
|
+
document.querySelectorAll("nav button").forEach((b, i) => {
|
|
415
|
+
b.classList.toggle("active", ["chronicle","proposals","coverage"][i] === name)
|
|
416
|
+
})
|
|
417
|
+
document.getElementById(`tab-${name}`).classList.add("active")
|
|
418
|
+
activeTab = name
|
|
419
|
+
if (name === "coverage" && !coverageData) loadCoverage()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Toast ──────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
let toastTimer
|
|
425
|
+
function toast(msg, type = "ok") {
|
|
426
|
+
const el = document.getElementById("toast")
|
|
427
|
+
el.textContent = msg
|
|
428
|
+
el.className = `show ${type}`
|
|
429
|
+
clearTimeout(toastTimer)
|
|
430
|
+
toastTimer = setTimeout(() => { el.className = "" }, 3200)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
function statusBadge(status) {
|
|
436
|
+
const cls = {
|
|
437
|
+
validated: "status-validated",
|
|
438
|
+
open: "status-open",
|
|
439
|
+
refuted: "status-refuted",
|
|
440
|
+
}[status] ?? "status-open"
|
|
441
|
+
return `<span class="status ${cls}">${status ?? "open"}</span>`
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function confidenceBar(conf) {
|
|
445
|
+
const pct = Math.round((conf ?? 0.7) * 100)
|
|
446
|
+
const col = pct >= 80 ? "var(--green)" : pct >= 50 ? "var(--accent)" : "var(--yellow)"
|
|
447
|
+
return `<span class="confidence">
|
|
448
|
+
<span class="conf-bar"><span class="conf-fill" style="width:${pct}%;background:${col}"></span></span>
|
|
449
|
+
<span style="color:var(--muted);font-size:11px">${pct}%</span>
|
|
450
|
+
</span>`
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function shortId(id) {
|
|
454
|
+
return (id ?? "").slice(0, 8)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function areas(arr) {
|
|
458
|
+
if (!arr?.length) return ""
|
|
459
|
+
return `<div class="areas">${arr.map(a => `<span class="area-tag">${esc(a)}</span>`).join("")}</div>`
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function esc(s) {
|
|
463
|
+
return String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function timeAgo(iso) {
|
|
467
|
+
if (!iso) return ""
|
|
468
|
+
const diff = Date.now() - new Date(iso).getTime()
|
|
469
|
+
const d = Math.floor(diff / 86400000)
|
|
470
|
+
if (d > 30) return `${Math.floor(d/30)}mo ago`
|
|
471
|
+
if (d > 0) return `${d}d ago`
|
|
472
|
+
const h = Math.floor(diff / 3600000)
|
|
473
|
+
if (h > 0) return `${h}h ago`
|
|
474
|
+
return "just now"
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Chronicle ─────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
async function loadChronicle(q = "") {
|
|
480
|
+
const url = q ? `/api/entries?q=${encodeURIComponent(q)}` : "/api/entries"
|
|
481
|
+
try {
|
|
482
|
+
const res = await fetch(url)
|
|
483
|
+
const data = await res.json()
|
|
484
|
+
allEntries = data
|
|
485
|
+
renderEntries(data, q)
|
|
486
|
+
} catch (err) {
|
|
487
|
+
document.getElementById("chronicleList").innerHTML =
|
|
488
|
+
`<div class="empty">Failed to load entries<small>${esc(err.message)}</small></div>`
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function renderEntries(entries, q) {
|
|
493
|
+
const el = document.getElementById("chronicleList")
|
|
494
|
+
if (!entries.length) {
|
|
495
|
+
el.innerHTML = q
|
|
496
|
+
? `<div class="empty">No entries matching "${esc(q)}"</div>`
|
|
497
|
+
: `<div class="empty">No Chronicle entries yet<small>Run <code>quorum commit</code> to index a proposal</small></div>`
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
el.innerHTML = entries.map(e => `
|
|
501
|
+
<div class="card">
|
|
502
|
+
<div class="card-header">
|
|
503
|
+
<div class="card-title">${esc(e.topic ?? e.decision ?? e.key_insight)}</div>
|
|
504
|
+
${statusBadge(e.status)}
|
|
505
|
+
</div>
|
|
506
|
+
<div class="card-body">${esc(e.decision ?? e.key_insight ?? "")}</div>
|
|
507
|
+
${areas(e.affected_areas)}
|
|
508
|
+
<div class="card-meta" style="margin-top:10px">
|
|
509
|
+
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${shortId(e.id)}</span>
|
|
510
|
+
${confidenceBar(e.confidence)}
|
|
511
|
+
<span>${timeAgo(e.timestamp)}</span>
|
|
512
|
+
${e.work_ref?.ref ? `<span>${esc(e.work_ref.ref)}</span>` : ""}
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
`).join("")
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function onSearch(val) {
|
|
519
|
+
clearTimeout(searchTimer)
|
|
520
|
+
searchTimer = setTimeout(() => loadChronicle(val.trim()), 240)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Proposals ────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
async function loadProposals() {
|
|
526
|
+
try {
|
|
527
|
+
const res = await fetch("/api/proposals")
|
|
528
|
+
allProposals = await res.json()
|
|
529
|
+
renderProposals(allProposals)
|
|
530
|
+
const count = allProposals.length
|
|
531
|
+
const badge = document.getElementById("proposalCount")
|
|
532
|
+
if (count > 0) {
|
|
533
|
+
badge.textContent = count
|
|
534
|
+
badge.style.display = "inline-block"
|
|
535
|
+
} else {
|
|
536
|
+
badge.style.display = "none"
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
document.getElementById("proposalList").innerHTML =
|
|
540
|
+
`<div class="empty">Failed to load proposals<small>${esc(err.message)}</small></div>`
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function renderProposals(proposals) {
|
|
545
|
+
const el = document.getElementById("proposalList")
|
|
546
|
+
if (!proposals.length) {
|
|
547
|
+
el.innerHTML = `<div class="empty">No pending proposals<small>AI agents stage proposals via <code>chronicle_propose</code> or <code>quorum commit --list</code></small></div>`
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
el.innerHTML = proposals.map(p => `
|
|
551
|
+
<div class="card" id="proposal-${esc(p.proposalId)}">
|
|
552
|
+
<div class="card-header">
|
|
553
|
+
<div class="card-title">${esc(p.topic)}</div>
|
|
554
|
+
<span class="status status-pending">pending</span>
|
|
555
|
+
</div>
|
|
556
|
+
<div class="card-body">${esc(p.decision ?? p.key_insight ?? "")}</div>
|
|
557
|
+
${areas(p.affected_areas)}
|
|
558
|
+
<div class="card-meta" style="margin-top:10px">
|
|
559
|
+
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${esc(p.proposalId?.slice(0,8))}</span>
|
|
560
|
+
${confidenceBar(p.confidence)}
|
|
561
|
+
</div>
|
|
562
|
+
<div class="actions">
|
|
563
|
+
<button class="btn btn-approve" onclick="approveProposal('${esc(p.proposalId)}', this)">✓ Approve</button>
|
|
564
|
+
<button class="btn btn-reject" onclick="rejectProposal('${esc(p.proposalId)}', this)">✕ Reject</button>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
`).join("")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function approveProposal(id, btn) {
|
|
571
|
+
btn.disabled = true
|
|
572
|
+
btn.textContent = "Approving…"
|
|
573
|
+
try {
|
|
574
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}/commit`, { method: "POST" })
|
|
575
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
576
|
+
const card = document.getElementById(`proposal-${id}`)
|
|
577
|
+
card.style.opacity = "0.4"
|
|
578
|
+
card.style.pointerEvents = "none"
|
|
579
|
+
setTimeout(() => { card.remove(); loadProposals() }, 600)
|
|
580
|
+
toast("Proposal approved and committed")
|
|
581
|
+
} catch (err) {
|
|
582
|
+
btn.disabled = false
|
|
583
|
+
btn.textContent = "✓ Approve"
|
|
584
|
+
toast(err.message, "err")
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function rejectProposal(id, btn) {
|
|
589
|
+
if (!confirm("Delete this proposal? This cannot be undone.")) return
|
|
590
|
+
btn.disabled = true
|
|
591
|
+
btn.textContent = "Deleting…"
|
|
592
|
+
try {
|
|
593
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}`, { method: "DELETE" })
|
|
594
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
595
|
+
const card = document.getElementById(`proposal-${id}`)
|
|
596
|
+
card.style.opacity = "0.4"
|
|
597
|
+
setTimeout(() => { card.remove(); loadProposals() }, 400)
|
|
598
|
+
toast("Proposal deleted")
|
|
599
|
+
} catch (err) {
|
|
600
|
+
btn.disabled = false
|
|
601
|
+
btn.textContent = "✕ Reject"
|
|
602
|
+
toast(err.message, "err")
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ── Coverage ──────────────────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
async function loadCoverage() {
|
|
609
|
+
document.getElementById("coverageView").innerHTML = `<div class="loading">Scanning files…</div>`
|
|
610
|
+
try {
|
|
611
|
+
const res = await fetch("/api/coverage")
|
|
612
|
+
coverageData = await res.json()
|
|
613
|
+
renderCoverage(coverageData)
|
|
614
|
+
} catch (err) {
|
|
615
|
+
document.getElementById("coverageView").innerHTML =
|
|
616
|
+
`<div class="empty">Failed to load coverage<small>${esc(err.message)}</small></div>`
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function renderCoverage(data) {
|
|
621
|
+
const { percentage, totalFiles, coveredFiles, coverageByFile } = data
|
|
622
|
+
const covered = coverageByFile.filter(f => f.covered)
|
|
623
|
+
const uncovered = coverageByFile.filter(f => !f.covered)
|
|
624
|
+
|
|
625
|
+
const colorPct = percentage >= 70 ? "var(--green)" : percentage >= 40 ? "var(--accent)" : "var(--yellow)"
|
|
626
|
+
|
|
627
|
+
document.getElementById("coverageView").innerHTML = `
|
|
628
|
+
<div class="coverage-header">
|
|
629
|
+
<svg class="pct-ring" viewBox="0 0 72 72">
|
|
630
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="var(--border)" stroke-width="7"/>
|
|
631
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="${colorPct}" stroke-width="7"
|
|
632
|
+
stroke-dasharray="${2*Math.PI*28}"
|
|
633
|
+
stroke-dashoffset="${2*Math.PI*28 * (1 - percentage/100)}"
|
|
634
|
+
stroke-linecap="round"
|
|
635
|
+
transform="rotate(-90 36 36)"/>
|
|
636
|
+
<text x="36" y="41" text-anchor="middle" fill="${colorPct}" font-size="14" font-weight="700" font-family="var(--font)">${percentage}%</text>
|
|
637
|
+
</svg>
|
|
638
|
+
<div class="coverage-bar-wrap">
|
|
639
|
+
<div class="cov-bar"><div class="cov-fill" style="width:${percentage}%"></div></div>
|
|
640
|
+
<div class="cov-stats">
|
|
641
|
+
<span><strong>${coveredFiles}</strong> covered</span>
|
|
642
|
+
<span><strong>${totalFiles - coveredFiles}</strong> uncovered</span>
|
|
643
|
+
<span><strong>${totalFiles}</strong> total files</span>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
${covered.length ? `
|
|
649
|
+
<div class="cov-section">
|
|
650
|
+
<div class="cov-section-title">Covered — ${covered.length} files</div>
|
|
651
|
+
${covered.map(f => `
|
|
652
|
+
<div class="file-row">
|
|
653
|
+
<span class="file-dot dot-green"></span>
|
|
654
|
+
<span class="file-name">${esc(f.file)}</span>
|
|
655
|
+
<span class="file-entries">${f.entryIds.length} ${f.entryIds.length === 1 ? "entry" : "entries"}</span>
|
|
656
|
+
</div>
|
|
657
|
+
`).join("")}
|
|
658
|
+
</div>` : ""}
|
|
659
|
+
|
|
660
|
+
${uncovered.length ? `
|
|
661
|
+
<div class="cov-section">
|
|
662
|
+
<div class="cov-section-title">Uncovered — ${uncovered.length} files</div>
|
|
663
|
+
${uncovered.map(f => `
|
|
664
|
+
<div class="file-row">
|
|
665
|
+
<span class="file-dot dot-red"></span>
|
|
666
|
+
<span class="file-name">${esc(f.file)}</span>
|
|
667
|
+
</div>
|
|
668
|
+
`).join("")}
|
|
669
|
+
</div>` : ""}
|
|
670
|
+
|
|
671
|
+
${totalFiles === 0 ? `<div class="empty">No source files found</div>` : ""}
|
|
672
|
+
`
|
|
673
|
+
}
|
|
674
|
+
</script>
|
|
675
|
+
</body>
|
|
676
|
+
</html>
|