kryptheon 0.1.21 → 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.
@@ -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,122 @@
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');
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 ----------------------------------- */
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' };
412
244
 
413
- function renderRange() {
414
- var box = byId('range');
245
+ function renderSteps() {
246
+ var box = byId('steps');
415
247
  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);
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);
571
279
  });
572
280
  }
573
281
 
574
282
  /* ---------------------------------- findings ---------------------------------- */
575
283
 
284
+ var statusFilter = 'all';
285
+ var query = '';
286
+
576
287
  function renderStatusFilter() {
577
288
  var box = byId('statusFilter');
578
289
  clear(box);
@@ -583,7 +294,6 @@
583
294
  box.appendChild(b);
584
295
  });
585
296
  }
586
-
587
297
  function statusTag(f) {
588
298
  var t = el('span', 'tag ' + (f.status === 'confirmed' ? 'ember' : 'amber'));
589
299
  t.appendChild(el('span', f.status === 'confirmed' ? 'g-dot' : 'g-half'));
@@ -594,23 +304,10 @@
594
304
  var tone = /CRITICAL/.test(sev) ? 'ember' : sev === 'HIGH' ? 'sun' : sev === 'MEDIUM' ? 'amber' : 'leaf';
595
305
  return el('span', 'tag ' + tone, sev);
596
306
  }
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
307
 
608
308
  var VERDICT_TONE = { fixed: 'leaf', 'still open': 'ember', 'gone with its file': 'amber', 'could not confirm': 'amber' };
609
309
 
610
- function detailRow(f) {
611
- var tr = el('tr', 'detail');
612
- var td = el('td');
613
- td.colSpan = 6;
310
+ function detailBox(f) {
614
311
  var box = el('div', 'detail-box');
615
312
  box.appendChild(el('div', 'd-step', 'WHAT IS WRONG'));
616
313
  box.appendChild(el('div', 'd-text', f.detail || f.headline));
@@ -635,70 +332,53 @@
635
332
  if (f.attempts && f.attempts.length) {
636
333
  var list = el('div', 'attempts');
637
334
  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);
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);
643
339
  });
644
340
  box.appendChild(list);
645
341
  }
646
- td.appendChild(box);
647
- tr.appendChild(td);
648
- return tr;
342
+ return box;
649
343
  }
650
344
 
651
345
  function renderFindings() {
652
- var body = byId('findingRows');
346
+ var body = byId('findingList');
653
347
  clear(body);
654
- var q = query.trim().toLowerCase();
655
348
  var shown = state.findings.filter(function (f) {
656
349
  if (statusFilter === 'confirmed' && f.status !== 'confirmed') return false;
657
350
  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;
351
+ return true;
660
352
  });
661
353
  byId('findingBadge').textContent = shown.length + (shown.length !== state.findings.length ? ' of ' + state.findings.length : '');
662
354
  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);
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.'));
668
356
  return;
669
357
  }
670
358
  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));
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);
695
376
  });
696
377
  }
697
378
 
698
379
  /* -------------------------------- other views -------------------------------- */
699
380
 
700
381
  var LOOKS_SHOWN = 12;
701
-
702
382
  function renderTimeline() {
703
383
  var box = byId('timeline');
704
384
  clear(box);
@@ -730,11 +410,7 @@
730
410
  box.appendChild(item);
731
411
  });
732
412
  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);
413
+ box.appendChild(button('sm', null, allLooks ? 'Show the latest ' + LOOKS_SHOWN : 'Show all ' + looks.length, function () { allLooks = !allLooks; renderTimeline(); }));
738
414
  }
739
415
  }
740
416
 
@@ -743,23 +419,19 @@
743
419
  clear(box);
744
420
  var list = state.proven || [];
745
421
  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
