kryptheon 0.1.21 → 0.1.23
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 +4 -0
- package/bin/kryptheon.js +2 -0
- package/kryptheon-archivo.woff2 +0 -0
- package/kryptheon-code.js +221 -14
- package/kryptheon-dashboard-page.js +247 -553
- package/kryptheon-dashboard.html +378 -446
- package/kryptheon-dashboard.js +27 -9
- package/kryptheon-fonts-OFL.txt +98 -0
- package/kryptheon-spacemono.woff2 +0 -0
- package/package.json +4 -1
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// Every value on this page can hold text from the app being checked - a line
|
|
4
4
|
// of its code, the heading of a page that broke. So nothing here is ever
|
|
5
5
|
// written as HTML: every node is made with createElement (or createElementNS
|
|
6
|
-
// for the
|
|
7
|
-
//
|
|
8
|
-
// must come back with nothing to report.
|
|
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.
|
|
9
8
|
|
|
10
9
|
(function () {
|
|
11
10
|
'use strict';
|
|
@@ -15,9 +14,6 @@
|
|
|
15
14
|
var NS = 'http://www.w3.org/2000/svg';
|
|
16
15
|
var state = null;
|
|
17
16
|
var busy = false;
|
|
18
|
-
var statusFilter = 'all';
|
|
19
|
-
var range = 'all';
|
|
20
|
-
var query = '';
|
|
21
17
|
var openRow = null;
|
|
22
18
|
var allLooks = false;
|
|
23
19
|
|
|
@@ -34,20 +30,8 @@
|
|
|
34
30
|
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, String(attrs[k])); });
|
|
35
31
|
return node;
|
|
36
32
|
}
|
|
37
|
-
function clear(node) {
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
function byId(id) {
|
|
41
|
-
return document.getElementById(id);
|
|
42
|
-
}
|
|
43
|
-
// A sentence with its important words marked: parts are plain strings, or
|
|
44
|
-
// { hl: text } for the words someone skimming must not miss.
|
|
45
|
-
function rich(node, parts) {
|
|
46
|
-
parts.forEach(function (p) {
|
|
47
|
-
node.appendChild(typeof p === 'string' ? document.createTextNode(p) : el('span', 'hl', p.hl));
|
|
48
|
-
});
|
|
49
|
-
return node;
|
|
50
|
-
}
|
|
33
|
+
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
34
|
+
function byId(id) { return document.getElementById(id); }
|
|
51
35
|
|
|
52
36
|
// Line icons from fixed path data only.
|
|
53
37
|
var ICONS = {
|
|
@@ -57,10 +41,7 @@
|
|
|
57
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',
|
|
58
42
|
toggle: 'M8 7h8a5 5 0 010 10H8A5 5 0 018 7zm0 3a2 2 0 100 4 2 2 0 000-4z',
|
|
59
43
|
play: 'M7 5l12 7-12 7V5z',
|
|
60
|
-
shield: 'M12 2l8 3v6c0 5-3.4 9.4-8 11-4.6-1.6-8-6-8-11V5l8-3z',
|
|
61
44
|
box: 'M4 7l8-4 8 4v10l-8 4-8-4V7zm0 0l8 4 8-4M12 11v10',
|
|
62
|
-
search: 'M11 18a7 7 0 100-14 7 7 0 000 14zm9 3l-4.3-4.3',
|
|
63
|
-
bell: 'M6 16V11a6 6 0 1112 0v5l2 2H4l2-2zm4 4h4',
|
|
64
45
|
alert: 'M12 3l10 18H2L12 3zm0 7v5m0 3v.5',
|
|
65
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',
|
|
66
47
|
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
@@ -69,7 +50,8 @@
|
|
|
69
50
|
refresh: 'M20 12a8 8 0 10-2.3 5.7M20 12V6m0 6h-6',
|
|
70
51
|
arrow: 'M5 12h14m-6-6l6 6-6 6',
|
|
71
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',
|
|
72
|
-
|
|
53
|
+
terminal: 'M4 5h16v14H4zM7 9l3 3-3 3M13 15h4',
|
|
54
|
+
dot: 'M12 12h.01',
|
|
73
55
|
};
|
|
74
56
|
function icon(name, size) {
|
|
75
57
|
var s = svgEl('svg', {
|
|
@@ -85,10 +67,6 @@
|
|
|
85
67
|
var d = new Date(iso);
|
|
86
68
|
return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
|
|
87
69
|
}
|
|
88
|
-
function shortDate(iso) {
|
|
89
|
-
var d = new Date(iso);
|
|
90
|
-
return isNaN(d.getTime()) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
91
|
-
}
|
|
92
70
|
function ago(iso) {
|
|
93
71
|
var d = new Date(iso);
|
|
94
72
|
if (isNaN(d.getTime())) return '';
|
|
@@ -98,9 +76,7 @@
|
|
|
98
76
|
if (s < 86400) return Math.round(s / 3600) + ' h ago';
|
|
99
77
|
return Math.round(s / 86400) + ' d ago';
|
|
100
78
|
}
|
|
101
|
-
function plural(n, one, many) {
|
|
102
|
-
return n + ' ' + (n === 1 ? one : many || one + 's');
|
|
103
|
-
}
|
|
79
|
+
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many || one + 's'); }
|
|
104
80
|
|
|
105
81
|
function toast(text) {
|
|
106
82
|
var t = byId('toast');
|
|
@@ -127,13 +103,11 @@
|
|
|
127
103
|
busy = on;
|
|
128
104
|
Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
|
|
129
105
|
}
|
|
130
|
-
|
|
131
106
|
function copy(text, done) {
|
|
132
107
|
navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
|
|
133
108
|
toast('Could not copy - select the text instead');
|
|
134
109
|
});
|
|
135
110
|
}
|
|
136
|
-
|
|
137
111
|
function button(cls, iconName, text, onClick) {
|
|
138
112
|
var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
|
|
139
113
|
b.type = 'button';
|
|
@@ -145,67 +119,35 @@
|
|
|
145
119
|
|
|
146
120
|
var CHECK_NAME = { regression: 'Recorded flows', frontend: 'Frontend code', data: 'Database' };
|
|
147
121
|
var CHECK_TONE = { regression: 'sun', frontend: 'amber', data: 'dusk' };
|
|
148
|
-
var CHECK_COLOR = { regression: '#ec7433', frontend: '#d49b33', data: '#6d4a7c' };
|
|
149
122
|
|
|
150
123
|
function counts() {
|
|
151
124
|
var confirmed = state.findings.filter(function (f) { return f.status === 'confirmed'; }).length;
|
|
152
|
-
return {
|
|
153
|
-
confirmed: confirmed,
|
|
154
|
-
toVerify: state.findings.length - confirmed,
|
|
155
|
-
lastRun: state.runs[0],
|
|
156
|
-
lastLook: (state.looks || [])[0],
|
|
157
|
-
proven: (state.proven || []).length,
|
|
158
|
-
built: state.checks.filter(function (c) { return c.available; }).length,
|
|
159
|
-
};
|
|
125
|
+
return { confirmed: confirmed, toVerify: state.findings.length - confirmed, proven: (state.proven || []).length };
|
|
160
126
|
}
|
|
161
127
|
|
|
162
128
|
/* ----------------------------------- views ----------------------------------- */
|
|
163
129
|
|
|
164
|
-
// One section on screen at a time; the address keeps it, so a reload
|
|
165
|
-
//
|
|
130
|
+
// One section on screen at a time; the address keeps it, so a reload lands
|
|
131
|
+
// on the same one.
|
|
166
132
|
var VIEWS = [
|
|
167
|
-
{ id: 'dashboard', title: 'Dashboard', icon: 'grid'
|
|
168
|
-
{ id: 'findings', title: 'Findings', icon: 'list'
|
|
169
|
-
{ id: 'changes', title: 'What changed', icon: 'change'
|
|
170
|
-
{ id: 'fixes', title: 'Proven fixes', icon: 'wrench'
|
|
171
|
-
{ id: 'checks', title: 'Checks', icon: 'toggle'
|
|
172
|
-
{ id: 'runs', title: 'Runs', icon: 'play'
|
|
173
|
-
{ id: '
|
|
174
|
-
{ id: 'storage', title: 'Where this lives', icon: 'box', group: 'navWorkspace' },
|
|
133
|
+
{ id: 'dashboard', title: 'Dashboard', icon: 'grid' },
|
|
134
|
+
{ id: 'findings', title: 'Findings', icon: 'list' },
|
|
135
|
+
{ id: 'changes', title: 'What changed', icon: 'change' },
|
|
136
|
+
{ id: 'fixes', title: 'Proven fixes', icon: 'wrench' },
|
|
137
|
+
{ id: 'checks', title: 'Checks', icon: 'toggle' },
|
|
138
|
+
{ id: 'runs', title: 'Runs', icon: 'play' },
|
|
139
|
+
{ id: 'storage', title: 'Where this lives', icon: 'box' },
|
|
175
140
|
];
|
|
176
141
|
|
|
177
|
-
// What each section is for, with the words that decide it marked.
|
|
178
|
-
var WHEN = {
|
|
179
|
-
findings: ['Open this when something ', { hl: 'broke or looks risky' }, '. Click a row, ', { hl: 'copy the fix prompt' },
|
|
180
|
-
' into your AI tool, then press ', { hl: 'Re-check' }, ' - it says FIXED only when it is.'],
|
|
181
|
-
changes: ['Open this to see ', { hl: 'what your AI changed' }, ' - every file, and what broke or got fixed with it. ',
|
|
182
|
-
'Kryptheon looks by itself while this page is open.'],
|
|
183
|
-
fixes: ['Open this for ', { hl: 'proof' }, '. A fix is listed only after a ', { hl: 're-check passed' },
|
|
184
|
-
' - never just because the code changed.'],
|
|
185
|
-
checks: ['Open this to ', { hl: 'switch checks on or off' }, ', or run one now. Checks that are not built yet are listed, and ',
|
|
186
|
-
{ hl: 'never counted as passed' }, '.'],
|
|
187
|
-
runs: ['Open this when a ', { hl: 'recorded flow' }, ' - sign-up, login, checkout - ', { hl: 'stopped working' },
|
|
188
|
-
'. Each run replays your recordings against your app.'],
|
|
189
|
-
verification: ['Open this for the ', { hl: 'Kryptheon Verified' }, ' badge. It is ', { hl: 'not built yet' },
|
|
190
|
-
', so nothing here is verified.'],
|
|
191
|
-
storage: ['Open this to see ', { hl: 'where your records are kept' }, '. Nothing is sent anywhere, and ',
|
|
192
|
-
{ hl: 'nothing is written into your project' }, '.'],
|
|
193
|
-
};
|
|
194
|
-
|
|
195
142
|
function currentView() {
|
|
196
143
|
var id = (location.hash || '').replace(/^#/, '');
|
|
197
144
|
return VIEWS.some(function (v) { return v.id === id; }) ? id : 'dashboard';
|
|
198
145
|
}
|
|
199
|
-
|
|
200
146
|
function go(id, filter) {
|
|
201
|
-
if (filter) {
|
|
202
|
-
statusFilter = filter;
|
|
203
|
-
if (state) { renderStatusFilter(); renderFindings(); }
|
|
204
|
-
}
|
|
147
|
+
if (filter) { statusFilter = filter; if (state) { renderStatusFilter(); renderFindings(); } }
|
|
205
148
|
if (currentView() === id) return showView();
|
|
206
149
|
location.hash = id;
|
|
207
150
|
}
|
|
208
|
-
|
|
209
151
|
function showView() {
|
|
210
152
|
var id = currentView();
|
|
211
153
|
VIEWS.forEach(function (v) { byId('view-' + v.id).hidden = v.id !== id; });
|
|
@@ -213,8 +155,6 @@
|
|
|
213
155
|
byId('viewTitle').textContent = view.title;
|
|
214
156
|
document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
|
|
215
157
|
if (state) renderSide();
|
|
216
|
-
// The chart measures itself, so it is drawn again once it is on screen.
|
|
217
|
-
if (state && id === 'dashboard') renderChart();
|
|
218
158
|
window.scrollTo(0, 0);
|
|
219
159
|
}
|
|
220
160
|
window.addEventListener('hashchange', showView);
|
|
@@ -228,351 +168,216 @@
|
|
|
228
168
|
findings: [state.findings.length, c.confirmed > 0],
|
|
229
169
|
changes: [(state.looks || []).length, false],
|
|
230
170
|
fixes: [c.proven, false],
|
|
231
|
-
runs: [state.runs.length, !!(
|
|
171
|
+
runs: [state.runs.length, !!(state.runs[0] && state.runs[0].failed)],
|
|
232
172
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
box.appendChild(b);
|
|
246
|
-
});
|
|
173
|
+
var box = byId('nav');
|
|
174
|
+
clear(box);
|
|
175
|
+
VIEWS.forEach(function (v) {
|
|
176
|
+
var b = el('button', active === v.id ? 'on' : '');
|
|
177
|
+
b.type = 'button';
|
|
178
|
+
b.appendChild(icon(v.icon, 18));
|
|
179
|
+
b.appendChild(el('span', null, v.title));
|
|
180
|
+
var n = badge[v.id];
|
|
181
|
+
if (n && n[0]) b.appendChild(el('span', 'n-count' + (n[1] ? ' hot' : ''), n[0]));
|
|
182
|
+
if (active === v.id) b.setAttribute('aria-current', 'page');
|
|
183
|
+
b.addEventListener('click', function () { go(v.id); });
|
|
184
|
+
box.appendChild(b);
|
|
247
185
|
});
|
|
248
|
-
|
|
249
|
-
byId('
|
|
250
|
-
byId('
|
|
186
|
+
var built = state.checks.filter(function (x) { return x.available; }).length;
|
|
187
|
+
byId('builtText').textContent = built + ' / ' + state.checks.length;
|
|
188
|
+
byId('builtBar').style.width = Math.round((built / state.checks.length) * 100) + '%';
|
|
189
|
+
byId('builtNote').textContent = (state.checks.length - built) + ' more are listed as not available - never as passed.';
|
|
251
190
|
var name = state.project.name || 'Project';
|
|
252
|
-
byId('avatar').textContent = name.replace(/[^A-Za-z0-9]/g, '').slice(0, 2).toUpperCase() || 'K';
|
|
253
|
-
byId('projName').textContent = name;
|
|
254
|
-
byId('projPath').textContent = state.project.root;
|
|
255
|
-
byId('projPath').title = state.project.root;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/* ---------------------------------- topbar ---------------------------------- */
|
|
259
|
-
|
|
260
|
-
function renderTop() {
|
|
261
191
|
var last = (state.looks || [])[0];
|
|
262
|
-
byId('subline').textContent = last ? '
|
|
263
|
-
var si = byId('searchIcon');
|
|
264
|
-
clear(si);
|
|
265
|
-
si.appendChild(icon('search', 16));
|
|
266
|
-
var bell = byId('bell');
|
|
267
|
-
clear(bell);
|
|
268
|
-
bell.appendChild(icon('bell', 20));
|
|
269
|
-
var fresh = last && !last.first ? last.newFindings : 0;
|
|
270
|
-
if (fresh) bell.appendChild(el('span', 'bdot'));
|
|
271
|
-
bell.title = fresh ? plural(fresh, 'new finding') + ' in the last look - open What changed' : 'Nothing new in the last look';
|
|
272
|
-
var run = byId('runAll');
|
|
273
|
-
if (!busy) {
|
|
274
|
-
clear(run);
|
|
275
|
-
run.appendChild(icon('play', 15));
|
|
276
|
-
run.appendChild(el('span', null, 'Run checks'));
|
|
277
|
-
}
|
|
192
|
+
byId('subline').textContent = name + ' - ' + (last ? 'watching, last look ' + ago(last.at) : 'watching for changes');
|
|
278
193
|
}
|
|
279
194
|
|
|
280
|
-
/*
|
|
195
|
+
/* ------------------------------ right now + steps ------------------------------ */
|
|
281
196
|
|
|
282
|
-
function
|
|
283
|
-
var c = counts();
|
|
197
|
+
function renderNow() {
|
|
284
198
|
var sky = byId('sky');
|
|
285
199
|
var ic = byId('skyIcon');
|
|
286
200
|
var title = byId('skyTitle');
|
|
287
201
|
var sub = byId('skySub');
|
|
288
202
|
var actions = byId('skyActions');
|
|
289
|
-
clear(ic);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (c.confirmed) {
|
|
203
|
+
clear(ic); clear(title); clear(sub); clear(actions);
|
|
204
|
+
var next = state.next || { kind: 'quiet', title: 'Nothing open' };
|
|
205
|
+
byId('skyEyebrow').textContent = next.kind === 'quiet' ? 'ALL QUIET' : 'DO THIS NEXT';
|
|
206
|
+
if (next.kind === 'fix') {
|
|
294
207
|
sky.className = 'sky bad';
|
|
295
208
|
ic.style.color = 'var(--ember)';
|
|
296
209
|
ic.appendChild(icon('alert', 28));
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
rich(sub, ['Proven by a check that ran. Open ', { hl: 'Findings' }, ', copy the fix prompt into your AI tool, then press ',
|
|
300
|
-
{ hl: 'Re-check' }, '.' + (c.toVerify ? ' ' + c.toVerify + (c.toVerify === 1 ? ' more needs' : ' more need') + ' a look.' : '')]);
|
|
210
|
+
title.textContent = next.title;
|
|
211
|
+
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.';
|
|
301
212
|
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'confirmed'); }));
|
|
302
|
-
} else if (
|
|
213
|
+
} else if (next.kind === 'step') {
|
|
214
|
+
sky.className = 'sky';
|
|
215
|
+
ic.style.color = 'var(--sun-deep)';
|
|
216
|
+
ic.appendChild(icon('arrow', 28));
|
|
217
|
+
title.textContent = next.title;
|
|
218
|
+
sub.textContent = 'The next step in setting Kryptheon up. Do it below in Start here.';
|
|
219
|
+
actions.appendChild(button('sun', 'arrow', 'Go to Start here', function () { go('dashboard'); scrollToId('steps'); }));
|
|
220
|
+
} else if (next.kind === 'verify') {
|
|
303
221
|
sky.className = 'sky';
|
|
304
222
|
ic.style.color = 'var(--amber)';
|
|
305
223
|
ic.appendChild(icon('eye', 28));
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
rich(sub, ['Nothing confirmed broken. These were spotted ', { hl: 'without running anything' },
|
|
309
|
-
' - each needs a person to decide.']);
|
|
224
|
+
title.textContent = next.title;
|
|
225
|
+
sub.textContent = 'Spotted without running anything. Each needs a person to decide.';
|
|
310
226
|
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'verify'); }));
|
|
311
227
|
} else {
|
|
312
228
|
sky.className = 'sky ok';
|
|
313
229
|
ic.style.color = 'var(--leaf)';
|
|
314
230
|
ic.appendChild(icon('sun', 28));
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
' - ' + (state.checks.length - c.built) + ' checks are not built yet.']);
|
|
319
|
-
actions.appendChild(button('sun', 'arrow', 'See the checks', function () { go('checks'); }));
|
|
320
|
-
}
|
|
321
|
-
if (c.lastLook && !c.lastLook.first && c.lastLook.fileCount) {
|
|
322
|
-
actions.appendChild(button('', 'change', 'What changed', function () { go('changes'); }));
|
|
231
|
+
title.textContent = 'Nothing open';
|
|
232
|
+
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.';
|
|
233
|
+
actions.appendChild(button('', 'toggle', 'See the checks', function () { go('checks'); }));
|
|
323
234
|
}
|
|
324
235
|
}
|
|
325
236
|
|
|
326
|
-
function
|
|
327
|
-
var
|
|
328
|
-
|
|
329
|
-
var top = el('div', 'kpi-top');
|
|
330
|
-
var chip = el('div', 'ichip ' + tone);
|
|
331
|
-
chip.appendChild(icon(iconName, 18));
|
|
332
|
-
top.appendChild(chip);
|
|
333
|
-
top.appendChild(el('div', 'kpi-label', label));
|
|
334
|
-
box.appendChild(top);
|
|
335
|
-
box.appendChild(el('div', 'kpi-num', value));
|
|
336
|
-
var f = el('div', 'kpi-foot');
|
|
337
|
-
foot.forEach(function (part) { f.appendChild(part); });
|
|
338
|
-
box.appendChild(f);
|
|
339
|
-
box.appendChild(el('div', 'kpi-go', goText + ' →'));
|
|
340
|
-
box.addEventListener('click', onOpen);
|
|
341
|
-
return box;
|
|
342
|
-
}
|
|
343
|
-
function delta(kind, text) {
|
|
344
|
-
return el('span', 'delta ' + kind, (kind === 'up' ? '▲ ' : kind === 'down' ? '▼ ' : '') + text);
|
|
237
|
+
function scrollToId(id) {
|
|
238
|
+
var t = byId(id);
|
|
239
|
+
if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
345
240
|
}
|
|
346
241
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
var
|
|
352
|
-
var week = Date.now() - 7 * 86400000;
|
|
353
|
-
var fixedWeek = (state.proven || []).filter(function (p) { return new Date(p.at).getTime() > week; }).length;
|
|
354
|
-
box.appendChild(kpi('CONFIRMED', c.confirmed, 'alert', 'ember', newFound
|
|
355
|
-
? [delta('down', newFound + ' new'), el('span', null, 'in the last look')]
|
|
356
|
-
: c.confirmed ? [el('span', null, 'open now, each with a fix prompt')] : [delta('up', 'none open')],
|
|
357
|
-
'Open Findings', function () { go('findings', 'confirmed'); }));
|
|
358
|
-
box.appendChild(kpi('TO VERIFY', c.toVerify, 'eye', 'amber', [el('span', null, 'spotted without running anything')],
|
|
359
|
-
'Open Findings', function () { go('findings', 'verify'); }));
|
|
360
|
-
box.appendChild(kpi('PROVEN FIXES', c.proven, 'wrench', 'leaf', fixedWeek ? [delta('up', fixedWeek + ' this week')] : [delta('flat', 'none this week')],
|
|
361
|
-
'Open Proven fixes', function () { go('fixes'); }));
|
|
362
|
-
var r = c.lastRun;
|
|
363
|
-
box.appendChild(kpi('FLOWS HELD', r ? r.passed + ' / ' + (r.passed + r.failed) : '-', 'film', 'sun', r
|
|
364
|
-
? [delta(r.failed ? 'down' : 'up', r.failed ? r.failed + ' broke' : 'all held'), el('span', null, ago(r.runAt))]
|
|
365
|
-
: [el('span', null, plural(state.recordings, 'recording') + ', never run')],
|
|
366
|
-
'Open Runs', function () { go('runs'); }));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function renderGuide() {
|
|
370
|
-
var box = byId('guide');
|
|
242
|
+
var STEP_WORD = { done: 'Done', todo: 'To do', waiting: 'Waiting', off: 'Switched off' };
|
|
243
|
+
var STEP_TONE = { done: 'leaf', todo: 'sun', waiting: 'amber', off: 'muted' };
|
|
244
|
+
|
|
245
|
+
function renderSteps() {
|
|
246
|
+
var box = byId('steps');
|
|
371
247
|
clear(box);
|
|
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
|
-
row.appendChild(
|
|
402
|
-
var to = el('span', 'g-to');
|
|
403
|
-
to.appendChild(el('span', 'ul', v.title));
|
|
404
|
-
to.appendChild(icon('arrow', 15));
|
|
405
|
-
row.appendChild(to);
|
|
406
|
-
row.addEventListener('click', function () { go(it.view); });
|
|
248
|
+
(state.steps || []).forEach(function (s, i) {
|
|
249
|
+
var row = el('div', 'step ' + s.state);
|
|
250
|
+
var num = el('div', 'step-num');
|
|
251
|
+
if (s.state === 'done') num.appendChild(icon('check', 18));
|
|
252
|
+
else num.textContent = String(i + 1).padStart(2, '0');
|
|
253
|
+
row.appendChild(num);
|
|
254
|
+
var body = el('div', 'step-body');
|
|
255
|
+
var head = el('div', 'step-head');
|
|
256
|
+
head.appendChild(el('div', 'step-title', s.title));
|
|
257
|
+
head.appendChild(el('span', 'tag ' + STEP_TONE[s.state], STEP_WORD[s.state] || s.state));
|
|
258
|
+
body.appendChild(head);
|
|
259
|
+
body.appendChild(el('div', 'step-why', s.why + (s.at ? ' - last ' + ago(s.at) : '') + (s.count ? ' - ' + plural(s.count, 'recording') + ' held' : '')));
|
|
260
|
+
if (s.action && s.action.kind === 'run') {
|
|
261
|
+
var run = button('sun sm', 'play', s.action.label, function () { runChecks([s.action.check], run); });
|
|
262
|
+
run.setAttribute('data-action', 'run');
|
|
263
|
+
run.disabled = busy;
|
|
264
|
+
body.appendChild(run);
|
|
265
|
+
} else if (s.action && s.action.kind === 'command') {
|
|
266
|
+
var cmd = el('div', 'cmd');
|
|
267
|
+
cmd.appendChild(icon('terminal', 15));
|
|
268
|
+
cmd.appendChild(el('code', null, s.action.command));
|
|
269
|
+
var cp = el('button', 'cmd-copy');
|
|
270
|
+
cp.type = 'button';
|
|
271
|
+
cp.title = 'Copy';
|
|
272
|
+
cp.appendChild(icon('copy', 14));
|
|
273
|
+
cp.addEventListener('click', function () { copy(s.action.command, 'Command copied - run it in your terminal'); });
|
|
274
|
+
cmd.appendChild(cp);
|
|
275
|
+
body.appendChild(cmd);
|
|
276
|
+
}
|
|
277
|
+
row.appendChild(body);
|
|
407
278
|
box.appendChild(row);
|
|
408
279
|
});
|
|
409
280
|
}
|
|
410
281
|
|
|
411
|
-
/*
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
282
|
+
/* ------------------------------ connect your AI tool ------------------------------ */
|
|
283
|
+
|
|
284
|
+
// The same set-up kryptheon.tech shows, so the two never disagree. Every
|
|
285
|
+
// step comes from that tool's own documentation, except where a tool is
|
|
286
|
+
// marked tested - there it was run by hand. Static text: nothing here comes
|
|
287
|
+
// from the project being checked.
|
|
288
|
+
var SERVER = { command: 'npx', args: ['-y', 'kryptheon-mcp'] };
|
|
289
|
+
var MCP_JSON = JSON.stringify({ mcpServers: { kryptheon: SERVER } }, null, 2);
|
|
290
|
+
var VSCODE_JSON = JSON.stringify({ servers: { kryptheon: SERVER } }, null, 2);
|
|
291
|
+
var AI_TOOLS = [
|
|
292
|
+
{ id: 'claude-code', name: 'Claude Code', tested: true,
|
|
293
|
+
steps: ['Run this once, in any terminal. That is the whole set-up.'],
|
|
294
|
+
code: 'claude mcp add kryptheon -- npx -y kryptheon-mcp', file: 'terminal' },
|
|
295
|
+
{ id: 'cursor', name: 'Cursor', tested: false,
|
|
296
|
+
oneClick: { href: 'cursor://anysphere.cursor-deeplink/mcp/install?name=kryptheon&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImtyeXB0aGVvbi1tY3AiXX0=', label: 'Add to Cursor' },
|
|
297
|
+
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.'],
|
|
298
|
+
code: MCP_JSON, file: '~/.cursor/mcp.json' },
|
|
299
|
+
{ id: 'vscode', name: 'VS Code', tested: false,
|
|
300
|
+
oneClick: { href: 'vscode:mcp/install?' + encodeURIComponent(JSON.stringify({ name: 'kryptheon', command: 'npx', args: ['-y', 'kryptheon-mcp'] })), label: 'Install in VS Code' },
|
|
301
|
+
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".'],
|
|
302
|
+
code: VSCODE_JSON, file: '.vscode/mcp.json' },
|
|
303
|
+
{ id: 'antigravity', name: 'Antigravity', tested: false,
|
|
304
|
+
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.'],
|
|
305
|
+
code: MCP_JSON, file: '~/.gemini/config/mcp_config.json' },
|
|
306
|
+
{ id: 'windsurf', name: 'Windsurf', tested: false,
|
|
307
|
+
steps: ['In the Cascade panel, open the ... menu at the top right and click Open MCP config file.', 'Paste the config and save.'],
|
|
308
|
+
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.',
|
|
309
|
+
code: MCP_JSON, file: 'mcp_config.json' },
|
|
310
|
+
{ id: 'claude-desktop', name: 'Claude Desktop', tested: false,
|
|
311
|
+
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.'],
|
|
312
|
+
code: MCP_JSON, file: 'claude_desktop_config.json' },
|
|
313
|
+
{ id: 'codex', name: 'Codex', tested: false,
|
|
314
|
+
steps: ['Run this once, in any terminal.'],
|
|
315
|
+
code: 'codex mcp add kryptheon -- npx -y kryptheon-mcp', file: 'terminal' },
|
|
316
|
+
];
|
|
317
|
+
var aiTool = 'claude-code';
|
|
318
|
+
|
|
319
|
+
function renderAI() {
|
|
320
|
+
var tabs = byId('aiTabs');
|
|
321
|
+
var panel = byId('aiPanel');
|
|
322
|
+
if (!tabs || !panel) return;
|
|
323
|
+
clear(tabs);
|
|
324
|
+
clear(panel);
|
|
325
|
+
AI_TOOLS.forEach(function (t, i) {
|
|
326
|
+
var b = el('button', 'ai-tab' + (t.id === aiTool ? ' on' : ''));
|
|
418
327
|
b.type = 'button';
|
|
419
|
-
b.
|
|
420
|
-
|
|
328
|
+
b.setAttribute('role', 'tab');
|
|
329
|
+
b.setAttribute('aria-selected', t.id === aiTool ? 'true' : 'false');
|
|
330
|
+
b.appendChild(el('span', 'ai-num', String(i + 1).padStart(2, '0')));
|
|
331
|
+
b.appendChild(el('span', 'ai-name', t.name));
|
|
332
|
+
if (t.oneClick) b.appendChild(el('span', 'ai-tag', '1-click'));
|
|
333
|
+
b.addEventListener('click', function () { aiTool = t.id; renderAI(); });
|
|
334
|
+
tabs.appendChild(b);
|
|
421
335
|
});
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
byId('chartSub').textContent = runs.length
|
|
433
|
-
? plural(runs.length, 'run') + ' - ' + shortDate(runs[0].runAt) + ' to ' + shortDate(runs[runs.length - 1].runAt) + ' - red dots broke'
|
|
434
|
-
: 'No runs in this range';
|
|
435
|
-
if (!runs.length) {
|
|
436
|
-
box.appendChild(el('p', 'empty', 'No recorded flows have been replayed in this range.'));
|
|
437
|
-
return;
|
|
336
|
+
var tool = AI_TOOLS.filter(function (t) { return t.id === aiTool; })[0] || AI_TOOLS[0];
|
|
337
|
+
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"));
|
|
338
|
+
panel.appendChild(el('h4', null, tool.name));
|
|
339
|
+
if (tool.oneClick) {
|
|
340
|
+
var oc = el('div', 'ai-oneclick');
|
|
341
|
+
var link = el('a', 'btn sun', tool.oneClick.label + ' →');
|
|
342
|
+
link.href = tool.oneClick.href;
|
|
343
|
+
oc.appendChild(link);
|
|
344
|
+
oc.appendChild(el('small', null, 'or by hand, below'));
|
|
345
|
+
panel.appendChild(oc);
|
|
438
346
|
}
|
|
439
|
-
var
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
var top = Math.max(1, Math.max.apply(null, runs.map(function (r) { return (r.passed || 0) + (r.failed || 0); })));
|
|
446
|
-
var x = function (i) { return runs.length === 1 ? L + (W - L - R) / 2 : L + (i * (W - L - R)) / (runs.length - 1); };
|
|
447
|
-
var y = function (v) { return T + (H - T - B) * (1 - v / top); };
|
|
448
|
-
var svg = svgEl('svg', { viewBox: '0 0 ' + W + ' ' + H, preserveAspectRatio: 'none', role: 'img', 'aria-label': 'Recorded flows that held, per run' });
|
|
449
|
-
var defs = svgEl('defs');
|
|
450
|
-
var grad = svgEl('linearGradient', { id: 'kfill', x1: 0, y1: 0, x2: 0, y2: 1 });
|
|
451
|
-
grad.appendChild(svgEl('stop', { offset: '0%', 'stop-color': '#f39a5c', 'stop-opacity': 0.42 }));
|
|
452
|
-
grad.appendChild(svgEl('stop', { offset: '100%', 'stop-color': '#fbd3b5', 'stop-opacity': 0.04 }));
|
|
453
|
-
defs.appendChild(grad);
|
|
454
|
-
var stroke = svgEl('linearGradient', { id: 'kline', x1: 0, y1: 0, x2: 1, y2: 0 });
|
|
455
|
-
stroke.appendChild(svgEl('stop', { offset: '0%', 'stop-color': '#d49b33' }));
|
|
456
|
-
stroke.appendChild(svgEl('stop', { offset: '100%', 'stop-color': '#e5652b' }));
|
|
457
|
-
defs.appendChild(stroke);
|
|
458
|
-
svg.appendChild(defs);
|
|
459
|
-
var steps = top <= 4 ? top : 4;
|
|
460
|
-
for (var g = 0; g <= steps; g++) {
|
|
461
|
-
var v = Math.round((top * g) / steps);
|
|
462
|
-
svg.appendChild(svgEl('line', { x1: L, x2: W - R, y1: y(v), y2: y(v), stroke: 'rgba(138,109,94,0.14)', 'stroke-width': 1 }));
|
|
463
|
-
var lab = svgEl('text', { x: L - 10, y: y(v) + 4, 'text-anchor': 'end', 'font-size': 11, fill: '#8a6d5e' });
|
|
464
|
-
lab.textContent = String(v);
|
|
465
|
-
svg.appendChild(lab);
|
|
466
|
-
}
|
|
467
|
-
var pts = runs.map(function (r, i) { return [x(i), y(r.passed || 0)]; });
|
|
468
|
-
var line = pts.map(function (p, i) { return (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1); }).join(' ');
|
|
469
|
-
var area = line + ' L' + pts[pts.length - 1][0].toFixed(1) + ' ' + (H - B) + ' L' + pts[0][0].toFixed(1) + ' ' + (H - B) + ' Z';
|
|
470
|
-
svg.appendChild(svgEl('path', { d: area, fill: 'url(#kfill)' }));
|
|
471
|
-
svg.appendChild(svgEl('path', { d: line, fill: 'none', stroke: 'url(#kline)', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }));
|
|
472
|
-
runs.forEach(function (r, i) {
|
|
473
|
-
if (r.failed) svg.appendChild(svgEl('circle', { cx: pts[i][0], cy: pts[i][1], r: 5, fill: '#c2412d', stroke: '#fffaf5', 'stroke-width': 2 }));
|
|
347
|
+
var ol = el('ol', 'ai-steps');
|
|
348
|
+
tool.steps.forEach(function (s, i) {
|
|
349
|
+
var li = el('li');
|
|
350
|
+
li.appendChild(el('span', null, String(i + 1)));
|
|
351
|
+
li.appendChild(el('div', null, s));
|
|
352
|
+
ol.appendChild(li);
|
|
474
353
|
});
|
|
475
|
-
|
|
476
|
-
var
|
|
477
|
-
var
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
svg.appendChild(t);
|
|
490
|
-
}
|
|
491
|
-
var ring = svgEl('circle', { r: 7, fill: '#fffaf5', stroke: '#e5652b', 'stroke-width': 3 });
|
|
492
|
-
svg.appendChild(ring);
|
|
493
|
-
box.appendChild(svg);
|
|
494
|
-
var stem = el('div', 'stem');
|
|
495
|
-
var tip = el('div', 'tip');
|
|
496
|
-
var tDate = el('div', 't-date');
|
|
497
|
-
var tVal = el('div', 't-val');
|
|
498
|
-
tip.appendChild(tDate);
|
|
499
|
-
tip.appendChild(tVal);
|
|
500
|
-
box.appendChild(stem);
|
|
501
|
-
box.appendChild(tip);
|
|
502
|
-
|
|
503
|
-
function point(i) {
|
|
504
|
-
var r = runs[i];
|
|
505
|
-
ring.setAttribute('cx', pts[i][0]);
|
|
506
|
-
ring.setAttribute('cy', pts[i][1]);
|
|
507
|
-
ring.setAttribute('stroke', r.failed ? '#c2412d' : '#e5652b');
|
|
508
|
-
var rect = svg.getBoundingClientRect();
|
|
509
|
-
var px = (pts[i][0] / W) * rect.width;
|
|
510
|
-
var py = (pts[i][1] / H) * rect.height;
|
|
511
|
-
tDate.textContent = when(r.runAt);
|
|
512
|
-
clear(tVal);
|
|
513
|
-
var dot = el('i');
|
|
514
|
-
dot.style.background = r.failed ? '#c2412d' : '#3b8657';
|
|
515
|
-
tVal.appendChild(dot);
|
|
516
|
-
tVal.appendChild(el('span', null, (r.passed || 0) + ' held' + (r.failed ? ', ' + r.failed + ' broke' : '')));
|
|
517
|
-
tip.style.left = px + 'px';
|
|
518
|
-
tip.style.top = Math.max(0, py - 22) + 'px';
|
|
519
|
-
stem.style.left = px + 'px';
|
|
520
|
-
stem.style.top = Math.max(0, py - 22) + 'px';
|
|
521
|
-
stem.style.height = '14px';
|
|
354
|
+
panel.appendChild(ol);
|
|
355
|
+
var plate = el('div', 'ai-plate');
|
|
356
|
+
var bar = el('div', 'ai-plate-bar');
|
|
357
|
+
bar.appendChild(el('span', null, tool.file));
|
|
358
|
+
var cp = el('button', null, 'Copy');
|
|
359
|
+
cp.type = 'button';
|
|
360
|
+
cp.addEventListener('click', function () { copy(tool.code, tool.file === 'terminal' ? 'Command copied - run it in a terminal' : 'Config copied - paste it into ' + tool.file); });
|
|
361
|
+
bar.appendChild(cp);
|
|
362
|
+
plate.appendChild(bar);
|
|
363
|
+
plate.appendChild(el('pre', null, tool.code));
|
|
364
|
+
panel.appendChild(plate);
|
|
365
|
+
if (tool.note) panel.appendChild(el('p', 'ai-note', tool.note));
|
|
366
|
+
if (tool.file !== 'terminal') {
|
|
367
|
+
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.'));
|
|
522
368
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
point(best);
|
|
529
|
-
});
|
|
530
|
-
requestAnimationFrame(function () { if (!byId('view-dashboard').hidden) point(runs.length - 1); });
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/* ----------------------------------- donut ----------------------------------- */
|
|
534
|
-
|
|
535
|
-
function renderDonut() {
|
|
536
|
-
var box = byId('donut');
|
|
537
|
-
clear(box);
|
|
538
|
-
var by = { regression: 0, frontend: 0, data: 0 };
|
|
539
|
-
state.findings.forEach(function (f) { if (by[f.check] !== undefined) by[f.check]++; });
|
|
540
|
-
var total = state.findings.length;
|
|
541
|
-
var svg = svgEl('svg', { viewBox: '0 0 120 120', role: 'img', 'aria-label': 'Open findings by check' });
|
|
542
|
-
var R = 44;
|
|
543
|
-
var C = 2 * Math.PI * R;
|
|
544
|
-
svg.appendChild(svgEl('circle', { cx: 60, cy: 60, r: R, fill: 'none', stroke: '#f7e4d4', 'stroke-width': 16 }));
|
|
545
|
-
var offset = 0;
|
|
546
|
-
Object.keys(by).forEach(function (k) {
|
|
547
|
-
if (!by[k]) return;
|
|
548
|
-
var len = (by[k] / total) * C;
|
|
549
|
-
svg.appendChild(svgEl('circle', {
|
|
550
|
-
cx: 60, cy: 60, r: R, fill: 'none', stroke: CHECK_COLOR[k], 'stroke-width': 16,
|
|
551
|
-
'stroke-dasharray': Math.max(0, len - 2).toFixed(2) + ' ' + (C - Math.max(0, len - 2)).toFixed(2),
|
|
552
|
-
'stroke-dashoffset': (-offset).toFixed(2), transform: 'rotate(-90 60 60)', 'stroke-linecap': 'butt',
|
|
553
|
-
}));
|
|
554
|
-
offset += len;
|
|
555
|
-
});
|
|
556
|
-
box.appendChild(svg);
|
|
557
|
-
var center = el('div', 'center');
|
|
558
|
-
center.appendChild(el('b', null, total));
|
|
559
|
-
center.appendChild(el('span', null, 'open'));
|
|
560
|
-
box.appendChild(center);
|
|
561
|
-
var legend = byId('legend');
|
|
562
|
-
clear(legend);
|
|
563
|
-
Object.keys(by).forEach(function (k) {
|
|
564
|
-
var row = el('div', 'row');
|
|
565
|
-
var dot = el('i');
|
|
566
|
-
dot.style.background = CHECK_COLOR[k];
|
|
567
|
-
row.appendChild(dot);
|
|
568
|
-
row.appendChild(el('span', null, CHECK_NAME[k]));
|
|
569
|
-
row.appendChild(el('span', 'v', by[k]));
|
|
570
|
-
legend.appendChild(row);
|
|
571
|
-
});
|
|
369
|
+
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.'));
|
|
370
|
+
var then = el('p', 'ai-then');
|
|
371
|
+
then.appendChild(document.createTextNode('Then just ask your assistant: '));
|
|
372
|
+
then.appendChild(el('b', null, '“Check my app.”'));
|
|
373
|
+
panel.appendChild(then);
|
|
572
374
|
}
|
|
573
375
|
|
|
574
376
|
/* ---------------------------------- findings ---------------------------------- */
|
|
575
377
|
|
|
378
|
+
var statusFilter = 'all';
|
|
379
|
+
var query = '';
|
|
380
|
+
|
|
576
381
|
function renderStatusFilter() {
|
|
577
382
|
var box = byId('statusFilter');
|
|
578
383
|
clear(box);
|
|
@@ -583,7 +388,6 @@
|
|
|
583
388
|
box.appendChild(b);
|
|
584
389
|
});
|
|
585
390
|
}
|
|
586
|
-
|
|
587
391
|
function statusTag(f) {
|
|
588
392
|
var t = el('span', 'tag ' + (f.status === 'confirmed' ? 'ember' : 'amber'));
|
|
589
393
|
t.appendChild(el('span', f.status === 'confirmed' ? 'g-dot' : 'g-half'));
|
|
@@ -594,23 +398,10 @@
|
|
|
594
398
|
var tone = /CRITICAL/.test(sev) ? 'ember' : sev === 'HIGH' ? 'sun' : sev === 'MEDIUM' ? 'amber' : 'leaf';
|
|
595
399
|
return el('span', 'tag ' + tone, sev);
|
|
596
400
|
}
|
|
597
|
-
function confidence(level) {
|
|
598
|
-
var box = el('div', 'conf');
|
|
599
|
-
var m = el('div', 'meter sunken');
|
|
600
|
-
var bar = el('i');
|
|
601
|
-
bar.style.width = (level === 'high' ? 100 : level === 'medium' ? 66 : 33) + '%';
|
|
602
|
-
m.appendChild(bar);
|
|
603
|
-
box.appendChild(m);
|
|
604
|
-
box.appendChild(el('span', null, level));
|
|
605
|
-
return box;
|
|
606
|
-
}
|
|
607
401
|
|
|
608
402
|
var VERDICT_TONE = { fixed: 'leaf', 'still open': 'ember', 'gone with its file': 'amber', 'could not confirm': 'amber' };
|
|
609
403
|
|
|
610
|
-
function
|
|
611
|
-
var tr = el('tr', 'detail');
|
|
612
|
-
var td = el('td');
|
|
613
|
-
td.colSpan = 6;
|
|
404
|
+
function detailBox(f) {
|
|
614
405
|
var box = el('div', 'detail-box');
|
|
615
406
|
box.appendChild(el('div', 'd-step', 'WHAT IS WRONG'));
|
|
616
407
|
box.appendChild(el('div', 'd-text', f.detail || f.headline));
|
|
@@ -635,70 +426,53 @@
|
|
|
635
426
|
if (f.attempts && f.attempts.length) {
|
|
636
427
|
var list = el('div', 'attempts');
|
|
637
428
|
f.attempts.slice(-4).reverse().forEach(function (a) {
|
|
638
|
-
var
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
list.appendChild(row);
|
|
429
|
+
var r = el('div');
|
|
430
|
+
r.appendChild(el('span', 'tag ' + (VERDICT_TONE[a.verdict] || ''), a.verdict));
|
|
431
|
+
r.appendChild(el('span', null, 'Re-checked ' + ago(a.at) + (a.why ? ' - ' + a.why : '') + (a.newProblems ? ' - ' + plural(a.newProblems, 'new problem') : '')));
|
|
432
|
+
list.appendChild(r);
|
|
643
433
|
});
|
|
644
434
|
box.appendChild(list);
|
|
645
435
|
}
|
|
646
|
-
|
|
647
|
-
tr.appendChild(td);
|
|
648
|
-
return tr;
|
|
436
|
+
return box;
|
|
649
437
|
}
|
|
650
438
|
|
|
651
439
|
function renderFindings() {
|
|
652
|
-
var body = byId('
|
|
440
|
+
var body = byId('findingList');
|
|
653
441
|
clear(body);
|
|
654
|
-
var q = query.trim().toLowerCase();
|
|
655
442
|
var shown = state.findings.filter(function (f) {
|
|
656
443
|
if (statusFilter === 'confirmed' && f.status !== 'confirmed') return false;
|
|
657
444
|
if (statusFilter === 'verify' && f.status === 'confirmed') return false;
|
|
658
|
-
|
|
659
|
-
return [f.headline, f.where, f.detail, f.check, f.id].join(' ').toLowerCase().indexOf(q) !== -1;
|
|
445
|
+
return true;
|
|
660
446
|
});
|
|
661
447
|
byId('findingBadge').textContent = shown.length + (shown.length !== state.findings.length ? ' of ' + state.findings.length : '');
|
|
662
448
|
if (!shown.length) {
|
|
663
|
-
|
|
664
|
-
var td = el('td', '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.');
|
|
665
|
-
td.colSpan = 6;
|
|
666
|
-
tr.appendChild(td);
|
|
667
|
-
body.appendChild(tr);
|
|
449
|
+
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.'));
|
|
668
450
|
return;
|
|
669
451
|
}
|
|
670
452
|
shown.forEach(function (f) {
|
|
671
|
-
var
|
|
672
|
-
var
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
tr.appendChild(c6);
|
|
689
|
-
tr.addEventListener('click', function () {
|
|
690
|
-
openRow = openRow === f.id ? null : f.id;
|
|
691
|
-
renderFindings();
|
|
692
|
-
});
|
|
693
|
-
body.appendChild(tr);
|
|
694
|
-
if (openRow === f.id) body.appendChild(detailRow(f));
|
|
453
|
+
var card = el('div', 'finding ' + (f.status === 'confirmed' ? 'bad' : 'warn'));
|
|
454
|
+
var head = el('button', 'finding-head');
|
|
455
|
+
head.type = 'button';
|
|
456
|
+
var left = el('div', 'finding-left');
|
|
457
|
+
left.appendChild(el('div', 'f-title', f.headline));
|
|
458
|
+
var meta = el('div', 'f-meta');
|
|
459
|
+
meta.appendChild(sevTag(f.severity));
|
|
460
|
+
meta.appendChild(statusTag(f));
|
|
461
|
+
meta.appendChild(el('span', 'tag ' + (CHECK_TONE[f.check] || ''), CHECK_NAME[f.check] || f.check));
|
|
462
|
+
meta.appendChild(el('span', 'f-where', f.where));
|
|
463
|
+
left.appendChild(meta);
|
|
464
|
+
head.appendChild(left);
|
|
465
|
+
head.appendChild(icon(openRow === f.id ? 'change' : 'arrow', 18));
|
|
466
|
+
head.addEventListener('click', function () { openRow = openRow === f.id ? null : f.id; renderFindings(); });
|
|
467
|
+
card.appendChild(head);
|
|
468
|
+
if (openRow === f.id) card.appendChild(detailBox(f));
|
|
469
|
+
body.appendChild(card);
|
|
695
470
|
});
|
|
696
471
|
}
|
|
697
472
|
|
|
698
473
|
/* -------------------------------- other views -------------------------------- */
|
|
699
474
|
|
|
700
475
|
var LOOKS_SHOWN = 12;
|
|
701
|
-
|
|
702
476
|
function renderTimeline() {
|
|
703
477
|
var box = byId('timeline');
|
|
704
478
|
clear(box);
|
|
@@ -730,11 +504,7 @@
|
|
|
730
504
|
box.appendChild(item);
|
|
731
505
|
});
|
|
732
506
|
if (looks.length > LOOKS_SHOWN) {
|
|
733
|
-
|
|
734
|
-
allLooks = !allLooks;
|
|
735
|
-
renderTimeline();
|
|
736
|
-
});
|
|
737
|
-
box.appendChild(more);
|
|
507
|
+
box.appendChild(button('sm', null, allLooks ? 'Show the latest ' + LOOKS_SHOWN : 'Show all ' + looks.length, function () { allLooks = !allLooks; renderTimeline(); }));
|
|
738
508
|
}
|
|
739
509
|
}
|
|
740
510
|
|
|
@@ -743,23 +513,19 @@
|
|
|
743
513
|
clear(box);
|
|
744
514
|
var list = state.proven || [];
|
|
745
515
|
byId('fixBadge').textContent = String(list.length);
|
|
746
|
-
if (!list.length) {
|
|
747
|
-
box.appendChild(el('p', 'empty', 'No fix has been proven yet. Open a finding, fix it, and press Re-check.'));
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
516
|
+
if (!list.length) { box.appendChild(el('p', 'empty', 'No fix has been proven yet. Open a finding, fix it, and press Re-check.')); return; }
|
|
750
517
|
list.forEach(function (p) {
|
|
751
|
-
var row = el('div', '
|
|
518
|
+
var row = el('div', 'line-row');
|
|
752
519
|
row.appendChild(el('span', 'tag leaf', 'FIXED'));
|
|
753
|
-
var text = el('div', '
|
|
520
|
+
var text = el('div', 'lr-text');
|
|
754
521
|
text.appendChild(el('div', null, p.headline));
|
|
755
|
-
text.appendChild(el('div', '
|
|
522
|
+
text.appendChild(el('div', 'lr-sub', p.where + ' - proven ' + ago(p.at) + (p.newProblems ? ' - ' + plural(p.newProblems, 'new problem') + ' at the time' : '')));
|
|
756
523
|
row.appendChild(text);
|
|
757
524
|
box.appendChild(row);
|
|
758
525
|
});
|
|
759
526
|
}
|
|
760
527
|
|
|
761
528
|
var STATE_TONE = { 'problems found': 'ember', 'things to check': 'amber', 'last run held': 'leaf', 'nothing found': 'leaf' };
|
|
762
|
-
|
|
763
529
|
function checkSwitch(c) {
|
|
764
530
|
var sw = el('label', 'switch');
|
|
765
531
|
var input = el('input');
|
|
@@ -772,7 +538,6 @@
|
|
|
772
538
|
sw.appendChild(el('span', 'track'));
|
|
773
539
|
return sw;
|
|
774
540
|
}
|
|
775
|
-
|
|
776
541
|
function renderChecks() {
|
|
777
542
|
var box = byId('checkList');
|
|
778
543
|
clear(box);
|
|
@@ -782,31 +547,31 @@
|
|
|
782
547
|
state.checks.forEach(function (c) {
|
|
783
548
|
if (!c.available) {
|
|
784
549
|
missing++;
|
|
785
|
-
var row = el('div', 'na
|
|
550
|
+
var row = el('div', 'na');
|
|
786
551
|
row.appendChild(checkSwitch(c));
|
|
787
|
-
var
|
|
788
|
-
|
|
789
|
-
row.appendChild(
|
|
552
|
+
var nm = el('div', null, c.label);
|
|
553
|
+
nm.appendChild(el('span', 'na-lock', 'NOT AVAILABLE'));
|
|
554
|
+
row.appendChild(nm);
|
|
790
555
|
na.appendChild(row);
|
|
791
556
|
return;
|
|
792
557
|
}
|
|
793
|
-
var card = el('div', 'check
|
|
558
|
+
var card = el('div', 'check');
|
|
794
559
|
var top = el('div', 'c-top');
|
|
795
560
|
top.appendChild(el('div', 'c-name', c.label));
|
|
796
561
|
top.appendChild(checkSwitch(c));
|
|
797
562
|
card.appendChild(top);
|
|
798
|
-
|
|
563
|
+
var st = el('div');
|
|
564
|
+
st.appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
|
|
565
|
+
card.appendChild(st);
|
|
799
566
|
if (c.what) card.appendChild(el('div', 'c-what', c.what));
|
|
800
|
-
var foot = el('div', 'c-foot');
|
|
801
567
|
if (c.runnable && c.enabled) {
|
|
802
568
|
var run = button('sm', 'play', 'Run now', function () { runChecks([c.id], run); });
|
|
803
569
|
run.setAttribute('data-action', 'run');
|
|
804
570
|
run.disabled = busy;
|
|
805
|
-
|
|
571
|
+
card.appendChild(run);
|
|
806
572
|
} else if (!c.enabled) {
|
|
807
|
-
|
|
573
|
+
card.appendChild(el('div', 'c-what', 'Switched off - it will not run.'));
|
|
808
574
|
}
|
|
809
|
-
card.appendChild(foot);
|
|
810
575
|
box.appendChild(card);
|
|
811
576
|
});
|
|
812
577
|
byId('naBadge').textContent = String(missing);
|
|
@@ -816,43 +581,19 @@
|
|
|
816
581
|
var box = byId('runList');
|
|
817
582
|
clear(box);
|
|
818
583
|
byId('runBadge').textContent = String(state.runs.length);
|
|
819
|
-
if (!state.runs.length) {
|
|
820
|
-
box.appendChild(el('p', 'empty', 'No recorded flows have been replayed yet. Record one with npx kryptheon record, then press Run checks.'));
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
584
|
+
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; }
|
|
823
585
|
state.runs.slice(0, 30).forEach(function (r) {
|
|
824
|
-
var row = el('div', '
|
|
586
|
+
var row = el('div', 'line-row');
|
|
825
587
|
row.appendChild(el('span', 'tag ' + (r.failed ? 'ember' : 'leaf'), r.failed ? r.failed + ' broke' : 'held'));
|
|
826
|
-
var text = el('div', '
|
|
588
|
+
var text = el('div', 'lr-text');
|
|
827
589
|
text.appendChild(el('div', null, (r.passed || 0) + ' held, ' + (r.failed || 0) + ' broke'));
|
|
828
|
-
text.appendChild(el('div', '
|
|
590
|
+
text.appendChild(el('div', 'lr-sub', when(r.runAt)));
|
|
829
591
|
row.appendChild(text);
|
|
830
592
|
if (r.failed) row.appendChild(button('sm', 'arrow', 'See the finding', function () { go('findings', 'confirmed'); }));
|
|
831
593
|
box.appendChild(row);
|
|
832
594
|
});
|
|
833
595
|
}
|
|
834
596
|
|
|
835
|
-
var BADGE_STATES = [
|
|
836
|
-
['Active', 'This exact version was checked and held.'],
|
|
837
|
-
['Outdated', 'The project changed after it was verified.'],
|
|
838
|
-
['Revalidation required', 'A check found something new - verify again.'],
|
|
839
|
-
['Revoked', 'Withdrawn. The badge says so publicly.'],
|
|
840
|
-
];
|
|
841
|
-
|
|
842
|
-
function renderVerification() {
|
|
843
|
-
byId('verifyBadge').textContent = state.verification.state;
|
|
844
|
-
byId('verifyText').textContent = 'Not verified. ' + state.verification.why +
|
|
845
|
-
' When it is built, a verification will stand for one version of this project, and a changed version will need it again. These are the states it will have:';
|
|
846
|
-
var box = byId('verifyStates');
|
|
847
|
-
clear(box);
|
|
848
|
-
BADGE_STATES.forEach(function (s) {
|
|
849
|
-
var card = el('div', 'state-card sunken');
|
|
850
|
-
card.appendChild(el('b', null, s[0]));
|
|
851
|
-
card.appendChild(el('span', null, s[1]));
|
|
852
|
-
box.appendChild(card);
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
|
|
856
597
|
var STORE_FILES = [
|
|
857
598
|
['baselines.json', 'what each page looked like the last time it worked'],
|
|
858
599
|
['history.jsonl', 'every run of your recorded flows'],
|
|
@@ -864,10 +605,8 @@
|
|
|
864
605
|
['fixes.jsonl', 'every re-check and its verdict'],
|
|
865
606
|
['config.json', 'which checks are switched on'],
|
|
866
607
|
];
|
|
867
|
-
|
|
868
608
|
function renderStorage() {
|
|
869
|
-
byId('storeNote').textContent = 'Everything on this dashboard is read from this folder on your machine. Nothing is sent anywhere. '
|
|
870
|
-
'Your project keeps only its recordings in tests/.';
|
|
609
|
+
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/.';
|
|
871
610
|
byId('storePath').textContent = state.project.storeDir;
|
|
872
611
|
var box = byId('storeFiles');
|
|
873
612
|
clear(box);
|
|
@@ -879,15 +618,6 @@
|
|
|
879
618
|
});
|
|
880
619
|
}
|
|
881
620
|
|
|
882
|
-
function renderWhen() {
|
|
883
|
-
Object.keys(WHEN).forEach(function (id) {
|
|
884
|
-
var box = byId('when-' + id);
|
|
885
|
-
clear(box);
|
|
886
|
-
box.appendChild(el('b', 'lbl', 'THIS SECTION'));
|
|
887
|
-
box.appendChild(rich(el('span'), WHEN[id]));
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
621
|
/* ---------------------------------- actions ---------------------------------- */
|
|
892
622
|
|
|
893
623
|
function recheck(f, btn) {
|
|
@@ -898,21 +628,13 @@
|
|
|
898
628
|
btn.appendChild(el('span', null, f.check === 'regression' ? 'Replaying...' : 'Re-checking...'));
|
|
899
629
|
post('/api/recheck', { id: f.id }).then(function (r) {
|
|
900
630
|
var 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' };
|
|
901
|
-
toast((words[r.verdict] || r.verdict) + (r.why ? ' - ' + r.why : '') +
|
|
902
|
-
|
|
903
|
-
}).catch(function (err) {
|
|
904
|
-
toast('Re-check failed: ' + err.message);
|
|
905
|
-
}).then(function () {
|
|
906
|
-
setBusy(false);
|
|
907
|
-
return load();
|
|
908
|
-
});
|
|
631
|
+
toast((words[r.verdict] || r.verdict) + (r.why ? ' - ' + r.why : '') + (r.newProblems && r.newProblems.length ? ' - ' + plural(r.newProblems.length, 'new problem') : ''));
|
|
632
|
+
}).catch(function (err) { toast('Re-check failed: ' + err.message); }).then(function () { setBusy(false); return load(); });
|
|
909
633
|
}
|
|
910
634
|
|
|
911
635
|
function runChecks(ids, btn) {
|
|
912
636
|
if (busy || !ids.length) return;
|
|
913
637
|
setBusy(true);
|
|
914
|
-
var error = byId('checkError');
|
|
915
|
-
error.hidden = true;
|
|
916
638
|
clear(btn);
|
|
917
639
|
btn.appendChild(el('span', 'spin'));
|
|
918
640
|
btn.appendChild(el('span', null, 'Running...'));
|
|
@@ -920,84 +642,56 @@
|
|
|
920
642
|
return chain.then(function () { return post('/api/run', { id: id }); });
|
|
921
643
|
}, Promise.resolve()).then(function () {
|
|
922
644
|
toast('Finished: ' + ids.map(function (id) { return CHECK_NAME[id] || id; }).join(', '));
|
|
923
|
-
}).catch(function (err) {
|
|
924
|
-
error.textContent = 'Run failed: ' + err.message;
|
|
925
|
-
error.hidden = false;
|
|
926
|
-
toast('Run failed: ' + err.message);
|
|
927
|
-
}).then(function () {
|
|
928
|
-
setBusy(false);
|
|
929
|
-
return load();
|
|
930
|
-
});
|
|
645
|
+
}).catch(function (err) { toast('Run failed: ' + err.message); }).then(function () { setBusy(false); return load(); });
|
|
931
646
|
}
|
|
932
647
|
|
|
933
648
|
function setCheck(id, on, input) {
|
|
934
|
-
var error = byId('checkError');
|
|
935
|
-
error.hidden = true;
|
|
936
649
|
post('/api/check', { id: id, on: on }).then(function () {
|
|
937
650
|
toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
|
|
938
651
|
return load();
|
|
939
|
-
}).catch(function (err) {
|
|
940
|
-
input.checked = !on;
|
|
941
|
-
error.textContent = 'Not saved: ' + err.message;
|
|
942
|
-
error.hidden = false;
|
|
943
|
-
});
|
|
652
|
+
}).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
|
|
944
653
|
}
|
|
945
654
|
|
|
946
|
-
byId('search').addEventListener('input', function (e) {
|
|
947
|
-
query = e.target.value;
|
|
948
|
-
if (query.trim() && currentView() !== 'findings') go('findings');
|
|
949
|
-
if (state) renderFindings();
|
|
950
|
-
});
|
|
951
|
-
byId('bell').addEventListener('click', function () { go('changes'); });
|
|
952
655
|
byId('runAll').addEventListener('click', function () {
|
|
953
656
|
if (!state) return;
|
|
954
657
|
var ids = state.checks.filter(function (c) { return c.runnable && c.enabled; }).map(function (c) { return c.id; });
|
|
955
|
-
if (!ids.length) {
|
|
956
|
-
toast('Nothing can run from here yet - switch a check on, or record a flow.');
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
658
|
+
if (!ids.length) { toast('Nothing can run from here yet - switch a check on, or record a flow.'); return; }
|
|
959
659
|
runChecks(ids, byId('runAll'));
|
|
960
660
|
});
|
|
961
661
|
|
|
962
662
|
/* ----------------------------------- load ----------------------------------- */
|
|
963
663
|
|
|
664
|
+
function renderTop() {
|
|
665
|
+
var run = byId('runAll');
|
|
666
|
+
if (!busy) { clear(run); run.appendChild(icon('play', 15)); run.appendChild(el('span', null, 'Run checks')); }
|
|
667
|
+
}
|
|
668
|
+
|
|
964
669
|
function render(next) {
|
|
965
670
|
if (!next) return;
|
|
966
671
|
state = next;
|
|
967
672
|
renderSide();
|
|
968
673
|
renderTop();
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
renderGuide();
|
|
972
|
-
renderRange();
|
|
973
|
-
renderChart();
|
|
974
|
-
renderDonut();
|
|
674
|
+
renderNow();
|
|
675
|
+
renderSteps();
|
|
975
676
|
renderStatusFilter();
|
|
976
677
|
renderFindings();
|
|
977
678
|
renderTimeline();
|
|
978
679
|
renderFixes();
|
|
979
680
|
renderChecks();
|
|
980
681
|
renderRuns();
|
|
981
|
-
renderVerification();
|
|
982
682
|
renderStorage();
|
|
983
683
|
}
|
|
984
684
|
|
|
985
685
|
function load() {
|
|
986
686
|
return fetch('/api/state', { cache: 'no-store' })
|
|
987
|
-
.then(function (res) {
|
|
988
|
-
if (!res.ok) throw new Error('the dashboard answered ' + res.status);
|
|
989
|
-
return res.json();
|
|
990
|
-
})
|
|
687
|
+
.then(function (res) { if (!res.ok) throw new Error('the dashboard answered ' + res.status); return res.json(); })
|
|
991
688
|
.then(render)
|
|
992
|
-
.catch(function (err) {
|
|
993
|
-
byId('subline').textContent = 'Could not load: ' + err.message;
|
|
994
|
-
});
|
|
689
|
+
.catch(function (err) { byId('subline').textContent = 'Could not load: ' + err.message; });
|
|
995
690
|
}
|
|
996
691
|
|
|
997
|
-
|
|
692
|
+
renderAI();
|
|
998
693
|
showView();
|
|
999
694
|
load();
|
|
1000
|
-
// Not while something runs, and not while a finding is open being read
|
|
1001
|
-
// a re-render would take the spinner away or close what is being read.
|
|
695
|
+
// Not while something runs, and not while a finding is open being read.
|
|
1002
696
|
setInterval(function () { if (!busy && !openRow) load(); }, 5000);
|
|
1003
697
|
})();
|