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.
@@ -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 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.
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
- 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
- }
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
- lock: 'M6 11h12v10H6zM8 11V7a4 4 0 018 0v4',
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 or the
165
- // back button lands on the same one.
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', 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' },
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, !!(c.lastRun && c.lastRun.failed)],
171
+ runs: [state.runs.length, !!(state.runs[0] && state.runs[0].failed)],
232
172
  };
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
- });
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
- 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.';
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 ? '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
- }
192
+ byId('subline').textContent = name + ' - ' + (last ? 'watching, last look ' + ago(last.at) : 'watching for changes');
278
193
  }
279
194
 
280
- /* --------------------------------- dashboard --------------------------------- */
195
+ /* ------------------------------ right now + steps ------------------------------ */
281
196
 
282
- function renderSky() {
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
- clear(title);
291
- clear(sub);
292
- clear(actions);
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
- 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.' : '')]);
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 (c.toVerify) {
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
- 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.']);
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
- 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'); }));
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 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);
237
+ function scrollToId(id) {
238
+ var t = byId(id);
239
+ if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' });
345
240
  }
346
241
 
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');
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
- 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); });
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
- /* ----------------------------------- 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]);
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.addEventListener('click', function () { range = r[0]; renderRange(); renderChart(); });
420
- box.appendChild(b);
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
- 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;
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 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 }));
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
- // 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';
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
- 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
- });
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 detailRow(f) {
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 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);
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
- td.appendChild(box);
647
- tr.appendChild(td);
648
- return tr;
436
+ return box;
649
437
  }
650
438
 
651
439
  function renderFindings() {
652
- var body = byId('findingRows');
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
- if (!q) return true;
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
- 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);
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 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));
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
- 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);
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', 'fix-row');
518
+ var row = el('div', 'line-row');
752
519
  row.appendChild(el('span', 'tag leaf', 'FIXED'));
753
- var text = el('div', 'fr-text');
520
+ var text = el('div', 'lr-text');
754
521
  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' : '')));
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 sunken');
550
+ var row = el('div', 'na');
786
551
  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);
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 raised');
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
- card.appendChild(el('div', null)).appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
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
- foot.appendChild(run);
571
+ card.appendChild(run);
806
572
  } else if (!c.enabled) {
807
- foot.appendChild(el('span', 'note', 'Switched off - it will not run.'));
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', 'fix-row');
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', 'fr-text');
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', 'fr-sub', when(r.runAt)));
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
- (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
- });
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
- renderSky();
970
- renderKpis();
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
- renderWhen();
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
  })();