- }
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; }
750
423
  list.forEach(function (p) {
751
- var row = el('div', 'fix-row');
424
+ var row = el('div', 'line-row');
752
425
  row.appendChild(el('span', 'tag leaf', 'FIXED'));
753
- var text = el('div', 'fr-text');
426
+ var text = el('div', 'lr-text');
754
427
  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' : '')));
428
+ text.appendChild(el('div', 'lr-sub', p.where + ' - proven ' + ago(p.at) + (p.newProblems ? ' - ' + plural(p.newProblems, 'new problem') + ' at the time' : '')));
756
429
  row.appendChild(text);
757
430
  box.appendChild(row);
758
431
  });
759
432
  }
760
433
 
761
434
  var STATE_TONE = { 'problems found': 'ember', 'things to check': 'amber', 'last run held': 'leaf', 'nothing found': 'leaf' };
762
-
763
435
  function checkSwitch(c) {
764
436
  var sw = el('label', 'switch');
765
437
  var input = el('input');
@@ -772,7 +444,6 @@
772
444
  sw.appendChild(el('span', 'track'));
773
445
  return sw;
774
446
  }
775
-
776
447
  function renderChecks() {
777
448
  var box = byId('checkList');
778
449
  clear(box);
@@ -782,31 +453,31 @@
782
453
  state.checks.forEach(function (c) {
783
454
  if (!c.available) {
784
455
  missing++;
785
- var row = el('div', 'na sunken');
456
+ var row = el('div', 'na');
786
457
  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);
458
+ var nm = el('div', null, c.label);
459
+ nm.appendChild(el('span', 'na-lock', 'NOT AVAILABLE'));
460
+ row.appendChild(nm);
790
461
  na.appendChild(row);
791
462
  return;
792
463
  }
793
- var card = el('div', 'check raised');
464
+ var card = el('div', 'check');
794
465
  var top = el('div', 'c-top');
795
466
  top.appendChild(el('div', 'c-name', c.label));
796
467
  top.appendChild(checkSwitch(c));
797
468
  card.appendChild(top);
798
- card.appendChild(el('div', null)).appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
469
+ var st = el('div');
470
+ st.appendChild(el('span', 'tag ' + (STATE_TONE[c.state] || ''), c.state));
471
+ card.appendChild(st);
799
472
  if (c.what) card.appendChild(el('div', 'c-what', c.what));
800
- var foot = el('div', 'c-foot');
801
473
  if (c.runnable && c.enabled) {
802
474
  var run = button('sm', 'play', 'Run now', function () { runChecks([c.id], run); });
803
475
  run.setAttribute('data-action', 'run');
804
476
  run.disabled = busy;
805
- foot.appendChild(run);
477
+ card.appendChild(run);
806
478
  } else if (!c.enabled) {
807
- foot.appendChild(el('span', 'note', 'Switched off - it will not run.'));
479
+ card.appendChild(el('div', 'c-what', 'Switched off - it will not run.'));
808
480
  }
809
- card.appendChild(foot);
810
481
  box.appendChild(card);
811
482
  });
812
483
  byId('naBadge').textContent = String(missing);
@@ -816,43 +487,19 @@
816
487
  var box = byId('runList');
817
488
  clear(box);
818
489
  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
- }
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; }
823
491
  state.runs.slice(0, 30).forEach(function (r) {
824
- var row = el('div', 'fix-row');
492
+ var row = el('div', 'line-row');
825
493
  row.appendChild(el('span', 'tag ' + (r.failed ? 'ember' : 'leaf'), r.failed ? r.failed + ' broke' : 'held'));
826
- var text = el('div', 'fr-text');
494
+ var text = el('div', 'lr-text');
827
495
  text.appendChild(el('div', null, (r.passed || 0) + ' held, ' + (r.failed || 0) + ' broke'));
828
- text.appendChild(el('div', 'fr-sub', when(r.runAt)));
496
+ text.appendChild(el('div', 'lr-sub', when(r.runAt)));
829
497
  row.appendChild(text);
830
498
  if (r.failed) row.appendChild(button('sm', 'arrow', 'See the finding', function () { go('findings', 'confirmed'); }));
831
499
  box.appendChild(row);
832
500
  });
833
501
  }
