kryptheon 0.1.20 → 0.1.22

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.
@@ -0,0 +1,602 @@
1
+ // The dashboard page. Served from 127.0.0.1 by kryptheon-dashboard.js.
2
+ //
3
+ // Every value on this page can hold text from the app being checked - a line
4
+ // of its code, the heading of a page that broke. So nothing here is ever
5
+ // written as HTML: every node is made with createElement (or createElementNS
6
+ // for the icons, from fixed path data) and filled with textContent. This file
7
+ // is read by kryptheon-code.js in its own check, and must come back clean.
8
+
9
+ (function () {
10
+ 'use strict';
11
+
12
+ var tokenMeta = document.querySelector('meta[name="kryptheon-token"]');
13
+ var TOKEN = tokenMeta ? tokenMeta.getAttribute('content') : '';
14
+ var NS = 'http://www.w3.org/2000/svg';
15
+ var state = null;
16
+ var busy = false;
17
+ var openRow = null;
18
+ var allLooks = false;
19
+
20
+ /* ------------------------------ building blocks ------------------------------ */
21
+
22
+ function el(tag, cls, text) {
23
+ var node = document.createElement(tag);
24
+ if (cls) node.className = cls;
25
+ if (text !== undefined && text !== null) node.textContent = String(text);
26
+ return node;
27
+ }
28
+ function svgEl(tag, attrs) {
29
+ var node = document.createElementNS(NS, tag);
30
+ Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, String(attrs[k])); });
31
+ return node;
32
+ }
33
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
34
+ function byId(id) { return document.getElementById(id); }
35
+
36
+ // Line icons from fixed path data only.
37
+ var ICONS = {
38
+ grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
39
+ list: 'M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01',
40
+ change: 'M4 12h4l3-7 4 14 3-7h2',
41
+ wrench: 'M14.7 6.3a4 4 0 00-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 005.4-5.4l-2.6 2.6-2.4-.6-.6-2.4 2.6-2.6z',
42
+ toggle: 'M8 7h8a5 5 0 010 10H8A5 5 0 018 7zm0 3a2 2 0 100 4 2 2 0 000-4z',
43
+ play: 'M7 5l12 7-12 7V5z',
44
+ box: 'M4 7l8-4 8 4v10l-8 4-8-4V7zm0 0l8 4 8-4M12 11v10',
45
+ alert: 'M12 3l10 18H2L12 3zm0 7v5m0 3v.5',
46
+ eye: 'M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12zm10 3a3 3 0 100-6 3 3 0 000 6z',
47
+ check: 'M5 12.5l4.5 4.5L19 7.5',
48
+ film: 'M4 5h16v14H4zM4 9h16M4 15h16M8 5v14M16 5v14',
49
+ copy: 'M9 9h10v12H9zM5 15V3h10',
50
+ refresh: 'M20 12a8 8 0 10-2.3 5.7M20 12V6m0 6h-6',
51
+ arrow: 'M5 12h14m-6-6l6 6-6 6',
52
+ sun: 'M12 16a4 4 0 100-8 4 4 0 000 8zM12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
53
+ terminal: 'M4 5h16v14H4zM7 9l3 3-3 3M13 15h4',
54
+ dot: 'M12 12h.01',
55
+ };
56
+ function icon(name, size) {
57
+ var s = svgEl('svg', {
58
+ width: size || 18, height: size || 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
59
+ 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
60
+ });
61
+ s.appendChild(svgEl('path', { d: ICONS[name] || ICONS.box }));
62
+ return s;
63
+ }
64
+
65
+ function when(iso) {
66
+ if (!iso) return 'unknown time';
67
+ var d = new Date(iso);
68
+ return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
69
+ }
70
+ function ago(iso) {
71
+ var d = new Date(iso);
72
+ if (isNaN(d.getTime())) return '';
73
+ var s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1000));
74
+ if (s < 60) return s + 's ago';
75
+ if (s < 3600) return Math.round(s / 60) + ' min ago';
76
+ if (s < 86400) return Math.round(s / 3600) + ' h ago';
77
+ return Math.round(s / 86400) + ' d ago';
78
+ }
79
+ function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many || one + 's'); }
80
+
81
+ function toast(text) {
82
+ var t = byId('toast');
83
+ t.textContent = text;
84
+ t.classList.add('show');
85
+ clearTimeout(toast.timer);
86
+ toast.timer = setTimeout(function () { t.classList.remove('show'); }, 3200);
87
+ }
88
+
89
+ function post(path, body) {
90
+ return fetch(path, {
91
+ method: 'POST',
92
+ headers: { 'content-type': 'application/json', 'x-kryptheon-token': TOKEN },
93
+ body: JSON.stringify(body),
94
+ }).then(function (res) {
95
+ return res.json().then(function (data) {
96
+ if (!res.ok || data.ok === false) throw new Error(data.why || 'the dashboard answered ' + res.status);
97
+ return data;
98
+ });
99
+ });
100
+ }
101
+
102
+ function setBusy(on) {
103
+ busy = on;
104
+ Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
105
+ }
106
+ function copy(text, done) {
107
+ navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
108
+ toast('Could not copy - select the text instead');
109
+ });
110
+ }
111
+ function button(cls, iconName, text, onClick) {
112
+ var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
113
+ b.type = 'button';
114
+ if (iconName) b.appendChild(icon(iconName, 15));
115
+ b.appendChild(el('span', null, text));
116
+ b.addEventListener('click', onClick);
117
+ return b;
118
+ }
119
+
120
+ var CHECK_NAME = { regression: 'Recorded flows', frontend: 'Frontend code', data: 'Database' };
121
+ var CHECK_TONE = { regression: 'sun', frontend: 'amber', data: 'dusk' };
122
+
123
+ function counts() {
124
+ var confirmed = state.findings.filter(function (f) { return f.status === 'confirmed'; }).length;
125
+ return { confirmed: confirmed, toVerify: state.findings.length - confirmed, proven: (state.proven || []).length };
126
+ }
127
+
128
+ /* ----------------------------------- views ----------------------------------- */
129
+
130
+ // One section on screen at a time; the address keeps it, so a reload lands
131
+ // on the same one.
132
+ var VIEWS = [
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' },
140
+ ];
141
+
142
+ function currentView() {
143
+ var id = (location.hash || '').replace(/^#/, '');
144
+ return VIEWS.some(function (v) { return v.id === id; }) ? id : 'dashboard';
145
+ }
146
+ function go(id, filter) {
147
+ if (filter) { statusFilter = filter; if (state) { renderStatusFilter(); renderFindings(); } }
148
+ if (currentView() === id) return showView();
149
+ location.hash = id;
150
+ }
151
+ function showView() {
152
+ var id = currentView();
153
+ VIEWS.forEach(function (v) { byId('view-' + v.id).hidden = v.id !== id; });
154
+ var view = VIEWS.filter(function (v) { return v.id === id; })[0];
155
+ byId('viewTitle').textContent = view.title;
156
+ document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
157
+ if (state) renderSide();
158
+ window.scrollTo(0, 0);
159
+ }
160
+ window.addEventListener('hashchange', showView);
161
+
162
+ /* ---------------------------------- sidebar ---------------------------------- */
163
+
164
+ function renderSide() {
165
+ var c = counts();
166
+ var active = currentView();
167
+ var badge = {
168
+ findings: [state.findings.length, c.confirmed > 0],
169
+ changes: [(state.looks || []).length, false],
170
+ fixes: [c.proven, false],
171
+ runs: [state.runs.length, !!(state.runs[0] && state.runs[0].failed)],
172
+ };
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);
185
+ });
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.';
190
+ var name = state.project.name || 'Project';
191
+ var last = (state.looks || [])[0];
192
+ byId('subline').textContent = name + ' - ' + (last ? 'watching, last look ' + ago(last.at) : 'watching for changes');
193
+ }
194
+
195
+ /* ------------------------------ right now + steps ------------------------------ */
196
+
197
+ function renderNow() {
198
+ var sky = byId('sky');
199
+ var ic = byId('skyIcon');
200
+ var title = byId('skyTitle');
201
+ var sub = byId('skySub');
202
+ var actions = byId('skyActions');
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') {
207
+ sky.className = 'sky bad';
208
+ ic.style.color = 'var(--ember)';
209
+ ic.appendChild(icon('alert', 28));
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.';
212
+ actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'confirmed'); }));
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') {
221
+ sky.className = 'sky';
222
+ ic.style.color = 'var(--amber)';
223
+ ic.appendChild(icon('eye', 28));
224
+ title.textContent = next.title;
225
+ sub.textContent = 'Spotted without running anything. Each needs a person to decide.';
226
+ actions.appendChild(button('sun', 'arrow', 'Open Findings', function () { go('findings', 'verify'); }));
227
+ } else {
228
+ sky.className = 'sky ok';
229
+ ic.style.color = 'var(--leaf)';
230
+ ic.appendChild(icon('sun', 28));
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'); }));
234
+ }
235
+ }
236
+
237
+ function scrollToId(id) {
238
+ var t = byId(id);
239
+ if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' });
240
+ }
241
+
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');
247
+ clear(box);
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);
278
+ box.appendChild(row);
279
+ });
280
+ }
281
+
282
+ /* ---------------------------------- findings ---------------------------------- */
283
+
284
+ var statusFilter = 'all';
285
+ var query = '';
286
+
287
+ function renderStatusFilter() {
288
+ var box = byId('statusFilter');
289
+ clear(box);
290
+ [['all', 'All'], ['confirmed', 'Confirmed'], ['verify', 'To verify']].forEach(function (s) {
291
+ var b = el('button', statusFilter === s[0] ? 'on' : '', s[1]);
292
+ b.type = 'button';
293
+ b.addEventListener('click', function () { statusFilter = s[0]; renderStatusFilter(); renderFindings(); });
294
+ box.appendChild(b);
295
+ });
296
+ }
297
+ function statusTag(f) {
298
+ var t = el('span', 'tag ' + (f.status === 'confirmed' ? 'ember' : 'amber'));
299
+ t.appendChild(el('span', f.status === 'confirmed' ? 'g-dot' : 'g-half'));
300
+ t.appendChild(el('span', null, f.status === 'confirmed' ? 'Confirmed' : 'To verify'));
301
+ return t;
302
+ }
303
+ function sevTag(sev) {
304
+ var tone = /CRITICAL/.test(sev) ? 'ember' : sev === 'HIGH' ? 'sun' : sev === 'MEDIUM' ? 'amber' : 'leaf';
305
+ return el('span', 'tag ' + tone, sev);
306
+ }
307
+
308
+ var VERDICT_TONE = { fixed: 'leaf', 'still open': 'ember', 'gone with its file': 'amber', 'could not confirm': 'amber' };
309
+
310
+ function detailBox(f) {
311
+ var box = el('div', 'detail-box');
312
+ box.appendChild(el('div', 'd-step', 'WHAT IS WRONG'));
313
+ box.appendChild(el('div', 'd-text', f.detail || f.headline));
314
+ box.appendChild(el('div', 'd-text', 'Seen ' + when(f.when) + ' - ' + f.evidence + ' - id ' + f.id));
315
+ if (f.fixPrompt) {
316
+ box.appendChild(el('div', 'd-step', 'STEP 1 - PASTE THIS INTO YOUR AI TOOL'));
317
+ box.appendChild(el('pre', null, f.fixPrompt));
318
+ }
319
+ box.appendChild(el('div', 'd-step', f.fixPrompt ? 'STEP 2 - AFTER IT CHANGES THE CODE, RE-CHECK' : 'RE-CHECK'));
320
+ var actions = el('div', 'd-actions');
321
+ if (f.fixPrompt) {
322
+ actions.appendChild(button('', 'copy', 'Copy fix prompt', function (e) {
323
+ e.stopPropagation();
324
+ copy(f.fixPrompt, 'Fix prompt copied - paste it into your AI tool');
325
+ }));
326
+ }
327
+ var re = button('sun sm', 'refresh', 'Re-check', function (e) { e.stopPropagation(); recheck(f, re); });
328
+ re.setAttribute('data-action', 'recheck');
329
+ re.disabled = busy;
330
+ actions.appendChild(re);
331
+ box.appendChild(actions);
332
+ if (f.attempts && f.attempts.length) {
333
+ var list = el('div', 'attempts');
334
+ f.attempts.slice(-4).reverse().forEach(function (a) {
335
+ var r = el('div');
336
+ r.appendChild(el('span', 'tag ' + (VERDICT_TONE[a.verdict] || ''), a.verdict));
337
+ r.appendChild(el('span', null, 'Re-checked ' + ago(a.at) + (a.why ? ' - ' + a.why : '') + (a.newProblems ? ' - ' + plural(a.newProblems, 'new problem') : '')));
338
+ list.appendChild(r);
339
+ });
340
+ box.appendChild(list);
341
+ }
342
+ return box;
343
+ }
344
+
345
+ function renderFindings() {
346
+ var body = byId('findingList');
347
+ clear(body);
348
+ var shown = state.findings.filter(function (f) {
349
+ if (statusFilter === 'confirmed' && f.status !== 'confirmed') return false;
350
+ if (statusFilter === 'verify' && f.status === 'confirmed') return false;
351
+ return true;
352
+ });
353
+ byId('findingBadge').textContent = shown.length + (shown.length !== state.findings.length ? ' of ' + state.findings.length : '');
354
+ if (!shown.length) {
355
+ 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.'));
356
+ return;
357
+ }
358
+ shown.forEach(function (f) {
359
+ var card = el('div', 'finding ' + (f.status === 'confirmed' ? 'bad' : 'warn'));
360
+ var head = el('button', 'finding-head');
361
+ head.type = 'button';
362
+ var left = el('div', 'finding-left');
363
+ left.appendChild(el('div', 'f-title', f.headline));
364
+ var meta = el('div', 'f-meta');
365
+ meta.appendChild(sevTag(f.severity));
366
+ meta.appendChild(statusTag(f));
367
+ meta.appendChild(el('span', 'tag ' + (CHECK_TONE[f.check] || ''), CHECK_NAME[f.check] || f.check));
368
+ meta.appendChild(el('span', 'f-where', f.where));
369
+ left.appendChild(meta);
370
+ head.appendChild(left);
371
+ head.appendChild(icon(openRow === f.id ? 'change' : 'arrow', 18));
372
+ head.addEventListener('click', function () { openRow = openRow === f.id ? null : f.id; renderFindings(); });
373
+ card.appendChild(head);
374
+ if (openRow === f.id) card.appendChild(detailBox(f));
375
+ body.appendChild(card);
376
+ });
377
+ }
378
+
379
+ /* -------------------------------- other views -------------------------------- */
380
+
381
+ var LOOKS_SHOWN = 12;
382
+ function renderTimeline() {
383
+ var box = byId('timeline');
384
+ clear(box);
385
+ var looks = state.looks || [];
386
+ byId('lookBadge').textContent = String(looks.length);
387
+ if (!looks.length) {
388
+ 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.'));
389
+ return;
390
+ }
391
+ (allLooks ? looks : looks.slice(0, LOOKS_SHOWN)).forEach(function (look) {
392
+ var item = el('div', 'tl');
393
+ item.appendChild(el('div', 'knob' + (look.newFindings ? ' bad' : look.fixed ? ' ok' : look.goneWithFile ? ' mid' : '')));
394
+ var body = el('div');
395
+ body.appendChild(el('div', 'tl-head', look.first ? 'Starting point'
396
+ : look.fileCount ? plural(look.fileCount, 'file') + ' changed - ' + look.parts.join(', ') : 'Nothing changed'));
397
+ body.appendChild(el('div', 'tl-meta', (look.by === 'watch' ? 'Noticed by itself' : look.by === 'recheck' ? 'Re-check' : 'npx kryptheon verify') + ' - ' + when(look.at)));
398
+ var tags = el('div', 'tl-body');
399
+ if (look.newFindings) tags.appendChild(el('span', 'tag ember', look.newFindings + ' new'));
400
+ if (look.fixed) tags.appendChild(el('span', 'tag leaf', look.fixed + ' fixed'));
401
+ if (look.goneWithFile) tags.appendChild(el('span', 'tag amber', look.goneWithFile + ' gone with its file'));
402
+ look.checks.forEach(function (c) { tags.appendChild(el('span', 'tag', c.id + ': ' + c.state)); });
403
+ body.appendChild(tags);
404
+ var files = el('div', 'tl-files');
405
+ look.files.slice(0, 5).forEach(function (f) { files.appendChild(el('div', null, f.state + ' ' + f.path)); });
406
+ if (look.fileCount > 5) files.appendChild(el('div', null, '... and ' + (look.fileCount - 5) + ' more'));
407
+ look.dependencies.forEach(function (d) { files.appendChild(el('div', null, 'package ' + d.name + ': ' + d.change + (d.to ? ' ' + d.to : ''))); });
408
+ body.appendChild(files);
409
+ item.appendChild(body);
410
+ box.appendChild(item);
411
+ });
412
+ if (looks.length > LOOKS_SHOWN) {
413
+ box.appendChild(button('sm', null, allLooks ? 'Show the latest ' + LOOKS_SHOWN : 'Show all ' + looks.length, function () { allLooks = !allLooks; renderTimeline(); }));
414
+ }
415
+ }
416
+
417
+ function renderFixes() {
418
+ var box = byId('fixList');
419
+ clear(box);
420
+ var list = state.proven || [];
421
+ byId('fixBadge').textContent = String(list.length);
422
+ if (!list.length) { box.appendChild(el('p', 'empty', 'No fix has been proven yet. Open a finding, fix it, and press Re-check.')); return; }
423
+ list.forEach(function (p) {
424
+ var row = el('div', 'line-row');
425
+ row.appendChild(el('span', 'tag leaf', 'FIXED'));
426
+ var text = el('div', 'lr-text');
427
+ text.appendChild(el('div', null, p.headline));
428
+ text.appendChild(el('div', 'lr-sub', p.where + ' - proven ' + ago(p.at) + (p.newProblems ? ' - ' + plural(p.newProblems, 'new problem') + ' at the time' : '')));
429
+ row.appendChild(text);
430
+ box.appendChild(row);
431
+ });
432
+ }
433
+
434
+ var STATE_TONE = { 'problems found': 'ember', 'things to check': 'amber', 'last run held': 'leaf', 'nothing found': 'leaf' };
435
+ function checkSwitch(c) {
436
+ var sw = el('label', 'switch');
437
+ var input = el('input');
438
+ input.type = 'checkbox';
439
+ input.checked = !!c.enabled;
440
+ input.disabled = !c.available;
441
+ input.setAttribute('aria-label', c.label + (c.available ? '' : ' - not available'));
442
+ input.addEventListener('change', function () { setCheck(c.id, input.checked, input); });
443
+ sw.appendChild(input);
444
+ sw.appendChild(el('span', 'track'));
445
+ return sw;
446
+ }
447
+ function renderChecks() {
448
+ var box = byId('checkList');
449
+ clear(box);
450
+ var na = byId('naList');
451
+ clear(na);
452
+ var missing = 0;
453
+ state.checks.forEach(function (c) {
454
+ if (!c.available) {
455
+ missing++;
456
+ var row = el('div', 'na');
457
+ row.appendChild(checkSwitch(c));
458
+ var nm = el('div', null, c.label);
459
+ nm.appendChild(el('span', 'na-lock', 'NOT AVAILABLE'));
460
+ row.appendChild(nm);
461
+ na.appendChild(row);
462
+ return;
463
+ }
464
+ var card = el('div', 'check');
465
+ var top = el('div', 'c-top');
466
+ top.appendChild(el('div', 'c-name', c.label));
467
+ top.appendChild(checkSwitch(c));
468
+ card.appendChild(top);
469
+ var st = el('div');
470
+ st.appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
471
+ card.appendChild(st);
472
+ if (c.what) card.appendChild(el('div', 'c-what', c.what));
473
+ if (c.runnable && c.enabled) {
474
+ var run = button('sm', 'play', 'Run now', function () { runChecks([c.id], run); });
475
+ run.setAttribute('data-action', 'run');
476
+ run.disabled = busy;
477
+ card.appendChild(run);
478
+ } else if (!c.enabled) {
479
+ card.appendChild(el('div', 'c-what', 'Switched off - it will not run.'));
480
+ }
481
+ box.appendChild(card);
482
+ });
483
+ byId('naBadge').textContent = String(missing);
484
+ }
485
+
486
+ function renderRuns() {
487
+ var box = byId('runList');
488
+ clear(box);
489
+ byId('runBadge').textContent = String(state.runs.length);
490
+ 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; }
491
+ state.runs.slice(0, 30).forEach(function (r) {
492
+ var row = el('div', 'line-row');
493
+ row.appendChild(el('span', 'tag ' + (r.failed ? 'ember' : 'leaf'), r.failed ? r.failed + ' broke' : 'held'));
494
+ var text = el('div', 'lr-text');
495
+ text.appendChild(el('div', null, (r.passed || 0) + ' held, ' + (r.failed || 0) + ' broke'));
496
+ text.appendChild(el('div', 'lr-sub', when(r.runAt)));
497
+ row.appendChild(text);
498
+ if (r.failed) row.appendChild(button('sm', 'arrow', 'See the finding', function () { go('findings', 'confirmed'); }));
499
+ box.appendChild(row);
500
+ });
501
+ }
502
+
503
+ var STORE_FILES = [
504
+ ['baselines.json', 'what each page looked like the last time it worked'],
505
+ ['history.jsonl', 'every run of your recorded flows'],
506
+ ['test-results/', 'screenshots and traces from runs that broke'],
507
+ ['code-findings.json', 'the last read of your frontend code'],
508
+ ['night-last.json', 'the last database check'],
509
+ ['snapshot.json', 'what your files looked like at the last look'],
510
+ ['changes.jsonl', 'every look: what changed, what broke, what got fixed'],
511
+ ['fixes.jsonl', 'every re-check and its verdict'],
512
+ ['config.json', 'which checks are switched on'],
513
+ ];
514
+ function renderStorage() {
515
+ 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/.';
516
+ byId('storePath').textContent = state.project.storeDir;
517
+ var box = byId('storeFiles');
518
+ clear(box);
519
+ STORE_FILES.forEach(function (f) {
520
+ var row = el('div');
521
+ row.appendChild(el('code', null, f[0]));
522
+ row.appendChild(el('span', null, f[1]));
523
+ box.appendChild(row);
524
+ });
525
+ }
526
+
527
+ /* ---------------------------------- actions ---------------------------------- */
528
+
529
+ function recheck(f, btn) {
530
+ if (busy) return;
531
+ setBusy(true);
532
+ clear(btn);
533
+ btn.appendChild(el('span', 'spin'));
534
+ btn.appendChild(el('span', null, f.check === 'regression' ? 'Replaying...' : 'Re-checking...'));
535
+ post('/api/recheck', { id: f.id }).then(function (r) {
536
+ 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' };
537
+ toast((words[r.verdict] || r.verdict) + (r.why ? ' - ' + r.why : '') + (r.newProblems && r.newProblems.length ? ' - ' + plural(r.newProblems.length, 'new problem') : ''));
538
+ }).catch(function (err) { toast('Re-check failed: ' + err.message); }).then(function () { setBusy(false); return load(); });
539
+ }
540
+
541
+ function runChecks(ids, btn) {
542
+ if (busy || !ids.length) return;
543
+ setBusy(true);
544
+ clear(btn);
545
+ btn.appendChild(el('span', 'spin'));
546
+ btn.appendChild(el('span', null, 'Running...'));
547
+ ids.reduce(function (chain, id) {
548
+ return chain.then(function () { return post('/api/run', { id: id }); });
549
+ }, Promise.resolve()).then(function () {
550
+ toast('Finished: ' + ids.map(function (id) { return CHECK_NAME[id] || id; }).join(', '));
551
+ }).catch(function (err) { toast('Run failed: ' + err.message); }).then(function () { setBusy(false); return load(); });
552
+ }
553
+
554
+ function setCheck(id, on, input) {
555
+ post('/api/check', { id: id, on: on }).then(function () {
556
+ toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
557
+ return load();
558
+ }).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
559
+ }
560
+
561
+ byId('runAll').addEventListener('click', function () {
562
+ if (!state) return;
563
+ var ids = state.checks.filter(function (c) { return c.runnable && c.enabled; }).map(function (c) { return c.id; });
564
+ if (!ids.length) { toast('Nothing can run from here yet - switch a check on, or record a flow.'); return; }
565
+ runChecks(ids, byId('runAll'));
566
+ });
567
+
568
+ /* ----------------------------------- load ----------------------------------- */
569
+
570
+ function renderTop() {
571
+ var run = byId('runAll');
572
+ if (!busy) { clear(run); run.appendChild(icon('play', 15)); run.appendChild(el('span', null, 'Run checks')); }
573
+ }
574
+
575
+ function render(next) {
576
+ if (!next) return;
577
+ state = next;
578
+ renderSide();
579
+ renderTop();
580
+ renderNow();
581
+ renderSteps();
582
+ renderStatusFilter();
583
+ renderFindings();
584
+ renderTimeline();
585
+ renderFixes();
586
+ renderChecks();
587
+ renderRuns();
588
+ renderStorage();
589
+ }
590
+
591
+ function load() {
592
+ return fetch('/api/state', { cache: 'no-store' })
593
+ .then(function (res) { if (!res.ok) throw new Error('the dashboard answered ' + res.status); return res.json(); })
594
+ .then(render)
595
+ .catch(function (err) { byId('subline').textContent = 'Could not load: ' + err.message; });
596
+ }
597
+
598
+ showView();
599
+ load();
600
+ // Not while something runs, and not while a finding is open being read.
601
+ setInterval(function () { if (!busy && !openRow) load(); }, 5000);
602
+ })();