kryptheon 0.1.19 → 0.1.21
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 +48 -10
- package/bin/kryptheon.js +466 -6
- package/kryptheon-changes.js +222 -0
- package/kryptheon-code.js +564 -0
- package/kryptheon-dashboard-page.js +1003 -0
- package/kryptheon-dashboard.html +446 -0
- package/kryptheon-dashboard.js +532 -0
- package/kryptheon-fixture.js +7 -3
- package/kryptheon-logo.png +0 -0
- package/kryptheon-look.js +259 -0
- package/kryptheon-reporter.js +18 -6
- package/kryptheon-secrets.js +5 -3
- package/kryptheon-store.js +332 -0
- package/package.json +93 -78
- package/playwright.config.js +5 -2
|
@@ -0,0 +1,1003 @@
|
|
|
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 charts and icons, from numbers and fixed path data) and filled with
|
|
7
|
+
// textContent. This file is read by kryptheon-code.js in its own check, and
|
|
8
|
+
// must come back with nothing to report.
|
|
9
|
+
|
|
10
|
+
(function () {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
var tokenMeta = document.querySelector('meta[name="kryptheon-token"]');
|
|
14
|
+
var TOKEN = tokenMeta ? tokenMeta.getAttribute('content') : '';
|
|
15
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
16
|
+
var state = null;
|
|
17
|
+
var busy = false;
|
|
18
|
+
var statusFilter = 'all';
|
|
19
|
+
var range = 'all';
|
|
20
|
+
var query = '';
|
|
21
|
+
var openRow = null;
|
|
22
|
+
var allLooks = false;
|
|
23
|
+
|
|
24
|
+
/* ------------------------------ building blocks ------------------------------ */
|
|
25
|
+
|
|
26
|
+
function el(tag, cls, text) {
|
|
27
|
+
var node = document.createElement(tag);
|
|
28
|
+
if (cls) node.className = cls;
|
|
29
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
function svgEl(tag, attrs) {
|
|
33
|
+
var node = document.createElementNS(NS, tag);
|
|
34
|
+
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, String(attrs[k])); });
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
function clear(node) {
|
|
38
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
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
|
+
}
|
|
51
|
+
|
|
52
|
+
// Line icons from fixed path data only.
|
|
53
|
+
var ICONS = {
|
|
54
|
+
grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
|
|
55
|
+
list: 'M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01',
|
|
56
|
+
change: 'M4 12h4l3-7 4 14 3-7h2',
|
|
57
|
+
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
|
+
toggle: 'M8 7h8a5 5 0 010 10H8A5 5 0 018 7zm0 3a2 2 0 100 4 2 2 0 000-4z',
|
|
59
|
+
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
|
+
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
|
+
alert: 'M12 3l10 18H2L12 3zm0 7v5m0 3v.5',
|
|
65
|
+
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
|
+
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
67
|
+
film: 'M4 5h16v14H4zM4 9h16M4 15h16M8 5v14M16 5v14',
|
|
68
|
+
copy: 'M9 9h10v12H9zM5 15V3h10',
|
|
69
|
+
refresh: 'M20 12a8 8 0 10-2.3 5.7M20 12V6m0 6h-6',
|
|
70
|
+
arrow: 'M5 12h14m-6-6l6 6-6 6',
|
|
71
|
+
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
|
+
lock: 'M6 11h12v10H6zM8 11V7a4 4 0 018 0v4',
|
|
73
|
+
};
|
|
74
|
+
function icon(name, size) {
|
|
75
|
+
var s = svgEl('svg', {
|
|
76
|
+
width: size || 18, height: size || 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
77
|
+
'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
78
|
+
});
|
|
79
|
+
s.appendChild(svgEl('path', { d: ICONS[name] || ICONS.box }));
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function when(iso) {
|
|
84
|
+
if (!iso) return 'unknown time';
|
|
85
|
+
var d = new Date(iso);
|
|
86
|
+
return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
|
|
87
|
+
}
|
|
88
|
+
function shortDate(iso) {
|
|
89
|
+
var d = new Date(iso);
|
|
90
|
+
return isNaN(d.getTime()) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
91
|
+
}
|
|
92
|
+
function ago(iso) {
|
|
93
|
+
var d = new Date(iso);
|
|
94
|
+
if (isNaN(d.getTime())) return '';
|
|
95
|
+
var s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1000));
|
|
96
|
+
if (s < 60) return s + 's ago';
|
|
97
|
+
if (s < 3600) return Math.round(s / 60) + ' min ago';
|
|
98
|
+
if (s < 86400) return Math.round(s / 3600) + ' h ago';
|
|
99
|
+
return Math.round(s / 86400) + ' d ago';
|
|
100
|
+
}
|
|
101
|
+
function plural(n, one, many) {
|
|
102
|
+
return n + ' ' + (n === 1 ? one : many || one + 's');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toast(text) {
|
|
106
|
+
var t = byId('toast');
|
|
107
|
+
t.textContent = text;
|
|
108
|
+
t.classList.add('show');
|
|
109
|
+
clearTimeout(toast.timer);
|
|
110
|
+
toast.timer = setTimeout(function () { t.classList.remove('show'); }, 3200);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function post(path, body) {
|
|
114
|
+
return fetch(path, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'content-type': 'application/json', 'x-kryptheon-token': TOKEN },
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
}).then(function (res) {
|
|
119
|
+
return res.json().then(function (data) {
|
|
120
|
+
if (!res.ok || data.ok === false) throw new Error(data.why || 'the dashboard answered ' + res.status);
|
|
121
|
+
return data;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setBusy(on) {
|
|
127
|
+
busy = on;
|
|
128
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function copy(text, done) {
|
|
132
|
+
navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
|
|
133
|
+
toast('Could not copy - select the text instead');
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function button(cls, iconName, text, onClick) {
|
|
138
|
+
var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
|
|
139
|
+
b.type = 'button';
|
|
140
|
+
if (iconName) b.appendChild(icon(iconName, 15));
|
|
141
|
+
b.appendChild(el('span', null, text));
|
|
142
|
+
b.addEventListener('click', onClick);
|
|
143
|
+
return b;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
var CHECK_NAME = { regression: 'Recorded flows', frontend: 'Frontend code', data: 'Database' };
|
|
147
|
+
var CHECK_TONE = { regression: 'sun', frontend: 'amber', data: 'dusk' };
|
|
148
|
+
var CHECK_COLOR = { regression: '#ec7433', frontend: '#d49b33', data: '#6d4a7c' };
|
|
149
|
+
|
|
150
|
+
function counts() {
|
|
151
|
+
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
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ----------------------------------- views ----------------------------------- */
|
|
163
|
+
|
|
164
|
+
// One section on screen at a time; the address keeps it, so a reload or the
|
|
165
|
+
// back button lands on the same one.
|
|
166
|
+
var VIEWS = [
|
|
167
|
+
{ id: 'dashboard', title: 'Dashboard', icon: 'grid', group: 'navOverview' },
|
|
168
|
+
{ id: 'findings', title: 'Findings', icon: 'list', group: 'navOverview' },
|
|
169
|
+
{ id: 'changes', title: 'What changed', icon: 'change', group: 'navOverview' },
|
|
170
|
+
{ id: 'fixes', title: 'Proven fixes', icon: 'wrench', group: 'navOverview' },
|
|
171
|
+
{ id: 'checks', title: 'Checks', icon: 'toggle', group: 'navWorkspace' },
|
|
172
|
+
{ id: 'runs', title: 'Runs', icon: 'play', group: 'navWorkspace' },
|
|
173
|
+
{ id: 'verification', title: 'Verification', icon: 'shield', group: 'navWorkspace' },
|
|
174
|
+
{ id: 'storage', title: 'Where this lives', icon: 'box', group: 'navWorkspace' },
|
|
175
|
+
];
|
|
176
|
+
|
|
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
|
+
function currentView() {
|
|
196
|
+
var id = (location.hash || '').replace(/^#/, '');
|
|
197
|
+
return VIEWS.some(function (v) { return v.id === id; }) ? id : 'dashboard';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function go(id, filter) {
|
|
201
|
+
if (filter) {
|
|
202
|
+
statusFilter = filter;
|
|
203
|
+
if (state) { renderStatusFilter(); renderFindings(); }
|
|
204
|
+
}
|
|
205
|
+
if (currentView() === id) return showView();
|
|
206
|
+
location.hash = id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function showView() {
|
|
210
|
+
var id = currentView();
|
|
211
|
+
VIEWS.forEach(function (v) { byId('view-' + v.id).hidden = v.id !== id; });
|
|
212
|
+
var view = VIEWS.filter(function (v) { return v.id === id; })[0];
|
|
213
|
+
byId('viewTitle').textContent = view.title;
|
|
214
|
+
document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
|
|
215
|
+
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
|
+
window.scrollTo(0, 0);
|
|
219
|
+
}
|
|
220
|
+
window.addEventListener('hashchange', showView);
|
|
221
|
+
|
|
222
|
+
/* ---------------------------------- sidebar ---------------------------------- */
|
|
223
|
+
|
|
224
|
+
function renderSide() {
|
|
225
|
+
var c = counts();
|
|
226
|
+
var active = currentView();
|
|
227
|
+
var badge = {
|
|
228
|
+
findings: [state.findings.length, c.confirmed > 0],
|
|
229
|
+
changes: [(state.looks || []).length, false],
|
|
230
|
+
fixes: [c.proven, false],
|
|
231
|
+
runs: [state.runs.length, !!(c.lastRun && c.lastRun.failed)],
|
|
232
|
+
};
|
|
233
|
+
['navOverview', 'navWorkspace'].forEach(function (group) {
|
|
234
|
+
var box = byId(group);
|
|
235
|
+
clear(box);
|
|
236
|
+
VIEWS.filter(function (v) { return v.group === group; }).forEach(function (v) {
|
|
237
|
+
var b = el('button', active === v.id ? 'on' : '');
|
|
238
|
+
b.type = 'button';
|
|
239
|
+
b.appendChild(icon(v.icon, 18));
|
|
240
|
+
b.appendChild(el('span', null, v.title));
|
|
241
|
+
var n = badge[v.id];
|
|
242
|
+
if (n && n[0]) b.appendChild(el('span', 'n-count' + (n[1] ? ' hot' : ''), n[0]));
|
|
243
|
+
if (active === v.id) b.setAttribute('aria-current', 'page');
|
|
244
|
+
b.addEventListener('click', function () { go(v.id); });
|
|
245
|
+
box.appendChild(b);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
byId('builtText').textContent = c.built + ' / ' + state.checks.length;
|
|
249
|
+
byId('builtBar').style.width = Math.round((c.built / state.checks.length) * 100) + '%';
|
|
250
|
+
byId('builtNote').textContent = (state.checks.length - c.built) + ' more are listed as not available - never as passed.';
|
|
251
|
+
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
|
+
var last = (state.looks || [])[0];
|
|
262
|
+
byId('subline').textContent = last ? 'Watching - last look ' + ago(last.at) : 'Watching for changes';
|
|
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
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* --------------------------------- dashboard --------------------------------- */
|
|
281
|
+
|
|
282
|
+
function renderSky() {
|
|
283
|
+
var c = counts();
|
|
284
|
+
var sky = byId('sky');
|
|
285
|
+
var ic = byId('skyIcon');
|
|
286
|
+
var title = byId('skyTitle');
|
|
287
|
+
var sub = byId('skySub');
|
|
288
|
+
var actions = byId('skyActions');
|
|
289
|
+
clear(ic);
|
|
290
|
+
clear(title);
|
|
291
|
+
clear(sub);
|
|
292
|
+
clear(actions);
|
|
293
|
+
if (c.confirmed) {
|
|
294
|
+
sky.className = 'sky bad';
|
|
295
|
+
ic.style.color = 'var(--ember)';
|
|
296
|
+
ic.appendChild(icon('alert', 28));
|
|
297
|
+
byId('skyEyebrow').textContent = 'NEEDS YOU NOW';
|
|
298
|
+
rich(title, [{ hl: plural(c.confirmed, 'confirmed problem') }]);
|
|
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.' : '')]);
|
|
301
|
+
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'confirmed'); }));
|
|
302
|
+
} else if (c.toVerify) {
|
|
303
|
+
sky.className = 'sky';
|
|
304
|
+
ic.style.color = 'var(--amber)';
|
|
305
|
+
ic.appendChild(icon('eye', 28));
|
|
306
|
+
byId('skyEyebrow').textContent = 'WORTH A LOOK';
|
|
307
|
+
rich(title, [{ hl: plural(c.toVerify, 'thing') + ' to verify' }]);
|
|
308
|
+
rich(sub, ['Nothing confirmed broken. These were spotted ', { hl: 'without running anything' },
|
|
309
|
+
' - each needs a person to decide.']);
|
|
310
|
+
actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'verify'); }));
|
|
311
|
+
} else {
|
|
312
|
+
sky.className = 'sky ok';
|
|
313
|
+
ic.style.color = 'var(--leaf)';
|
|
314
|
+
ic.appendChild(icon('sun', 28));
|
|
315
|
+
byId('skyEyebrow').textContent = 'ALL QUIET';
|
|
316
|
+
rich(title, ['Holding']);
|
|
317
|
+
rich(sub, ['Nothing open from the checks that are on and have run. That is ', { hl: 'not the same as nothing wrong' },
|
|
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'); }));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function kpi(label, value, iconName, tone, foot, goText, onOpen) {
|
|
327
|
+
var box = el('button', 'kpi raised');
|
|
328
|
+
box.type = 'button';
|
|
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);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderKpis() {
|
|
348
|
+
var box = byId('kpis');
|
|
349
|
+
clear(box);
|
|
350
|
+
var c = counts();
|
|
351
|
+
var newFound = c.lastLook && !c.lastLook.first ? c.lastLook.newFindings : 0;
|
|
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');
|
|
371
|
+
clear(box);
|
|
372
|
+
var c = counts();
|
|
373
|
+
var looks = state.looks || [];
|
|
374
|
+
var items = [
|
|
375
|
+
{ view: 'findings', parts: ['Something ', { hl: 'broke or looks risky' }, ' after your AI changed code'],
|
|
376
|
+
state: state.findings.length ? c.confirmed + ' confirmed, ' + c.toVerify + ' to verify' : 'nothing open',
|
|
377
|
+
tone: c.confirmed ? 'hot' : c.toVerify ? 'warm' : 'cool' },
|
|
378
|
+
{ view: 'runs', parts: ['A ', { hl: 'recorded flow' }, ' (sign-up, login, checkout) stopped working'],
|
|
379
|
+
state: c.lastRun ? (c.lastRun.failed ? plural(c.lastRun.failed, 'flow') + ' broke in the last run' : 'last run held') : 'never replayed',
|
|
380
|
+
tone: c.lastRun ? (c.lastRun.failed ? 'hot' : 'cool') : '' },
|
|
381
|
+
{ view: 'changes', parts: ['You want to see ', { hl: 'what your AI touched' }],
|
|
382
|
+
state: looks.length ? plural(looks.length, 'look') + ', last ' + ago(looks[0].at) : 'no look yet',
|
|
383
|
+
tone: c.lastLook && !c.lastLook.first && c.lastLook.newFindings ? 'hot' : looks.length ? 'cool' : '' },
|
|
384
|
+
{ view: 'fixes', parts: ['You fixed something and want ', { hl: 'proof it is fixed' }],
|
|
385
|
+
state: c.proven ? plural(c.proven, 'fix', 'fixes') + ' proven' : 'none proven yet', tone: c.proven ? 'cool' : '' },
|
|
386
|
+
{ view: 'checks', parts: ['You want to ', { hl: 'switch a check' }, ' on or off, or run one'],
|
|
387
|
+
state: c.built + ' of ' + state.checks.length + ' built', tone: '' },
|
|
388
|
+
{ view: 'verification', parts: ['You want the ', { hl: 'Kryptheon Verified' }, ' badge'],
|
|
389
|
+
state: state.verification.state, tone: 'warm' },
|
|
390
|
+
{ view: 'storage', parts: ['You want to know ', { hl: 'where your records are kept' }],
|
|
391
|
+
state: 'on this machine only', tone: 'cool' },
|
|
392
|
+
];
|
|
393
|
+
items.forEach(function (it) {
|
|
394
|
+
var v = VIEWS.filter(function (x) { return x.id === it.view; })[0];
|
|
395
|
+
var row = el('button', 'g-row');
|
|
396
|
+
row.type = 'button';
|
|
397
|
+
row.appendChild(el('span', 'g-dot' + (it.tone ? ' ' + it.tone : '')));
|
|
398
|
+
var text = el('span');
|
|
399
|
+
rich(text, it.parts);
|
|
400
|
+
text.appendChild(el('span', 'g-state', it.state));
|
|
401
|
+
row.appendChild(text);
|
|
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); });
|
|
407
|
+
box.appendChild(row);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ----------------------------------- chart ----------------------------------- */
|
|
412
|
+
|
|
413
|
+
function renderRange() {
|
|
414
|
+
var box = byId('range');
|
|
415
|
+
clear(box);
|
|
416
|
+
[['all', 'All'], ['30', '30D'], ['7', '7D']].forEach(function (r) {
|
|
417
|
+
var b = el('button', range === r[0] ? 'on' : '', r[1]);
|
|
418
|
+
b.type = 'button';
|
|
419
|
+
b.addEventListener('click', function () { range = r[0]; renderRange(); renderChart(); });
|
|
420
|
+
box.appendChild(b);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderChart() {
|
|
425
|
+
var box = byId('chart');
|
|
426
|
+
clear(box);
|
|
427
|
+
var runs = state.runs.slice().reverse();
|
|
428
|
+
if (range !== 'all') {
|
|
429
|
+
var since = Date.now() - Number(range) * 86400000;
|
|
430
|
+
runs = runs.filter(function (r) { return new Date(r.runAt).getTime() >= since; });
|
|
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;
|
|
438
|
+
}
|
|
439
|
+
var W = 720;
|
|
440
|
+
var H = 210;
|
|
441
|
+
var L = 40;
|
|
442
|
+
var R = 12;
|
|
443
|
+
var T = 18;
|
|
444
|
+
var B = 28;
|
|
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 }));
|
|
474
|
+
});
|
|
475
|
+
// All on one day: the date says nothing, the time does.
|
|
476
|
+
var sameDay = shortDate(runs[0].runAt) === shortDate(runs[runs.length - 1].runAt);
|
|
477
|
+
var labels = Math.min(runs.length, 6);
|
|
478
|
+
var lastLabel = '';
|
|
479
|
+
for (var k = 0; k < labels; k++) {
|
|
480
|
+
var idx = labels === 1 ? 0 : Math.round((k * (runs.length - 1)) / (labels - 1));
|
|
481
|
+
var text = sameDay
|
|
482
|
+
? new Date(runs[idx].runAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
483
|
+
: shortDate(runs[idx].runAt);
|
|
484
|
+
// Two runs in the same minute (or day) would print the same label twice.
|
|
485
|
+
if (text === lastLabel) continue;
|
|
486
|
+
lastLabel = text;
|
|
487
|
+
var t = svgEl('text', { x: x(idx), y: H - 6, 'text-anchor': 'middle', 'font-size': 11, fill: '#8a6d5e' });
|
|
488
|
+
t.textContent = text;
|
|
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';
|
|
522
|
+
}
|
|
523
|
+
svg.addEventListener('mousemove', function (e) {
|
|
524
|
+
var rect = svg.getBoundingClientRect();
|
|
525
|
+
var mx = ((e.clientX - rect.left) / rect.width) * W;
|
|
526
|
+
var best = 0;
|
|
527
|
+
pts.forEach(function (p, i) { if (Math.abs(p[0] - mx) < Math.abs(pts[best][0] - mx)) best = i; });
|
|
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
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* ---------------------------------- findings ---------------------------------- */
|
|
575
|
+
|
|
576
|
+
function renderStatusFilter() {
|
|
577
|
+
var box = byId('statusFilter');
|
|
578
|
+
clear(box);
|
|
579
|
+
[['all', 'All'], ['confirmed', 'Confirmed'], ['verify', 'To verify']].forEach(function (s) {
|
|
580
|
+
var b = el('button', statusFilter === s[0] ? 'on' : '', s[1]);
|
|
581
|
+
b.type = 'button';
|
|
582
|
+
b.addEventListener('click', function () { statusFilter = s[0]; renderStatusFilter(); renderFindings(); });
|
|
583
|
+
box.appendChild(b);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function statusTag(f) {
|
|
588
|
+
var t = el('span', 'tag ' + (f.status === 'confirmed' ? 'ember' : 'amber'));
|
|
589
|
+
t.appendChild(el('span', f.status === 'confirmed' ? 'g-dot' : 'g-half'));
|
|
590
|
+
t.appendChild(el('span', null, f.status === 'confirmed' ? 'Confirmed' : 'To verify'));
|
|
591
|
+
return t;
|
|
592
|
+
}
|
|
593
|
+
function sevTag(sev) {
|
|
594
|
+
var tone = /CRITICAL/.test(sev) ? 'ember' : sev === 'HIGH' ? 'sun' : sev === 'MEDIUM' ? 'amber' : 'leaf';
|
|
595
|
+
return el('span', 'tag ' + tone, sev);
|
|
596
|
+
}
|
|
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
|
+
|
|
608
|
+
var VERDICT_TONE = { fixed: 'leaf', 'still open': 'ember', 'gone with its file': 'amber', 'could not confirm': 'amber' };
|
|
609
|
+
|
|
610
|
+
function detailRow(f) {
|
|
611
|
+
var tr = el('tr', 'detail');
|
|
612
|
+
var td = el('td');
|
|
613
|
+
td.colSpan = 6;
|
|
614
|
+
var box = el('div', 'detail-box');
|
|
615
|
+
box.appendChild(el('div', 'd-step', 'WHAT IS WRONG'));
|
|
616
|
+
box.appendChild(el('div', 'd-text', f.detail || f.headline));
|
|
617
|
+
box.appendChild(el('div', 'd-text', 'Seen ' + when(f.when) + ' - ' + f.evidence + ' - id ' + f.id));
|
|
618
|
+
if (f.fixPrompt) {
|
|
619
|
+
box.appendChild(el('div', 'd-step', 'STEP 1 - PASTE THIS INTO YOUR AI TOOL'));
|
|
620
|
+
box.appendChild(el('pre', null, f.fixPrompt));
|
|
621
|
+
}
|
|
622
|
+
box.appendChild(el('div', 'd-step', f.fixPrompt ? 'STEP 2 - AFTER IT CHANGES THE CODE, RE-CHECK' : 'RE-CHECK'));
|
|
623
|
+
var actions = el('div', 'd-actions');
|
|
624
|
+
if (f.fixPrompt) {
|
|
625
|
+
actions.appendChild(button('', 'copy', 'Copy fix prompt', function (e) {
|
|
626
|
+
e.stopPropagation();
|
|
627
|
+
copy(f.fixPrompt, 'Fix prompt copied - paste it into your AI tool');
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
630
|
+
var re = button('sun sm', 'refresh', 'Re-check', function (e) { e.stopPropagation(); recheck(f, re); });
|
|
631
|
+
re.setAttribute('data-action', 'recheck');
|
|
632
|
+
re.disabled = busy;
|
|
633
|
+
actions.appendChild(re);
|
|
634
|
+
box.appendChild(actions);
|
|
635
|
+
if (f.attempts && f.attempts.length) {
|
|
636
|
+
var list = el('div', 'attempts');
|
|
637
|
+
f.attempts.slice(-4).reverse().forEach(function (a) {
|
|
638
|
+
var row = el('div');
|
|
639
|
+
row.appendChild(el('span', 'tag ' + (VERDICT_TONE[a.verdict] || ''), a.verdict));
|
|
640
|
+
row.appendChild(el('span', null, 'Re-checked ' + ago(a.at) + (a.why ? ' - ' + a.why : '') +
|
|
641
|
+
(a.newProblems ? ' - ' + plural(a.newProblems, 'new problem') : '')));
|
|
642
|
+
list.appendChild(row);
|
|
643
|
+
});
|
|
644
|
+
box.appendChild(list);
|
|
645
|
+
}
|
|
646
|
+
td.appendChild(box);
|
|
647
|
+
tr.appendChild(td);
|
|
648
|
+
return tr;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderFindings() {
|
|
652
|
+
var body = byId('findingRows');
|
|
653
|
+
clear(body);
|
|
654
|
+
var q = query.trim().toLowerCase();
|
|
655
|
+
var shown = state.findings.filter(function (f) {
|
|
656
|
+
if (statusFilter === 'confirmed' && f.status !== 'confirmed') return false;
|
|
657
|
+
if (statusFilter === 'verify' && f.status === 'confirmed') return false;
|
|
658
|
+
if (!q) return true;
|
|
659
|
+
return [f.headline, f.where, f.detail, f.check, f.id].join(' ').toLowerCase().indexOf(q) !== -1;
|
|
660
|
+
});
|
|
661
|
+
byId('findingBadge').textContent = shown.length + (shown.length !== state.findings.length ? ' of ' + state.findings.length : '');
|
|
662
|
+
if (!shown.length) {
|
|
663
|
+
var tr = el('tr');
|
|
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);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
shown.forEach(function (f) {
|
|
671
|
+
var tr = el('tr', 'row ' + (f.status === 'confirmed' ? 'bad' : 'warn'));
|
|
672
|
+
var c1 = el('td');
|
|
673
|
+
c1.appendChild(el('div', 'f-title', f.headline));
|
|
674
|
+
c1.appendChild(el('div', 'f-id', '#' + f.id + (openRow === f.id ? '' : ' - click for the fix prompt')));
|
|
675
|
+
tr.appendChild(c1);
|
|
676
|
+
var c2 = el('td');
|
|
677
|
+
c2.appendChild(el('span', 'tag ' + (CHECK_TONE[f.check] || ''), CHECK_NAME[f.check] || f.check));
|
|
678
|
+
tr.appendChild(c2);
|
|
679
|
+
var c3 = el('td');
|
|
680
|
+
c3.appendChild(statusTag(f));
|
|
681
|
+
tr.appendChild(c3);
|
|
682
|
+
var c4 = el('td');
|
|
683
|
+
c4.appendChild(sevTag(f.severity));
|
|
684
|
+
tr.appendChild(c4);
|
|
685
|
+
tr.appendChild(el('td', 'where', f.where));
|
|
686
|
+
var c6 = el('td');
|
|
687
|
+
c6.appendChild(confidence(f.confidence));
|
|
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));
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/* -------------------------------- other views -------------------------------- */
|
|
699
|
+
|
|
700
|
+
var LOOKS_SHOWN = 12;
|
|
701
|
+
|
|
702
|
+
function renderTimeline() {
|
|
703
|
+
var box = byId('timeline');
|
|
704
|
+
clear(box);
|
|
705
|
+
var looks = state.looks || [];
|
|
706
|
+
byId('lookBadge').textContent = String(looks.length);
|
|
707
|
+
if (!looks.length) {
|
|
708
|
+
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.'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
(allLooks ? looks : looks.slice(0, LOOKS_SHOWN)).forEach(function (look) {
|
|
712
|
+
var item = el('div', 'tl');
|
|
713
|
+
item.appendChild(el('div', 'knob' + (look.newFindings ? ' bad' : look.fixed ? ' ok' : look.goneWithFile ? ' mid' : '')));
|
|
714
|
+
var body = el('div');
|
|
715
|
+
body.appendChild(el('div', 'tl-head', look.first ? 'Starting point'
|
|
716
|
+
: look.fileCount ? plural(look.fileCount, 'file') + ' changed - ' + look.parts.join(', ') : 'Nothing changed'));
|
|
717
|
+
body.appendChild(el('div', 'tl-meta', (look.by === 'watch' ? 'Noticed by itself' : look.by === 'recheck' ? 'Re-check' : 'npx kryptheon verify') + ' - ' + when(look.at)));
|
|
718
|
+
var tags = el('div', 'tl-body');
|
|
719
|
+
if (look.newFindings) tags.appendChild(el('span', 'tag ember', look.newFindings + ' new'));
|
|
720
|
+
if (look.fixed) tags.appendChild(el('span', 'tag leaf', look.fixed + ' fixed'));
|
|
721
|
+
if (look.goneWithFile) tags.appendChild(el('span', 'tag amber', look.goneWithFile + ' gone with its file'));
|
|
722
|
+
look.checks.forEach(function (c) { tags.appendChild(el('span', 'tag', c.id + ': ' + c.state)); });
|
|
723
|
+
body.appendChild(tags);
|
|
724
|
+
var files = el('div', 'tl-files');
|
|
725
|
+
look.files.slice(0, 5).forEach(function (f) { files.appendChild(el('div', null, f.state + ' ' + f.path)); });
|
|
726
|
+
if (look.fileCount > 5) files.appendChild(el('div', null, '... and ' + (look.fileCount - 5) + ' more'));
|
|
727
|
+
look.dependencies.forEach(function (d) { files.appendChild(el('div', null, 'package ' + d.name + ': ' + d.change + (d.to ? ' ' + d.to : ''))); });
|
|
728
|
+
body.appendChild(files);
|
|
729
|
+
item.appendChild(body);
|
|
730
|
+
box.appendChild(item);
|
|
731
|
+
});
|
|
732
|
+
if (looks.length > LOOKS_SHOWN) {
|
|
733
|
+
var more = button('sm more', null, allLooks ? 'Show the latest ' + LOOKS_SHOWN : 'Show all ' + looks.length, function () {
|
|
734
|
+
allLooks = !allLooks;
|
|
735
|
+
renderTimeline();
|
|
736
|
+
});
|
|
737
|
+
box.appendChild(more);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function renderFixes() {
|
|
742
|
+
var box = byId('fixList');
|
|
743
|
+
clear(box);
|
|
744
|
+
var list = state.proven || [];
|
|
745
|
+
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
|
+
}
|
|
750
|
+
list.forEach(function (p) {
|
|
751
|
+
var row = el('div', 'fix-row');
|
|
752
|
+
row.appendChild(el('span', 'tag leaf', 'FIXED'));
|
|
753
|
+
var text = el('div', 'fr-text');
|
|
754
|
+
text.appendChild(el('div', null, p.headline));
|
|
755
|
+
text.appendChild(el('div', 'fr-sub', p.where + ' - proven ' + ago(p.at) + (p.newProblems ? ' - ' + plural(p.newProblems, 'new problem') + ' at the time' : '')));
|
|
756
|
+
row.appendChild(text);
|
|
757
|
+
box.appendChild(row);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
var STATE_TONE = { 'problems found': 'ember', 'things to check': 'amber', 'last run held': 'leaf', 'nothing found': 'leaf' };
|
|
762
|
+
|
|
763
|
+
function checkSwitch(c) {
|
|
764
|
+
var sw = el('label', 'switch');
|
|
765
|
+
var input = el('input');
|
|
766
|
+
input.type = 'checkbox';
|
|
767
|
+
input.checked = !!c.enabled;
|
|
768
|
+
input.disabled = !c.available;
|
|
769
|
+
input.setAttribute('aria-label', c.label + (c.available ? '' : ' - not available'));
|
|
770
|
+
input.addEventListener('change', function () { setCheck(c.id, input.checked, input); });
|
|
771
|
+
sw.appendChild(input);
|
|
772
|
+
sw.appendChild(el('span', 'track'));
|
|
773
|
+
return sw;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function renderChecks() {
|
|
777
|
+
var box = byId('checkList');
|
|
778
|
+
clear(box);
|
|
779
|
+
var na = byId('naList');
|
|
780
|
+
clear(na);
|
|
781
|
+
var missing = 0;
|
|
782
|
+
state.checks.forEach(function (c) {
|
|
783
|
+
if (!c.available) {
|
|
784
|
+
missing++;
|
|
785
|
+
var row = el('div', 'na sunken');
|
|
786
|
+
row.appendChild(checkSwitch(c));
|
|
787
|
+
var name = el('div', null, c.label);
|
|
788
|
+
name.appendChild(el('span', 'na-lock', 'NOT AVAILABLE'));
|
|
789
|
+
row.appendChild(name);
|
|
790
|
+
na.appendChild(row);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
var card = el('div', 'check raised');
|
|
794
|
+
var top = el('div', 'c-top');
|
|
795
|
+
top.appendChild(el('div', 'c-name', c.label));
|
|
796
|
+
top.appendChild(checkSwitch(c));
|
|
797
|
+
card.appendChild(top);
|
|
798
|
+
card.appendChild(el('div', null)).appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
|
|
799
|
+
if (c.what) card.appendChild(el('div', 'c-what', c.what));
|
|
800
|
+
var foot = el('div', 'c-foot');
|
|
801
|
+
if (c.runnable && c.enabled) {
|
|
802
|
+
var run = button('sm', 'play', 'Run now', function () { runChecks([c.id], run); });
|
|
803
|
+
run.setAttribute('data-action', 'run');
|
|
804
|
+
run.disabled = busy;
|
|
805
|
+
foot.appendChild(run);
|
|
806
|
+
} else if (!c.enabled) {
|
|
807
|
+
foot.appendChild(el('span', 'note', 'Switched off - it will not run.'));
|
|
808
|
+
}
|
|
809
|
+
card.appendChild(foot);
|
|
810
|
+
box.appendChild(card);
|
|
811
|
+
});
|
|
812
|
+
byId('naBadge').textContent = String(missing);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderRuns() {
|
|
816
|
+
var box = byId('runList');
|
|
817
|
+
clear(box);
|
|
818
|
+
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
|
+
}
|
|
823
|
+
state.runs.slice(0, 30).forEach(function (r) {
|
|
824
|
+
var row = el('div', 'fix-row');
|
|
825
|
+
row.appendChild(el('span', 'tag ' + (r.failed ? 'ember' : 'leaf'), r.failed ? r.failed + ' broke' : 'held'));
|
|
826
|
+
var text = el('div', 'fr-text');
|
|
827
|
+
text.appendChild(el('div', null, (r.passed || 0) + ' held, ' + (r.failed || 0) + ' broke'));
|
|
828
|
+
text.appendChild(el('div', 'fr-sub', when(r.runAt)));
|
|
829
|
+
row.appendChild(text);
|
|
830
|
+
if (r.failed) row.appendChild(button('sm', 'arrow', 'See the finding', function () { go('findings', 'confirmed'); }));
|
|
831
|
+
box.appendChild(row);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
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
|
+
var STORE_FILES = [
|
|
857
|
+
['baselines.json', 'what each page looked like the last time it worked'],
|
|
858
|
+
['history.jsonl', 'every run of your recorded flows'],
|
|
859
|
+
['test-results/', 'screenshots and traces from runs that broke'],
|
|
860
|
+
['code-findings.json', 'the last read of your frontend code'],
|
|
861
|
+
['night-last.json', 'the last database check'],
|
|
862
|
+
['snapshot.json', 'what your files looked like at the last look'],
|
|
863
|
+
['changes.jsonl', 'every look: what changed, what broke, what got fixed'],
|
|
864
|
+
['fixes.jsonl', 'every re-check and its verdict'],
|
|
865
|
+
['config.json', 'which checks are switched on'],
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
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/.';
|
|
871
|
+
byId('storePath').textContent = state.project.storeDir;
|
|
872
|
+
var box = byId('storeFiles');
|
|
873
|
+
clear(box);
|
|
874
|
+
STORE_FILES.forEach(function (f) {
|
|
875
|
+
var row = el('div');
|
|
876
|
+
row.appendChild(el('code', null, f[0]));
|
|
877
|
+
row.appendChild(el('span', null, f[1]));
|
|
878
|
+
box.appendChild(row);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
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
|
+
/* ---------------------------------- actions ---------------------------------- */
|
|
892
|
+
|
|
893
|
+
function recheck(f, btn) {
|
|
894
|
+
if (busy) return;
|
|
895
|
+
setBusy(true);
|
|
896
|
+
clear(btn);
|
|
897
|
+
btn.appendChild(el('span', 'spin'));
|
|
898
|
+
btn.appendChild(el('span', null, f.check === 'regression' ? 'Replaying...' : 'Re-checking...'));
|
|
899
|
+
post('/api/recheck', { id: f.id }).then(function (r) {
|
|
900
|
+
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
|
+
(r.newProblems && r.newProblems.length ? ' - ' + plural(r.newProblems.length, 'new problem') : ''));
|
|
903
|
+
}).catch(function (err) {
|
|
904
|
+
toast('Re-check failed: ' + err.message);
|
|
905
|
+
}).then(function () {
|
|
906
|
+
setBusy(false);
|
|
907
|
+
return load();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function runChecks(ids, btn) {
|
|
912
|
+
if (busy || !ids.length) return;
|
|
913
|
+
setBusy(true);
|
|
914
|
+
var error = byId('checkError');
|
|
915
|
+
error.hidden = true;
|
|
916
|
+
clear(btn);
|
|
917
|
+
btn.appendChild(el('span', 'spin'));
|
|
918
|
+
btn.appendChild(el('span', null, 'Running...'));
|
|
919
|
+
ids.reduce(function (chain, id) {
|
|
920
|
+
return chain.then(function () { return post('/api/run', { id: id }); });
|
|
921
|
+
}, Promise.resolve()).then(function () {
|
|
922
|
+
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
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function setCheck(id, on, input) {
|
|
934
|
+
var error = byId('checkError');
|
|
935
|
+
error.hidden = true;
|
|
936
|
+
post('/api/check', { id: id, on: on }).then(function () {
|
|
937
|
+
toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
|
|
938
|
+
return load();
|
|
939
|
+
}).catch(function (err) {
|
|
940
|
+
input.checked = !on;
|
|
941
|
+
error.textContent = 'Not saved: ' + err.message;
|
|
942
|
+
error.hidden = false;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
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
|
+
byId('runAll').addEventListener('click', function () {
|
|
953
|
+
if (!state) return;
|
|
954
|
+
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
|
+
}
|
|
959
|
+
runChecks(ids, byId('runAll'));
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
/* ----------------------------------- load ----------------------------------- */
|
|
963
|
+
|
|
964
|
+
function render(next) {
|
|
965
|
+
if (!next) return;
|
|
966
|
+
state = next;
|
|
967
|
+
renderSide();
|
|
968
|
+
renderTop();
|
|
969
|
+
renderSky();
|
|
970
|
+
renderKpis();
|
|
971
|
+
renderGuide();
|
|
972
|
+
renderRange();
|
|
973
|
+
renderChart();
|
|
974
|
+
renderDonut();
|
|
975
|
+
renderStatusFilter();
|
|
976
|
+
renderFindings();
|
|
977
|
+
renderTimeline();
|
|
978
|
+
renderFixes();
|
|
979
|
+
renderChecks();
|
|
980
|
+
renderRuns();
|
|
981
|
+
renderVerification();
|
|
982
|
+
renderStorage();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function load() {
|
|
986
|
+
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
|
+
})
|
|
991
|
+
.then(render)
|
|
992
|
+
.catch(function (err) {
|
|
993
|
+
byId('subline').textContent = 'Could not load: ' + err.message;
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
renderWhen();
|
|
998
|
+
showView();
|
|
999
|
+
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.
|
|
1002
|
+
setInterval(function () { if (!busy && !openRow) load(); }, 5000);
|
|
1003
|
+
})();
|