kryptheon 0.1.26 → 0.1.28
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 +33 -7
- package/bin/kryptheon.js +199 -43
- package/kryptheon-dashboard-page.js +1026 -702
- package/kryptheon-dashboard.html +411 -378
- package/kryptheon-dashboard.js +864 -608
- package/package.json +1 -1
|
@@ -1,702 +1,1026 @@
|
|
|
1
|
-
// The dashboard page. Served from 127.0.0.1 by kryptheon-dashboard.js.
|
|
2
|
-
//
|
|
3
|
-
// Every value on this page can hold text from the app being checked - a line
|
|
4
|
-
// of its code, the heading of a page that broke. So nothing here is ever
|
|
5
|
-
// written as HTML: every node is made with createElement (or createElementNS
|
|
6
|
-
// for the icons, from fixed path data) and filled with textContent. This file
|
|
7
|
-
// is read by kryptheon-code.js in its own check, and must come back clean.
|
|
8
|
-
|
|
9
|
-
(function () {
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
var tokenMeta = document.querySelector('meta[name="kryptheon-token"]');
|
|
13
|
-
var TOKEN = tokenMeta ? tokenMeta.getAttribute('content') : '';
|
|
14
|
-
var NS = 'http://www.w3.org/2000/svg';
|
|
15
|
-
var state = null;
|
|
16
|
-
var busy = false;
|
|
17
|
-
var openRow = null;
|
|
18
|
-
var allLooks = false;
|
|
19
|
-
|
|
20
|
-
/* ------------------------------ building blocks ------------------------------ */
|
|
21
|
-
|
|
22
|
-
function el(tag, cls, text) {
|
|
23
|
-
var node = document.createElement(tag);
|
|
24
|
-
if (cls) node.className = cls;
|
|
25
|
-
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
26
|
-
return node;
|
|
27
|
-
}
|
|
28
|
-
function svgEl(tag, attrs) {
|
|
29
|
-
var node = document.createElementNS(NS, tag);
|
|
30
|
-
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, String(attrs[k])); });
|
|
31
|
-
return node;
|
|
32
|
-
}
|
|
33
|
-
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
34
|
-
function byId(id) { return document.getElementById(id); }
|
|
35
|
-
|
|
36
|
-
// Line icons from fixed path data only.
|
|
37
|
-
var ICONS = {
|
|
38
|
-
grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
|
|
39
|
-
list: 'M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01',
|
|
40
|
-
change: 'M4 12h4l3-7 4 14 3-7h2',
|
|
41
|
-
wrench: 'M14.7 6.3a4 4 0 00-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 005.4-5.4l-2.6 2.6-2.4-.6-.6-2.4 2.6-2.6z',
|
|
42
|
-
toggle: 'M8 7h8a5 5 0 010 10H8A5 5 0 018 7zm0 3a2 2 0 100 4 2 2 0 000-4z',
|
|
43
|
-
play: 'M7 5l12 7-12 7V5z',
|
|
44
|
-
box: 'M4 7l8-4 8 4v10l-8 4-8-4V7zm0 0l8 4 8-4M12 11v10',
|
|
45
|
-
alert: 'M12 3l10 18H2L12 3zm0 7v5m0 3v.5',
|
|
46
|
-
eye: 'M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12zm10 3a3 3 0 100-6 3 3 0 000 6z',
|
|
47
|
-
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
48
|
-
film: 'M4 5h16v14H4zM4 9h16M4 15h16M8 5v14M16 5v14',
|
|
49
|
-
copy: 'M9 9h10v12H9zM5 15V3h10',
|
|
50
|
-
refresh: 'M20 12a8 8 0 10-2.3 5.7M20 12V6m0 6h-6',
|
|
51
|
-
arrow: 'M5 12h14m-6-6l6 6-6 6',
|
|
52
|
-
sun: 'M12 16a4 4 0 100-8 4 4 0 000 8zM12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
53
|
-
terminal: 'M4 5h16v14H4zM7 9l3 3-3 3M13 15h4',
|
|
54
|
-
dot: 'M12 12h.01',
|
|
55
|
-
};
|
|
56
|
-
function icon(name, size) {
|
|
57
|
-
var s = svgEl('svg', {
|
|
58
|
-
width: size || 18, height: size || 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
59
|
-
'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
60
|
-
});
|
|
61
|
-
s.appendChild(svgEl('path', { d: ICONS[name] || ICONS.box }));
|
|
62
|
-
return s;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function when(iso) {
|
|
66
|
-
if (!iso) return 'unknown time';
|
|
67
|
-
var d = new Date(iso);
|
|
68
|
-
return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
|
|
69
|
-
}
|
|
70
|
-
function ago(iso) {
|
|
71
|
-
var d = new Date(iso);
|
|
72
|
-
if (isNaN(d.getTime())) return '';
|
|
73
|
-
var s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1000));
|
|
74
|
-
if (s < 60) return s + 's ago';
|
|
75
|
-
if (s < 3600) return Math.round(s / 60) + ' min ago';
|
|
76
|
-
if (s < 86400) return Math.round(s / 3600) + ' h ago';
|
|
77
|
-
return Math.round(s / 86400) + ' d ago';
|
|
78
|
-
}
|
|
79
|
-
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many || one + 's'); }
|
|
80
|
-
|
|
81
|
-
function toast(text) {
|
|
82
|
-
var t = byId('toast');
|
|
83
|
-
t.textContent = text;
|
|
84
|
-
t.classList.add('show');
|
|
85
|
-
clearTimeout(toast.timer);
|
|
86
|
-
toast.timer = setTimeout(function () { t.classList.remove('show'); }, 3200);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function post(path, body) {
|
|
90
|
-
return fetch(path, {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
headers: { 'content-type': 'application/json', 'x-kryptheon-token': TOKEN },
|
|
93
|
-
body: JSON.stringify(body),
|
|
94
|
-
}).then(function (res) {
|
|
95
|
-
return res.json().then(function (data) {
|
|
96
|
-
if (!res.ok || data.ok === false) throw new Error(data.why || 'the dashboard answered ' + res.status);
|
|
97
|
-
return data;
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function setBusy(on) {
|
|
103
|
-
busy = on;
|
|
104
|
-
Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
|
|
105
|
-
}
|
|
106
|
-
function copy(text, done) {
|
|
107
|
-
navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
|
|
108
|
-
toast('Could not copy - select the text instead');
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function button(cls, iconName, text, onClick) {
|
|
112
|
-
var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
|
|
113
|
-
b.type = 'button';
|
|
114
|
-
if (iconName) b.appendChild(icon(iconName, 15));
|
|
115
|
-
b.appendChild(el('span', null, text));
|
|
116
|
-
b.addEventListener('click', onClick);
|
|
117
|
-
return b;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
var
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
var
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
var
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
})
|
|
1
|
+
// The dashboard page. Served from 127.0.0.1 by kryptheon-dashboard.js.
|
|
2
|
+
//
|
|
3
|
+
// Every value on this page can hold text from the app being checked - a line
|
|
4
|
+
// of its code, the heading of a page that broke. So nothing here is ever
|
|
5
|
+
// written as HTML: every node is made with createElement (or createElementNS
|
|
6
|
+
// for the icons, from fixed path data) and filled with textContent. This file
|
|
7
|
+
// is read by kryptheon-code.js in its own check, and must come back clean.
|
|
8
|
+
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
var tokenMeta = document.querySelector('meta[name="kryptheon-token"]');
|
|
13
|
+
var TOKEN = tokenMeta ? tokenMeta.getAttribute('content') : '';
|
|
14
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
15
|
+
var state = null;
|
|
16
|
+
var busy = false;
|
|
17
|
+
var openRow = null;
|
|
18
|
+
var allLooks = false;
|
|
19
|
+
|
|
20
|
+
/* ------------------------------ building blocks ------------------------------ */
|
|
21
|
+
|
|
22
|
+
function el(tag, cls, text) {
|
|
23
|
+
var node = document.createElement(tag);
|
|
24
|
+
if (cls) node.className = cls;
|
|
25
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
function svgEl(tag, attrs) {
|
|
29
|
+
var node = document.createElementNS(NS, tag);
|
|
30
|
+
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, String(attrs[k])); });
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
34
|
+
function byId(id) { return document.getElementById(id); }
|
|
35
|
+
|
|
36
|
+
// Line icons from fixed path data only.
|
|
37
|
+
var ICONS = {
|
|
38
|
+
grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
|
|
39
|
+
list: 'M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01',
|
|
40
|
+
change: 'M4 12h4l3-7 4 14 3-7h2',
|
|
41
|
+
wrench: 'M14.7 6.3a4 4 0 00-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 005.4-5.4l-2.6 2.6-2.4-.6-.6-2.4 2.6-2.6z',
|
|
42
|
+
toggle: 'M8 7h8a5 5 0 010 10H8A5 5 0 018 7zm0 3a2 2 0 100 4 2 2 0 000-4z',
|
|
43
|
+
play: 'M7 5l12 7-12 7V5z',
|
|
44
|
+
box: 'M4 7l8-4 8 4v10l-8 4-8-4V7zm0 0l8 4 8-4M12 11v10',
|
|
45
|
+
alert: 'M12 3l10 18H2L12 3zm0 7v5m0 3v.5',
|
|
46
|
+
eye: 'M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12zm10 3a3 3 0 100-6 3 3 0 000 6z',
|
|
47
|
+
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
48
|
+
film: 'M4 5h16v14H4zM4 9h16M4 15h16M8 5v14M16 5v14',
|
|
49
|
+
copy: 'M9 9h10v12H9zM5 15V3h10',
|
|
50
|
+
refresh: 'M20 12a8 8 0 10-2.3 5.7M20 12V6m0 6h-6',
|
|
51
|
+
arrow: 'M5 12h14m-6-6l6 6-6 6',
|
|
52
|
+
sun: 'M12 16a4 4 0 100-8 4 4 0 000 8zM12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
53
|
+
terminal: 'M4 5h16v14H4zM7 9l3 3-3 3M13 15h4',
|
|
54
|
+
dot: 'M12 12h.01',
|
|
55
|
+
};
|
|
56
|
+
function icon(name, size) {
|
|
57
|
+
var s = svgEl('svg', {
|
|
58
|
+
width: size || 18, height: size || 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
59
|
+
'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
60
|
+
});
|
|
61
|
+
s.appendChild(svgEl('path', { d: ICONS[name] || ICONS.box }));
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function when(iso) {
|
|
66
|
+
if (!iso) return 'unknown time';
|
|
67
|
+
var d = new Date(iso);
|
|
68
|
+
return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
|
|
69
|
+
}
|
|
70
|
+
function ago(iso) {
|
|
71
|
+
var d = new Date(iso);
|
|
72
|
+
if (isNaN(d.getTime())) return '';
|
|
73
|
+
var s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1000));
|
|
74
|
+
if (s < 60) return s + 's ago';
|
|
75
|
+
if (s < 3600) return Math.round(s / 60) + ' min ago';
|
|
76
|
+
if (s < 86400) return Math.round(s / 3600) + ' h ago';
|
|
77
|
+
return Math.round(s / 86400) + ' d ago';
|
|
78
|
+
}
|
|
79
|
+
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many || one + 's'); }
|
|
80
|
+
|
|
81
|
+
function toast(text) {
|
|
82
|
+
var t = byId('toast');
|
|
83
|
+
t.textContent = text;
|
|
84
|
+
t.classList.add('show');
|
|
85
|
+
clearTimeout(toast.timer);
|
|
86
|
+
toast.timer = setTimeout(function () { t.classList.remove('show'); }, 3200);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function post(path, body) {
|
|
90
|
+
return fetch(path, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/json', 'x-kryptheon-token': TOKEN },
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
}).then(function (res) {
|
|
95
|
+
return res.json().then(function (data) {
|
|
96
|
+
if (!res.ok || data.ok === false) throw new Error(data.why || 'the dashboard answered ' + res.status);
|
|
97
|
+
return data;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setBusy(on) {
|
|
103
|
+
busy = on;
|
|
104
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
|
|
105
|
+
}
|
|
106
|
+
function copy(text, done) {
|
|
107
|
+
navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
|
|
108
|
+
toast('Could not copy - select the text instead');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function button(cls, iconName, text, onClick) {
|
|
112
|
+
var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
|
|
113
|
+
b.type = 'button';
|
|
114
|
+
if (iconName) b.appendChild(icon(iconName, 15));
|
|
115
|
+
b.appendChild(el('span', null, text));
|
|
116
|
+
b.addEventListener('click', onClick);
|
|
117
|
+
return b;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A button with the terminal command that does the same thing underneath it,
|
|
122
|
+
* small - for when the page cannot be used. Only ever this page's own fixed
|
|
123
|
+
* commands, or one with an address the person typed, set as text.
|
|
124
|
+
*/
|
|
125
|
+
function withCommand(btn, command) {
|
|
126
|
+
var wrap = el('div', 'act');
|
|
127
|
+
wrap.appendChild(btn);
|
|
128
|
+
if (command) wrap.appendChild(el('div', 'fallback', 'or in a terminal: ' + command));
|
|
129
|
+
return wrap;
|
|
130
|
+
}
|
|
131
|
+
function whenTag(when, more) {
|
|
132
|
+
return el('span', 'tag when', when + (more ? ' - ' + more : ''));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Made once and moved, never rebuilt: the page re-renders every few seconds,
|
|
136
|
+
// and a rebuilt box would lose what is being typed into it. The connection
|
|
137
|
+
// string lives only in this one box - it is never put anywhere else on the
|
|
138
|
+
// page, never saved, and gone when the page is closed or reloaded.
|
|
139
|
+
var recordInput = el('input', 'field');
|
|
140
|
+
recordInput.type = 'url';
|
|
141
|
+
recordInput.placeholder = 'http://localhost:3000';
|
|
142
|
+
recordInput.setAttribute('aria-label', 'Your app address');
|
|
143
|
+
recordInput.spellcheck = false;
|
|
144
|
+
var dbInput = el('input', 'field');
|
|
145
|
+
dbInput.type = 'password';
|
|
146
|
+
dbInput.placeholder = 'postgresql://postgres.<project>:<password>@...pooler.supabase.com:5432/postgres';
|
|
147
|
+
dbInput.setAttribute('aria-label', 'Your database connection string');
|
|
148
|
+
dbInput.autocomplete = 'off';
|
|
149
|
+
dbInput.spellcheck = false;
|
|
150
|
+
dbInput.addEventListener('input', function () { if (pending) { pending = null; renderSteps(); } });
|
|
151
|
+
|
|
152
|
+
/* ------------------------------------ jobs ------------------------------------ */
|
|
153
|
+
|
|
154
|
+
// The one thing running, as the server tells it. Followed until it ends; its
|
|
155
|
+
// lines are what the command printed, already scrubbed of any secret.
|
|
156
|
+
var job = null;
|
|
157
|
+
var lastRecheck = null;
|
|
158
|
+
|
|
159
|
+
function renderJob() {
|
|
160
|
+
var box = byId('job');
|
|
161
|
+
clear(box);
|
|
162
|
+
if (!job) { box.hidden = true; return; }
|
|
163
|
+
box.hidden = false;
|
|
164
|
+
box.className = 'job' + (job.running ? ' on' : job.result && job.result.ok ? ' ok' : ' bad');
|
|
165
|
+
var head = el('div', 'job-head');
|
|
166
|
+
if (job.running) head.appendChild(el('span', 'spin'));
|
|
167
|
+
head.appendChild(el('b', null, job.title));
|
|
168
|
+
head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : job.result && job.result.ok ? 'leaf' : 'ember'),
|
|
169
|
+
job.running ? 'running' : job.result && job.result.ok ? 'finished' : 'did not finish'));
|
|
170
|
+
if (!job.running) {
|
|
171
|
+
var close = el('button', 'job-close', 'Hide');
|
|
172
|
+
close.type = 'button';
|
|
173
|
+
close.addEventListener('click', function () { job = null; renderJob(); });
|
|
174
|
+
head.appendChild(close);
|
|
175
|
+
}
|
|
176
|
+
box.appendChild(head);
|
|
177
|
+
var lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
|
|
178
|
+
box.appendChild(lines);
|
|
179
|
+
if (!job.running && job.result) {
|
|
180
|
+
var r = job.result;
|
|
181
|
+
box.appendChild(el('div', 'job-result', r.ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see the lines above')));
|
|
182
|
+
}
|
|
183
|
+
if (job.command) box.appendChild(el('div', 'fallback', 'The same in a terminal: ' + job.command));
|
|
184
|
+
lines.scrollTop = lines.scrollHeight;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getJob() {
|
|
188
|
+
return fetch('/api/job', { cache: 'no-store' }).then(function (res) { return res.json(); }).then(function (d) { return d.job; });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Starts a job and follows it to its end. Resolves with the finished job. */
|
|
192
|
+
function runJob(path, body) {
|
|
193
|
+
if (busy) { toast('Something is already running - wait for it to finish.'); return Promise.reject(new Error('busy')); }
|
|
194
|
+
setBusy(true);
|
|
195
|
+
return post(path, body).then(function (started) {
|
|
196
|
+
job = started.job;
|
|
197
|
+
renderJob();
|
|
198
|
+
return follow();
|
|
199
|
+
}).then(function (done) {
|
|
200
|
+
setBusy(false);
|
|
201
|
+
return load().then(function () { return done; });
|
|
202
|
+
}, function (err) {
|
|
203
|
+
setBusy(false);
|
|
204
|
+
if (err.message !== 'busy') toast('Could not start: ' + err.message);
|
|
205
|
+
throw err;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function follow() {
|
|
209
|
+
return new Promise(function (resolve) {
|
|
210
|
+
(function tick() {
|
|
211
|
+
getJob().then(function (now) {
|
|
212
|
+
if (now) { job = now; renderJob(); }
|
|
213
|
+
if (now && now.running) setTimeout(tick, 700);
|
|
214
|
+
else resolve(now);
|
|
215
|
+
}, function () { setTimeout(tick, 1500); });
|
|
216
|
+
})();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var CHECK_NAME = { regression: 'Recorded flows', frontend: 'Frontend code', data: 'Database' };
|
|
221
|
+
var CHECK_TONE = { regression: 'sun', frontend: 'amber', data: 'dusk' };
|
|
222
|
+
|
|
223
|
+
function counts() {
|
|
224
|
+
var confirmed = state.findings.filter(function (f) { return f.status === 'confirmed'; }).length;
|
|
225
|
+
return { confirmed: confirmed, toVerify: state.findings.length - confirmed, proven: (state.proven || []).length };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ----------------------------------- views ----------------------------------- */
|
|
229
|
+
|
|
230
|
+
// One section on screen at a time; the address keeps it, so a reload lands
|
|
231
|
+
// on the same one.
|
|
232
|
+
var VIEWS = [
|
|
233
|
+
{ id: 'dashboard', title: 'Dashboard', icon: 'grid' },
|
|
234
|
+
{ id: 'findings', title: 'Findings', icon: 'list' },
|
|
235
|
+
{ id: 'changes', title: 'What changed', icon: 'change' },
|
|
236
|
+
{ id: 'fixes', title: 'Proven fixes', icon: 'wrench' },
|
|
237
|
+
{ id: 'checks', title: 'Checks', icon: 'toggle' },
|
|
238
|
+
{ id: 'runs', title: 'Runs', icon: 'play' },
|
|
239
|
+
{ id: 'storage', title: 'Where this lives', icon: 'box' },
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
function currentView() {
|
|
243
|
+
var id = (location.hash || '').replace(/^#/, '');
|
|
244
|
+
return VIEWS.some(function (v) { return v.id === id; }) ? id : 'dashboard';
|
|
245
|
+
}
|
|
246
|
+
function go(id, filter) {
|
|
247
|
+
if (filter) { statusFilter = filter; if (state) { renderStatusFilter(); renderFindings(); } }
|
|
248
|
+
if (currentView() === id) return showView();
|
|
249
|
+
location.hash = id;
|
|
250
|
+
}
|
|
251
|
+
function showView() {
|
|
252
|
+
var id = currentView();
|
|
253
|
+
VIEWS.forEach(function (v) { byId('view-' + v.id).hidden = v.id !== id; });
|
|
254
|
+
var view = VIEWS.filter(function (v) { return v.id === id; })[0];
|
|
255
|
+
byId('viewTitle').textContent = view.title;
|
|
256
|
+
document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
|
|
257
|
+
if (state) renderSide();
|
|
258
|
+
window.scrollTo(0, 0);
|
|
259
|
+
}
|
|
260
|
+
window.addEventListener('hashchange', showView);
|
|
261
|
+
|
|
262
|
+
/* ---------------------------------- sidebar ---------------------------------- */
|
|
263
|
+
|
|
264
|
+
function renderSide() {
|
|
265
|
+
var c = counts();
|
|
266
|
+
var active = currentView();
|
|
267
|
+
var badge = {
|
|
268
|
+
findings: [state.findings.length, c.confirmed > 0],
|
|
269
|
+
changes: [(state.looks || []).length, false],
|
|
270
|
+
fixes: [c.proven, false],
|
|
271
|
+
runs: [state.runs.length, !!(state.runs[0] && state.runs[0].failed)],
|
|
272
|
+
};
|
|
273
|
+
var box = byId('nav');
|
|
274
|
+
clear(box);
|
|
275
|
+
VIEWS.forEach(function (v) {
|
|
276
|
+
var b = el('button', active === v.id ? 'on' : '');
|
|
277
|
+
b.type = 'button';
|
|
278
|
+
b.appendChild(icon(v.icon, 18));
|
|
279
|
+
b.appendChild(el('span', null, v.title));
|
|
280
|
+
var n = badge[v.id];
|
|
281
|
+
if (n && n[0]) b.appendChild(el('span', 'n-count' + (n[1] ? ' hot' : ''), n[0]));
|
|
282
|
+
if (active === v.id) b.setAttribute('aria-current', 'page');
|
|
283
|
+
b.addEventListener('click', function () { go(v.id); });
|
|
284
|
+
box.appendChild(b);
|
|
285
|
+
});
|
|
286
|
+
var built = state.checks.filter(function (x) { return x.available; }).length;
|
|
287
|
+
byId('builtText').textContent = built + ' / ' + state.checks.length;
|
|
288
|
+
byId('builtBar').style.width = Math.round((built / state.checks.length) * 100) + '%';
|
|
289
|
+
byId('builtNote').textContent = (state.checks.length - built) + ' more are listed as not available - never as passed.';
|
|
290
|
+
var name = state.project.name || 'Project';
|
|
291
|
+
var last = (state.looks || [])[0];
|
|
292
|
+
byId('subline').textContent = name + ' - ' + (last ? 'watching, last look ' + ago(last.at) : 'watching for changes');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ------------------------------ right now + steps ------------------------------ */
|
|
296
|
+
|
|
297
|
+
function renderNow() {
|
|
298
|
+
var sky = byId('sky');
|
|
299
|
+
var ic = byId('skyIcon');
|
|
300
|
+
var title = byId('skyTitle');
|
|
301
|
+
var sub = byId('skySub');
|
|
302
|
+
var actions = byId('skyActions');
|
|
303
|
+
clear(ic); clear(title); clear(sub); clear(actions);
|
|
304
|
+
var next = state.next || { kind: 'quiet', title: 'Nothing open' };
|
|
305
|
+
byId('skyEyebrow').textContent = next.kind === 'quiet' ? 'ALL QUIET' : 'DO THIS NEXT';
|
|
306
|
+
if (next.kind === 'fix') {
|
|
307
|
+
sky.className = 'sky bad';
|
|
308
|
+
ic.style.color = 'var(--ember)';
|
|
309
|
+
ic.appendChild(icon('alert', 28));
|
|
310
|
+
title.textContent = next.title;
|
|
311
|
+
sub.textContent = 'Proven by a check that ran. Open it, copy the fix prompt into your AI tool, then press Re-check - it says FIXED only when it is.';
|
|
312
|
+
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'confirmed'); }));
|
|
313
|
+
} else if (next.kind === 'step') {
|
|
314
|
+
sky.className = 'sky';
|
|
315
|
+
ic.style.color = 'var(--sun-deep)';
|
|
316
|
+
ic.appendChild(icon('arrow', 28));
|
|
317
|
+
title.textContent = next.title;
|
|
318
|
+
sub.textContent = 'The next step in setting Kryptheon up. Do it below in Start here.';
|
|
319
|
+
actions.appendChild(button('sun', 'arrow', 'Go to Start here', function () { go('dashboard'); scrollToId('steps'); }));
|
|
320
|
+
} else if (next.kind === 'verify') {
|
|
321
|
+
sky.className = 'sky';
|
|
322
|
+
ic.style.color = 'var(--amber)';
|
|
323
|
+
ic.appendChild(icon('eye', 28));
|
|
324
|
+
title.textContent = next.title;
|
|
325
|
+
sub.textContent = 'Spotted without running anything. Each needs a person to decide.';
|
|
326
|
+
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'verify'); }));
|
|
327
|
+
} else {
|
|
328
|
+
sky.className = 'sky ok';
|
|
329
|
+
ic.style.color = 'var(--leaf)';
|
|
330
|
+
ic.appendChild(icon('sun', 28));
|
|
331
|
+
title.textContent = 'Nothing open';
|
|
332
|
+
sub.textContent = 'Nothing open from the checks that are on and have run. That is not the same as nothing wrong - see Checks for what is not built yet.';
|
|
333
|
+
actions.appendChild(button('', 'toggle', 'See the checks', function () { go('checks'); }));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function scrollToId(id) {
|
|
338
|
+
var t = byId(id);
|
|
339
|
+
if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
var STEP_WORD = { done: 'Done', todo: 'To do', waiting: 'Waiting', off: 'Switched off' };
|
|
343
|
+
var STEP_TONE = { done: 'leaf', todo: 'sun', waiting: 'amber', off: 'muted' };
|
|
344
|
+
|
|
345
|
+
function renderSteps() {
|
|
346
|
+
var box = byId('steps');
|
|
347
|
+
clear(box);
|
|
348
|
+
(state.steps || []).forEach(function (s, i) {
|
|
349
|
+
var row = el('div', 'step ' + s.state);
|
|
350
|
+
var num = el('div', 'step-num');
|
|
351
|
+
if (s.state === 'done') num.appendChild(icon('check', 18));
|
|
352
|
+
else num.textContent = String(i + 1).padStart(2, '0');
|
|
353
|
+
row.appendChild(num);
|
|
354
|
+
var body = el('div', 'step-body');
|
|
355
|
+
var head = el('div', 'step-head');
|
|
356
|
+
head.appendChild(el('div', 'step-title', s.title));
|
|
357
|
+
head.appendChild(el('span', 'tag ' + STEP_TONE[s.state], STEP_WORD[s.state] || s.state));
|
|
358
|
+
if (s.when) head.appendChild(whenTag(s.when, s.whenMore));
|
|
359
|
+
body.appendChild(head);
|
|
360
|
+
body.appendChild(el('div', 'step-why', s.why + (s.at ? ' - last ' + ago(s.at) : '') + (s.count ? ' - ' + plural(s.count, 'recording') + ' held' : '')));
|
|
361
|
+
var a = s.action;
|
|
362
|
+
if (a && a.kind === 'run') {
|
|
363
|
+
var run = button('sun sm', 'play', a.label, function () { runChecks([a.check], run); });
|
|
364
|
+
run.setAttribute('data-action', 'run');
|
|
365
|
+
run.disabled = busy;
|
|
366
|
+
body.appendChild(withCommand(run, a.command));
|
|
367
|
+
} else if (a && a.kind === 'record') {
|
|
368
|
+
body.appendChild(recordControls(a));
|
|
369
|
+
} else if (a && a.kind === 'connect') {
|
|
370
|
+
body.appendChild(connectControls());
|
|
371
|
+
} else if (a && a.kind === 'db') {
|
|
372
|
+
body.appendChild(databaseControls(a));
|
|
373
|
+
} else if (a && a.kind === 'nightly') {
|
|
374
|
+
body.appendChild(nightlyControls(a));
|
|
375
|
+
}
|
|
376
|
+
row.appendChild(body);
|
|
377
|
+
box.appendChild(row);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ------------------------- record, database, nightly ------------------------- */
|
|
382
|
+
|
|
383
|
+
function recordControls(a) {
|
|
384
|
+
var wrap = el('div', 'controls');
|
|
385
|
+
var line = el('div', 'field-row');
|
|
386
|
+
line.appendChild(recordInput);
|
|
387
|
+
var go = button('sun sm', 'film', a.label, function () {
|
|
388
|
+
var address = recordInput.value.trim();
|
|
389
|
+
if (!address) { toast('Put your app\'s address in the box first - for example http://localhost:3000'); recordInput.focus(); return; }
|
|
390
|
+
runJob('/api/record', { url: address }).then(function (done) {
|
|
391
|
+
var r = done && done.result;
|
|
392
|
+
toast(r && r.ok ? 'Saved: ' + (r.recordings || []).join(', ') : 'Nothing was saved' + (r && r.why ? ' - ' + r.why : ''));
|
|
393
|
+
}, function () {});
|
|
394
|
+
});
|
|
395
|
+
go.setAttribute('data-action', 'record');
|
|
396
|
+
go.disabled = busy;
|
|
397
|
+
line.appendChild(go);
|
|
398
|
+
wrap.appendChild(line);
|
|
399
|
+
wrap.appendChild(el('div', 'fallback', 'or in a terminal: ' + a.command));
|
|
400
|
+
return wrap;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// What kryptheon-night itself says - its help, consent screens and warnings -
|
|
404
|
+
// asked once for the help and again, with the string, before every run.
|
|
405
|
+
var nightWords = null;
|
|
406
|
+
var pending = null;
|
|
407
|
+
|
|
408
|
+
function loadHelp() {
|
|
409
|
+
if (nightWords || loadHelp.asked) return;
|
|
410
|
+
loadHelp.asked = true;
|
|
411
|
+
post('/api/db/words', {}).then(function (d) { nightWords = d.words; renderSteps(); }, function () { loadHelp.asked = false; });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function linesBox(cls, lines) {
|
|
415
|
+
return el('pre', cls, (lines || []).join('\n'));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function connectControls() {
|
|
419
|
+
var wrap = el('div', 'controls');
|
|
420
|
+
wrap.appendChild(dbInput);
|
|
421
|
+
wrap.appendChild(el('div', 'field-note', 'Kept only in this box. It goes to this dashboard on your machine, and to kryptheon-night for one run - never saved, never shown again.'));
|
|
422
|
+
loadHelp();
|
|
423
|
+
wrap.appendChild(nightWords ? linesBox('help', nightWords.help) : el('div', 'field-note', 'Loading the steps for finding it...'));
|
|
424
|
+
return wrap;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Asks kryptheon-night what it makes of the string, then shows its own
|
|
429
|
+
* screen for this action - warnings first, then what it will do - and waits
|
|
430
|
+
* for a yes. A string it cannot use stops here, before any run.
|
|
431
|
+
*/
|
|
432
|
+
function prepare(action, step, finding) {
|
|
433
|
+
var connection = dbInput.value.trim();
|
|
434
|
+
if (!connection) {
|
|
435
|
+
toast('Paste your connection string in "Connect your database" first.');
|
|
436
|
+
go('dashboard');
|
|
437
|
+
setTimeout(function () { dbInput.focus(); dbInput.scrollIntoView({ block: 'center' }); }, 50);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (busy) return;
|
|
441
|
+
post('/api/db/words', { connection: connection }).then(function (d) {
|
|
442
|
+
pending = { action: action, step: step, words: d.words, finding: finding || null };
|
|
443
|
+
renderSteps();
|
|
444
|
+
renderFindings();
|
|
445
|
+
}, function (err) { toast('Could not ask kryptheon-night: ' + err.message); });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function consentBox(yesLabel, lines) {
|
|
449
|
+
var p = pending;
|
|
450
|
+
var box = el('div', 'consent');
|
|
451
|
+
if (p.words.unusable) {
|
|
452
|
+
box.appendChild(el('div', 'd-step', 'THIS STRING WILL NOT WORK'));
|
|
453
|
+
box.appendChild(linesBox('warn', p.words.unusable));
|
|
454
|
+
box.appendChild(button('sm', null, 'Change it', function () { pending = null; renderSteps(); dbInput.focus(); }));
|
|
455
|
+
return box;
|
|
456
|
+
}
|
|
457
|
+
if (p.words.warning) {
|
|
458
|
+
box.appendChild(el('div', 'd-step', 'BEFORE YOU GO ON'));
|
|
459
|
+
box.appendChild(linesBox('warn', p.words.warning));
|
|
460
|
+
}
|
|
461
|
+
if (lines) box.appendChild(linesBox('help', lines));
|
|
462
|
+
var row = el('div', 'd-actions');
|
|
463
|
+
var yes = button('sun sm', 'check', yesLabel, function () {
|
|
464
|
+
var connection = dbInput.value.trim();
|
|
465
|
+
var action = p.action;
|
|
466
|
+
var finding = p.finding;
|
|
467
|
+
pending = null;
|
|
468
|
+
renderSteps();
|
|
469
|
+
renderFindings();
|
|
470
|
+
runJob('/api/db', { action: action, connection: connection, id: finding ? finding.id : undefined }).then(function (done) {
|
|
471
|
+
var r = (done && done.result) || {};
|
|
472
|
+
if (finding) return heard(finding, r);
|
|
473
|
+
toast(r.ok ? 'Finished: ' + (r.summary || 'done') : 'It could not finish' + (r.why ? ' - ' + r.why : ''));
|
|
474
|
+
}, function () {});
|
|
475
|
+
});
|
|
476
|
+
yes.setAttribute('data-action', 'db');
|
|
477
|
+
yes.disabled = busy;
|
|
478
|
+
row.appendChild(yes);
|
|
479
|
+
row.appendChild(button('sm', null, 'Cancel', function () { pending = null; renderSteps(); renderFindings(); }));
|
|
480
|
+
box.appendChild(row);
|
|
481
|
+
return box;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function databaseControls(a) {
|
|
485
|
+
var wrap = el('div', 'controls');
|
|
486
|
+
if (pending && pending.step === 'data') {
|
|
487
|
+
wrap.appendChild(consentBox('Yes, check it', pending.words.consent));
|
|
488
|
+
return wrap;
|
|
489
|
+
}
|
|
490
|
+
var go = button('sun sm', 'play', a.label, function () { prepare('scan', 'data'); });
|
|
491
|
+
go.setAttribute('data-action', 'db');
|
|
492
|
+
go.disabled = busy;
|
|
493
|
+
wrap.appendChild(withCommand(go, a.command));
|
|
494
|
+
return wrap;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The nightly time, from the job's own schedule, in the person's own time. */
|
|
498
|
+
function nightlyTime(cron) {
|
|
499
|
+
var m = /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/.exec(String(cron || '').trim());
|
|
500
|
+
if (!m) return null;
|
|
501
|
+
var now = new Date();
|
|
502
|
+
var at = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), Number(m[2]), Number(m[1])));
|
|
503
|
+
var utc = String(m[2]).padStart(2, '0') + ':' + String(m[1]).padStart(2, '0') + ' UTC';
|
|
504
|
+
return utc + ' - ' + at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' your time';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function nightlyControls(a) {
|
|
508
|
+
var wrap = el('div', 'controls');
|
|
509
|
+
var time = nightlyTime(a.at || (nightWords && nightWords.nightlyAt));
|
|
510
|
+
wrap.appendChild(el('div', 'field-note', (a.installed ? 'Set up. ' : 'Not set up yet. ') +
|
|
511
|
+
'It runs every night' + (time ? ' at ' + time : '') + '.'));
|
|
512
|
+
if (pending && pending.step === 'nightly') {
|
|
513
|
+
wrap.appendChild(pending.action === 'install'
|
|
514
|
+
? consentBox('Yes, set it up', pending.words.installConsent)
|
|
515
|
+
: consentBox('Yes, remove it', ['This removes the nightly job, the "kryptheon" schema in your database, and any',
|
|
516
|
+
'extension kryptheon-night had to add - and leaves alone anything that was there before.']));
|
|
517
|
+
return wrap;
|
|
518
|
+
}
|
|
519
|
+
var row = el('div', 'act-row');
|
|
520
|
+
var setUp = button(a.installed ? 'sm' : 'sun sm', 'sun', a.installed ? 'Set it up again' : 'Set up nightly check', function () { prepare('install', 'nightly'); });
|
|
521
|
+
setUp.setAttribute('data-action', 'db');
|
|
522
|
+
setUp.disabled = busy;
|
|
523
|
+
var setWrap = withCommand(setUp, 'npx kryptheon-night install');
|
|
524
|
+
setWrap.insertBefore(whenTag('one time'), setWrap.firstChild);
|
|
525
|
+
row.appendChild(setWrap);
|
|
526
|
+
var see = button('sm', 'eye', 'See last night\'s result', function () {
|
|
527
|
+
var connection = dbInput.value.trim();
|
|
528
|
+
if (!connection) { toast('Paste your connection string in "Connect your database" first.'); dbInput.focus(); return; }
|
|
529
|
+
runJob('/api/db', { action: 'night', connection: connection }).then(function (done) {
|
|
530
|
+
var r = done && done.result;
|
|
531
|
+
toast(r && r.ok ? 'Read back - see the lines above and Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
|
|
532
|
+
}, function () {});
|
|
533
|
+
});
|
|
534
|
+
see.setAttribute('data-action', 'db');
|
|
535
|
+
see.disabled = busy;
|
|
536
|
+
var seeWrap = withCommand(see, 'npx kryptheon-night night');
|
|
537
|
+
seeWrap.insertBefore(whenTag('any morning'), seeWrap.firstChild);
|
|
538
|
+
row.appendChild(seeWrap);
|
|
539
|
+
if (a.installed) {
|
|
540
|
+
var remove = button('sm', null, 'Remove nightly check', function () { prepare('uninstall', 'nightly'); });
|
|
541
|
+
remove.setAttribute('data-action', 'db');
|
|
542
|
+
remove.disabled = busy;
|
|
543
|
+
var rmWrap = withCommand(remove, 'npx kryptheon-night uninstall');
|
|
544
|
+
rmWrap.insertBefore(whenTag('one time'), rmWrap.firstChild);
|
|
545
|
+
row.appendChild(rmWrap);
|
|
546
|
+
}
|
|
547
|
+
wrap.appendChild(row);
|
|
548
|
+
return wrap;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* ------------------------------ connect your AI tool ------------------------------ */
|
|
552
|
+
|
|
553
|
+
// The same set-up kryptheon.tech shows, so the two never disagree. Every
|
|
554
|
+
// step comes from that tool's own documentation, except where a tool is
|
|
555
|
+
// marked tested - there it was run by hand. Static text: nothing here comes
|
|
556
|
+
// from the project being checked.
|
|
557
|
+
var SERVER = { command: 'npx', args: ['-y', 'kryptheon-mcp'] };
|
|
558
|
+
var MCP_JSON = JSON.stringify({ mcpServers: { kryptheon: SERVER } }, null, 2);
|
|
559
|
+
var VSCODE_JSON = JSON.stringify({ servers: { kryptheon: SERVER } }, null, 2);
|
|
560
|
+
var AI_TOOLS = [
|
|
561
|
+
{ id: 'claude-code', name: 'Claude Code', tested: true,
|
|
562
|
+
steps: ['Run this once, in any terminal. That is the whole set-up.'],
|
|
563
|
+
code: 'claude mcp add kryptheon -- npx -y kryptheon-mcp', file: 'terminal' },
|
|
564
|
+
{ id: 'cursor', name: 'Cursor', tested: false,
|
|
565
|
+
oneClick: { href: 'cursor://anysphere.cursor-deeplink/mcp/install?name=kryptheon&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImtyeXB0aGVvbi1tY3AiXX0=', label: 'Add to Cursor' },
|
|
566
|
+
steps: ['Press Add to Cursor. Cursor asks you to confirm - say yes.', 'Or by hand: paste the config into mcp.json in the .cursor folder of your home folder.'],
|
|
567
|
+
code: MCP_JSON, file: '~/.cursor/mcp.json' },
|
|
568
|
+
{ id: 'vscode', name: 'VS Code', tested: false,
|
|
569
|
+
oneClick: { href: 'vscode:mcp/install?' + encodeURIComponent(JSON.stringify({ name: 'kryptheon', command: 'npx', args: ['-y', 'kryptheon-mcp'] })), label: 'Install in VS Code' },
|
|
570
|
+
steps: ['Press Install in VS Code and confirm.', 'Or by hand: run MCP: Add Server from the Command Palette, or put the config in .vscode/mcp.json - VS Code calls the list "servers", not "mcpServers".'],
|
|
571
|
+
code: VSCODE_JSON, file: '.vscode/mcp.json' },
|
|
572
|
+
{ id: 'antigravity', name: 'Antigravity', tested: false,
|
|
573
|
+
steps: ['Open Settings, bottom left, then Customizations, then Add MCP. The MCP Store opens.', 'Click Manage MCP Servers, then View raw config.', 'Paste the config and save. Antigravity reloads it by itself.'],
|
|
574
|
+
code: MCP_JSON, file: '~/.gemini/config/mcp_config.json' },
|
|
575
|
+
{ id: 'windsurf', name: 'Windsurf', tested: false,
|
|
576
|
+
steps: ['In the Cascade panel, open the ... menu at the top right and click Open MCP config file.', 'Paste the config and save.'],
|
|
577
|
+
note: 'Windsurf is Devin Desktop now. Use the menu rather than hunting for the file: newer versions keep it in %APPDATA%\\devin, older ones in .codeium\\windsurf.',
|
|
578
|
+
code: MCP_JSON, file: 'mcp_config.json' },
|
|
579
|
+
{ id: 'claude-desktop', name: 'Claude Desktop', tested: false,
|
|
580
|
+
steps: ['From the Claude menu choose Settings..., then Developer, then Edit Config.', 'Paste the config and save.', 'Quit Claude completely - not just the window - and open it again.'],
|
|
581
|
+
code: MCP_JSON, file: 'claude_desktop_config.json' },
|
|
582
|
+
{ id: 'codex', name: 'Codex', tested: false,
|
|
583
|
+
steps: ['Run this once, in any terminal.'],
|
|
584
|
+
code: 'codex mcp add kryptheon -- npx -y kryptheon-mcp', file: 'terminal' },
|
|
585
|
+
];
|
|
586
|
+
var aiTool = 'claude-code';
|
|
587
|
+
|
|
588
|
+
function renderAI() {
|
|
589
|
+
var tabs = byId('aiTabs');
|
|
590
|
+
var panel = byId('aiPanel');
|
|
591
|
+
if (!tabs || !panel) return;
|
|
592
|
+
clear(tabs);
|
|
593
|
+
clear(panel);
|
|
594
|
+
AI_TOOLS.forEach(function (t, i) {
|
|
595
|
+
var b = el('button', 'ai-tab' + (t.id === aiTool ? ' on' : ''));
|
|
596
|
+
b.type = 'button';
|
|
597
|
+
b.setAttribute('role', 'tab');
|
|
598
|
+
b.setAttribute('aria-selected', t.id === aiTool ? 'true' : 'false');
|
|
599
|
+
b.appendChild(el('span', 'ai-num', String(i + 1).padStart(2, '0')));
|
|
600
|
+
b.appendChild(el('span', 'ai-name', t.name));
|
|
601
|
+
if (t.oneClick) b.appendChild(el('span', 'ai-tag', '1-click'));
|
|
602
|
+
b.addEventListener('click', function () { aiTool = t.id; renderAI(); });
|
|
603
|
+
tabs.appendChild(b);
|
|
604
|
+
});
|
|
605
|
+
var tool = AI_TOOLS.filter(function (t) { return t.id === aiTool; })[0] || AI_TOOLS[0];
|
|
606
|
+
panel.appendChild(el('span', 'ai-badge' + (tool.tested ? ' tested' : ''), tool.tested ? 'Tested by hand' : "From " + tool.name + "'s own docs - not tested by hand yet"));
|
|
607
|
+
panel.appendChild(el('h4', null, tool.name));
|
|
608
|
+
if (tool.oneClick) {
|
|
609
|
+
var oc = el('div', 'ai-oneclick');
|
|
610
|
+
var link = el('a', 'btn sun', tool.oneClick.label + ' →');
|
|
611
|
+
link.href = tool.oneClick.href;
|
|
612
|
+
oc.appendChild(link);
|
|
613
|
+
oc.appendChild(el('small', null, 'or by hand, below'));
|
|
614
|
+
panel.appendChild(oc);
|
|
615
|
+
}
|
|
616
|
+
var ol = el('ol', 'ai-steps');
|
|
617
|
+
tool.steps.forEach(function (s, i) {
|
|
618
|
+
var li = el('li');
|
|
619
|
+
li.appendChild(el('span', null, String(i + 1)));
|
|
620
|
+
li.appendChild(el('div', null, s));
|
|
621
|
+
ol.appendChild(li);
|
|
622
|
+
});
|
|
623
|
+
panel.appendChild(ol);
|
|
624
|
+
var plate = el('div', 'ai-plate');
|
|
625
|
+
var bar = el('div', 'ai-plate-bar');
|
|
626
|
+
bar.appendChild(el('span', null, tool.file));
|
|
627
|
+
var cp = el('button', null, 'Copy');
|
|
628
|
+
cp.type = 'button';
|
|
629
|
+
cp.addEventListener('click', function () { copy(tool.code, tool.file === 'terminal' ? 'Command copied - run it in a terminal' : 'Config copied - paste it into ' + tool.file); });
|
|
630
|
+
bar.appendChild(cp);
|
|
631
|
+
plate.appendChild(bar);
|
|
632
|
+
plate.appendChild(el('pre', null, tool.code));
|
|
633
|
+
panel.appendChild(plate);
|
|
634
|
+
if (tool.note) panel.appendChild(el('p', 'ai-note', tool.note));
|
|
635
|
+
if (tool.file !== 'terminal') {
|
|
636
|
+
panel.appendChild(el('p', 'ai-note', 'On Windows, if it says it cannot start npx: use "command": "cmd" and "args": ["/c", "npx", "-y", "kryptheon-mcp"] instead.'));
|
|
637
|
+
}
|
|
638
|
+
panel.appendChild(el('p', 'ai-note', 'To let it check your database too, add "env": { "KN_DATABASE_URL": "postgresql://..." } to the kryptheon entry. Never paste the connection string into the chat - that puts the password to your whole database in a transcript.'));
|
|
639
|
+
var then = el('p', 'ai-then');
|
|
640
|
+
then.appendChild(document.createTextNode('Then just ask your assistant: '));
|
|
641
|
+
then.appendChild(el('b', null, '“Check my app.”'));
|
|
642
|
+
panel.appendChild(then);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* ---------------------------------- findings ---------------------------------- */
|
|
646
|
+
|
|
647
|
+
var statusFilter = 'all';
|
|
648
|
+
var query = '';
|
|
649
|
+
|
|
650
|
+
function renderStatusFilter() {
|
|
651
|
+
var box = byId('statusFilter');
|
|
652
|
+
clear(box);
|
|
653
|
+
[['all', 'All'], ['confirmed', 'Confirmed'], ['verify', 'To verify']].forEach(function (s) {
|
|
654
|
+
var b = el('button', statusFilter === s[0] ? 'on' : '', s[1]);
|
|
655
|
+
b.type = 'button';
|
|
656
|
+
b.addEventListener('click', function () { statusFilter = s[0]; renderStatusFilter(); renderFindings(); });
|
|
657
|
+
box.appendChild(b);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function statusTag(f) {
|
|
661
|
+
var t = el('span', 'tag ' + (f.status === 'confirmed' ? 'ember' : 'amber'));
|
|
662
|
+
t.appendChild(el('span', f.status === 'confirmed' ? 'g-dot' : 'g-half'));
|
|
663
|
+
t.appendChild(el('span', null, f.status === 'confirmed' ? 'Confirmed' : 'To verify'));
|
|
664
|
+
return t;
|
|
665
|
+
}
|
|
666
|
+
function sevTag(sev) {
|
|
667
|
+
var tone = /CRITICAL/.test(sev) ? 'ember' : sev === 'HIGH' ? 'sun' : sev === 'MEDIUM' ? 'amber' : 'leaf';
|
|
668
|
+
return el('span', 'tag ' + tone, sev);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
var VERDICT_TONE = { fixed: 'leaf', 'still open': 'ember', 'gone with its file': 'amber', 'could not confirm': 'amber' };
|
|
672
|
+
|
|
673
|
+
function detailBox(f) {
|
|
674
|
+
var box = el('div', 'detail-box');
|
|
675
|
+
box.appendChild(el('div', 'd-step', 'WHAT IS WRONG'));
|
|
676
|
+
box.appendChild(el('div', 'd-text', f.detail || f.headline));
|
|
677
|
+
box.appendChild(el('div', 'd-text', 'Seen ' + when(f.when) + ' - ' + f.evidence + ' - id ' + f.id));
|
|
678
|
+
if (f.fixPrompt) {
|
|
679
|
+
box.appendChild(el('div', 'd-step', 'STEP 1 - PASTE THIS INTO YOUR AI TOOL'));
|
|
680
|
+
box.appendChild(el('pre', null, f.fixPrompt));
|
|
681
|
+
}
|
|
682
|
+
box.appendChild(el('div', 'd-step', f.fixPrompt ? 'STEP 2 - AFTER IT CHANGES THE CODE, RE-CHECK' : 'RE-CHECK'));
|
|
683
|
+
var actions = el('div', 'd-actions');
|
|
684
|
+
if (f.fixPrompt) {
|
|
685
|
+
actions.appendChild(button('', 'copy', 'Copy fix prompt', function (e) {
|
|
686
|
+
e.stopPropagation();
|
|
687
|
+
copy(f.fixPrompt, 'Fix prompt copied - paste it into your AI tool');
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
var re = button('sun sm', 'refresh', 'Re-check', function (e) { e.stopPropagation(); recheck(f); });
|
|
691
|
+
re.setAttribute('data-action', 'recheck');
|
|
692
|
+
re.disabled = busy;
|
|
693
|
+
actions.appendChild(withCommand(re, f.check === 'data' ? 'npx kryptheon-night' : 'npx kryptheon recheck ' + f.id));
|
|
694
|
+
box.appendChild(actions);
|
|
695
|
+
if (f.attempts && f.attempts.length) {
|
|
696
|
+
var list = el('div', 'attempts');
|
|
697
|
+
f.attempts.slice(-4).reverse().forEach(function (a) {
|
|
698
|
+
var r = el('div');
|
|
699
|
+
r.appendChild(el('span', 'tag ' + (VERDICT_TONE[a.verdict] || ''), a.verdict));
|
|
700
|
+
r.appendChild(el('span', null, 'Re-checked ' + ago(a.at) + (a.why ? ' - ' + a.why : '') + (a.newProblems ? ' - ' + plural(a.newProblems, 'new problem') : '')));
|
|
701
|
+
list.appendChild(r);
|
|
702
|
+
});
|
|
703
|
+
box.appendChild(list);
|
|
704
|
+
}
|
|
705
|
+
return box;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function renderFindings() {
|
|
709
|
+
var body = byId('findingList');
|
|
710
|
+
clear(body);
|
|
711
|
+
var shown = state.findings.filter(function (f) {
|
|
712
|
+
if (statusFilter === 'confirmed' && f.status !== 'confirmed') return false;
|
|
713
|
+
if (statusFilter === 'verify' && f.status === 'confirmed') return false;
|
|
714
|
+
return true;
|
|
715
|
+
});
|
|
716
|
+
byId('findingBadge').textContent = shown.length + (shown.length !== state.findings.length ? ' of ' + state.findings.length : '');
|
|
717
|
+
// A finding the last re-check closed is no longer in the list, so its
|
|
718
|
+
// answer is said here instead - with what the fix newly broke, if anything.
|
|
719
|
+
var open = state.findings.some(function (f) { return lastRecheck && f.id === lastRecheck.id; });
|
|
720
|
+
if (lastRecheck && !open) {
|
|
721
|
+
var gone = el('div', 'rechecked');
|
|
722
|
+
gone.appendChild(el('span', 'tag ' + (VERDICT_TONE[lastRecheck.verdict] || ''), VERDICT_WORDS[lastRecheck.verdict] || lastRecheck.verdict));
|
|
723
|
+
gone.appendChild(el('span', null, 'Just re-checked: ' + lastRecheck.headline + (lastRecheck.why ? ' - ' + lastRecheck.why : '')));
|
|
724
|
+
if (lastRecheck.newProblems.length) gone.appendChild(el('span', 'tag ember', plural(lastRecheck.newProblems.length, 'problem') + ' newly broken - marked below'));
|
|
725
|
+
body.appendChild(gone);
|
|
726
|
+
}
|
|
727
|
+
if (!shown.length) {
|
|
728
|
+
body.appendChild(el('p', 'empty', state.findings.length ? 'Nothing matches.' : 'Nothing found by the checks that are on and have run. That is not the same as nothing wrong - see which checks are not built yet.'));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
var newlyBroken = (lastRecheck ? lastRecheck.newProblems : []).map(function (p) { return p.id; });
|
|
732
|
+
shown.forEach(function (f) {
|
|
733
|
+
var card = el('div', 'finding ' + (f.status === 'confirmed' ? 'bad' : 'warn'));
|
|
734
|
+
var head = el('button', 'finding-head');
|
|
735
|
+
head.type = 'button';
|
|
736
|
+
var left = el('div', 'finding-left');
|
|
737
|
+
left.appendChild(el('div', 'f-title', f.headline));
|
|
738
|
+
var meta = el('div', 'f-meta');
|
|
739
|
+
meta.appendChild(sevTag(f.severity));
|
|
740
|
+
meta.appendChild(statusTag(f));
|
|
741
|
+
meta.appendChild(el('span', 'tag ' + (CHECK_TONE[f.check] || ''), CHECK_NAME[f.check] || f.check));
|
|
742
|
+
if (f.from === 'nightly' || f.from === 'both') meta.appendChild(el('span', 'tag', f.from === 'both' ? 'also nightly' : 'nightly'));
|
|
743
|
+
// The latest re-check, on the finding itself: still open, or newly
|
|
744
|
+
// broken by a fix for something else.
|
|
745
|
+
var latest = (f.attempts || [])[f.attempts ? f.attempts.length - 1 : 0];
|
|
746
|
+
if (latest) meta.appendChild(el('span', 'tag ' + (VERDICT_TONE[latest.verdict] || ''), (VERDICT_WORDS[latest.verdict] || latest.verdict) + ' ' + ago(latest.at)));
|
|
747
|
+
if (newlyBroken.indexOf(f.id) !== -1) meta.appendChild(el('span', 'tag ember', 'NEWLY BROKEN'));
|
|
748
|
+
meta.appendChild(el('span', 'f-where', f.where));
|
|
749
|
+
left.appendChild(meta);
|
|
750
|
+
head.appendChild(left);
|
|
751
|
+
head.appendChild(icon(openRow === f.id ? 'change' : 'arrow', 18));
|
|
752
|
+
head.addEventListener('click', function () { openRow = openRow === f.id ? null : f.id; renderFindings(); });
|
|
753
|
+
card.appendChild(head);
|
|
754
|
+
if (pending && pending.step === 'finding' && pending.finding && pending.finding.id === f.id) {
|
|
755
|
+
var asking = el('div', 'controls f-quick');
|
|
756
|
+
asking.appendChild(consentBox('Yes, check it again', pending.words.consent));
|
|
757
|
+
card.appendChild(asking);
|
|
758
|
+
} else if (openRow !== f.id) {
|
|
759
|
+
// The two things to do with a finding, without opening it first.
|
|
760
|
+
var quick = el('div', 'f-quick');
|
|
761
|
+
if (f.fixPrompt) quick.appendChild(button('sm', 'copy', 'Copy fix prompt', function () { copy(f.fixPrompt, 'Fix prompt copied - paste it into your AI tool'); }));
|
|
762
|
+
var qre = button('sm', 'refresh', 'Re-check', function () { recheck(f); });
|
|
763
|
+
qre.setAttribute('data-action', 'recheck');
|
|
764
|
+
qre.disabled = busy;
|
|
765
|
+
quick.appendChild(withCommand(qre, f.check === 'data' ? 'npx kryptheon-night' : 'npx kryptheon recheck ' + f.id));
|
|
766
|
+
quick.appendChild(whenTag('after every change'));
|
|
767
|
+
card.appendChild(quick);
|
|
768
|
+
}
|
|
769
|
+
if (openRow === f.id) card.appendChild(detailBox(f));
|
|
770
|
+
body.appendChild(card);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* -------------------------------- other views -------------------------------- */
|
|
775
|
+
|
|
776
|
+
var LOOKS_SHOWN = 12;
|
|
777
|
+
function renderTimeline() {
|
|
778
|
+
var box = byId('timeline');
|
|
779
|
+
clear(box);
|
|
780
|
+
var looks = state.looks || [];
|
|
781
|
+
byId('lookBadge').textContent = String(looks.length);
|
|
782
|
+
if (!looks.length) {
|
|
783
|
+
box.appendChild(el('p', 'empty', 'Kryptheon has not looked at a change yet. It does by itself while this page is open, and whenever npx kryptheon verify runs.'));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
(allLooks ? looks : looks.slice(0, LOOKS_SHOWN)).forEach(function (look) {
|
|
787
|
+
var item = el('div', 'tl');
|
|
788
|
+
item.appendChild(el('div', 'knob' + (look.newFindings ? ' bad' : look.fixed ? ' ok' : look.goneWithFile ? ' mid' : '')));
|
|
789
|
+
var body = el('div');
|
|
790
|
+
body.appendChild(el('div', 'tl-head', look.first ? 'Starting point'
|
|
791
|
+
: look.fileCount ? plural(look.fileCount, 'file') + ' changed - ' + look.parts.join(', ') : 'Nothing changed'));
|
|
792
|
+
body.appendChild(el('div', 'tl-meta', (look.by === 'watch' ? 'Noticed by itself' : look.by === 'recheck' ? 'Re-check' : 'npx kryptheon verify') + ' - ' + when(look.at)));
|
|
793
|
+
var tags = el('div', 'tl-body');
|
|
794
|
+
if (look.newFindings) tags.appendChild(el('span', 'tag ember', look.newFindings + ' new'));
|
|
795
|
+
if (look.fixed) tags.appendChild(el('span', 'tag leaf', look.fixed + ' fixed'));
|
|
796
|
+
if (look.goneWithFile) tags.appendChild(el('span', 'tag amber', look.goneWithFile + ' gone with its file'));
|
|
797
|
+
look.checks.forEach(function (c) { tags.appendChild(el('span', 'tag', c.id + ': ' + c.state)); });
|
|
798
|
+
body.appendChild(tags);
|
|
799
|
+
var files = el('div', 'tl-files');
|
|
800
|
+
look.files.slice(0, 5).forEach(function (f) { files.appendChild(el('div', null, f.state + ' ' + f.path)); });
|
|
801
|
+
if (look.fileCount > 5) files.appendChild(el('div', null, '... and ' + (look.fileCount - 5) + ' more'));
|
|
802
|
+
look.dependencies.forEach(function (d) { files.appendChild(el('div', null, 'package ' + d.name + ': ' + d.change + (d.to ? ' ' + d.to : ''))); });
|
|
803
|
+
body.appendChild(files);
|
|
804
|
+
item.appendChild(body);
|
|
805
|
+
box.appendChild(item);
|
|
806
|
+
});
|
|
807
|
+
if (looks.length > LOOKS_SHOWN) {
|
|
808
|
+
box.appendChild(button('sm', null, allLooks ? 'Show the latest ' + LOOKS_SHOWN : 'Show all ' + looks.length, function () { allLooks = !allLooks; renderTimeline(); }));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function renderFixes() {
|
|
813
|
+
var box = byId('fixList');
|
|
814
|
+
clear(box);
|
|
815
|
+
var list = state.proven || [];
|
|
816
|
+
byId('fixBadge').textContent = String(list.length);
|
|
817
|
+
if (!list.length) { box.appendChild(el('p', 'empty', 'No fix has been proven yet. Open a finding, fix it, and press Re-check.')); return; }
|
|
818
|
+
list.forEach(function (p) {
|
|
819
|
+
var row = el('div', 'line-row');
|
|
820
|
+
row.appendChild(el('span', 'tag leaf', 'FIXED'));
|
|
821
|
+
var text = el('div', 'lr-text');
|
|
822
|
+
text.appendChild(el('div', null, p.headline));
|
|
823
|
+
text.appendChild(el('div', 'lr-sub', p.where + ' - proven ' + ago(p.at) + (p.newProblems ? ' - ' + plural(p.newProblems, 'new problem') + ' at the time' : '')));
|
|
824
|
+
row.appendChild(text);
|
|
825
|
+
box.appendChild(row);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
var STATE_TONE = { 'problems found': 'ember', 'things to check': 'amber', 'last run held': 'leaf', 'nothing found': 'leaf' };
|
|
830
|
+
function checkSwitch(c) {
|
|
831
|
+
var sw = el('label', 'switch');
|
|
832
|
+
var input = el('input');
|
|
833
|
+
input.type = 'checkbox';
|
|
834
|
+
input.checked = !!c.enabled;
|
|
835
|
+
input.disabled = !c.available;
|
|
836
|
+
input.setAttribute('aria-label', c.label + (c.available ? '' : ' - not available'));
|
|
837
|
+
input.addEventListener('change', function () { setCheck(c.id, input.checked, input); });
|
|
838
|
+
sw.appendChild(input);
|
|
839
|
+
sw.appendChild(el('span', 'track'));
|
|
840
|
+
return sw;
|
|
841
|
+
}
|
|
842
|
+
function renderChecks() {
|
|
843
|
+
var box = byId('checkList');
|
|
844
|
+
clear(box);
|
|
845
|
+
var na = byId('naList');
|
|
846
|
+
clear(na);
|
|
847
|
+
var missing = 0;
|
|
848
|
+
state.checks.forEach(function (c) {
|
|
849
|
+
if (!c.available) {
|
|
850
|
+
missing++;
|
|
851
|
+
var row = el('div', 'na');
|
|
852
|
+
row.appendChild(checkSwitch(c));
|
|
853
|
+
var nm = el('div', null, c.label);
|
|
854
|
+
nm.appendChild(el('span', 'na-lock', 'NOT AVAILABLE'));
|
|
855
|
+
row.appendChild(nm);
|
|
856
|
+
na.appendChild(row);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
var card = el('div', 'check');
|
|
860
|
+
var top = el('div', 'c-top');
|
|
861
|
+
top.appendChild(el('div', 'c-name', c.label));
|
|
862
|
+
top.appendChild(checkSwitch(c));
|
|
863
|
+
card.appendChild(top);
|
|
864
|
+
var st = el('div');
|
|
865
|
+
st.appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
|
|
866
|
+
card.appendChild(st);
|
|
867
|
+
if (c.what) card.appendChild(el('div', 'c-what', c.what));
|
|
868
|
+
// What the nightly run inside the database last said - its own answer,
|
|
869
|
+
// not this terminal's, so it gets its own line.
|
|
870
|
+
if (c.nightly) card.appendChild(el('div', 'c-what', c.nightly));
|
|
871
|
+
if (c.runnable && c.enabled) {
|
|
872
|
+
var run = button('sm', 'play', 'Run now', function () { runChecks([c.id], run); });
|
|
873
|
+
run.setAttribute('data-action', 'run');
|
|
874
|
+
run.disabled = busy;
|
|
875
|
+
card.appendChild(run);
|
|
876
|
+
} else if (!c.enabled) {
|
|
877
|
+
card.appendChild(el('div', 'c-what', 'Switched off - it will not run.'));
|
|
878
|
+
}
|
|
879
|
+
box.appendChild(card);
|
|
880
|
+
});
|
|
881
|
+
byId('naBadge').textContent = String(missing);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function renderRuns() {
|
|
885
|
+
var box = byId('runList');
|
|
886
|
+
clear(box);
|
|
887
|
+
byId('runBadge').textContent = String(state.runs.length);
|
|
888
|
+
if (!state.runs.length) { box.appendChild(el('p', 'empty', 'No recorded flows have been replayed yet. Record one with npx kryptheon record, then press Run checks.')); return; }
|
|
889
|
+
state.runs.slice(0, 30).forEach(function (r) {
|
|
890
|
+
var row = el('div', 'line-row');
|
|
891
|
+
row.appendChild(el('span', 'tag ' + (r.failed ? 'ember' : 'leaf'), r.failed ? r.failed + ' broke' : 'held'));
|
|
892
|
+
var text = el('div', 'lr-text');
|
|
893
|
+
text.appendChild(el('div', null, (r.passed || 0) + ' held, ' + (r.failed || 0) + ' broke'));
|
|
894
|
+
text.appendChild(el('div', 'lr-sub', when(r.runAt)));
|
|
895
|
+
row.appendChild(text);
|
|
896
|
+
if (r.failed) row.appendChild(button('sm', 'arrow', 'See the finding', function () { go('findings', 'confirmed'); }));
|
|
897
|
+
box.appendChild(row);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
var STORE_FILES = [
|
|
902
|
+
['baselines.json', 'what each page looked like the last time it worked'],
|
|
903
|
+
['history.jsonl', 'every run of your recorded flows'],
|
|
904
|
+
['test-results/', 'screenshots and traces from runs that broke'],
|
|
905
|
+
['code-findings.json', 'the last read of your frontend code'],
|
|
906
|
+
['night-last.json', 'the last database check'],
|
|
907
|
+
['night-nightly.json', 'what the nightly run inside your database last said'],
|
|
908
|
+
['snapshot.json', 'what your files looked like at the last look'],
|
|
909
|
+
['changes.jsonl', 'every look: what changed, what broke, what got fixed'],
|
|
910
|
+
['fixes.jsonl', 'every re-check and its verdict'],
|
|
911
|
+
['config.json', 'which checks are switched on'],
|
|
912
|
+
];
|
|
913
|
+
function renderStorage() {
|
|
914
|
+
byId('storeNote').textContent = 'Everything on this dashboard is read from this folder on your machine. Nothing is sent anywhere. Your project keeps only its recordings in tests/.';
|
|
915
|
+
byId('storePath').textContent = state.project.storeDir;
|
|
916
|
+
var box = byId('storeFiles');
|
|
917
|
+
clear(box);
|
|
918
|
+
STORE_FILES.forEach(function (f) {
|
|
919
|
+
var row = el('div');
|
|
920
|
+
row.appendChild(el('code', null, f[0]));
|
|
921
|
+
row.appendChild(el('span', null, f[1]));
|
|
922
|
+
box.appendChild(row);
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* ---------------------------------- actions ---------------------------------- */
|
|
927
|
+
|
|
928
|
+
var VERDICT_WORDS = { fixed: 'FIXED', 'still open': 'STILL OPEN', 'gone with its file': 'GONE WITH ITS FILE - not the same as fixed', 'could not confirm': 'COULD NOT CONFIRM' };
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Re-checks one finding. Its answer stays on the page: on the finding while
|
|
932
|
+
* it is still open, as a line at the top of Findings once it is fixed and
|
|
933
|
+
* gone from the list, and on every problem the fix newly broke.
|
|
934
|
+
*/
|
|
935
|
+
function recheck(f) {
|
|
936
|
+
// A database finding is re-checked in the database: the same check, the
|
|
937
|
+
// same yes first, then this one finding judged.
|
|
938
|
+
if (f.check === 'data') { prepare('recheck', 'finding', f); return; }
|
|
939
|
+
runJob('/api/recheck', { id: f.id }).then(function (done) { heard(f, (done && done.result) || {}); }, function () {});
|
|
940
|
+
}
|
|
941
|
+
function heard(f, r) {
|
|
942
|
+
if (!r.verdict) { toast('The re-check gave no verdict' + (r.why ? ' - ' + r.why : '')); return; }
|
|
943
|
+
lastRecheck = { id: f.id, headline: f.headline, verdict: r.verdict, why: r.why || '', newProblems: r.newProblems || [] };
|
|
944
|
+
renderFindings();
|
|
945
|
+
toast((VERDICT_WORDS[r.verdict] || r.verdict) + (r.why ? ' - ' + r.why : '') + (lastRecheck.newProblems.length ? ' - ' + plural(lastRecheck.newProblems.length, 'problem') + ' newly broken' : ''));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Runs each check in turn, each as its own job, and says when all are done. */
|
|
949
|
+
function runChecks(ids) {
|
|
950
|
+
if (busy || !ids.length) return;
|
|
951
|
+
var failed = [];
|
|
952
|
+
ids.reduce(function (chain, id) {
|
|
953
|
+
return chain.then(function () {
|
|
954
|
+
return runJob('/api/run', { id: id }).then(function (done) {
|
|
955
|
+
if (!done || !done.result || !done.result.ok) failed.push(CHECK_NAME[id] || id);
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
}, Promise.resolve()).then(function () {
|
|
959
|
+
toast(failed.length ? 'Did not finish: ' + failed.join(', ') : 'Finished: ' + ids.map(function (id) { return CHECK_NAME[id] || id; }).join(', '));
|
|
960
|
+
}, function () {});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function setCheck(id, on, input) {
|
|
964
|
+
post('/api/check', { id: id, on: on }).then(function () {
|
|
965
|
+
toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
|
|
966
|
+
return load();
|
|
967
|
+
}).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
byId('runAll').addEventListener('click', function () {
|
|
971
|
+
if (!state) return;
|
|
972
|
+
var ids = state.checks.filter(function (c) { return c.runnable && c.enabled; }).map(function (c) { return c.id; });
|
|
973
|
+
if (!ids.length) { toast('Nothing can run from here yet - switch a check on, or record a flow.'); return; }
|
|
974
|
+
runChecks(ids, byId('runAll'));
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
/* ----------------------------------- load ----------------------------------- */
|
|
978
|
+
|
|
979
|
+
function renderTop() {
|
|
980
|
+
var run = byId('runAll');
|
|
981
|
+
if (!busy) { clear(run); run.appendChild(icon('play', 15)); run.appendChild(el('span', null, 'Run checks')); }
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function render(next) {
|
|
985
|
+
if (!next) return;
|
|
986
|
+
state = next;
|
|
987
|
+
renderSide();
|
|
988
|
+
renderTop();
|
|
989
|
+
renderNow();
|
|
990
|
+
renderSteps();
|
|
991
|
+
renderStatusFilter();
|
|
992
|
+
renderFindings();
|
|
993
|
+
renderTimeline();
|
|
994
|
+
renderFixes();
|
|
995
|
+
renderChecks();
|
|
996
|
+
renderRuns();
|
|
997
|
+
renderStorage();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function load() {
|
|
1001
|
+
return fetch('/api/state', { cache: 'no-store' })
|
|
1002
|
+
.then(function (res) { if (!res.ok) throw new Error('the dashboard answered ' + res.status); return res.json(); })
|
|
1003
|
+
.then(render)
|
|
1004
|
+
.catch(function (err) { byId('subline').textContent = 'Could not load: ' + err.message; });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
renderAI();
|
|
1008
|
+
showView();
|
|
1009
|
+
load().then(function () {
|
|
1010
|
+
// A run started before this page was opened - or before a reload - is
|
|
1011
|
+
// still followed to its end, and nothing else starts meanwhile.
|
|
1012
|
+
return getJob().then(function (now) {
|
|
1013
|
+
if (!now || !now.running) return;
|
|
1014
|
+
job = now;
|
|
1015
|
+
setBusy(true);
|
|
1016
|
+
renderJob();
|
|
1017
|
+
return follow().then(function () { setBusy(false); return load(); });
|
|
1018
|
+
});
|
|
1019
|
+
}).catch(function () {});
|
|
1020
|
+
// Not while something runs, not while a finding is open being read, not
|
|
1021
|
+
// while a yes is being asked for, and not while someone is typing.
|
|
1022
|
+
setInterval(function () {
|
|
1023
|
+
var typing = document.activeElement === dbInput || document.activeElement === recordInput;
|
|
1024
|
+
if (!busy && !openRow && !pending && !typing) load();
|
|
1025
|
+
}, 5000);
|
|
1026
|
+
})();
|