@balpal4495/quorum 3.3.3 → 3.5.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 +110 -0
- package/bin/__tests__/ingest.test.js +220 -0
- package/bin/__tests__/mcp-server.test.js +740 -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 +71 -0
- package/bin/mcp/server.js +335 -0
- package/bin/mcp/tools.js +519 -0
- package/bin/quorum.js +51 -0
- package/bin/shared/chronicle.js +40 -0
- package/bin/ui/app.html +1089 -0
- package/dist/oracle/adapters/lance-db.d.ts.map +1 -1
- package/dist/oracle/adapters/lance-db.js +2 -1
- package/dist/oracle/adapters/lance-db.js.map +1 -1
- package/package.json +1 -1
package/bin/ui/app.html
ADDED
|
@@ -0,0 +1,1089 @@
|
|
|
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
|
+
|
|
355
|
+
/* ── Growth ── */
|
|
356
|
+
.health-grid {
|
|
357
|
+
display: grid;
|
|
358
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
359
|
+
gap: 12px;
|
|
360
|
+
margin-bottom: 24px;
|
|
361
|
+
}
|
|
362
|
+
.health-stat {
|
|
363
|
+
background: var(--surface);
|
|
364
|
+
border: 1px solid var(--border);
|
|
365
|
+
border-radius: var(--radius);
|
|
366
|
+
padding: 14px 16px;
|
|
367
|
+
}
|
|
368
|
+
.health-stat-val {
|
|
369
|
+
font-size: 28px;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
color: var(--text);
|
|
372
|
+
line-height: 1;
|
|
373
|
+
}
|
|
374
|
+
.health-stat-lbl {
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
color: var(--muted);
|
|
377
|
+
margin-top: 4px;
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
letter-spacing: .06em;
|
|
380
|
+
}
|
|
381
|
+
.health-score-wrap {
|
|
382
|
+
display: flex;
|
|
383
|
+
align-items: center;
|
|
384
|
+
gap: 16px;
|
|
385
|
+
margin-bottom: 24px;
|
|
386
|
+
background: var(--surface);
|
|
387
|
+
border: 1px solid var(--border);
|
|
388
|
+
border-radius: var(--radius);
|
|
389
|
+
padding: 20px 24px;
|
|
390
|
+
}
|
|
391
|
+
.health-score-num {
|
|
392
|
+
font-size: 52px;
|
|
393
|
+
font-weight: 800;
|
|
394
|
+
line-height: 1;
|
|
395
|
+
}
|
|
396
|
+
.health-score-text { flex: 1; }
|
|
397
|
+
.health-score-label {
|
|
398
|
+
font-size: 13px;
|
|
399
|
+
font-weight: 700;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
letter-spacing: .08em;
|
|
402
|
+
margin-bottom: 4px;
|
|
403
|
+
}
|
|
404
|
+
.health-hint { font-size: 13px; color: var(--muted); }
|
|
405
|
+
.hint-thriving { color: var(--green); }
|
|
406
|
+
.hint-healthy { color: var(--accent); }
|
|
407
|
+
.hint-slow { color: var(--yellow); }
|
|
408
|
+
.hint-stalled { color: var(--red); }
|
|
409
|
+
|
|
410
|
+
/* ── Compass ── */
|
|
411
|
+
.compass-output {
|
|
412
|
+
background: var(--surface);
|
|
413
|
+
border: 1px solid var(--border);
|
|
414
|
+
border-radius: var(--radius);
|
|
415
|
+
padding: 16px;
|
|
416
|
+
font-family: var(--mono);
|
|
417
|
+
font-size: 12px;
|
|
418
|
+
white-space: pre-wrap;
|
|
419
|
+
color: var(--muted);
|
|
420
|
+
max-height: 500px;
|
|
421
|
+
overflow-y: auto;
|
|
422
|
+
line-height: 1.6;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ── Edit modal ── */
|
|
426
|
+
.modal-overlay {
|
|
427
|
+
display: none;
|
|
428
|
+
position: fixed;
|
|
429
|
+
inset: 0;
|
|
430
|
+
background: rgba(0,0,0,.65);
|
|
431
|
+
z-index: 200;
|
|
432
|
+
align-items: center;
|
|
433
|
+
justify-content: center;
|
|
434
|
+
}
|
|
435
|
+
.modal-overlay.open { display: flex; }
|
|
436
|
+
.modal {
|
|
437
|
+
background: var(--surface);
|
|
438
|
+
border: 1px solid var(--border);
|
|
439
|
+
border-radius: 10px;
|
|
440
|
+
padding: 24px;
|
|
441
|
+
width: min(560px, 94vw);
|
|
442
|
+
max-height: 90vh;
|
|
443
|
+
overflow-y: auto;
|
|
444
|
+
}
|
|
445
|
+
.modal h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
|
|
446
|
+
.field { margin-bottom: 14px; }
|
|
447
|
+
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px; }
|
|
448
|
+
.field input, .field textarea, .field select {
|
|
449
|
+
width: 100%;
|
|
450
|
+
padding: 8px 12px;
|
|
451
|
+
background: var(--bg);
|
|
452
|
+
border: 1px solid var(--border);
|
|
453
|
+
border-radius: var(--radius);
|
|
454
|
+
color: var(--text);
|
|
455
|
+
font: inherit;
|
|
456
|
+
font-size: 13px;
|
|
457
|
+
outline: none;
|
|
458
|
+
transition: border-color .15s;
|
|
459
|
+
resize: vertical;
|
|
460
|
+
}
|
|
461
|
+
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--accent); }
|
|
462
|
+
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
|
|
463
|
+
|
|
464
|
+
/* ── Evidence/jury breakdown on proposals ── */
|
|
465
|
+
.evidence-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
466
|
+
.evidence-tag {
|
|
467
|
+
font-family: var(--mono);
|
|
468
|
+
font-size: 10px;
|
|
469
|
+
padding: 2px 6px;
|
|
470
|
+
border-radius: 4px;
|
|
471
|
+
background: rgba(124,110,255,.12);
|
|
472
|
+
color: var(--accent);
|
|
473
|
+
border: 1px solid rgba(124,110,255,.2);
|
|
474
|
+
}
|
|
475
|
+
.breakdown-row {
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 8px;
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
color: var(--muted);
|
|
481
|
+
margin-top: 3px;
|
|
482
|
+
}
|
|
483
|
+
.breakdown-label { width: 128px; flex-shrink: 0; }
|
|
484
|
+
.breakdown-bar {
|
|
485
|
+
flex: 1;
|
|
486
|
+
height: 3px;
|
|
487
|
+
border-radius: 2px;
|
|
488
|
+
background: var(--border);
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
}
|
|
491
|
+
.breakdown-fill { height: 100%; border-radius: 2px; background: var(--accent); }
|
|
492
|
+
.breakdown-pct { width: 28px; text-align: right; color: var(--muted); }
|
|
493
|
+
.jury-section {
|
|
494
|
+
margin-top: 10px;
|
|
495
|
+
padding: 8px 10px;
|
|
496
|
+
background: rgba(124,110,255,.05);
|
|
497
|
+
border: 1px solid rgba(124,110,255,.12);
|
|
498
|
+
border-radius: 6px;
|
|
499
|
+
}
|
|
500
|
+
.jury-section-title {
|
|
501
|
+
font-size: 10px;
|
|
502
|
+
font-weight: 700;
|
|
503
|
+
text-transform: uppercase;
|
|
504
|
+
letter-spacing: .07em;
|
|
505
|
+
color: var(--muted);
|
|
506
|
+
margin-bottom: 6px;
|
|
507
|
+
}
|
|
508
|
+
.council-section {
|
|
509
|
+
margin-top: 8px;
|
|
510
|
+
padding: 8px 10px;
|
|
511
|
+
background: rgba(82,168,224,.05);
|
|
512
|
+
border: 1px solid rgba(82,168,224,.12);
|
|
513
|
+
border-radius: 6px;
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
color: var(--blue);
|
|
516
|
+
}
|
|
517
|
+
.council-section-title {
|
|
518
|
+
font-size: 10px;
|
|
519
|
+
font-weight: 700;
|
|
520
|
+
text-transform: uppercase;
|
|
521
|
+
letter-spacing: .07em;
|
|
522
|
+
color: var(--muted);
|
|
523
|
+
margin-bottom: 6px;
|
|
524
|
+
}
|
|
525
|
+
.council-section ul { margin: 0 0 0 14px; }
|
|
526
|
+
.council-section li { margin-bottom: 2px; color: var(--blue); }
|
|
527
|
+
</style>
|
|
528
|
+
</head>
|
|
529
|
+
<body>
|
|
530
|
+
|
|
531
|
+
<header>
|
|
532
|
+
<span class="logo">Quorum</span>
|
|
533
|
+
<nav>
|
|
534
|
+
<button class="active" onclick="showTab('chronicle')">Chronicle</button>
|
|
535
|
+
<button onclick="showTab('proposals')">Proposals <span class="badge" id="proposalCount" style="display:none"></span></button>
|
|
536
|
+
<button onclick="showTab('coverage')">Coverage</button>
|
|
537
|
+
<button onclick="showTab('growth')">Growth</button>
|
|
538
|
+
<button onclick="showTab('compass')">Compass</button>
|
|
539
|
+
</nav>
|
|
540
|
+
</header>
|
|
541
|
+
|
|
542
|
+
<main>
|
|
543
|
+
<!-- ── Chronicle tab ────────────────────────────────────────────── -->
|
|
544
|
+
<div id="tab-chronicle" class="tab active">
|
|
545
|
+
<div class="toolbar">
|
|
546
|
+
<input type="search" id="chronicleSearch" placeholder="Search entries…" oninput="onSearch(this.value)" autocomplete="off">
|
|
547
|
+
</div>
|
|
548
|
+
<div id="chronicleList"><div class="loading">Loading…</div></div>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<!-- ── Proposals tab ────────────────────────────────────────────── -->
|
|
552
|
+
<div id="tab-proposals" class="tab">
|
|
553
|
+
<h2 class="section-heading">Proposals</h2>
|
|
554
|
+
<p class="section-sub">Review and approve Chronicle entries staged by AI agents.</p>
|
|
555
|
+
<div id="proposalList"><div class="loading">Loading…</div></div>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<!-- ── Coverage tab ─────────────────────────────────────────────── -->
|
|
559
|
+
<div id="tab-coverage" class="tab">
|
|
560
|
+
<h2 class="section-heading">Coverage</h2>
|
|
561
|
+
<p class="section-sub">Source files with Chronicle entries referencing them.</p>
|
|
562
|
+
<div id="coverageView"><div class="loading">Loading…</div></div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<!-- ── Growth tab ───────────────────────────────────────────────── -->
|
|
566
|
+
<div id="tab-growth" class="tab">
|
|
567
|
+
<h2 class="section-heading">Growth</h2>
|
|
568
|
+
<p class="section-sub">Chronicle memory health — how actively this codebase is learning.</p>
|
|
569
|
+
<div id="growthView"><div class="loading">Loading…</div></div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<!-- ── Compass tab ──────────────────────────────────────────────── -->
|
|
573
|
+
<div id="tab-compass" class="tab">
|
|
574
|
+
<h2 class="section-heading">Compass</h2>
|
|
575
|
+
<p class="section-sub">Product-direction synthesis — behaviours, gaps, and opportunities.</p>
|
|
576
|
+
<div class="toolbar" style="margin-bottom:20px">
|
|
577
|
+
<select id="compassSubcmd" style="max-width:200px;flex:none;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font:inherit;font-size:13px;outline:none">
|
|
578
|
+
<option value="map">Behaviour map</option>
|
|
579
|
+
<option value="brief">Direction brief</option>
|
|
580
|
+
<option value="opportunities">Opportunities</option>
|
|
581
|
+
<option value="bets">Strategic bets</option>
|
|
582
|
+
</select>
|
|
583
|
+
<button class="btn" onclick="loadCompass()" style="flex:none">Run</button>
|
|
584
|
+
</div>
|
|
585
|
+
<div id="compassView"><div class="empty">Select a subcommand and click Run.<small>Requires an LLM provider configured for quorum serve.</small></div></div>
|
|
586
|
+
</div>
|
|
587
|
+
</main>
|
|
588
|
+
|
|
589
|
+
<!-- ── Edit proposal modal ───────────────────────────────────────────── -->
|
|
590
|
+
<div class="modal-overlay" id="editModal" onclick="closeEditModal(event)">
|
|
591
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
592
|
+
<h3>Edit proposal</h3>
|
|
593
|
+
<input type="hidden" id="editId">
|
|
594
|
+
<div class="field">
|
|
595
|
+
<label>Topic</label>
|
|
596
|
+
<input type="text" id="editTopic">
|
|
597
|
+
</div>
|
|
598
|
+
<div class="field">
|
|
599
|
+
<label>Decision / key insight</label>
|
|
600
|
+
<textarea id="editDecision" rows="5"></textarea>
|
|
601
|
+
</div>
|
|
602
|
+
<div class="field">
|
|
603
|
+
<label>Status</label>
|
|
604
|
+
<select id="editStatus">
|
|
605
|
+
<option value="open">open</option>
|
|
606
|
+
<option value="validated">validated</option>
|
|
607
|
+
<option value="refuted">refuted</option>
|
|
608
|
+
</select>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="field">
|
|
611
|
+
<label>Confidence (0–1)</label>
|
|
612
|
+
<input type="number" id="editConfidence" min="0" max="1" step="0.05">
|
|
613
|
+
</div>
|
|
614
|
+
<div class="modal-actions">
|
|
615
|
+
<button class="btn" onclick="closeEditModal()">Cancel</button>
|
|
616
|
+
<button class="btn btn-approve" onclick="saveEdit()">Save changes</button>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<div id="toast"></div>
|
|
622
|
+
|
|
623
|
+
<script>
|
|
624
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
let allEntries = []
|
|
627
|
+
let allProposals = []
|
|
628
|
+
let coverageData = null
|
|
629
|
+
let growthData = null
|
|
630
|
+
let searchTimer = null
|
|
631
|
+
let activeTab = "chronicle"
|
|
632
|
+
|
|
633
|
+
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
window.addEventListener("DOMContentLoaded", () => {
|
|
636
|
+
loadChronicle()
|
|
637
|
+
loadProposals()
|
|
638
|
+
// Other tabs loaded lazily on first open
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// ── Tab switching ──────────────────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
const TAB_NAMES = ["chronicle", "proposals", "coverage", "growth", "compass"]
|
|
644
|
+
|
|
645
|
+
function showTab(name) {
|
|
646
|
+
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"))
|
|
647
|
+
document.querySelectorAll("nav button").forEach((b, i) => {
|
|
648
|
+
b.classList.toggle("active", TAB_NAMES[i] === name)
|
|
649
|
+
})
|
|
650
|
+
document.getElementById(`tab-${name}`).classList.add("active")
|
|
651
|
+
activeTab = name
|
|
652
|
+
if (name === "coverage" && !coverageData) loadCoverage()
|
|
653
|
+
if (name === "growth" && !growthData) loadGrowth()
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── Toast ──────────────────────────────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
let toastTimer
|
|
659
|
+
function toast(msg, type = "ok") {
|
|
660
|
+
const el = document.getElementById("toast")
|
|
661
|
+
el.textContent = msg
|
|
662
|
+
el.className = `show ${type}`
|
|
663
|
+
clearTimeout(toastTimer)
|
|
664
|
+
toastTimer = setTimeout(() => { el.className = "" }, 3200)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
function statusBadge(status) {
|
|
670
|
+
const cls = {
|
|
671
|
+
validated: "status-validated",
|
|
672
|
+
open: "status-open",
|
|
673
|
+
refuted: "status-refuted",
|
|
674
|
+
}[status] ?? "status-open"
|
|
675
|
+
return `<span class="status ${cls}">${status ?? "open"}</span>`
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function confidenceBar(conf) {
|
|
679
|
+
const pct = Math.round((conf ?? 0.7) * 100)
|
|
680
|
+
const col = pct >= 80 ? "var(--green)" : pct >= 50 ? "var(--accent)" : "var(--yellow)"
|
|
681
|
+
return `<span class="confidence">
|
|
682
|
+
<span class="conf-bar"><span class="conf-fill" style="width:${pct}%;background:${col}"></span></span>
|
|
683
|
+
<span style="color:var(--muted);font-size:11px">${pct}%</span>
|
|
684
|
+
</span>`
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function shortId(id) {
|
|
688
|
+
return (id ?? "").slice(0, 8)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function areas(arr) {
|
|
692
|
+
if (!arr?.length) return ""
|
|
693
|
+
return `<div class="areas">${arr.map(a => `<span class="area-tag">${esc(a)}</span>`).join("")}</div>`
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function esc(s) {
|
|
697
|
+
return String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function timeAgo(iso) {
|
|
701
|
+
if (!iso) return ""
|
|
702
|
+
const diff = Date.now() - new Date(iso).getTime()
|
|
703
|
+
const d = Math.floor(diff / 86400000)
|
|
704
|
+
if (d > 30) return `${Math.floor(d/30)}mo ago`
|
|
705
|
+
if (d > 0) return `${d}d ago`
|
|
706
|
+
const h = Math.floor(diff / 3600000)
|
|
707
|
+
if (h > 0) return `${h}h ago`
|
|
708
|
+
return "just now"
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Chronicle ─────────────────────────────────────────────────────────────
|
|
712
|
+
|
|
713
|
+
async function loadChronicle(q = "") {
|
|
714
|
+
const url = q ? `/api/entries?q=${encodeURIComponent(q)}` : "/api/entries"
|
|
715
|
+
try {
|
|
716
|
+
const res = await fetch(url)
|
|
717
|
+
const data = await res.json()
|
|
718
|
+
allEntries = data
|
|
719
|
+
renderEntries(data, q)
|
|
720
|
+
} catch (err) {
|
|
721
|
+
document.getElementById("chronicleList").innerHTML =
|
|
722
|
+
`<div class="empty">Failed to load entries<small>${esc(err.message)}</small></div>`
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function renderEntries(entries, q) {
|
|
727
|
+
const el = document.getElementById("chronicleList")
|
|
728
|
+
if (!entries.length) {
|
|
729
|
+
el.innerHTML = q
|
|
730
|
+
? `<div class="empty">No entries matching "${esc(q)}"</div>`
|
|
731
|
+
: `<div class="empty">No Chronicle entries yet<small>Run <code>quorum commit</code> to index a proposal</small></div>`
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
el.innerHTML = entries.map(e => `
|
|
735
|
+
<div class="card">
|
|
736
|
+
<div class="card-header">
|
|
737
|
+
<div class="card-title">${esc(e.topic ?? e.decision ?? e.key_insight)}</div>
|
|
738
|
+
${statusBadge(e.status)}
|
|
739
|
+
</div>
|
|
740
|
+
<div class="card-body">${esc(e.decision ?? e.key_insight ?? "")}</div>
|
|
741
|
+
${areas(e.affected_areas)}
|
|
742
|
+
<div class="card-meta" style="margin-top:10px">
|
|
743
|
+
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${shortId(e.id)}</span>
|
|
744
|
+
${confidenceBar(e.confidence)}
|
|
745
|
+
<span>${timeAgo(e.timestamp)}</span>
|
|
746
|
+
${e.work_ref?.ref ? `<span>${esc(e.work_ref.ref)}</span>` : ""}
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
`).join("")
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function onSearch(val) {
|
|
753
|
+
clearTimeout(searchTimer)
|
|
754
|
+
searchTimer = setTimeout(() => loadChronicle(val.trim()), 240)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── Proposals ────────────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
async function loadProposals() {
|
|
760
|
+
try {
|
|
761
|
+
const res = await fetch("/api/proposals")
|
|
762
|
+
allProposals = await res.json()
|
|
763
|
+
renderProposals(allProposals)
|
|
764
|
+
const count = allProposals.length
|
|
765
|
+
const badge = document.getElementById("proposalCount")
|
|
766
|
+
if (count > 0) {
|
|
767
|
+
badge.textContent = count
|
|
768
|
+
badge.style.display = "inline-block"
|
|
769
|
+
} else {
|
|
770
|
+
badge.style.display = "none"
|
|
771
|
+
}
|
|
772
|
+
} catch (err) {
|
|
773
|
+
document.getElementById("proposalList").innerHTML =
|
|
774
|
+
`<div class="empty">Failed to load proposals<small>${esc(err.message)}</small></div>`
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function juryBreakdown(jury) {
|
|
779
|
+
if (!jury?.breakdown) return ""
|
|
780
|
+
const dims = [
|
|
781
|
+
["Evidence support", jury.breakdown.evidence_support],
|
|
782
|
+
["Feasibility", jury.breakdown.feasibility],
|
|
783
|
+
["Risk", jury.breakdown.risk],
|
|
784
|
+
["Completeness", jury.breakdown.completeness],
|
|
785
|
+
]
|
|
786
|
+
const rows = dims.filter(([,v]) => v != null).map(([label, v]) => {
|
|
787
|
+
const pct = Math.round(v * 100)
|
|
788
|
+
return `<div class="breakdown-row">
|
|
789
|
+
<span class="breakdown-label">${esc(label)}</span>
|
|
790
|
+
<span class="breakdown-bar"><span class="breakdown-fill" style="width:${pct}%"></span></span>
|
|
791
|
+
<span class="breakdown-pct">${pct}%</span>
|
|
792
|
+
</div>`
|
|
793
|
+
}).join("")
|
|
794
|
+
if (!rows) return ""
|
|
795
|
+
return `<div class="jury-section">
|
|
796
|
+
<div class="jury-section-title">Jury · confidence ${Math.round((jury.confidence ?? 0) * 100)}% · ${esc(jury.recommendation ?? "")}</div>
|
|
797
|
+
${rows}
|
|
798
|
+
</div>`
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function councilConditions(council) {
|
|
802
|
+
if (!council?.conditions?.length) return ""
|
|
803
|
+
const items = council.conditions.map(c => `<li>${esc(c)}</li>`).join("")
|
|
804
|
+
const sat = council.satisfied ? "✓ satisfied" : "✗ not satisfied"
|
|
805
|
+
return `<div class="council-section">
|
|
806
|
+
<div class="council-section-title">Council · ${esc(sat)} · ${esc(council.recommendation ?? "")}</div>
|
|
807
|
+
<ul>${items}</ul>
|
|
808
|
+
</div>`
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function evidenceTags(ids) {
|
|
812
|
+
if (!ids?.length) return ""
|
|
813
|
+
const tags = ids.map(id => `<span class="evidence-tag">${esc(String(id).slice(0,8))}</span>`).join("")
|
|
814
|
+
return `<div class="evidence-tags">${tags}</div>`
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function renderProposals(proposals) {
|
|
818
|
+
const el = document.getElementById("proposalList")
|
|
819
|
+
if (!proposals.length) {
|
|
820
|
+
el.innerHTML = `<div class="empty">No pending proposals<small>AI agents stage proposals via <code>quorum_stage</code> MCP tool or <code>oracle.propose()</code></small></div>`
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
el.innerHTML = proposals.map(p => `
|
|
824
|
+
<div class="card" id="proposal-${esc(p.proposalId)}">
|
|
825
|
+
<div class="card-header">
|
|
826
|
+
<div class="card-title">${esc(p.topic)}</div>
|
|
827
|
+
<span class="status status-pending">pending</span>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="card-body">${esc(p.decision ?? p.key_insight ?? "")}</div>
|
|
830
|
+
${areas(p.affected_areas)}
|
|
831
|
+
${evidenceTags(p.evidence_cited)}
|
|
832
|
+
${juryBreakdown(p.jury)}
|
|
833
|
+
${councilConditions(p.council)}
|
|
834
|
+
<div class="card-meta" style="margin-top:10px">
|
|
835
|
+
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${esc(p.proposalId?.slice(0,8))}</span>
|
|
836
|
+
${confidenceBar(p.confidence)}
|
|
837
|
+
${p.source_module ? `<span style="font-size:11px;color:var(--muted)">${esc(p.source_module)}</span>` : ""}
|
|
838
|
+
</div>
|
|
839
|
+
<div class="actions">
|
|
840
|
+
<button class="btn btn-approve" onclick="approveProposal('${esc(p.proposalId)}', this)">✓ Approve</button>
|
|
841
|
+
<button class="btn btn-reject" onclick="rejectProposal('${esc(p.proposalId)}', this)">✕ Reject</button>
|
|
842
|
+
<button class="btn" onclick="openEditModal('${esc(p.proposalId)}')">Edit</button>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
`).join("")
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function approveProposal(id, btn) {
|
|
849
|
+
btn.disabled = true
|
|
850
|
+
btn.textContent = "Approving…"
|
|
851
|
+
try {
|
|
852
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}/commit`, { method: "POST" })
|
|
853
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
854
|
+
const card = document.getElementById(`proposal-${id}`)
|
|
855
|
+
card.style.opacity = "0.4"
|
|
856
|
+
card.style.pointerEvents = "none"
|
|
857
|
+
setTimeout(() => { card.remove(); loadProposals() }, 600)
|
|
858
|
+
toast("Proposal approved and committed")
|
|
859
|
+
} catch (err) {
|
|
860
|
+
btn.disabled = false
|
|
861
|
+
btn.textContent = "✓ Approve"
|
|
862
|
+
toast(err.message, "err")
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function rejectProposal(id, btn) {
|
|
867
|
+
if (!confirm("Delete this proposal? This cannot be undone.")) return
|
|
868
|
+
btn.disabled = true
|
|
869
|
+
btn.textContent = "Deleting…"
|
|
870
|
+
try {
|
|
871
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}`, { method: "DELETE" })
|
|
872
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
873
|
+
const card = document.getElementById(`proposal-${id}`)
|
|
874
|
+
card.style.opacity = "0.4"
|
|
875
|
+
setTimeout(() => { card.remove(); loadProposals() }, 400)
|
|
876
|
+
toast("Proposal deleted")
|
|
877
|
+
} catch (err) {
|
|
878
|
+
btn.disabled = false
|
|
879
|
+
btn.textContent = "✕ Reject"
|
|
880
|
+
toast(err.message, "err")
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ── Edit modal ────────────────────────────────────────────────────────────
|
|
885
|
+
|
|
886
|
+
function openEditModal(id) {
|
|
887
|
+
const p = allProposals.find(x => x.proposalId === id)
|
|
888
|
+
if (!p) return
|
|
889
|
+
document.getElementById("editId").value = id
|
|
890
|
+
document.getElementById("editTopic").value = p.topic ?? ""
|
|
891
|
+
document.getElementById("editDecision").value = p.decision ?? p.key_insight ?? ""
|
|
892
|
+
document.getElementById("editStatus").value = p.status ?? "open"
|
|
893
|
+
document.getElementById("editConfidence").value = p.confidence ?? 0.7
|
|
894
|
+
document.getElementById("editModal").classList.add("open")
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function closeEditModal(e) {
|
|
898
|
+
if (e && e.target !== document.getElementById("editModal")) return
|
|
899
|
+
document.getElementById("editModal").classList.remove("open")
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function saveEdit() {
|
|
903
|
+
const id = document.getElementById("editId").value
|
|
904
|
+
const topic = document.getElementById("editTopic").value.trim()
|
|
905
|
+
const decision = document.getElementById("editDecision").value.trim()
|
|
906
|
+
const status = document.getElementById("editStatus").value
|
|
907
|
+
const confidence = parseFloat(document.getElementById("editConfidence").value)
|
|
908
|
+
|
|
909
|
+
if (!topic || !decision) { toast("Topic and decision are required", "err"); return }
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const res = await fetch(`/api/proposals/${encodeURIComponent(id)}`, {
|
|
913
|
+
method: "PATCH",
|
|
914
|
+
headers: { "content-type": "application/json" },
|
|
915
|
+
body: JSON.stringify({ topic, decision, key_insight: decision, status, confidence }),
|
|
916
|
+
})
|
|
917
|
+
if (!res.ok) throw new Error((await res.json()).error)
|
|
918
|
+
document.getElementById("editModal").classList.remove("open")
|
|
919
|
+
toast("Proposal updated")
|
|
920
|
+
loadProposals()
|
|
921
|
+
} catch (err) {
|
|
922
|
+
toast(err.message, "err")
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ── Coverage ──────────────────────────────────────────────────────────────
|
|
927
|
+
|
|
928
|
+
async function loadCoverage() {
|
|
929
|
+
document.getElementById("coverageView").innerHTML = `<div class="loading">Scanning files…</div>`
|
|
930
|
+
try {
|
|
931
|
+
const res = await fetch("/api/coverage")
|
|
932
|
+
coverageData = await res.json()
|
|
933
|
+
renderCoverage(coverageData)
|
|
934
|
+
} catch (err) {
|
|
935
|
+
document.getElementById("coverageView").innerHTML =
|
|
936
|
+
`<div class="empty">Failed to load coverage<small>${esc(err.message)}</small></div>`
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function renderCoverage(data) {
|
|
941
|
+
const { percentage, totalFiles, coveredFiles, coverageByFile } = data
|
|
942
|
+
const covered = coverageByFile.filter(f => f.covered)
|
|
943
|
+
const uncovered = coverageByFile.filter(f => !f.covered)
|
|
944
|
+
|
|
945
|
+
const colorPct = percentage >= 70 ? "var(--green)" : percentage >= 40 ? "var(--accent)" : "var(--yellow)"
|
|
946
|
+
|
|
947
|
+
document.getElementById("coverageView").innerHTML = `
|
|
948
|
+
<div class="coverage-header">
|
|
949
|
+
<svg class="pct-ring" viewBox="0 0 72 72">
|
|
950
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="var(--border)" stroke-width="7"/>
|
|
951
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="${colorPct}" stroke-width="7"
|
|
952
|
+
stroke-dasharray="${2*Math.PI*28}"
|
|
953
|
+
stroke-dashoffset="${2*Math.PI*28 * (1 - percentage/100)}"
|
|
954
|
+
stroke-linecap="round"
|
|
955
|
+
transform="rotate(-90 36 36)"/>
|
|
956
|
+
<text x="36" y="41" text-anchor="middle" fill="${colorPct}" font-size="14" font-weight="700" font-family="var(--font)">${percentage}%</text>
|
|
957
|
+
</svg>
|
|
958
|
+
<div class="coverage-bar-wrap">
|
|
959
|
+
<div class="cov-bar"><div class="cov-fill" style="width:${percentage}%"></div></div>
|
|
960
|
+
<div class="cov-stats">
|
|
961
|
+
<span><strong>${coveredFiles}</strong> covered</span>
|
|
962
|
+
<span><strong>${totalFiles - coveredFiles}</strong> uncovered</span>
|
|
963
|
+
<span><strong>${totalFiles}</strong> total files</span>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
${covered.length ? `
|
|
969
|
+
<div class="cov-section">
|
|
970
|
+
<div class="cov-section-title">Covered — ${covered.length} files</div>
|
|
971
|
+
${covered.map(f => `
|
|
972
|
+
<div class="file-row">
|
|
973
|
+
<span class="file-dot dot-green"></span>
|
|
974
|
+
<span class="file-name">${esc(f.file)}</span>
|
|
975
|
+
<span class="file-entries">${f.entryIds.length} ${f.entryIds.length === 1 ? "entry" : "entries"}</span>
|
|
976
|
+
</div>
|
|
977
|
+
`).join("")}
|
|
978
|
+
</div>` : ""}
|
|
979
|
+
|
|
980
|
+
${uncovered.length ? `
|
|
981
|
+
<div class="cov-section">
|
|
982
|
+
<div class="cov-section-title">Uncovered — ${uncovered.length} files</div>
|
|
983
|
+
${uncovered.map(f => `
|
|
984
|
+
<div class="file-row">
|
|
985
|
+
<span class="file-dot dot-red"></span>
|
|
986
|
+
<span class="file-name">${esc(f.file)}</span>
|
|
987
|
+
</div>
|
|
988
|
+
`).join("")}
|
|
989
|
+
</div>` : ""}
|
|
990
|
+
|
|
991
|
+
${totalFiles === 0 ? `<div class="empty">No source files found</div>` : ""}
|
|
992
|
+
`
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ── Growth ────────────────────────────────────────────────────────────────
|
|
996
|
+
|
|
997
|
+
async function loadGrowth() {
|
|
998
|
+
document.getElementById("growthView").innerHTML = `<div class="loading">Loading…</div>`
|
|
999
|
+
try {
|
|
1000
|
+
const res = await fetch("/api/growth")
|
|
1001
|
+
growthData = await res.json()
|
|
1002
|
+
renderGrowth(growthData)
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
document.getElementById("growthView").innerHTML =
|
|
1005
|
+
`<div class="empty">Failed to load growth data<small>${esc(err.message)}</small></div>`
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function renderGrowth(data) {
|
|
1010
|
+
const { health, entries, proposals, hint } = data
|
|
1011
|
+
const col = health >= 80 ? "var(--green)" : health >= 60 ? "var(--accent)" : health >= 40 ? "var(--yellow)" : "var(--red)"
|
|
1012
|
+
const label = health >= 80 ? "THRIVING" : health >= 60 ? "HEALTHY" : health >= 40 ? "SLOW" : "STALLED"
|
|
1013
|
+
|
|
1014
|
+
document.getElementById("growthView").innerHTML = `
|
|
1015
|
+
<div class="health-score-wrap">
|
|
1016
|
+
<div class="health-score-num" style="color:${col}">${health}</div>
|
|
1017
|
+
<div class="health-score-text">
|
|
1018
|
+
<div class="health-score-label" style="color:${col}">${label}</div>
|
|
1019
|
+
<div class="health-hint">${esc(hint ?? "")}</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
|
|
1023
|
+
<div class="health-grid">
|
|
1024
|
+
<div class="health-stat">
|
|
1025
|
+
<div class="health-stat-val">${entries.total}</div>
|
|
1026
|
+
<div class="health-stat-lbl">Total entries</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="health-stat">
|
|
1029
|
+
<div class="health-stat-val" style="color:var(--green)">${entries.byStatus.validated ?? 0}</div>
|
|
1030
|
+
<div class="health-stat-lbl">Validated</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
<div class="health-stat">
|
|
1033
|
+
<div class="health-stat-val" style="color:var(--blue)">${entries.byStatus.open ?? 0}</div>
|
|
1034
|
+
<div class="health-stat-lbl">Open</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="health-stat">
|
|
1037
|
+
<div class="health-stat-val" style="color:var(--red)">${entries.byStatus.refuted ?? 0}</div>
|
|
1038
|
+
<div class="health-stat-lbl">Refuted</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="health-stat">
|
|
1041
|
+
<div class="health-stat-val" style="color:var(--yellow)">${proposals.pending}</div>
|
|
1042
|
+
<div class="health-stat-lbl">Pending proposals</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div class="health-stat">
|
|
1045
|
+
<div class="health-stat-val">${Math.round((entries.avgConfidence ?? 0) * 100)}%</div>
|
|
1046
|
+
<div class="health-stat-lbl">Avg confidence</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
${proposals.pending > 0 ? `
|
|
1051
|
+
<div class="card" style="border-color:rgba(224,185,82,.3);background:rgba(224,185,82,.04)">
|
|
1052
|
+
<div class="card-body" style="color:var(--yellow)">
|
|
1053
|
+
${proposals.pending} proposal${proposals.pending === 1 ? "" : "s"} awaiting approval.
|
|
1054
|
+
<a href="#" onclick="showTab('proposals');return false" style="color:var(--accent);text-decoration:none;margin-left:6px">Review →</a>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>` : ""}
|
|
1057
|
+
`
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ── Compass ───────────────────────────────────────────────────────────────
|
|
1061
|
+
|
|
1062
|
+
async function loadCompass() {
|
|
1063
|
+
const subcommand = document.getElementById("compassSubcmd").value
|
|
1064
|
+
document.getElementById("compassView").innerHTML = `<div class="loading">Running compass ${esc(subcommand)}…</div>`
|
|
1065
|
+
try {
|
|
1066
|
+
const res = await fetch(`/api/compass?subcommand=${encodeURIComponent(subcommand)}`)
|
|
1067
|
+
const data = await res.json()
|
|
1068
|
+
renderCompass(data, subcommand)
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
document.getElementById("compassView").innerHTML =
|
|
1071
|
+
`<div class="empty">Failed to run compass<small>${esc(err.message)}</small></div>`
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function renderCompass(data, subcommand) {
|
|
1076
|
+
const el = document.getElementById("compassView")
|
|
1077
|
+
if (data.status === "no-llm") {
|
|
1078
|
+
el.innerHTML = `<div class="empty">${esc(data.message)}<small>Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and restart quorum serve.</small></div>`
|
|
1079
|
+
return
|
|
1080
|
+
}
|
|
1081
|
+
const output = data.output ?? JSON.stringify(data, null, 2)
|
|
1082
|
+
el.innerHTML = `
|
|
1083
|
+
<div style="font-size:12px;color:var(--muted);margin-bottom:10px">compass ${esc(subcommand)}</div>
|
|
1084
|
+
<div class="compass-output">${esc(output)}</div>
|
|
1085
|
+
`
|
|
1086
|
+
}
|
|
1087
|
+
</script>
|
|
1088
|
+
</body>
|
|
1089
|
+
</html>
|