kryptheon 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/kryptheon.js CHANGED
@@ -2321,7 +2321,9 @@ function recheckCommand(id) {
2321
2321
  // One page on this machine for everything Kryptheon knows about this project
2322
2322
  // (kryptheon-dashboard.js). Runs until Ctrl+C.
2323
2323
 
2324
- const DASHBOARD_PORT = 4789;
2324
+ // KRYPTHEON_DASHBOARD_PORT is for the checks, which must not use the port of
2325
+ // a dashboard the person has open.
2326
+ const DASHBOARD_PORT = Number(process.env.KRYPTHEON_DASHBOARD_PORT) || 4789;
2325
2327
 
2326
2328
  /**
2327
2329
  * Runs a command and hands every line it prints to `say`, as it prints it -
@@ -2511,12 +2513,52 @@ function openInBrowser(url) {
2511
2513
  }
2512
2514
  }
2513
2515
 
2516
+ /**
2517
+ * Whether this project's dashboard is already running on the usual port -
2518
+ * started at sign-in, most likely - by asking it which project it shows.
2519
+ * Never fatal: no answer, a slow one, or another project's means no.
2520
+ */
2521
+ function dashboardAlreadyRunning(port) {
2522
+ return new Promise((resolve) => {
2523
+ const req = require('http').get({ host: '127.0.0.1', port: port, path: '/api/state', headers: { host: '127.0.0.1:' + port }, timeout: 2000 }, (res) => {
2524
+ let body = '';
2525
+ res.setEncoding('utf8');
2526
+ res.on('data', (chunk) => { if (body.length < 5000000) body += chunk; });
2527
+ res.on('end', () => {
2528
+ try {
2529
+ const root = JSON.parse(body).project.root;
2530
+ const same = (a) => (process.platform === 'win32' ? path.resolve(a).toLowerCase() : path.resolve(a));
2531
+ resolve(res.statusCode === 200 && typeof root === 'string' && same(root) === same(USER_DIR));
2532
+ } catch (err) {
2533
+ resolve(false);
2534
+ }
2535
+ });
2536
+ });
2537
+ req.on('timeout', () => req.destroy());
2538
+ req.on('error', () => resolve(false));
2539
+ });
2540
+ }
2541
+
2514
2542
  async function dashboardCommand() {
2515
2543
  const here = project.inspectProject(USER_DIR);
2516
2544
  if (!here.ok) {
2517
2545
  for (const line of project.noProjectLines(here)) console.error(line);
2518
2546
  return 1;
2519
2547
  }
2548
+ // Running already: open that one rather than a second, which would watch
2549
+ // the same project twice. Exit code 0 is how the launcher knows.
2550
+ if (await dashboardAlreadyRunning(DASHBOARD_PORT)) {
2551
+ const address = 'http://127.0.0.1:' + DASHBOARD_PORT + '/';
2552
+ const shown = openInBrowser(address);
2553
+ console.log('');
2554
+ console.log(' The Kryptheon dashboard for ' + path.basename(USER_DIR) + ' is already running:');
2555
+ console.log('');
2556
+ console.log(' ' + address);
2557
+ console.log('');
2558
+ console.log(shown ? ' It is opening in your browser.' : ' Open that address in your browser.');
2559
+ console.log('');
2560
+ return 0;
2561
+ }
2520
2562
  const opened = openStore();
2521
2563
  if (!opened) return 1;
2522
2564
  const dash = require(path.join(PACKAGE_DIR, 'kryptheon-dashboard.js'));
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Start with Windows: the dashboard for one project, started when the person
5
+ * signs in, so it is watching whenever the laptop is on.
6
+ *
7
+ * It is one small .cmd file in the person's own Startup folder - the plainest
8
+ * way Windows has, seen and removable in Task Manager's Startup apps and in
9
+ * the folder itself. The file says what it is and runs one thing:
10
+ * npx kryptheon@latest dashboard, in a minimized window on the taskbar. A
11
+ * window, not a hidden process: something that starts by itself at sign-in
12
+ * should be seen, and closing it stops it.
13
+ *
14
+ * Only the file this module wrote is ever removed: it is recognised by its
15
+ * first lines, never by its name alone.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const crypto = require('crypto');
21
+
22
+ const MARK = 'rem Kryptheon - starts the Kryptheon dashboard for this project when you sign in to Windows.';
23
+ const CRLF = '\r\n';
24
+
25
+ /** The Startup folder, or null where there is none (not Windows). */
26
+ function startupDir(env, platform) {
27
+ const e = env || process.env;
28
+ // For the checks: a folder of their own, never the real Startup folder.
29
+ if (e.KRYPTHEON_STARTUP_DIR) return e.KRYPTHEON_STARTUP_DIR;
30
+ if ((platform || process.platform) !== 'win32' || !e.APPDATA) return null;
31
+ return path.join(e.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
32
+ }
33
+
34
+ /** The folder's name as it may be shown in a file name and a window title. */
35
+ function plainName(root) {
36
+ return path.basename(root).replace(/[^A-Za-z0-9 ._-]/g, '_').slice(0, 40) || 'project';
37
+ }
38
+
39
+ /**
40
+ * One file per project, named after the folder and told apart by its full
41
+ * path - two projects called "app" get two files. Windows paths are compared
42
+ * without regard to case, so the same folder spelled two ways is one file.
43
+ */
44
+ function fileFor(root, env, platform) {
45
+ const dir = startupDir(env, platform);
46
+ if (!dir) return null;
47
+ const full = path.resolve(root);
48
+ const key = (platform || process.platform) === 'win32' ? full.toLowerCase() : full;
49
+ const id = crypto.createHash('sha256').update(key).digest('hex').slice(0, 8);
50
+ return path.join(dir, 'Kryptheon dashboard - ' + plainName(root) + ' ' + id + '.cmd');
51
+ }
52
+
53
+ /**
54
+ * The file's text. The project's path appears only inside double quotes, so
55
+ * & ^ ( ) and spaces in it are read as themselves; % is doubled, the one
56
+ * character cmd still reads inside quotes. The file is UTF-8 and switches
57
+ * the window to UTF-8 before the path is read, for folder names in any
58
+ * language.
59
+ */
60
+ function script(root) {
61
+ const full = path.resolve(root);
62
+ const quoted = '"' + full.replace(/%/g, '%%') + '"';
63
+ const name = plainName(root);
64
+ return [
65
+ '@echo off',
66
+ 'chcp 65001 >nul',
67
+ MARK,
68
+ 'rem ' + quoted,
69
+ 'rem Put here by "Start with Windows" on the Kryptheon dashboard. To stop it, switch',
70
+ 'rem that off on the dashboard, or delete this file. What it runs:',
71
+ 'rem npx kryptheon@latest dashboard - nothing else, in a small window on the taskbar.',
72
+ 'if "%~1"=="run" goto run',
73
+ // cmd /c, not start's own cmd /k: a window whose file was removed while it
74
+ // ran - switched off on the dashboard - closes instead of waiting at a prompt.
75
+ 'start "Kryptheon dashboard - ' + name + '" /min cmd /d /c ""%~f0" run"',
76
+ 'exit /b',
77
+ ':run',
78
+ 'title Kryptheon dashboard - ' + name,
79
+ 'rem npx starts the command "kryptheon": never look for it in the current folder,',
80
+ 'rem where a Kryptheon.cmd launcher may sit, and the browser is not opened at sign-in.',
81
+ 'set NoDefaultCurrentDirectoryInExePath=1',
82
+ 'set KRYPTHEON_LAUNCHER=1',
83
+ 'set KRYPTHEON_NO_OPEN=1',
84
+ 'cd /d ' + quoted + ' || goto gone',
85
+ 'echo.',
86
+ 'echo Kryptheon dashboard, started when you signed in, for:',
87
+ 'echo ' + quoted,
88
+ 'echo Closing this window stops it until you sign in again.',
89
+ 'echo.',
90
+ 'call npx --yes kryptheon@latest dashboard',
91
+ 'if not errorlevel 1 exit',
92
+ 'rem At sign-in the network may not be up yet: then the copy npx already has.',
93
+ 'echo Trying again with the copy already on this computer...',
94
+ 'call npx --yes --offline kryptheon@latest dashboard',
95
+ 'if not errorlevel 1 exit',
96
+ 'echo.',
97
+ 'echo The Kryptheon dashboard stopped. The reason, if any, is above.',
98
+ 'pause',
99
+ 'exit',
100
+ ':gone',
101
+ 'echo.',
102
+ 'echo This project folder is gone, so its dashboard cannot start:',
103
+ 'echo ' + quoted,
104
+ 'echo To stop this message, delete this file:',
105
+ 'echo "%~f0"',
106
+ 'pause',
107
+ 'exit',
108
+ '',
109
+ ].join(CRLF);
110
+ }
111
+
112
+ /** Whether it is on for this project, and whether it can be at all. */
113
+ function status(root, env, platform) {
114
+ const file = fileFor(root, env, platform);
115
+ if (!file) return { available: false, on: false, why: 'Starting with the computer is only on Windows for now.' };
116
+ let on = false;
117
+ try {
118
+ on = fs.readFileSync(file, 'utf8').indexOf(MARK) !== -1;
119
+ } catch (err) {
120
+ on = false;
121
+ }
122
+ return { available: true, on: on, file: file };
123
+ }
124
+
125
+ /** Switches it on (writes the file) or off (removes only a file it wrote). */
126
+ function setAutostart(root, on, env, platform) {
127
+ if (typeof on !== 'boolean') return { ok: false, why: 'on must be true or false' };
128
+ const file = fileFor(root, env, platform);
129
+ if (!file) return { ok: false, why: 'Starting with the computer is only on Windows for now.' };
130
+ try {
131
+ if (on) {
132
+ fs.mkdirSync(path.dirname(file), { recursive: true });
133
+ fs.writeFileSync(file, script(root), 'utf8');
134
+ } else {
135
+ let text = null;
136
+ try { text = fs.readFileSync(file, 'utf8'); } catch (err) { text = null; }
137
+ if (text !== null) {
138
+ if (text.indexOf(MARK) === -1) return { ok: false, why: 'the file there was not written by Kryptheon, so it was left alone: ' + file };
139
+ fs.unlinkSync(file);
140
+ }
141
+ }
142
+ } catch (err) {
143
+ return { ok: false, why: 'could not ' + (on ? 'write' : 'remove') + ' ' + file + ': ' + err.message };
144
+ }
145
+ return Object.assign({ ok: true }, status(root, env, platform));
146
+ }
147
+
148
+ module.exports = { startupDir, fileFor, script, status, setAutostart, MARK };
@@ -103,10 +103,24 @@
103
103
  busy = on;
104
104
  Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
105
105
  }
106
+ // The clipboard, and when a browser refuses it, the older way: the text
107
+ // put in a box off the screen, selected and copied.
106
108
  function copy(text, done) {
107
- navigator.clipboard.writeText(text).then(function () { toast(done); }, function () {
108
- toast('Could not copy - select the text instead');
109
- });
109
+ function older() {
110
+ var box = el('textarea');
111
+ box.value = text;
112
+ box.setAttribute('readonly', '');
113
+ box.style.position = 'fixed';
114
+ box.style.left = '-9999px';
115
+ document.body.appendChild(box);
116
+ box.select();
117
+ var ok = false;
118
+ try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
119
+ document.body.removeChild(box);
120
+ toast(ok ? done : 'Could not copy - select the text instead');
121
+ }
122
+ if (!navigator.clipboard || !navigator.clipboard.writeText) return older();
123
+ navigator.clipboard.writeText(text).then(function () { toast(done); }, older);
110
124
  }
111
125
  function button(cls, iconName, text, onClick) {
112
126
  var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
@@ -155,33 +169,70 @@
155
169
  // lines are what the command printed, already scrubbed of any secret.
156
170
  var job = null;
157
171
  var lastRecheck = null;
158
-
172
+ // Whether the printed lines are shown: null follows the job - shown while it
173
+ // runs or when it failed, folded away once it finished well - until the
174
+ // person opens or folds them.
175
+ var linesOpen = null;
176
+ // Whether the one prompt for all findings is shown in full.
177
+ var allPromptOpen = false;
178
+
179
+ // The panel sits in the corner of the window, over the page; the page gets
180
+ // room at its foot so nothing stays hidden under it.
159
181
  function renderJob() {
160
182
  var box = byId('job');
161
183
  clear(box);
162
- if (!job) { box.hidden = true; return; }
184
+ var main = document.querySelector('main');
185
+ var root = document.documentElement;
186
+ if (!job) { box.hidden = true; linesOpen = null; main.style.paddingBottom = ''; root.style.removeProperty('--job-room'); return; }
163
187
  box.hidden = false;
164
- box.className = 'job' + (job.running ? ' on' : job.result && job.result.ok ? ' ok' : ' bad');
188
+ var ok = !!(job.result && job.result.ok);
189
+ box.className = 'job' + (job.running ? ' on' : ok ? ' ok' : ' bad');
165
190
  var head = el('div', 'job-head');
166
191
  if (job.running) head.appendChild(el('span', 'spin'));
167
192
  head.appendChild(el('b', null, job.title));
168
- head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : job.result && job.result.ok ? 'leaf' : 'ember'),
169
- job.running ? 'running' : job.result && job.result.ok ? 'finished' : 'did not finish'));
193
+ head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : ok ? 'leaf' : 'ember'),
194
+ job.running ? 'running' : ok ? 'finished' : 'did not finish'));
170
195
  if (!job.running) {
171
- var close = el('button', 'job-close', 'Hide');
196
+ var close = el('button', 'job-close', 'Close');
172
197
  close.type = 'button';
173
198
  close.addEventListener('click', function () { job = null; renderJob(); });
174
199
  head.appendChild(close);
175
200
  }