834
502
 
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
503
  var STORE_FILES = [
857
504
  ['baselines.json', 'what each page looked like the last time it worked'],
858
505
  ['history.jsonl', 'every run of your recorded flows'],
@@ -864,10 +511,8 @@
864
511
  ['fixes.jsonl', 'every re-check and its verdict'],
865
512
  ['config.json', 'which checks are switched on'],
866
513
  ];
867
-
868
514
  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/.';
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/.';
871
516
  byId('storePath').textContent = state.project.storeDir;
872
517
  var box = byId('storeFiles');
873
518
  clear(box);
@@ -879,15 +524,6 @@
879
524
  });
880
525
  }
881
526
 
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
527
  /* ---------------------------------- actions ---------------------------------- */
892
528
 
893
529
  function recheck(f, btn) {
@@ -898,21 +534,13 @@
898
534
  btn.appendChild(el('span', null, f.check === 'regression' ? 'Replaying...' : 'Re-checking...'));
899
535
  post('/api/recheck', { id: f.id }).then(function (r) {
900
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' };
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
- });
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(); });
909
539
  }
910
540
 
911
541
  function runChecks(ids, btn) {
912
542
  if (busy || !ids.length) return;
913
543
  setBusy(true);
914
- var error = byId('checkError');
915
- error.hidden = true;
916
544
  clear(btn);
917
545
  btn.appendChild(el('span', 'spin'));
918
546
  btn.appendChild(el('span', null, 'Running...'));
@@ -920,84 +548,55 @@
920
548
  return chain.then(function () { return post('/api/run', { id: id }); });
921
549
  }, Promise.resolve()).then(function () {
922
550
  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
- });
551
+ }).catch(function (err) { toast('Run failed: ' + err.message); }).then(function () { setBusy(false); return load(); });
931
552
  }
932
553
 
933
554
  function setCheck(id, on, input) {
934
- var error = byId('checkError');
935
- error.hidden = true;
936
555
  post('/api/check', { id: id, on: on }).then(function () {
937
556
  toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
938
557
  return load();
939
- }).catch(function (err) {
940
- input.checked = !on;
941
- error.textContent = 'Not saved: ' + err.message;
942
- error.hidden = false;
943
- });
558
+ }).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
944
559
  }
945
560
 
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
561
  byId('runAll').addEventListener('click', function () {
953
562
  if (!state) return;
954
563
  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
- }
564
+ if (!ids.length) { toast('Nothing can run from here yet - switch a check on, or record a flow.'); return; }
959
565
  runChecks(ids, byId('runAll'));
960
566
  });
961
567
 
962
568
  /* ----------------------------------- load ----------------------------------- */
963
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
+
964
575
  function render(next) {
965
576
  if (!next) return;
966
577
  state = next;
967
578
  renderSide();
968
579
  renderTop();
969
- renderSky();
970
- renderKpis();
971
- renderGuide();
972
- renderRange();
973
- renderChart();
974
- renderDonut();
580
+ renderNow();
581
+ renderSteps();
975
582
  renderStatusFilter();
976
583
  renderFindings();
977
584
  renderTimeline();
978
585
  renderFixes();
979
586
  renderChecks();
980
587
  renderRuns();
981
- renderVerification();
982
588
  renderStorage();
983
589
  }
984
590
 
985
591
  function load() {
986
592
  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
- })
593
+ .then(function (res) { if (!res.ok) throw new Error('the dashboard answered ' + res.status); return res.json(); })
991
594
  .then(render)
992
- .catch(function (err) {
993
- byId('subline').textContent = 'Could not load: ' + err.message;
994
- });
595
+ .catch(function (err) { byId('subline').textContent = 'Could not load: ' + err.message; });
995
596
  }
996
597
 
997
- renderWhen();
998
598
  showView();
999
599
  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.
600
+ // Not while something runs, and not while a finding is open being read.
1002
601
  setInterval(function () { if (!busy && !openRow) load(); }, 5000);
1003
602
  })();