176
201
  box.appendChild(head);
177
- var lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
178
- box.appendChild(lines);
202
+ var open = linesOpen === null ? job.running || !ok : linesOpen;
203
+ var lines = null;
204
+ if (open) {
205
+ lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
206
+ box.appendChild(lines);
207
+ }
179
208
  if (!job.running && job.result) {
180
209
  var r = job.result;
181
- box.appendChild(el('div', 'job-result', r.ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see the lines above')));
210
+ box.appendChild(el('div', 'job-result', ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see what it printed')));
211
+ }
212
+ if (!job.running) {
213
+ var acts = el('div', 'job-acts');
214
+ // What it found is on Findings; a recording is on Runs. Setting the
215
+ // nightly check up, or removing it, finds nothing to go and see.
216
+ var setUp = job.kind === 'db' && / (un)?install$/.test(job.command || '');
217
+ var next = !ok || setUp ? null : job.kind === 'record' ? { id: 'runs', label: 'See runs' } : { id: 'findings', label: 'See findings' };
218
+ if (next && currentView() !== next.id) {
219
+ var goBtn = el('button', 'go', next.label + ' →');
220
+ goBtn.type = 'button';
221
+ goBtn.addEventListener('click', function () { go(next.id); });
222
+ acts.appendChild(goBtn);
223
+ }
224
+ var fold = el('button', null, open ? 'Hide what it printed' : 'Show what it printed');
225
+ fold.type = 'button';
226
+ fold.setAttribute('aria-expanded', open ? 'true' : 'false');
227
+ fold.addEventListener('click', function () { linesOpen = !open; renderJob(); });
228
+ acts.appendChild(fold);
229
+ box.appendChild(acts);
182
230
  }
183
231
  if (job.command) box.appendChild(el('div', 'fallback', 'The same in a terminal: ' + job.command));
184
- lines.scrollTop = lines.scrollHeight;
232
+ if (lines) lines.scrollTop = lines.scrollHeight;
233
+ main.style.paddingBottom = (box.offsetHeight + 48) + 'px';
234
+ // A message pops up above the panel, never over it.
235
+ root.style.setProperty('--job-room', (box.offsetHeight + 12) + 'px');
185
236
  }
186
237
 
187
238
  function getJob() {
@@ -194,6 +245,7 @@
194
245
  setBusy(true);
195
246
  return post(path, body).then(function (started) {
196
247
  job = started.job;
248
+ linesOpen = null;
197
249
  renderJob();
198
250
  return follow();
199
251
  }).then(function (done) {
@@ -255,6 +307,8 @@
255
307
  byId('viewTitle').textContent = view.title;
256
308
  document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
257
309
  if (state) renderSide();
310
+ // Its "See findings" goes once Findings is the page on screen.
311
+ if (job && !job.running) renderJob();
258
312
  window.scrollTo(0, 0);
259
313
  }
260
314
  window.addEventListener('hashchange', showView);
@@ -528,7 +582,7 @@
528
582
  if (!connection) { toast('Paste your connection string in "Connect your database" first.'); dbInput.focus(); return; }
529
583
  runJob('/api/db', { action: 'night', connection: connection }).then(function (done) {
530
584
  var r = done && done.result;
531
- toast(r && r.ok ? 'Read back - see the lines above and Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
585
+ toast(r && r.ok ? 'Read back - see Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
532
586
  }, function () {});
533
587
  });
534
588
  see.setAttribute('data-action', 'db');
@@ -724,6 +778,28 @@
724
778
  if (lastRecheck.newProblems.length) gone.appendChild(el('span', 'tag ember', plural(lastRecheck.newProblems.length, 'problem') + ' newly broken - marked below'));
725
779
  body.appendChild(gone);
726
780
  }
781
+ // Every fix in one paste, with the rules that keep the AI from breaking
782
+ // something else - whatever the filter shows, it is all of them.
783
+ if (state.allFixPrompt) {
784
+ var many = state.findings.filter(function (f) { return f.fixPrompt; }).length;
785
+ var all = el('div', 'fix-all');
786
+ var words = el('div', 'fix-all-words');
787
+ words.appendChild(el('b', null, 'Fix all ' + many + ' at once'));
788
+ words.appendChild(el('span', null, 'One prompt with every fix, and rules that stop your AI from changing anything else. Then Re-check each one.'));
789
+ all.appendChild(words);
790
+ all.appendChild(button('sun', 'copy', 'Copy one prompt for all', function () {
791
+ copy(state.allFixPrompt, 'One prompt for all ' + many + ' copied - paste it into your AI tool');
792
+ }));
793
+ // To read before pasting, or to select by hand. It stays open across
794
+ // the page's own refresh every few seconds.
795
+ var whole = el('details', 'fix-all-text');
796
+ whole.open = allPromptOpen;
797
+ whole.addEventListener('toggle', function () { allPromptOpen = whole.open; });
798
+ whole.appendChild(el('summary', null, 'See the prompt'));
799
+ whole.appendChild(el('pre', null, state.allFixPrompt));
800
+ all.appendChild(whole);
801
+ body.appendChild(all);
802
+ }
727
803
  if (!shown.length) {
728
804
  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.'));
729
805
  return;
@@ -960,6 +1036,41 @@
960
1036
  }, function () {});
961
1037
  }
962
1038
 
1039
+ // Start with Windows, and what still runs when the laptop is off.
1040
+ function renderKeep() {
1041
+ var box = byId('keep');
1042
+ clear(box);
1043
+ var a = state.autostart || { available: false, on: false, why: '' };
1044
+ var row = el('div', 'keep-row');
1045
+ if (a.available) {
1046
+ var sw = el('label', 'switch');
1047
+ var input = el('input');
1048
+ input.type = 'checkbox';
1049
+ input.checked = !!a.on;
1050
+ input.setAttribute('aria-label', 'Start with Windows');
1051
+ input.addEventListener('change', function () { setAutostart(input.checked, input); });
1052
+ sw.appendChild(input);
1053
+ sw.appendChild(el('span', 'track'));
1054
+ row.appendChild(sw);
1055
+ }
1056
+ var text = el('div', 'keep-text');
1057
+ var title = el('b', null, 'Start with Windows ');
1058
+ if (a.available) title.appendChild(el('span', 'tag ' + (a.on ? 'leaf' : ''), a.on ? 'on' : 'off'));
1059
+ text.appendChild(title);
1060
+ text.appendChild(el('span', null, !a.available ? (a.why || 'Not on this computer.')
1061
+ : a.on ? 'When you sign in to Windows, this dashboard starts by itself in a small window on the taskbar, with nothing opened. To see it, double-click Kryptheon.cmd as always - it opens the one already running. Closing that taskbar window stops it until you sign in again.'
1062
+ : 'Switch it on and this dashboard starts by itself every time you sign in to Windows, in a small window on the taskbar. Nothing opens - it just watches for changes. Switch it off here any time.'));
1063
+ row.appendChild(text);
1064
+ box.appendChild(row);
1065
+ box.appendChild(el('div', 'keep-off', 'With the laptop off, nothing on it can run. The nightly database check is different: once set up in Start here, it runs inside your database itself, and its result is waiting the next time you open this page.'));
1066
+ }
1067
+ function setAutostart(on, input) {
1068
+ post('/api/autostart', { on: on }).then(function () {
1069
+ toast(on ? 'On: it starts by itself when you sign in to Windows' : 'Off: it no longer starts by itself');
1070
+ return load();
1071
+ }).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
1072
+ }
1073
+
963
1074
  function setCheck(id, on, input) {
964
1075
  post('/api/check', { id: id, on: on }).then(function () {
965
1076
  toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
@@ -988,6 +1099,7 @@
988
1099
  renderTop();
989
1100
  renderNow();
990
1101
  renderSteps();
1102
+ renderKeep();
991
1103
  renderStatusFilter();
992
1104
  renderFindings();
993
1105
  renderTimeline();
@@ -1012,6 +1124,7 @@
1012
1124
  return getJob().then(function (now) {
1013
1125
  if (!now || !now.running) return;
1014
1126
  job = now;
1127
+ linesOpen = null;
1015
1128
  setBusy(true);
1016
1129
  renderJob();
1017
1130
  return follow().then(function () { setBusy(false); return load(); });
@@ -265,14 +265,22 @@
265
265
  @media (max-width: 600px) { .files div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }
266
266
 
267
267
  /* --- a run in progress, and what it said ---------------------------------- */
268
- .job { margin: 0 0 28px; padding: 16px 18px 18px; color: var(--on-dark); background: var(--ink); border-left: 3px solid var(--orange); }
268
+ /* Pinned to the corner of the window, so it is seen from wherever the button
269
+ was pressed - never at the top of a page scrolled away from. */
270
+ .job { position: fixed; right: 24px; bottom: 24px; z-index: 9; display: flex; flex-direction: column; width: min(520px, calc(100vw - 48px)); max-height: calc(100vh - 48px); padding: 16px 18px 18px; color: var(--on-dark); background: var(--ink); border-left: 3px solid var(--orange); box-shadow: 0 18px 50px rgba(11, 14, 20, 0.35); animation: job-in 0.35s var(--ease); }
271
+ .job[hidden] { display: none; }
269
272
  .job.ok { border-left-color: var(--on-dark); }
273
+ @keyframes job-in { from { opacity: 0; transform: translateY(16px); } }
274
+ @media (max-width: 600px) { .job { right: 16px; bottom: 16px; width: calc(100vw - 32px); } }
270
275
  .job-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
271
276
  .job-head b { font-weight: 800; font-stretch: 74%; font-size: 1.1rem; text-transform: uppercase; overflow-wrap: anywhere; }
272
277
  .job-head .tag { color: var(--on-dark); border-color: var(--line-dark); }
273
278
  .job-close { margin-left: auto; padding: 4px 10px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; border: 1px solid var(--line-dark); }
274
- .job-lines { max-height: 260px; margin: 12px 0 0; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; opacity: 0.85; }
279
+ .job-lines { min-height: 0; max-height: 220px; margin: 12px 0 0; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; opacity: 0.85; }
275
280
  .job-result { margin-top: 12px; font-weight: 700; }
281
+ .job-acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
282
+ .job-acts button { padding: 6px 12px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--on-dark); border: 1px solid var(--line-dark); }
283
+ .job-acts button.go { color: var(--ink); background: var(--on-dark); border-color: var(--on-dark); }
276
284
  .job .fallback { color: rgba(233, 237, 242, 0.6); }
277
285
 
278
286
  /* --- boxes to type into, the words around them, and the yes -------------- */
@@ -293,9 +301,20 @@
293
301
  .fallback { font: 10.5px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
294
302
  .tag.when { color: var(--on-light); border-style: dotted; }
295
303
  .f-quick { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; padding: 0 8px 16px 20px; margin-top: -6px; }
304
+ .keep-row { display: flex; align-items: flex-start; gap: 16px; padding: 18px 8px 18px 0; }
305
+ .keep-row .switch { margin-top: 2px; }
306
+ .keep-text { display: grid; gap: 6px; min-width: 0; font-size: 14px; }
307
+ .keep-text b { font-weight: 800; }
308
+ .keep-off { padding-top: 14px; border-top: 1px solid var(--line); font-size: 14px; opacity: 0.8; }
309
+ .fix-all { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px; padding: 16px 8px 16px 20px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--orange); }
310
+ .fix-all-words { flex: 1 1 260px; display: grid; gap: 4px; font-size: 14px; }
311
+ .fix-all-words b { font-weight: 800; font-stretch: 74%; font-size: 1.05rem; text-transform: uppercase; }
312
+ .fix-all-text { flex: 1 1 100%; min-width: 0; }
313
+ .fix-all-text summary { width: max-content; font: 11px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; opacity: 0.75; }
314
+ .fix-all-text pre { max-height: 320px; margin: 10px 0 0; padding: 14px 16px; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid var(--line); }
296
315
  .rechecked { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 14px 8px 14px 20px; font-size: 14px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--on-light); }
297
316
 
298
- .toast { position: fixed; left: 50%; bottom: 24px; z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
317
+ .toast { position: fixed; left: 50%; bottom: calc(24px + var(--job-room, 0px)); z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
299
318
  .toast.show { opacity: 1; transform: translate(-50%, 0); }
300
319
 
301
320
  @media (prefers-reduced-motion: reduce) {
@@ -345,6 +364,10 @@
345
364
  <div class="card-head"><div><h3>Start here</h3><div class="c-sub">Do these in order. A step is done only once it has actually run.</div></div></div>
346
365
  <div class="steps" id="steps"></div>
347
366
  </div>
367
+ <div class="card gap" id="keepCard">
368
+ <div class="card-head"><div><h3>Keep it running</h3><div class="c-sub">So it is watching whenever your laptop is on - without opening anything</div></div></div>
369
+ <div class="keep" id="keep"></div>
370
+ </div>
348
371
  <div class="card gap">
349
372
  <div class="card-head"><div><h3>Connect your AI tool</h3><div class="c-sub">Optional - your assistant then checks its own work, and does not call a change done until the check passes</div></div></div>
350
373
  <div class="ai">
@@ -24,6 +24,7 @@ const crypto = require('crypto');
24
24
  const { spawnSync } = require('child_process');
25
25
  const store = require('./kryptheon-store.js');
26
26
  const changes = require('./kryptheon-changes.js');
27
+ const autostart = require('./kryptheon-autostart.js');
27
28
 
28
29
  /**
29
30
  * Every check the dashboard knows the name of, in the order the person asked
@@ -284,6 +285,44 @@ function nextAction(findings, steps) {
284
285
  return { kind: 'quiet', title: 'Nothing open' };
285
286
  }
286
287
 
288
+ /**
289
+ * One prompt that fixes every open finding at once, for pasting into an AI
290
+ * tool, or '' when fewer than two findings have a prompt (one is its own).
291
+ * Each finding's own prompt goes in as it is, in the order the page shows
292
+ * them, most severe first. Around them are the rules that keep a fix from
293
+ * breaking something else: only what is listed, the smallest change, one at
294
+ * a time with the app checked between, nothing weakened to make a finding go
295
+ * away, ask rather than guess. The re-checks come once, at the end.
296
+ *
297
+ * items: [{ id, severity, headline, where, prompt }] - prompt without its
298
+ * re-check line.
299
+ */
300
+ function combinedPrompt(items) {
301
+ const withPrompt = items.filter((f) => f.prompt);
302
+ if (withPrompt.length < 2) return '';
303
+ const n = withPrompt.length;
304
+ const lines = [
305
+ 'Kryptheon checked this project and found ' + n + ' problems. Fix all ' + n + ', carefully - not breaking anything matters more than speed.',
306
+ '',
307
+ 'Rules for every fix:',
308
+ '1. Fix only the problems listed below. Do not refactor, rename, reformat or "improve" anything else, and do not touch a file a fix does not need.',
309
+ '2. Make the smallest change that fixes each one. Every feature must keep working exactly as it does now for the people meant to use it.',
310
+ '3. Never delete or weaken a check, test, validation or security rule to make a problem go away, and never delete or rewrite existing data.',
311
+ '4. Do them one at a time, in the order below. After each one, make sure the app still builds and its existing tests still pass before starting the next.',
312
+ '5. If a fix needs a decision only I can make, or you are not sure a change is safe, stop and ask me instead of guessing.',
313
+ '6. When you are done, list every file you changed and why, one line each.',
314
+ ];
315
+ withPrompt.forEach((f, i) => {
316
+ lines.push('', '======== Problem ' + (i + 1) + ' of ' + n + ' - ' + f.severity + ': ' + f.headline + (f.where ? ' (' + f.where + ')' : ''), '', f.prompt.trim());
317
+ });
318
+ lines.push('', '======== When all ' + n + ' are fixed',
319
+ '',
320
+ 'Prove each fix: run these one at a time (or press Re-check on each finding in the Kryptheon dashboard). A fix counts only when it says FIXED, and a re-check also says if anything else newly broke - if it does, fix that before anything else.',
321
+ '');
322
+ withPrompt.forEach((f) => lines.push(' npx kryptheon recheck ' + f.id));
323
+ return lines.join('\n');
324
+ }
325
+
287
326
  /**
288
327
  * One sentence about the nightly run inside the database, or null when
289
328
  * kryptheon-night has never been asked. Its four states are four different
@@ -433,9 +472,11 @@ function buildState(root, env) {
433
472
  // prove the fix. Added here, where the loop lives, so every check's prompt
434
473
  // ends the same way and the detection code itself stays untouched.
435
474
  const attempts = readHistory(paths.fixes, 500);
475
+ const forAll = [];
436
476
  for (const f of findings) {
437
477
  f.id = changes.findingId(f);
438
478
  f.attempts = attempts.filter((a) => a.id === f.id).map((a) => ({ at: a.at, verdict: a.verdict, why: a.why || '', newProblems: (a.newProblems || []).length }));
479
+ forAll.push({ id: f.id, severity: f.severity, headline: f.headline, where: f.where, prompt: f.fixPrompt || '' });
439
480
  if (f.fixPrompt) {
440
481
  f.fixPrompt += '\n\nWhen you have fixed it, prove it: run npx kryptheon recheck ' + f.id +
441
482
  ' (or press Re-check on this finding in the Kryptheon dashboard). It is fixed only when that says FIXED.';
@@ -497,6 +538,7 @@ function buildState(root, env) {
497
538
  proven: proven,
498
539
  checks: checks,
499
540
  findings: findings,
541
+ allFixPrompt: combinedPrompt(forAll),
500
542
  runs: history.slice().reverse().map((r) => ({ runAt: r.runAt, status: r.status, passed: r.passed, failed: r.failed, durationMs: r.durationMs })),
501
543
  recordings: recordings,
502
544
  steps: steps,
@@ -506,6 +548,8 @@ function buildState(root, env) {
506
548
  // Part 3 is where a version gets verified. Until then this says so,
507
549
  // rather than leaving a blank that could be read either way.
508
550
  verification: { state: 'not verified', why: 'Kryptheon Verified is not built yet. No version of this project has been verified.' },
551
+ // Whether this dashboard starts by itself when the person signs in.
552
+ autostart: (() => { const s = autostart.status(root, env); return { available: s.available, on: s.on, why: s.why || '' }; })(),
509
553
  };
510
554
  }
511
555
 
@@ -751,6 +795,10 @@ function createServer(root, options) {
751
795
  const r = setCheck(store.pathsFor(root, env), String(input.id || ''), input.on);
752
796
  return json(res, r.ok ? 200 : 400, r);
753
797
  }
798
+ if (url.pathname === '/api/autostart') {
799
+ const r = autostart.setAutostart(root, input.on, env);
800
+ return json(res, r.ok ? 200 : 400, { ok: r.ok, on: !!r.on, why: r.why || '' });
801
+ }
754
802
  // Everything below starts a job: it answers at once with the job, and
755
803
  // the page follows it on /api/job. One at a time - two browsers
756
804
  // replaying against one app, or two attacks on one database, would
@@ -858,6 +906,7 @@ module.exports = {
858
906
  databaseVerdict: databaseVerdict,
859
907
  startSteps: startSteps,
860
908
  nextAction: nextAction,
909
+ combinedPrompt: combinedPrompt,
861
910
  readConfig: readConfig,
862
911
  setCheck: setCheck,
863
912
  createServer: createServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -42,7 +42,8 @@
42
42
  "kryptheon-spacemono.woff2",
43
43
  "kryptheon-archivo.woff2",
44
44
  "kryptheon-changes.js",
45
- "kryptheon-look.js"
45
+ "kryptheon-look.js",
46
+ "kryptheon-autostart.js"
46
47
  ],
47
48
  "engines": {
48
49
  "node": ">=20.6.0"
@@ -51,11 +52,12 @@
51
52
  "@playwright/test": "^1.62.1"
52
53
  },
53
54
  "scripts": {
54
- "check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.check.js && node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-env.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-setup.check.js && node kryptheon-browser.check.js && node kryptheon-heal.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
55
+ "check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.check.js && node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-env.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-setup.check.js && node kryptheon-browser.check.js && node kryptheon-heal.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
55
56
  "check:store": "node kryptheon-store.check.js",
56
57
  "check:code": "node kryptheon-code.check.js",
57
58
  "check:constdata": "node kryptheon-constdata.check.js",
58
59
  "check:dashboard": "node kryptheon-dashboard.check.js",
60
+ "check:autostart": "node kryptheon-autostart.check.js",
59
61
  "check:changes": "node kryptheon-changes.check.js",
60
62
  "check:look": "node kryptheon-look.check.js",
61
63
  "check:verify": "node kryptheon-verify.check.js",