umer-wasim commited on
Commit
f269f89
·
verified ·
1 Parent(s): 053057f

New website: computers only, download + check list + report viewer, no AI in the browser (chat and phone check removed)

Browse files
Files changed (13) hide show
  1. README.md +15 -29
  2. about.html +4 -160
  3. app/model-version.json +1 -7
  4. app/renderer/knowledge.js +1 -1
  5. chat/chat.css +0 -155
  6. chat/chat.js +0 -601
  7. chat/device.js +0 -48
  8. download.html +4 -213
  9. index.html +153 -81
  10. phone.html +4 -91
  11. site/site.css +214 -0
  12. site/site.js +252 -0
  13. training/build_chat.py +74 -5
README.md CHANGED
@@ -151,22 +151,22 @@ npm start # run the app
151
 
152
  Offline fine-tuning on CPU: `training/train_lora.py` (LoRA on Qwen2.5-0.5B-Instruct; see `--benchmark` and `--resume`).
153
 
154
- ## Security assistant (chat)
155
-
156
- The Space's front page is a chat assistant in the style of Devseis Nox: ask about securing a computer or phone,
157
- ISO 27001, GDPR or the EU AI Act, and run an audit from the chat. The model runs on the device with WebLLM (1.5B,
158
- about 1 GB on first use); answers are grounded in the Devseis knowledge base (`library/knowledge.json`, 321 passages
159
- generated by `library/build_knowledge.py` from the catalog and library), cite their sources, and are checked
160
- (`chatProblems` in `app/renderer/knowledge.js`). Without WebGPU, or if the GPU fails, it answers directly from the
161
- knowledge base. **Run audit**: on phones it starts the phone check (`phone.html`), which hands the report back to the
162
- chat; on computers it leads to the desktop app, or the app's `…-evidence.json` report can be attached so the chat answers
163
- from the user's own results and writes the full report. Conversations stay in the browser.
164
- Until model v0.4 (Qwen2.5-1.5B fine-tuned for chat and reports) is published, the chat uses the base Qwen2.5-1.5B-Instruct
165
- (`chat.version` in `app/model-version.json`).
166
 
167
  ## Install on a Mac
168
 
169
- Download from **https://devseis-arx.static.hf.space/download.html** (universal app for Apple silicon and
170
  Intel, macOS 13+; files and checksums in [Devseis/endpoint-auditor-releases](https://huggingface.co/datasets/Devseis/endpoint-auditor-releases)).
171
  Drag it to Applications; on first open, allow it once in *System Settings › Privacy & Security › Open Anyway* (the build
172
  is ad-hoc signed, not yet notarised). The app downloads the AI auditor model on first run and follows the model version
@@ -178,20 +178,6 @@ with `DEVSEIS_SMOKE=<folder> "<app>/Contents/MacOS/Devseis Endpoint Auditor"` (a
178
 
179
  Code collects the evidence and decides every verdict; the fine-tuned model explains, prioritises and writes the report, and every answer is checked against the evidence before it is used. See [docs/MODEL_LLM.md](docs/MODEL_LLM.md) for who does what and how the model's role can grow.
180
 
181
- ## Phone check (iPhone, iPad, Android)
182
-
183
- The phone check (`phone.html`, also reachable from the chat's Run audit) runs entirely in the browser:
184
-
185
- 1. **Detects** what a browser can see: OS and version (Safari 26+ hides the iOS version in its user agent, so Safari's own version is used; Android uses client hints), device model (Android), browser, whether a device unlock method is set up (WebAuthn), and WebGPU support.
186
- 2. **Asks** the rest, one question at a time with *where to look* in Settings (`catalog/mobile_questions.json`). Every result is labelled `detected` or `self-reported` in the evidence and the report.
187
- 3. **Matches** the iOS version against the signed phone subset of the vulnerability bundle (55 KB, signature checked with WebCrypto).
188
- 4. **Writes** the findings with the same auditor model in WebLLM (about 350 MB download, roughly 1 GB of GPU memory, so newer phones) or with the built-in writer, and builds the same report as the desktop app.
189
- 5. **Saves or shares** the report as a PDF (share sheet or download). Phones do not allow a silent auto-save.
190
-
191
- `mobile/mobile-core.js` turns detections and answers into evidence; the synthetic generator calls the same code through `mobile/build-evidence.mjs`, so training data has exactly the page's wording. Organisation answers can be passed in the link: `index.html#org=<base64 JSON {"ORG_ROPA": "yes", ...}>`.
192
-
193
- Test without a phone: serve the repo (`python3 -m http.server 8765`) and run `app/node_modules/.bin/electron mobile/test/e2e.cjs ios out.json` (or `android`); it answers every question in a phone-sized window with a phone user agent and builds the PDF; then check the evidence with `python3 tools/validate_evidence.py out.json`.
194
-
195
  ## Vulnerability matching (offline)
196
 
197
  `vuln.known_vulnerabilities` is decided by exact version comparison, never by the model:
@@ -209,8 +195,8 @@ Devseis builds the bundle with `python3 vulndb/build_bundle.py` (needs internet;
209
  | Catalog, library, synthetic data, training data | **0.3** | 74 checks: Windows 64, Linux 56, macOS 56, iPhone 29, Android 30. 0.3 adds phones, real CVEs in training data and wording identical to the collectors and the phone page; 39,500 grounded examples |
210
  | Collectors (Windows, Linux, macOS) | **0.2** (catalog 0.3) | macOS tested on a real Mac; Linux tested on Ubuntu 24.04 and Fedora 42 containers; Windows parse-checked only (needs a real Windows test). Known vulnerabilities: inventory only, matching comes with the offline vulnerability bundle |
211
  | Vulnerability matcher and bundle | 0.1 | Tested: this Mac (Chrome 154 → 58 critical/high, fixed in 155), Ubuntu 24.04 (OpenSSL), Rocky 9 (25 of 25 packages agree with dnf's own security list), Fedora reported as not covered |
212
- | Phone check (web) | **0.3** | Tested in a phone-sized window as iPhone (Safari 26) and Android (Chrome 141); needs testing on real phones |
213
- | Fine-tuned model | **v0.3 in use** | [Devseis/endpoint-auditor-0.5b](https://huggingface.co/Devseis/endpoint-auditor-0.5b) (+ q0f16 / q4f16_1 / q4f32_1 WebLLM builds), tag `v0.3`. v0.3 on 60 held-out examples (desktop and phones): 100% valid, 100% grounded, 100% exact fields; base model 0%. Used by the desktop apps and the phone check; in a real audit of a Mac it writes 36 of 36 report parts (v0.1: 22 of 36) |
214
 
215
  ## Test status (collector version 0.1.0)
216
 
 
151
 
152
  Offline fine-tuning on CPU: `training/train_lora.py` (LoRA on Qwen2.5-0.5B-Instruct; see `--benchmark` and `--resume`).
153
 
154
+ ## Website (this Space)
155
+
156
+ The Space's front page (`index.html`, `site/`) is for computers only (Mac, Windows, Linux). It detects the visitor's
157
+ system and offers the right download of the desktop app, lists all 68 computer checks from `catalog/checks.json` with
158
+ their risk and ISO 27001 / GDPR references, and shows a report: the app's *Ask Arx about this report* button (or
159
+ *Open a report* with the app's `…-evidence.json` file) opens the results page, written with the app's own templates
160
+ (`findingTemplate` / `summaryTemplate` in `app/renderer/auditor-core.js`). The page runs **no AI** and uploads nothing:
161
+ the AI model runs only inside the desktop app.
162
+
163
+ The earlier chat assistant and the phone check were removed on 2026-10-14: small models in the browser froze weak
164
+ computers and crashed phones, and the 0.5B chat model did not survive 4-bit rounding. They remain in the Space's git
165
+ history. `mobile/` stays because the synthetic data generator uses its evidence code.
166
 
167
  ## Install on a Mac
168
 
169
+ Download from **https://devseis-arx.static.hf.space/#download** (universal app for Apple silicon and
170
  Intel, macOS 13+; files and checksums in [Devseis/endpoint-auditor-releases](https://huggingface.co/datasets/Devseis/endpoint-auditor-releases)).
171
  Drag it to Applications; on first open, allow it once in *System Settings › Privacy & Security › Open Anyway* (the build
172
  is ad-hoc signed, not yet notarised). The app downloads the AI auditor model on first run and follows the model version
 
178
 
179
  Code collects the evidence and decides every verdict; the fine-tuned model explains, prioritises and writes the report, and every answer is checked against the evidence before it is used. See [docs/MODEL_LLM.md](docs/MODEL_LLM.md) for who does what and how the model's role can grow.
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  ## Vulnerability matching (offline)
182
 
183
  `vuln.known_vulnerabilities` is decided by exact version comparison, never by the model:
 
195
  | Catalog, library, synthetic data, training data | **0.3** | 74 checks: Windows 64, Linux 56, macOS 56, iPhone 29, Android 30. 0.3 adds phones, real CVEs in training data and wording identical to the collectors and the phone page; 39,500 grounded examples |
196
  | Collectors (Windows, Linux, macOS) | **0.2** (catalog 0.3) | macOS tested on a real Mac; Linux tested on Ubuntu 24.04 and Fedora 42 containers; Windows parse-checked only (needs a real Windows test). Known vulnerabilities: inventory only, matching comes with the offline vulnerability bundle |
197
  | Vulnerability matcher and bundle | 0.1 | Tested: this Mac (Chrome 154 → 58 critical/high, fixed in 155), Ubuntu 24.04 (OpenSSL), Rocky 9 (25 of 25 packages agree with dnf's own security list), Fedora reported as not covered |
198
+ | Website | **2026-10-14** | Computers only: download, check list, report viewer; no AI in the browser |
199
+ | Fine-tuned model | **v0.3 in use** | [Devseis/endpoint-auditor-0.5b](https://huggingface.co/Devseis/endpoint-auditor-0.5b) (+ q0f16 / q4f16_1 / q4f32_1 WebLLM builds), tag `v0.3`. v0.3 on 60 held-out examples (desktop and phones): 100% valid, 100% grounded, 100% exact fields; base model 0%. Used by the desktop apps; in a real audit of a Mac it writes 36 of 36 report parts (v0.1: 22 of 36) |
200
 
201
  ## Test status (collector version 0.1.0)
202
 
about.html CHANGED
@@ -1,161 +1,5 @@
1
  <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Arx · Devseis Endpoint Auditor</title>
7
- <meta name="description" content="Read-only endpoint audit for ISO 27001, GDPR and the EU AI Act. Stage 1, step 1: collectors, evidence format and synthetic data.">
8
- <style>
9
- :root {
10
- --bg: #f7f8fb; --card: #ffffff; --text: #16181d; --muted: #5b6270; --line: #e3e6ec;
11
- --red: #ED2939; --blue: #00A1DE; --ok: #1a7f37; --bad: #c4242f; --na: #6e7781; --pending: #9a6700; --err: #8250df;
12
- --code-bg: #0f1218; --code-text: #e6edf3;
13
- }
14
- @media (prefers-color-scheme: dark) {
15
- :root:not([data-theme="light"]) {
16
- --bg: #0d1117; --card: #161b22; --text: #e6edf3; --muted: #9da7b3; --line: #2b313a;
17
- --ok: #3fb950; --bad: #ff6b6b; --na: #8b949e; --pending: #d29922; --err: #b392f0; --code-bg: #0a0d12;
18
- }
19
- }
20
- * { box-sizing: border-box; }
21
- body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
22
- main { max-width: 1040px; margin: 0 auto; padding: 32px 16px 64px; }
23
- .tag { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--red); border: 1px solid var(--red); border-radius: 999px; padding: 2px 10px; }
24
- h1 { font-size: clamp(28px, 5vw, 40px); line-height: 1.15; margin: 14px 0 8px; }
25
- h2 { font-size: 20px; margin: 36px 0 12px; }
26
- p.lead { color: var(--muted); font-size: 18px; max-width: 760px; margin: 0; }
27
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-top: 20px; }
28
- .card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
29
- .card b { display: block; font-size: 15px; margin-bottom: 4px; }
30
- .card span { color: var(--muted); font-size: 14px; }
31
- .tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
32
- .tabs button { font: inherit; font-size: 14px; padding: 6px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--text); cursor: pointer; }
33
- .tabs button[aria-selected="true"] { background: var(--blue); border-color: var(--blue); color: #fff; }
34
- .counts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
35
- .pill { font-size: 13px; font-weight: 600; padding: 3px 10px; border-radius: 999px; background: var(--card); border: 1px solid var(--line); }
36
- .s-Compliant { color: var(--ok); } .s-Non-Compliant { color: var(--bad); } .s-NotApplicable { color: var(--na); }
37
- .s-Pending { color: var(--pending); } .s-Error { color: var(--err); }
38
- .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; }
39
- @media (max-width: 760px) { .split { grid-template-columns: 1fr; } }
40
- pre { margin: 0; background: var(--code-bg); color: var(--code-text); border-radius: 12px; padding: 14px; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow: auto; max-height: 560px; }
41
- table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
42
- .table-wrap { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: auto; max-height: 560px; }
43
- th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
44
- th { position: sticky; top: 0; background: var(--card); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); }
45
- td.ref { color: var(--muted); font-size: 12.5px; }
46
- code { font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; }
47
- .note { border-left: 4px solid var(--red); background: var(--card); padding: 12px 16px; border-radius: 0 12px 12px 0; color: var(--muted); }
48
- ol li { margin: 4px 0; }
49
- a { color: var(--blue); }
50
- footer { margin-top: 48px; color: var(--muted); font-size: 14px; }
51
- </style>
52
- </head>
53
- <body>
54
- <main>
55
- <span class="tag">Open source · stage 1</span>
56
- <h1>Arx <span style="font-weight:400;color:var(--muted)">· Devseis Endpoint Auditor</span></h1>
57
- <p class="lead">A read-only audit of Windows, Linux, macOS, iPhone and Android devices against a security baseline, GDPR technical controls, EU AI Act controls and organisational controls, written up by a small AI auditor model that runs on the device. <a href="index.html">ask the assistant</a>, <a href="phone.html">check your phone</a> or <a href="download.html">download the Mac app</a>.</p>
58
-
59
- <div class="grid">
60
- <div class="card"><b>Read-only collectors</b><span>PowerShell, Linux shell and macOS shell scripts. They read settings and never change the system.</span></div>
61
- <div class="card"><b>One evidence format</b><span>Same JSON for every OS, joined to a catalog with ISO 27001, GDPR and AI Act references.</span></div>
62
- <div class="card"><b>Synthetic data</b><span>Realistic evidence with the same rules, so the auditor model can be built before real data exists.</span></div>
63
- <div class="card"><b>Runs locally</b><span>Evidence stays on the device. Nothing is sent anywhere.</span></div>
64
- <div class="card"><b>Offline vulnerability matching</b><span>Installed software is matched against a signed OSV/NVD/CISA KEV bundle by exact version rules.</span></div>
65
- <div class="card"><b>Phones too</b><span>A browser check for iPhone and Android: what the browser can detect plus guided questions, each result labelled detected or self-reported.</span></div>
66
- </div>
67
-
68
- <h2>Sample audit (synthetic)</h2>
69
- <div class="tabs" role="tablist" id="tabs"></div>
70
- <div class="counts" id="counts"></div>
71
- <div class="split">
72
- <pre id="report" aria-label="Text report">Loading…</pre>
73
- <div class="table-wrap">
74
- <table>
75
- <thead><tr><th>Check</th><th>Status</th><th>Found</th><th>References</th></tr></thead>
76
- <tbody id="rows"></tbody>
77
- </table>
78
- </div>
79
- </div>
80
-
81
- <h2>Roadmap</h2>
82
- <ol>
83
- <li><b>Collectors, evidence format, catalog (74 checks), synthetic data</b> — done</li>
84
- <li><b>Control library and fix guidance</b> in Devseis's own wording — done; expert review welcome</li>
85
- <li><b>Training data</b> v0.3: 39,500 grounded examples for Windows, Linux, macOS, iPhone and Android — done</li>
86
- <li><b>Offline vulnerability matching</b> (OSV, NVD, CISA KEV, EPSS), signed bundle — done</li>
87
- <li><b>Desktop app</b> (Electron + WebLLM) and <b>phone check</b> (this Space) — done, with the base model</li>
88
- <li>Fine-tune Qwen2.5-0.5B-Instruct on this Mac (CPU, LoRA) — in progress</li>
89
- <li>Evaluate on held-out data, publish the scores and the model</li>
90
- <li>Installers per OS; tests on real Windows and Linux machines</li>
91
- </ol>
92
-
93
- <p class="note">Not a certification. ISO 27001 certification needs an accredited auditor, and much of GDPR and AI Act compliance is organisational. The tool collects evidence and flags gaps. See the <a href="README.md">README</a> for how to run the collectors.</p>
94
-
95
- <footer>Open source by Devseis: code Apache-2.0, data CC BY 4.0 · <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-synthetic">training data</a> · <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-vulndb">vulnerability data</a> · <a href="CITATION.cff">cite</a></footer>
96
- </main>
97
- <script>
98
- const SAMPLES = Object.fromEntries(['windows', 'linux', 'macos', 'ios', 'android'].map(os => [os, `synthetic/samples/synthetic-${os}-42000126.json`]));
99
- const LABELS = { windows: 'Windows', linux: 'Linux', macos: 'macOS', ios: 'iPhone', android: 'Android' };
100
- const LAYOUT = {
101
- windows: { title: 'WINDOWS ENDPOINT SECURITY BASELINE - AUDIT', results: 'COMPLIANCE RESULTS', end: 'END OF READ-ONLY AUDIT', blank: true, extra: ['Build', 'build'] },
102
- linux: { title: 'LINUX ENDPOINT SECURITY BASELINE - AUDIT', results: 'LINUX ENDPOINT SECURITY BASELINE - RESULTS', end: 'END OF LINUX ENDPOINT SECURITY BASELINE AUDIT', blank: false, extra: ['Kernel', 'kernel'] },
103
- macos: { title: 'macOS ENDPOINT SECURITY BASELINE - AUDIT', results: 'COMPLIANCE RESULTS', end: 'END OF AUDIT', blank: false, extra: ['Build', 'build'] },
104
- ios: { title: 'iPHONE SECURITY CHECK (BROWSER)', results: 'COMPLIANCE RESULTS', end: 'END OF PHONE CHECK', blank: false, extra: ['Browser', 'browser'] },
105
- android: { title: 'ANDROID SECURITY CHECK (BROWSER)', results: 'COMPLIANCE RESULTS', end: 'END OF PHONE CHECK', blank: false, extra: ['Browser', 'browser'] },
106
- };
107
- const SECTIONS = [['baseline', null], ['gdpr', 'GDPR TECHNICAL CONTROLS'], ['aiact', 'EU AI ACT CONTROLS'], ['org', 'ORGANISATIONAL CONTROLS']];
108
- let catalog = null;
109
-
110
- function render(rec) {
111
- const L = LAYOUT[rec.host.os_family], line = '='.repeat(60), pad = s => (s + ' '.repeat(16)).slice(0, 16);
112
- const out = [line, ' ' + L.title, line,
113
- pad('Hostname') + ': ' + rec.host.hostname, pad('Operating System') + ': ' + rec.host.os_name,
114
- pad('OS Version') + ': ' + rec.host.os_version, pad(L.extra[0]) + ': ' + (rec.host[L.extra[1]] || ''),
115
- pad('Serial Number') + ': ' + (rec.host.serial_number || ''), pad('Execution User') + ': ' + rec.audit.execution_user,
116
- pad('Audit Date') + ': ' + rec.audit.started_at.replace('T', ' ').slice(0, 19), pad('Audit Mode') + ': ' + rec.audit.mode, line];
117
- for (const [id, title] of SECTIONS) {
118
- if (L.blank) out.push('');
119
- out.push(line, ' ' + (title || L.results), line);
120
- for (const r of rec.results) if (r.section === id) out.push(r.name, r.status + (r.evidence ? ` (${r.evidence})` : ''));
121
- }
122
- out.push(line, ' ' + L.end, line);
123
- return out.join('\n');
124
- }
125
-
126
- function refs(id) {
127
- const c = catalog && catalog.checks.find(x => x.id === id);
128
- if (!c) return '';
129
- return [...c.controls.iso27001.map(x => 'ISO ' + x), ...c.controls.gdpr.map(x => 'GDPR ' + x), ...c.controls.ai_act.map(x => 'AI Act ' + x)].join(' · ');
130
- }
131
-
132
- function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
133
-
134
- async function show(os) {
135
- document.querySelectorAll('#tabs button').forEach(b => b.setAttribute('aria-selected', String(b.dataset.os === os)));
136
- try {
137
- const rec = await (await fetch(SAMPLES[os])).json();
138
- document.getElementById('report').textContent = render(rec);
139
- document.getElementById('counts').innerHTML = Object.entries(rec.summary)
140
- .map(([k, v]) => `<span class="pill s-${k}">${v} ${k}</span>`).join('');
141
- document.getElementById('rows').innerHTML = rec.results.map(r =>
142
- `<tr><td>${esc(r.name)}</td><td class="s-${r.status}"><b>${r.status}</b></td><td>${esc(r.actual)}</td><td class="ref">${esc(refs(r.id))}</td></tr>`).join('');
143
- } catch (e) {
144
- document.getElementById('report').textContent = 'Could not load the sample: ' + e.message;
145
- }
146
- }
147
-
148
- (async () => {
149
- try { catalog = await (await fetch('catalog/checks.json')).json(); } catch (e) { catalog = null; }
150
- const tabs = document.getElementById('tabs');
151
- for (const os of Object.keys(SAMPLES)) {
152
- const b = document.createElement('button');
153
- b.textContent = LABELS[os]; b.dataset.os = os; b.setAttribute('role', 'tab');
154
- b.addEventListener('click', () => show(os));
155
- tabs.appendChild(b);
156
- }
157
- show('windows');
158
- })();
159
- </script>
160
- </body>
161
- </html>
 
1
  <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
3
+ <title>Arx</title><meta http-equiv="refresh" content="0; url=index.html#about"><link rel="canonical" href="index.html">
4
+ <style>body{font-family:ui-monospace,Menlo,monospace;background:#f7f7f7;color:#0a0a0a;margin:0;padding:40px 16px;text-align:center}@media (prefers-color-scheme:dark){body{background:#0a0a0a;color:#fafafa}}a{color:inherit}</style></head>
5
+ <body><p>Arx has moved to <a href="index.html#about">the Arx home page</a>.</p></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/model-version.json CHANGED
@@ -1,10 +1,4 @@
1
  {
2
  "version": "v0.3",
3
- "note": "Model version the desktop app and phone check use (Devseis/endpoint-auditor-0.5b-*-MLC tags). Read at start-up, so installed apps follow updates.",
4
- "chat": {
5
- "version": "v0.5",
6
- "size": "0.5B",
7
- "note": "Chat model: Devseis/endpoint-auditor-<size>-<quant>-MLC tag. quant q0f16 = unrounded build (v0.5 breaks in 4-bit); omit to pick q4f16_1/q4f32_1 by GPU. null version = base Qwen2.5 Instruct.",
8
- "quant": "q0f16"
9
- }
10
  }
 
1
  {
2
  "version": "v0.3",
3
+ "note": "Model version the desktop app and phone check use (Devseis/endpoint-auditor-0.5b-*-MLC tags). Read at start-up, so installed apps follow updates."
 
 
 
 
 
 
4
  }
app/renderer/knowledge.js CHANGED
@@ -105,7 +105,7 @@ export function auditBrief(evidence, maxGaps = 8) {
105
  }
106
 
107
  // Messages for one chat turn. history: earlier [{role, content}] turns (already answered).
108
- // device: what the browser saw about the device the chat runs on (chat/device.js deviceBrief), or null.
109
  export function chatMessages(question, passages, audit = 'none', history = [], device = null) {
110
  const context = passages.length ? passages.map((p, n) => `[${n + 1}] ${p.title}: ${p.text}`).join('\n') : '(no matching passages)';
111
  const user = `CONTEXT:\n${context}\n\nMY AUDIT: ${audit}\n\n${device ? `MY DEVICE: ${device}\n\n` : ''}QUESTION: ${question}`;
 
105
  }
106
 
107
  // Messages for one chat turn. history: earlier [{role, content}] turns (already answered).
108
+ // device: what the browser saw about the device the chat runs on (deviceBrief in the former chat/device.js), or null.
109
  export function chatMessages(question, passages, audit = 'none', history = [], device = null) {
110
  const context = passages.length ? passages.map((p, n) => `[${n + 1}] ${p.title}: ${p.text}`).join('\n') : '(no matching passages)';
111
  const user = `CONTEXT:\n${context}\n\nMY AUDIT: ${audit}\n\n${device ? `MY DEVICE: ${device}\n\n` : ''}QUESTION: ${question}`;
chat/chat.css DELETED
@@ -1,155 +0,0 @@
1
- /* Devseis Security Assistant: layout follows Devseis Nox (header, empty state with chips, thread, pill composer, sidebar). */
2
- :root { --bg: #f4f6fb; --panel: #fff; --border: #e2e6ee; --text: #171a21; --muted: #666f80; --accent: #ED2939; --accent-dark: #C81E2B;
3
- --accent-text: #fff; --accent-soft: rgba(237, 41, 57, .08); --accent-border: rgba(237, 41, 57, .35); --bubble-bot: #fff; --bubble-user: #ED2939;
4
- --input-bg: #fff; --blue: #00A1DE; --ok: #1a7f37; --warn: #9a6700; }
5
- @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b0e14; --panel: #11151d; --border: #232a37; --text: #e8ecf4; --muted: #8b94a7;
6
- --accent: #ff4d5a; --accent-dark: #ED2939; --accent-soft: rgba(255, 77, 90, .14); --accent-border: rgba(255, 77, 90, .4); --bubble-bot: #161c27; --bubble-user: #c81e2b; --input-bg: #1a202b; --ok: #3fb950; --warn: #d29922; } }
7
- :root[data-theme="dark"] { --bg: #0b0e14; --panel: #11151d; --border: #232a37; --text: #e8ecf4; --muted: #8b94a7;
8
- --accent: #ff4d5a; --accent-dark: #ED2939; --accent-soft: rgba(255, 77, 90, .14); --accent-border: rgba(255, 77, 90, .4); --bubble-bot: #161c27; --bubble-user: #c81e2b; --input-bg: #1a202b; --ok: #3fb950; --warn: #d29922; }
9
- * { box-sizing: border-box; }
10
- html, body { height: 100%; }
11
- body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
12
- height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
13
- a { color: var(--blue); }
14
- .hidden { display: none !important; }
15
- .muted { color: var(--muted); } .tiny { font-size: 12px; }
16
- button { font: inherit; cursor: pointer; }
17
- button:focus-visible, a:focus-visible, .chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
18
-
19
- header { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; padding-top: calc(10px + env(safe-area-inset-top));
20
- border-bottom: 1px solid var(--border); background: var(--panel); border-top: 3px solid var(--accent); flex-shrink: 0; }
21
- .header-left, .header-actions { display: flex; align-items: center; gap: 6px; }
22
- .brand { display: flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; font-size: 15px; }
23
- .brand img { width: 28px; height: 28px; }
24
- .icon-btn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid transparent; background: transparent; color: var(--text); display: grid; place-items: center; }
25
- .icon-btn:hover { background: var(--accent-soft); }
26
- .audit-btn { background: var(--accent); color: var(--accent-text); border: 0; border-radius: 999px; padding: 8px 14px; font-weight: 700; font-size: 14px; white-space: nowrap; }
27
- .audit-btn:hover { background: var(--accent-dark); }
28
- .status-bar { padding: 6px 16px; background: var(--accent-soft); border-bottom: 1px solid var(--border); font-size: 13px; }
29
- .status-progress { height: 4px; background: var(--border); border-radius: 99px; overflow: hidden; margin-top: 4px; }
30
- #statusFill { height: 100%; width: 0; background: var(--accent); transition: width .3s; }
31
-
32
- main { flex: 1; overflow-y: auto; padding: 18px 14px 8px; }
33
- .empty-state { max-width: 680px; margin: 6vh auto 0; text-align: center; }
34
- .hero-icon img { width: 64px; height: 64px; }
35
- .empty-state h1 { font-size: clamp(24px, 4.5vw, 32px); line-height: 1.2; margin: 10px 0; }
36
- .hero-sub { color: var(--muted); font-size: 17px; margin: 0 auto 20px; max-width: 560px; }
37
- .chips { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; max-width: 600px; margin: 0 auto; }
38
- @media (max-width: 520px) { .chips { grid-template-columns: 1fr; } }
39
- .chip { text-align: left; background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 12px 14px; color: var(--text); font-size: 15px; }
40
- .chip:hover:not(:disabled) { border-color: var(--accent-border); background: var(--accent-soft); }
41
- .chip:disabled { opacity: .45; cursor: default; }
42
-
43
- #thread { max-width: 780px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px; }
44
- .msg { display: flex; gap: 10px; align-items: flex-start; }
45
- .msg.user { justify-content: flex-end; }
46
- .avatar { width: 30px; height: 30px; flex: none; border-radius: 8px; }
47
- .bubble { max-width: min(640px, 88%); padding: 11px 15px; border-radius: 18px; overflow-wrap: anywhere; }
48
- .msg.user .bubble { background: var(--bubble-user); color: #fff; border-bottom-right-radius: 6px; white-space: pre-wrap; }
49
- .msg.bot .bubble { background: var(--bubble-bot); border: 1px solid var(--border); border-bottom-left-radius: 6px; }
50
- .bubble p { margin: 0 0 8px; } .bubble p:last-child { margin-bottom: 0; }
51
- .bubble ol, .bubble ul { margin: 6px 0 8px 20px; padding: 0; } .bubble li { margin: 3px 0; }
52
- .bubble code { font: 13px ui-monospace, Menlo, monospace; background: var(--accent-soft); padding: 1px 5px; border-radius: 5px; }
53
- .cite { display: inline-block; font-size: 11px; font-weight: 700; color: var(--accent); background: var(--accent-soft); border-radius: 6px; padding: 0 5px; margin: 0 1px; cursor: pointer; vertical-align: 1px; border: 0; }
54
- .sources { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; }
55
- .source { font-size: 12px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 2px 9px; background: transparent; }
56
- .source.open { border-color: var(--accent-border); color: var(--text); }
57
- .source-text { font-size: 13px; color: var(--muted); border-left: 3px solid var(--accent-border); padding: 4px 10px; margin-top: 8px; }
58
- .note { font-size: 12px; color: var(--warn); margin-top: 8px; }
59
- .typing span { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); margin-right: 3px; animation: blink 1.2s infinite; }
60
- .typing span:nth-child(2) { animation-delay: .2s; } .typing span:nth-child(3) { animation-delay: .4s; }
61
- @keyframes blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
62
- .card { border: 1px solid var(--border); background: var(--panel); border-radius: 16px; padding: 14px; }
63
- .card .score { font-size: 30px; font-weight: 800; }
64
- .card .pill { display: inline-block; font-size: 12px; font-weight: 700; border: 1px solid; border-radius: 99px; padding: 0 8px; margin: 2px 4px 2px 0; }
65
- .btn-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
66
- .small-btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 10px; padding: 7px 12px; font-size: 14px; }
67
- .primary-btn { border: 0; background: var(--accent); color: #fff; border-radius: 10px; padding: 8px 14px; font-size: 14px; font-weight: 700; }
68
-
69
- #composer { display: flex; align-items: flex-end; gap: 8px; max-width: 812px; width: 100%; margin: 0 auto; padding: 8px 14px 4px; flex-shrink: 0; }
70
- .input-pill { position: relative; overflow: hidden; flex: 1; display: flex; background: var(--input-bg); border: 1px solid var(--border); border-radius: 22px; padding: 4px 6px 4px 16px; }
71
- .input-pill:focus-within { box-shadow: 0 0 0 2px var(--accent-border); }
72
- #input { flex: 1; border: 0; outline: 0; resize: none; background: transparent; color: var(--text); font: inherit; max-height: 180px; padding: 9px 0; }
73
- #input:disabled { cursor: not-allowed; }
74
- .send-btn { width: 44px; height: 44px; border-radius: 50%; border: 0; background: var(--accent); color: #fff; display: grid; place-items: center; flex: none; }
75
- .send-btn:disabled { opacity: .4; cursor: default; }
76
- .attach { width: 44px; height: 44px; flex: none; }
77
- .disclaimer { text-align: center; font-size: 11.5px; color: var(--muted); margin: 0 0 calc(8px + env(safe-area-inset-bottom)); padding: 0 12px; }
78
-
79
- .overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 9; }
80
- .sidebar { position: fixed; top: 0; bottom: 0; left: 0; width: min(320px, 86vw); background: var(--panel); border-right: 1px solid var(--border); z-index: 10;
81
- transform: translateX(-102%); transition: transform .2s; display: flex; flex-direction: column; padding-top: env(safe-area-inset-top); }
82
- .sidebar.open { transform: none; }
83
- .sidebar-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; border-bottom: 1px solid var(--border); }
84
- .sidebar-head h2 { font-size: 16px; margin: 0; }
85
- .sidebar-list { flex: 1; overflow-y: auto; padding: 8px; }
86
- .convo { display: flex; align-items: center; gap: 6px; padding: 9px 10px; border-radius: 10px; cursor: pointer; font-size: 14px; }
87
- .convo:hover, .convo.active { background: var(--accent-soft); }
88
- .convo span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
89
- .convo button { opacity: .5; border: 0; background: transparent; color: var(--text); }
90
- .sidebar-foot { border-top: 1px solid var(--border); padding: 12px 14px calc(12px + env(safe-area-inset-bottom)); font-size: 14px; }
91
- .sidebar-foot .row { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 8px; }
92
-
93
- .modal { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: grid; place-items: center; z-index: 20; padding: 16px; }
94
- .modal-card { background: var(--panel); border-radius: 18px; padding: 20px; max-width: 460px; width: 100%; border: 1px solid var(--border); }
95
- .modal-card h2 { margin: 0 0 6px; font-size: 20px; }
96
- .option { display: block; width: 100%; text-align: left; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 14px; padding: 12px 14px; margin: 10px 0; text-decoration: none; }
97
- .option:hover { border-color: var(--accent-border); }
98
- .option b { display: block; } .option span { color: var(--muted); font-size: 14px; }
99
- .option.recommended { border: 2px solid var(--accent); }
100
- .link-btn { border: 0; background: transparent; color: var(--muted); text-decoration: underline; padding: 6px 0; }
101
- @media (max-width: 560px) { .brand span { display: none; } .audit-btn { padding: 8px 12px; } .hero-sub { font-size: 16px; } }
102
-
103
- .ai-chip { border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 999px; padding: 6px 11px; font-size: 13px; white-space: nowrap; }
104
- .ai-chip[data-state="ready"] { border-color: var(--ok); color: var(--ok); }
105
- .ai-chip[data-state="loading"] { border-color: var(--accent-border); color: var(--accent); }
106
- .ai-card { text-align: left; background: var(--panel); border: 2px solid var(--accent-border); border-radius: 16px; padding: 14px 16px; margin: 0 auto 16px; max-width: 600px; font-size: 15px; }
107
- .ai-card .btn-row { margin-top: 10px; }
108
- .quick-actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
109
- @media (max-width: 560px) { .ai-chip { padding: 6px 8px; font-size: 12px; } }
110
-
111
- /* While the AI assistant downloads, the locked message box is the progress bar. */
112
- .input-pill.loading { background: linear-gradient(90deg, var(--accent-soft) var(--progress, 0%), var(--input-bg) var(--progress, 0%)); border-color: var(--accent-border); }
113
- .input-pill.loading::after { content: ""; position: absolute; left: 0; bottom: 0; height: 3px; width: var(--progress, 0%); background: var(--accent); transition: width .3s; }
114
- .input-pill.loading #input::placeholder { color: var(--accent); opacity: 1; font-weight: 600; }
115
-
116
- /* This device: a pill above the message box; details open as a popover (computers) or a bottom sheet (phones). */
117
- .context-wrap { position: relative; max-width: 812px; width: 100%; margin: 0 auto; padding: 0 14px; flex-shrink: 0; }
118
- .context-bar { display: flex; align-items: center; gap: 10px; padding-top: 6px; min-width: 0; }
119
- .device-pill { display: inline-flex; align-items: center; gap: 7px; min-width: 0; max-width: 100%; border: 1px solid var(--border); background: var(--panel);
120
- color: var(--text); border-radius: 999px; padding: 5px 12px 5px 9px; font-size: 13px; }
121
- .device-pill:hover { border-color: var(--accent-border); }
122
- .device-pill[data-kind="chosen"] { border-color: var(--blue); }
123
- .device-pill[data-kind="audit"] { border-color: var(--ok); }
124
- .device-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
125
- .device-ic { flex: none; } .chev { flex: none; color: var(--muted); font-size: 11px; }
126
- .context-note { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
127
- .device-panel { position: absolute; left: 14px; bottom: calc(100% + 8px); width: min(400px, calc(100% - 28px)); background: var(--panel); border: 1px solid var(--border);
128
- border-radius: 16px; padding: 6px 16px 16px; box-shadow: 0 12px 32px rgba(0, 0, 0, .18); z-index: 15; max-height: 70dvh; overflow-y: auto; }
129
- .device-head { display: flex; justify-content: space-between; align-items: center; }
130
- .device-head h2 { font-size: 16px; margin: 0; }
131
- .device-panel dl { margin: 4px 0 10px; }
132
- .drow { display: grid; grid-template-columns: 112px 1fr; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
133
- .drow:last-child { border-bottom: 0; }
134
- .drow dt { color: var(--muted); } .drow dd { margin: 0; overflow-wrap: anywhere; }
135
- .drow small { display: block; color: var(--muted); font-size: 12px; }
136
- .device-note { background: var(--bg); border-radius: 10px; padding: 9px 11px; font-size: 13px; color: var(--muted); margin: 0 0 12px; }
137
- .device-audit { width: 100%; padding: 10px 14px; }
138
- .device-switch { margin-top: 14px; }
139
- .seg { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
140
- .seg button { border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 999px; padding: 5px 10px; font-size: 13px; }
141
- .seg button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); font-weight: 700; }
142
- .sheet-backdrop { position: fixed; inset: 0; z-index: 14; background: transparent; }
143
- @media (max-width: 560px) {
144
- .context-note { display: none; }
145
- .device-pill { width: 100%; justify-content: flex-start; }
146
- .device-pill .chev { margin-left: auto; }
147
- .sheet-backdrop { background: rgba(0, 0, 0, .4); }
148
- .device-panel { position: fixed; left: 0; right: 0; bottom: 0; width: 100%; border-radius: 18px 18px 0 0; max-height: 85dvh;
149
- padding-bottom: calc(16px + env(safe-area-inset-bottom)); animation: sheet-up .2s ease-out; }
150
- .device-panel::before { content: ""; display: block; width: 40px; height: 4px; border-radius: 99px; background: var(--border); margin: 4px auto 6px; }
151
- .drow { grid-template-columns: 104px 1fr; }
152
- }
153
- @keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
154
- @media (prefers-reduced-motion: reduce) { .device-panel { animation: none; } .input-pill.loading::after { transition: none; } }
155
- .device-retry { width: 100%; margin-bottom: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chat/chat.js DELETED
@@ -1,601 +0,0 @@
1
- // Devseis Security Assistant: chat about endpoint security with the Devseis model running on the device (WebLLM),
2
- // grounded in the Devseis knowledge base (library/knowledge.json), plus audits: the phone check on phones, the desktop
3
- // app (or its report file) on computers. Conversations are stored only in this browser.
4
- import { Knowledge, chatMessages, auditBrief, chatProblems, searchWithAudit, repeatedText, tooShort } from '../app/renderer/knowledge.js';
5
- import { Library, findingPrompt, findingTemplate, summaryPrompt, summaryTemplate, aiPrompt, aiTemplate, writeAnswer, ANSWER_SCHEMAS } from '../app/renderer/auditor-core.js';
6
- import { buildReportHtml } from '../app/renderer/report.js';
7
- import { detectDevice } from '../mobile/detect.js';
8
- import { describeComputer, chosenDevice, deviceBrief, deviceIcon, osLabel, OS_CHOICES } from './device.js';
9
-
10
- const WEBLLM_URL = 'https://esm.run/@mlc-ai/web-llm@0.2.85';
11
- const STORE = 'devseis.chats.v1';
12
- const $ = id => document.getElementById(id);
13
- const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
14
-
15
- const state = { kb: null, device: null, config: null, engine: null, enginePromise: null, modelLabel: '', busy: false, chats: [], current: null, libData: null };
16
-
17
- // ---------- storage ----------
18
- function loadChats() { try { return JSON.parse(localStorage.getItem(STORE)) || []; } catch { return []; } }
19
- function saveChats() { try { localStorage.setItem(STORE, JSON.stringify(state.chats.slice(0, 50))); } catch { /* private mode or full */ } }
20
- function newChat() {
21
- const chat = { id: Date.now().toString(36), title: 'New conversation', messages: [], audit: null };
22
- state.chats.unshift(chat); state.current = chat; saveChats(); render(); return chat;
23
- }
24
-
25
- // ---------- model ----------
26
- // The model never blocks a question: until it is ready, answers come straight from the knowledge base.
27
- // It downloads in the background after the user agrees once (chatModel().download), and starts by itself when already stored.
28
- const AI_CONSENT = 'devseis.aiConsent';
29
- // Set when the AI wrote unreadable text on this device (a faulty graphics chip or driver); holds the model version, so a new one is tried again.
30
- const AI_FAULT = 'devseis.aiFault';
31
- const AI_FAULT_REASON = 'devseis.aiFaultReason';
32
- // Set while the AI loads or writes. Still set when the page starts again = the browser killed the page (out of memory:
33
- // iOS reloads a tab that uses too much), so the AI is turned off on this device instead of crashing every question.
34
- const AI_RUNNING = 'devseis.aiRunning';
35
- function aiRunning(on) { try { if (on) localStorage.setItem(AI_RUNNING, faultKey()); else localStorage.removeItem(AI_RUNNING); } catch { /* ignore */ } }
36
- const FIRST_WORDS_LIMIT = 60000, ANSWER_LIMIT = 180000;
37
- let webllmPromise = null;
38
- const loadWebllm = () => (webllmPromise ||= import(WEBLLM_URL));
39
-
40
- // Chat model size from app/model-version.json (chat.size): 0.5B for phones and weak graphics chips, 1.5B before v0.5.
41
- const CHAT_MODELS = { '0.5B': { repo: '0.5b', download: 'about 300 MB', mb: 280 }, '1.5B': { repo: '1.5b', download: 'about 850 MB', mb: 840 } };
42
- // chat.quant "q0f16" = the unrounded build (v0.5 loses its training when rounded to 4 bits): about 3x the download,
43
- // and only on graphics chips with 16-bit support. Without it, the 4-bit build that suits this chip.
44
- function chatModel() {
45
- const size = CHAT_MODELS[state.config?.chat?.size] ? state.config.chat.size : '1.5B', quant = state.config?.chat?.quant || null;
46
- const m = { size, quant, ...CHAT_MODELS[size] };
47
- return quant === 'q0f16' ? { ...m, download: `about ${Math.round(m.mb * 3.4 / 50) * 50} MB`, mb: Math.round(m.mb * 3.4) } : m;
48
- }
49
- // A broken-AI flag is kept per model build, so a new build gets a fresh try.
50
- function faultKey() { const c = state.config?.chat || {}; return [c.version || 'base', c.quant].filter(Boolean).join('-'); }
51
- // Why the AI cannot run here ('' = it can): the unrounded build is too big for a phone tab and needs 16-bit graphics.
52
- async function aiUnsuitable() {
53
- if (chatModel().quant !== 'q0f16') return '';
54
- if (state.device && state.device.desktop === false) return 'too large for phones until the smaller version is ready';
55
- try { const a = await navigator.gpu.requestAdapter(); return !a || !a.features.has('shader-f16') ? 'this graphics chip has no 16-bit support' : ''; } catch { return 'no graphics adapter'; }
56
- }
57
-
58
- async function modelChoice() {
59
- if (!('gpu' in navigator)) throw new Error('This browser has no WebGPU');
60
- const adapter = await navigator.gpu.requestAdapter();
61
- if (!adapter) throw new Error('No WebGPU graphics adapter');
62
- const { size, repo, quant: fixed } = chatModel();
63
- if (fixed === 'q0f16' && !adapter.features.has('shader-f16')) throw new Error('This graphics chip has no 16-bit support');
64
- const quant = fixed || (adapter.features.has('shader-f16') ? 'q4f16_1' : 'q4f32_1');
65
- const webllm = await loadWebllm();
66
- const base = webllm.prebuiltAppConfig.model_list.find(m => m.model_id === `Qwen2.5-${size}-Instruct-${quant}-MLC`);
67
- const v = state.config?.chat?.version;
68
- const custom = v ? { ...base, model: `https://huggingface.co/Devseis/endpoint-auditor-${repo}-${quant}-MLC/resolve/${v}/`, model_id: `devseis-assistant-${repo}-${v}-${quant}` } : null;
69
- const appConfig = { ...webllm.prebuiltAppConfig, model_list: custom ? [...webllm.prebuiltAppConfig.model_list, custom] : webllm.prebuiltAppConfig.model_list };
70
- return { webllm, base, custom, appConfig, v };
71
- }
72
-
73
- // Is the model already stored on this device (no download needed)?
74
- async function modelCached() {
75
- try {
76
- const { webllm, base, custom, appConfig } = await modelChoice();
77
- return await webllm.hasModelInCache((custom || base).model_id, appConfig);
78
- } catch { return false; }
79
- }
80
-
81
- function getEngine() {
82
- if (state.engine !== null || state.enginePromise) return state.enginePromise || Promise.resolve(state.engine);
83
- state.loading = true; state.progress = 0; aiChip(); render();
84
- state.enginePromise = (async () => {
85
- const { webllm, base, custom, appConfig, v } = await modelChoice();
86
- const initProgressCallback = r => {
87
- state.progress = r.progress || 0;
88
- const mb = /([\d.]+)MB fetched/.exec(r.text || '');
89
- // Shown inside the (locked) message box, which fills up as the model arrives.
90
- const pct = `${Math.round(state.progress * 100)}%`, narrow = matchMedia('(max-width: 560px)').matches;
91
- state.progressText = state.cachedStart ? `Starting the AI… ${pct}` : narrow ? `Downloading AI… ${pct}${mb ? ` · ${Math.round(+mb[1])} MB` : ''}`
92
- : `Downloading the AI assistant… ${pct}${mb ? ` · ${Math.round(+mb[1])} of ${chatModel().download}` : ''}`;
93
- aiChip();
94
- };
95
- let engine, label;
96
- if (custom) {
97
- try { engine = await webllm.CreateMLCEngine(custom.model_id, { appConfig, initProgressCallback }); label = `Devseis assistant ${v} (${chatModel().size})`; } catch (e) { console.warn('Devseis model failed, using base', e); }
98
- }
99
- if (!engine) { engine = await webllm.CreateMLCEngine(base.model_id, { appConfig, initProgressCallback }); label = `Qwen2.5 ${chatModel().size} (base, until the Devseis model loads)`; }
100
- state.modelLabel = label; $('modelName').textContent = label; status('');
101
- return engine;
102
- })().then(e => { state.engine = e; state.loading = false; aiChip(); render(); return e; },
103
- err => { state.engine = false; state.loading = false; state.modelLabel = `Not available: ${err.message}`; $('modelName').textContent = 'Knowledge base only'; status(''); aiChip(); render(); return false; });
104
- return state.enginePromise;
105
- }
106
-
107
- function enableAi() {
108
- try { localStorage.setItem(AI_CONSENT, 'yes'); localStorage.removeItem(AI_FAULT); } catch { /* ignore */ }
109
- state.consent = true; state.kbOnly = false; state.aiFault = false;
110
- getEngine();
111
- }
112
-
113
- // Small status chip in the header: what the assistant is running on right now.
114
- function aiChip() {
115
- const chip = $('aiChip');
116
- if (!chip) return;
117
- if (state.engine) { chip.textContent = '✨ AI ready'; chip.title = state.modelLabel; chip.dataset.state = 'ready'; }
118
- else if (state.loading) { chip.textContent = `⬇ AI ${Math.round((state.progress || 0) * 100)}%`; chip.title = 'The AI assistant is getting ready'; chip.dataset.state = 'loading'; }
119
- else if (state.engine === false || state.aiFault) { chip.textContent = '📚 Knowledge base'; chip.title = state.modelLabel; chip.dataset.state = 'off'; }
120
- else { chip.textContent = '⚡ Enable AI'; chip.title = `Download the AI assistant (${chatModel().download}, one time)`; chip.dataset.state = 'off'; }
121
- updateSend();
122
- if (!$('devicePanel').classList.contains('hidden')) renderDevicePanel();
123
- }
124
-
125
- // With WebGPU the chat waits for the AI; the knowledge base answers alone only without WebGPU, if the AI fails to load,
126
- // or when the visitor chose "Not now".
127
- function aiPending() { return 'gpu' in navigator && !state.kbOnly && state.engine !== false && !state.engine; }
128
-
129
- // Instant answer straight from the knowledge base, formatted for reading.
130
- function quickAnswer(passages) {
131
- if (!passages.length) return 'The Devseis knowledge base has nothing on that question. I cover device security, ISO 27001, GDPR and the EU AI Act.';
132
- const top = passages[0];
133
- const body = top.text.startsWith(top.title) ? top.text.slice(top.title.length).replace(/^[.:]\s*/, '') : top.text;
134
- const text = body.replace(/\s\((\d+)\)\s/g, '\n$1. ')
135
- .replace(/(^|\s)(How to fix on [A-Za-z]+|How to verify|Related controls|Why it matters|Risk if missing|Requirement|An auditor looks for|On endpoints|Where to look):/g, '\n\n**$2:**')
136
- .replace(/^\s+/, '');
137
- return `**${top.title}** [1]\n\n${text}`;
138
- }
139
-
140
- function status(text, fraction) {
141
- $('statusBar').classList.toggle('hidden', !text);
142
- $('statusText').textContent = text;
143
- $('statusFill').style.width = `${Math.round((fraction || 0) * 100)}%`;
144
- }
145
-
146
- // ---------- rendering ----------
147
- function markdown(text, passages, msgIndex) {
148
- const lines = esc(text).split('\n');
149
- let html = '', list = null;
150
- const close = () => { if (list) { html += `</${list}>`; list = null; } };
151
- for (const line of lines) {
152
- const ol = /^\s*\d+\.\s+(.*)$/.exec(line), ul = /^\s*[-•]\s+(.*)$/.exec(line);
153
- if (ol || ul) {
154
- const tag = ol ? 'ol' : 'ul';
155
- if (list !== tag) { close(); html += `<${tag}>`; list = tag; }
156
- html += `<li>${(ol || ul)[1]}</li>`;
157
- } else { close(); if (line.trim()) html += `<p>${line}</p>`; }
158
- }
159
- close();
160
- return html.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>').replace(/`([^`]+)`/g, '<code>$1</code>')
161
- .replace(/\[(\d+)\]/g, (m, n) => passages && passages[n - 1] ? `<button type="button" class="cite" data-msg="${msgIndex}" data-n="${n}" title="${esc(passages[n - 1].title)}">${n}</button>` : '');
162
- }
163
-
164
- function auditCard(audit) {
165
- const s = audit.summary, assessed = s.Compliant + s['Non-Compliant'];
166
- const pct = assessed ? Math.round(100 * s.Compliant / assessed) : 0;
167
- const colours = { Compliant: 'var(--ok)', 'Non-Compliant': 'var(--accent)', Pending: 'var(--warn)', NotApplicable: 'var(--muted)', Error: '#8250df' };
168
- const gaps = audit.results.filter(r => r.status === 'Non-Compliant').slice(0, 5);
169
- return `<div class="card"><div class="muted tiny">AUDIT REPORT · ${esc(audit.host.os_name)} ${esc(audit.host.os_version)} · ${esc(String(audit.audit.started_at).slice(0, 10))}</div>
170
- <div class="score">${pct}%</div><div class="muted">${s.Compliant} of ${assessed} assessed checks compliant</div>
171
- <div>${Object.entries(s).filter(([, v]) => v).map(([k, v]) => `<span class="pill" style="color:${colours[k]}">${v} ${k}</span>`).join('')}</div>
172
- ${gaps.length ? `<p style="margin:8px 0 0"><b>Top gaps:</b> ${gaps.map(g => esc(g.name)).join(', ')}</p>` : ''}
173
- <div class="btn-row"><button type="button" class="primary-btn" data-ask="What should I fix first?">What should I fix first?</button>
174
- <button type="button" class="small-btn" data-report="1">📄 Write my full report</button></div></div>`;
175
- }
176
-
177
- function render() {
178
- const chat = state.current;
179
- $('emptyState').classList.toggle('hidden', Boolean(chat && (chat.messages.length || chat.audit)));
180
- $('aiCard').classList.toggle('hidden', !state.showAiCard || state.loading || Boolean(state.engine) || state.engine === false);
181
- renderDevice();
182
- const thread = $('thread');
183
- thread.innerHTML = '';
184
- if (!chat) return renderSidebar();
185
- if (chat.audit) thread.insertAdjacentHTML('beforeend', `<div class="msg bot"><img class="avatar" src="app/build/icon.png" alt="">${auditCard(chat.audit)}</div>`);
186
- chat.messages.forEach((m, i) => {
187
- if (m.role === 'user') { thread.insertAdjacentHTML('beforeend', `<div class="msg user"><div class="bubble">${esc(m.content)}</div></div>`); return; }
188
- const sources = (m.passages || []).map((p, n) => `<button type="button" class="source" data-msg="${i}" data-n="${n + 1}">[${n + 1}] ${esc(p.title)}</button>`).join('');
189
- thread.insertAdjacentHTML('beforeend', `<div class="msg bot"><img class="avatar" src="app/build/icon.png" alt=""><div class="bubble" id="m${i}">
190
- ${m.pending ? '<div class="typing"><span></span><span></span><span></span></div>' : markdown(m.content, m.passages, i)}
191
- ${!m.pending && sources && !m.noSources ? `<div class="sources"><span class="muted tiny">Sources:</span>${sources}</div>` : ''}
192
- ${m.note ? `<div class="note">${esc(m.note)}</div>` : ''}
193
- ${m.quick && !m.pending && !m.writing ? (state.engine ? `<div class="quick-actions"><button type="button" class="primary-btn" data-rewrite="${i}">✨ Rewrite with AI</button></div>`
194
- : (!state.loading && state.engine !== false ? `<div class="quick-actions"><button type="button" class="small-btn" data-enable="1">⚡ Enable the AI assistant (${chatModel().download}, one time)</button></div>` : '')) : ''}
195
- <div class="source-slot"></div></div></div>`);
196
- });
197
- renderSidebar();
198
- $('chatMain').scrollTop = $('chatMain').scrollHeight;
199
- }
200
-
201
- function renderSidebar() {
202
- $('sidebarList').innerHTML = state.chats.map(c => `<div class="convo${c === state.current ? ' active' : ''}" data-id="${c.id}"><span>${esc(c.title)}</span><button type="button" data-del="${c.id}" aria-label="Delete">✕</button></div>`).join('')
203
- || '<p class="muted tiny" style="padding:8px">No conversations yet.</p>';
204
- }
205
-
206
- function showSource(msgIndex, n, anchor) {
207
- const m = state.current.messages[msgIndex];
208
- const p = m && m.passages && m.passages[n - 1];
209
- if (!p) return;
210
- const slot = document.querySelector(`#m${msgIndex} .source-slot`);
211
- slot.innerHTML = `<div class="source-text"><b>[${n}] ${esc(p.title)}</b><br>${esc(p.text)}</div>`;
212
- document.querySelectorAll(`#m${msgIndex} .source`).forEach(b => b.classList.toggle('open', b.dataset.n === String(n)));
213
- if (anchor) slot.scrollIntoView({ block: 'nearest' });
214
- }
215
-
216
- // ---------- asking ----------
217
- async function ask(question) {
218
- if (state.busy || aiPending() || !question.trim()) return;
219
- const chat = state.current || newChat();
220
- if (chat.title === 'New conversation') chat.title = question.slice(0, 60);
221
- // The model was trained on one question at a time: earlier answers in the prompt make it repeat them. A short
222
- // follow-up ("and on Windows?") borrows the previous question to find its sources instead.
223
- const history = [];
224
- const prev = [...chat.messages].reverse().find(m => m.role === 'user');
225
- const followUp = prev && /^(and|what about|how about|also|it|that|this|same)\b/i.test(question.trim()) && question.trim().split(/\s+/).length <= 6;
226
- const searchFor = followUp ? `${prev.content} ${question}` : question;
227
- chat.messages.push({ role: 'user', content: question });
228
- const reply = { role: 'assistant', content: '', pending: true, passages: [] };
229
- chat.messages.push(reply);
230
- state.busy = true; updateSend(); render();
231
- try {
232
- const device = activeDevice();
233
- const os = chat.audit ? chat.audit.host.os_family : (device.os_family === 'chromeos' ? null : device.os_family);
234
- const passages = searchWithAudit(state.kb, searchFor, chat.audit, { k: 4, os });
235
- reply.passages = passages;
236
- const messages = chatMessages(followUp ? `${question} (follow-up to: ${prev.content})` : question, passages, auditBrief(chat.audit), history, chat.audit ? null : deviceBrief(device));
237
- reply.question = question; reply.history = history;
238
- if (state.engine) {
239
- // Typing dots until the AI's first words arrive; the knowledge-base answer is only the fallback if the AI fails.
240
- reply.writing = true; reply.note = '✨ Writing answer…';
241
- render(); saveChats();
242
- aiRunning(true);
243
- try { await aiAnswer(chat, reply, messages, passages); } finally { aiRunning(false); }
244
- } else {
245
- reply.content = quickAnswer(passages);
246
- reply.cited = true; reply.quick = true;
247
- reply.note = state.loading ? `Quick answer from the knowledge base. The AI assistant is getting ready (${Math.round((state.progress || 0) * 100)}%); you can ask it to rewrite this when it is ready.`
248
- : state.engine === false ? 'Quick answer from the knowledge base (the AI is not available in this browser).'
249
- : 'Quick answer from the knowledge base. Enable the AI assistant for written answers.';
250
- }
251
- } catch (error) {
252
- reply.content = `Something went wrong: ${error.message}`;
253
- }
254
- reply.pending = false; state.busy = false; saveChats(); updateSend(); render();
255
- }
256
-
257
- // Write (or rewrite) an answer with the model; the GPU can be lost (memory pressure, sleep): restart once, else keep the quick answer.
258
- async function aiAnswer(chat, reply, messages, passages) {
259
- const index = chat.messages.indexOf(reply);
260
- let engine = state.engine, lastError = '';
261
- for (let attempt = 0; attempt < 2 && engine; attempt++) {
262
- try {
263
- const stream = await engine.chat.completions.create({ messages, temperature: 0, max_tokens: 700, stream: true });
264
- let last = 0, started = false, fault = null;
265
- // A weak graphics chip can take minutes; stop and use the knowledge base instead of leaving the visitor waiting.
266
- const t0 = Date.now();
267
- const watchdog = setInterval(() => {
268
- if (Date.now() - t0 > (started ? ANSWER_LIMIT : FIRST_WORDS_LIMIT)) { fault = 'slow'; engine.interruptGenerate(); }
269
- }, 1000);
270
- try {
271
- for await (const chunk of stream) {
272
- const delta = chunk.choices[0]?.delta?.content || '';
273
- if (!started && delta) { started = true; reply.pending = false; reply.content = ''; reply.quick = false; reply.writing = false; reply.note = ''; }
274
- reply.content += delta;
275
- // Looping text means the engine is broken on this device: stop at once, it only gets worse.
276
- if (reply.content.length > 40 && repeatedText(reply.content)) { fault = 'junk'; engine.interruptGenerate(); break; }
277
- if (Date.now() - last > 120) { last = Date.now(); const el = $(`m${index}`); if (el) el.innerHTML = markdown(clean(reply.content), passages, index); }
278
- }
279
- } finally { clearInterval(watchdog); }
280
- if (fault === 'junk') { turnOffBrokenAi(); return fallback(reply, passages, 'The AI wrote unreadable text on this device, so Arx switched to answers from the knowledge base. Details: tap the device bar.'); }
281
- if (fault === 'slow') return fallback(reply, passages, 'The AI was too slow on this device, so this answer comes from the knowledge base.');
282
- if (!started) throw new Error('The AI returned no answer');
283
- reply.content = clean(reply.content);
284
- // A few stray words means the graphics chip garbled the answer; twice in a row, stop using the AI here.
285
- if (tooShort(reply.content)) {
286
- state.scraps = (state.scraps || 0) + 1;
287
- if (state.scraps >= 2) { turnOffBrokenAi(); return fallback(reply, passages, 'The AI wrote unreadable text on this device, so Arx switched to answers from the knowledge base. Details: tap the device bar.'); }
288
- return fallback(reply, passages, 'The AI\'s answer was unreadable, so this one comes from the knowledge base.');
289
- }
290
- state.scraps = 0;
291
- reply.noSources = !/\[\d+\]/.test(reply.content) && /can only help|don't have|do not have|not able to|can't answer|cannot answer|sorry/i.test(reply.content);
292
- reply.cited = /\[\d+\]/.test(reply.content);
293
- if (chatProblems(messages[messages.length - 1].content, reply.content).length) reply.note = 'Some details in this answer could not be matched to the sources. Check them in the sources below before acting.';
294
- return;
295
- } catch (error) {
296
- lastError = error.message;
297
- if (!/lost|Instance reference|GPUDevice|disposed/i.test(error.message)) { console.warn('AI answer failed', error); break; } // keep the quick answer
298
- status('The graphics processor was reset; restarting the AI…', 0.5);
299
- state.engine = null; state.enginePromise = null;
300
- engine = attempt === 0 ? await getEngine() : null;
301
- }
302
- }
303
- fallback(reply, passages, `Quick answer from the knowledge base (the AI could not run just now${lastError ? `: ${lastError.slice(0, 140)}` : ''}).`);
304
- }
305
-
306
- function fallback(reply, passages, note) {
307
- reply.pending = false; reply.content = quickAnswer(passages); reply.cited = true; reply.quick = true; reply.writing = false; reply.noSources = false;
308
- reply.note = note;
309
- }
310
-
311
- // The AI wrote junk: free the graphics chip (a broken driver can hang the whole computer) and stay on the knowledge base here.
312
- function turnOffBrokenAi(reason = 'it wrote unreadable text on this device') {
313
- const engine = state.engine;
314
- state.engine = null; state.enginePromise = null; state.loading = false; state.kbOnly = true; state.aiFault = true;
315
- state.modelLabel = `Turned off: ${reason}`; $('modelName').textContent = 'Knowledge base only';
316
- try { localStorage.setItem(AI_FAULT, faultKey()); localStorage.setItem(AI_FAULT_REASON, reason); } catch { /* ignore */ }
317
- if (engine && engine.unload) engine.unload().catch(() => {});
318
- aiChip();
319
- }
320
-
321
- async function rewriteWithAi(index) {
322
- const chat = state.current; const reply = chat && chat.messages[index];
323
- if (!reply || !state.engine || state.busy) return;
324
- state.busy = true; updateSend();
325
- const messages = chatMessages(reply.question, reply.passages, auditBrief(chat.audit), reply.history || [], chat.audit ? null : deviceBrief(activeDevice()));
326
- saveChats(); aiRunning(true);
327
- try { await aiAnswer(chat, reply, messages, reply.passages); } catch (error) { reply.note = `The AI could not answer: ${error.message}`; } finally { aiRunning(false); }
328
- state.busy = false; saveChats(); updateSend(); render();
329
- }
330
-
331
- // Models sometimes emit HTML entities; show plain characters.
332
- function clean(text) { return text.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'"); }
333
-
334
- function desktopOs() { const ua = navigator.userAgent; return /Windows/.test(ua) ? 'windows' : /Mac/.test(ua) ? 'macos' : 'linux'; }
335
-
336
- // ---------- audits ----------
337
- function openAuditModal() {
338
- const d = state.device || {};
339
- const phone = d.os_family === 'ios' || d.os_family === 'android';
340
- const os = phone ? null : desktopOs();
341
- const osName = { macos: 'Mac', windows: 'Windows', linux: 'Linux' }[os];
342
- $('auditOptions').innerHTML = phone
343
- ? `<p class="muted">The check runs in this browser: the phone answers what it can, you answer the rest. About 5 minutes.</p>
344
- <a class="option recommended" href="phone.html?return=chat"><b>📱 Start the phone check</b><span>Your answers and report stay on this phone.</span></a>`
345
- : `<p class="muted">A web page can't read a computer's settings, so computers are audited with the free Devseis app. It only reads, changes nothing, and takes about 15 minutes.</p>
346
- <a class="option recommended" href="download.html#${os}"><b>💻 Get the desktop app for ${osName}${os === 'windows' ? ' (beta)' : ''}</b><span>Step-by-step install guide.</span></a>
347
- <button type="button" class="option" id="optAttach"><b>📎 I already ran an audit</b><span>Add the report file (<code>…-evidence.json</code>, saved next to the PDF) and I'll answer from your results.</span></button>
348
- <a class="option" href="phone.html"><b>📱 Check a phone instead</b><span>Open the phone check on the phone itself.</span></a>`;
349
- $('auditModal').classList.remove('hidden');
350
- const att = $('optAttach'); if (att) att.addEventListener('click', () => { $('auditModal').classList.add('hidden'); $('fileInput').click(); });
351
- }
352
-
353
- function attachAudit(evidence) {
354
- if (!evidence || evidence.schema_version !== '1.0' || !Array.isArray(evidence.results) || !evidence.host) throw new Error('This is not a Devseis audit report file.');
355
- const chat = state.current && !state.current.messages.length && !state.current.audit ? state.current : newChat();
356
- chat.audit = evidence; chat.title = `Audit: ${evidence.host.os_name} ${String(evidence.audit.started_at).slice(0, 10)}`;
357
- saveChats(); render();
358
- }
359
-
360
- // JSON from this site. A request can stall behind the browser's cache lock when the page was reloaded while it was
361
- // downloading (the desktop app does that when it opens the chat), so give up after a few seconds and retry past the cache.
362
- // The retry has no time limit, so a slow connection still gets there.
363
- async function getJson(p, { timeout = 5000, cache = 'default' } = {}) {
364
- for (const mode of [cache, 'reload']) {
365
- const ctl = new AbortController(); const timer = mode === 'reload' ? null : setTimeout(() => ctl.abort(), timeout);
366
- try {
367
- const r = await fetch(p, { cache: mode, signal: ctl.signal });
368
- if (!r.ok) throw new Error(`${p}: ${r.status}`);
369
- return await r.json();
370
- } catch (error) { if (error.name !== 'AbortError' || mode === 'reload') throw error; } finally { clearTimeout(timer); }
371
- }
372
- }
373
-
374
- async function loadLibrary() {
375
- if (state.libData) return state.libData;
376
- const [catalog, controls, guidance, aiTools] = await Promise.all(['catalog/checks.json', 'library/controls.json', 'library/check_guidance.json', 'library/ai_tools.json'].map(p => getJson(p)));
377
- state.libData = new Library({ catalog, controls, guidance, aiTools });
378
- return state.libData;
379
- }
380
-
381
- async function writeReport() {
382
- const chat = state.current; if (!chat || !chat.audit || state.busy) return;
383
- state.busy = true; updateSend();
384
- const evidence = chat.audit;
385
- try {
386
- const lib = await loadLibrary();
387
- const engine = state.engine || null;
388
- const generate = engine ? async (system, prompt, task) => (await engine.chat.completions.create({
389
- messages: [{ role: 'system', content: system }, { role: 'user', content: prompt }], temperature: 0, max_tokens: task === 'ai_classification' ? 1600 : 1200,
390
- response_format: { type: 'json_object', schema: JSON.stringify(ANSWER_SCHEMAS[task]) } })).choices[0].message.content : null;
391
- const targets = evidence.results.filter(r => ['Non-Compliant', 'Error', 'Pending'].includes(r.status));
392
- const findings = {}, sources = { model: 0, template: 0 };
393
- let i = 0;
394
- for (const r of targets) {
395
- status(`Writing your report: ${r.name} (${++i} of ${targets.length})`, i / (targets.length + 2));
396
- const out = await writeAnswer('finding', findingPrompt(evidence, r, lib), findingTemplate(evidence, r, lib), r.section === 'org' ? null : generate);
397
- findings[r.id] = out.answer; sources[out.source] += 1;
398
- }
399
- status('Writing your report: summary', 0.95);
400
- const base = summaryTemplate(evidence, lib);
401
- const so = await writeAnswer('summary', summaryPrompt(evidence, lib), base, generate);
402
- sources[so.source] += 1;
403
- const summary = so.source === 'model' ? { ...base, summary: so.answer.summary, next_steps: so.answer.next_steps } : base;
404
- const ai = aiPrompt(evidence, lib);
405
- let aiResult = null;
406
- if (ai) { const o = await writeAnswer('ai_classification', ai, aiTemplate(evidence, lib), generate); aiResult = o.answer; sources[o.source] += 1; }
407
- const html = buildReportHtml({ evidence, findings, summary, ai: aiResult, sha256: '(from attached file)', model: state.engine ? state.modelLabel : null, sources });
408
- const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
409
- chat.messages.push({ role: 'assistant', content: `Your full report is ready: ${sources.model} parts written by the AI, ${sources.template} by the built-in writer. Open it and use your browser's Print › Save as PDF to keep it.`, passages: [] });
410
- saveChats(); render();
411
- const last = $(`m${chat.messages.length - 1}`);
412
- last.insertAdjacentHTML('beforeend', `<div class="btn-row"><a class="primary-btn" href="${url}" target="_blank" rel="noopener" style="text-decoration:none">📄 Open the report</a></div>`);
413
- } catch (error) {
414
- chat.messages.push({ role: 'assistant', content: `The report could not be written: ${error.message}`, passages: [] }); render();
415
- }
416
- status(''); state.busy = false; updateSend();
417
- }
418
-
419
- // ---------- wiring ----------
420
- function updateSend() {
421
- const wait = aiPending();
422
- $('input').disabled = wait;
423
- const loading = wait && state.loading;
424
- $('inputPill').classList.toggle('loading', loading);
425
- $('inputPill').style.setProperty('--progress', `${Math.round((state.progress || 0) * 100)}%`);
426
- $('input').placeholder = !wait ? 'Ask Arx about security, or add your audit report…'
427
- : loading ? state.progressText || 'Getting the AI assistant ready…'
428
- : state.showAiCard ? 'Choose "Download now" or "Not now" above to start chatting…' : 'Tap ⚡ Enable AI at the top to start chatting…';
429
- document.querySelectorAll('.chip[data-prompt]').forEach(c => { c.disabled = wait; });
430
- $('sendBtn').disabled = wait || state.busy || !$('input').value.trim();
431
- }
432
- // ---------- this device ----------
433
- const DEVICE_CHOICE = 'devseis.deviceChoice';
434
- // The device answers are for: the one the browser detected, or one the visitor picked ("my Windows PC").
435
- function activeDevice() { return state.deviceChoice ? chosenDevice(state.deviceChoice) : (state.device || {}); }
436
-
437
- function renderDevice() {
438
- const audit = state.current && state.current.audit;
439
- const d = activeDevice();
440
- const ready = Boolean(state.device);
441
- let text, note;
442
- if (audit) {
443
- const s = audit.summary, assessed = s.Compliant + s['Non-Compliant'];
444
- text = `Audit · ${audit.host.os_name} ${audit.host.os_version} · ${assessed ? Math.round(s.Compliant / assessed * 100) : 0}% compliant`;
445
- note = 'Answers use your audit results';
446
- } else if (!ready) { text = 'Detecting this device…'; note = ''; }
447
- else if (!d.os_family) { text = 'Unknown device · choose yours'; note = 'Pick a system for tailored steps'; }
448
- else {
449
- text = [d.chosen ? `Asking about ${d.device_model}` : d.device_model !== d.os_name ? d.device_model : null, osLabel(d), d.chosen ? null : d.browser].filter(Boolean).join(' · ');
450
- note = d.chosen ? 'Steps are for the system you chose' : 'Answers are tailored to this device';
451
- }
452
- $('deviceIcon').textContent = audit ? '📋' : deviceIcon(d);
453
- $('deviceText').textContent = text;
454
- $('contextNote').textContent = note;
455
- $('deviceBtn').dataset.kind = audit ? 'audit' : d.chosen ? 'chosen' : 'detected';
456
- if (!$('devicePanel').classList.contains('hidden')) renderDevicePanel();
457
- }
458
-
459
- function renderDevicePanel() {
460
- const audit = state.current && state.current.audit;
461
- const real = state.device || {};
462
- const d = activeDevice();
463
- const row = (label, value, hint = '') => value ? `<div class="drow"><dt>${label}</dt><dd>${esc(value)}${hint ? `<small>${esc(hint)}</small>` : ''}</dd></div>` : '';
464
- const unlock = real.platform_authenticator === true ? 'Available (fingerprint, face or PIN)' : real.platform_authenticator === false ? 'None found by the browser' : '';
465
- const ai = state.aiFault ? state.modelLabel : state.engine ? `Ready · ${state.modelLabel}` : state.loading ? `Downloading · ${Math.round((state.progress || 0) * 100)}%`
466
- : 'gpu' in navigator && state.engine !== false ? 'Supported (WebGPU)' : 'Not supported here: answers come from the knowledge base';
467
- let html;
468
- if (audit) {
469
- const h = audit.host, s = audit.summary, assessed = s.Compliant + s['Non-Compliant'];
470
- $('deviceTitle').textContent = 'Audit in this chat';
471
- html = `<dl>${row('Device', h.device_model || h.hostname)}${row('System', `${h.os_name} ${h.os_version}`)}${row('Audited', String(audit.audit.started_at).slice(0, 10))}
472
- ${row('Result', `${s.Compliant} of ${assessed} checks compliant · ${s['Non-Compliant']} gaps`)}</dl>
473
- <p class="device-note">Arx answers from these results. Start a new chat to ask about this device instead.</p>`;
474
- } else {
475
- $('deviceTitle').textContent = d.chosen ? 'Device you are asking about' : 'This device';
476
- const version = d.os_version && !d.version_precise && !d.chosen && d.os_family !== 'windows' ? 'Approximate: browsers hide the exact version' : '';
477
- html = `<dl>${row('Device', d.device_model ? `${d.device_model}${d.arch ? ` (${d.arch})` : ''}` : '')}${row('System', d.os_family ? osLabel(d) : 'Not recognised', version)}
478
- ${d.chosen ? '' : row('Browser', real.browser) + row('Screen unlock', unlock) + row('Connection', real.cellular ? 'Mobile data' : '')}${row('On-device AI', ai)}</dl>
479
- <p class="device-note">🔒 Read by your browser and never uploaded. Arx uses it to give steps for this system. A web page can't see settings such as encryption or the firewall: an audit checks those.</p>
480
- ${state.aiFault && !d.chosen ? '<button type="button" class="small-btn device-retry" id="aiRetry">↻ Try the AI again (it may be slow or fail again)</button>' : ''}
481
- <button type="button" class="primary-btn device-audit" id="deviceAudit">🛡 Audit ${d.chosen ? 'a device' : 'this device'}</button>
482
- <div class="device-switch"><span class="muted tiny">Asking about another device?</span><div class="seg">
483
- <button type="button" data-os="" aria-pressed="${!state.deviceChoice}">${deviceIcon(real)} This device</button>
484
- ${OS_CHOICES.map(([os, label]) => `<button type="button" data-os="${os}" aria-pressed="${state.deviceChoice === os}">${label}</button>`).join('')}</div></div>`;
485
- }
486
- $('deviceBody').innerHTML = html;
487
- }
488
-
489
- function toggleDevicePanel(open) {
490
- $('devicePanel').classList.toggle('hidden', !open);
491
- $('deviceBackdrop').classList.toggle('hidden', !open);
492
- $('deviceBtn').setAttribute('aria-expanded', String(open));
493
- if (open) { renderDevicePanel(); $('deviceClose').focus(); } else $('deviceBtn').focus();
494
- }
495
- $('deviceBtn').addEventListener('click', () => toggleDevicePanel($('devicePanel').classList.contains('hidden')));
496
- $('deviceClose').addEventListener('click', () => toggleDevicePanel(false));
497
- $('deviceBackdrop').addEventListener('click', () => toggleDevicePanel(false));
498
- document.addEventListener('keydown', e => { if (e.key === 'Escape' && !$('devicePanel').classList.contains('hidden')) toggleDevicePanel(false); });
499
- $('deviceBody').addEventListener('click', e => {
500
- const pick = e.target.closest('[data-os]');
501
- if (pick) {
502
- state.deviceChoice = pick.dataset.os || null;
503
- try { if (state.deviceChoice) localStorage.setItem(DEVICE_CHOICE, state.deviceChoice); else localStorage.removeItem(DEVICE_CHOICE); } catch { /* ignore */ }
504
- renderDevice(); renderDevicePanel(); return;
505
- }
506
- if (e.target.closest('#deviceAudit')) { toggleDevicePanel(false); openAuditModal(); }
507
- if (e.target.closest('#aiRetry')) { toggleDevicePanel(false); enableAi(); }
508
- });
509
-
510
- function closeSidebar() { $('sidebar').classList.remove('open'); $('overlay').classList.add('hidden'); }
511
-
512
- $('composer').addEventListener('submit', e => { e.preventDefault(); const q = $('input').value.trim(); $('input').value = ''; $('input').style.height = ''; updateSend(); ask(q); });
513
- $('input').addEventListener('input', () => { const t = $('input'); t.style.height = 'auto'; t.style.height = `${Math.min(t.scrollHeight, 180)}px`; updateSend(); });
514
- $('input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); $('composer').requestSubmit(); } });
515
- document.querySelectorAll('.chip').forEach(c => c.addEventListener('click', () => (c.dataset.action === 'audit' ? openAuditModal() : ask(c.dataset.prompt))));
516
- $('auditBtn').addEventListener('click', openAuditModal);
517
- $('aiChip').addEventListener('click', () => { if (state.aiFault) toggleDevicePanel(true); else if (!state.engine && !state.loading && state.engine !== false) enableAi(); else if (state.engine) status(`AI assistant ready: ${state.modelLabel}`, 1) || setTimeout(() => status(''), 2500); });
518
- $('aiCardYes').addEventListener('click', () => { state.showAiCard = false; enableAi(); });
519
- $('aiCardNo').addEventListener('click', () => { state.showAiCard = false; state.kbOnly = true; aiChip(); try { localStorage.setItem(AI_CONSENT, 'later'); } catch { /* ignore */ } render(); });
520
- $('auditClose').addEventListener('click', () => $('auditModal').classList.add('hidden'));
521
- $('auditModal').addEventListener('click', e => { if (e.target === $('auditModal')) $('auditModal').classList.add('hidden'); });
522
- $('attachBtn').addEventListener('click', () => $('fileInput').click());
523
- $('fileInput').addEventListener('change', async () => {
524
- const f = $('fileInput').files[0]; $('fileInput').value = '';
525
- if (!f) return;
526
- try { attachAudit(JSON.parse((await f.text()).replace(/^/, ''))); } catch (error) {
527
- const chat = state.current || newChat(); chat.messages.push({ role: 'assistant', content: `I couldn't read that file: ${error.message} Use the …-evidence.json file the Devseis app saves next to the PDF report.`, passages: [] }); render();
528
- }
529
- });
530
- $('thread').addEventListener('click', e => {
531
- const t = e.target.closest('button'); if (!t) return;
532
- if (t.dataset.rewrite) rewriteWithAi(+t.dataset.rewrite);
533
- else if (t.dataset.enable) enableAi();
534
- else if (t.dataset.ask) ask(t.dataset.ask);
535
- else if (t.dataset.report) writeReport();
536
- else if (t.dataset.msg) showSource(+t.dataset.msg, +t.dataset.n, true);
537
- });
538
- $('newChat').addEventListener('click', () => { newChat(); $('input').focus(); });
539
- $('sidebarToggle').addEventListener('click', () => { $('sidebar').classList.add('open'); $('overlay').classList.remove('hidden'); });
540
- $('sidebarClose').addEventListener('click', closeSidebar);
541
- $('overlay').addEventListener('click', closeSidebar);
542
- $('sidebarList').addEventListener('click', e => {
543
- const del = e.target.closest('[data-del]');
544
- if (del) { state.chats = state.chats.filter(c => c.id !== del.dataset.del); if (state.current && state.current.id === del.dataset.del) state.current = state.chats[0] || null; saveChats(); render(); return; }
545
- const row = e.target.closest('.convo'); if (row) { state.current = state.chats.find(c => c.id === row.dataset.id); render(); closeSidebar(); }
546
- });
547
- $('clearAll').addEventListener('click', () => { state.chats = []; state.current = null; saveChats(); render(); });
548
- $('themeToggle').addEventListener('click', () => {
549
- const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
550
- document.documentElement.dataset.theme = dark ? 'light' : 'dark';
551
- try { localStorage.setItem('devseis.theme', document.documentElement.dataset.theme); } catch { /* ignore */ }
552
- });
553
-
554
- async function init() {
555
- try { const t = localStorage.getItem('devseis.theme'); if (t) document.documentElement.dataset.theme = t; } catch { /* ignore */ }
556
- state.chats = loadChats();
557
- // An answer still being written when the page was saved: the page was killed mid-answer. Show the knowledge-base one.
558
- for (const c of state.chats) for (const m of c.messages) if (m.role === 'assistant' && (m.pending || m.writing))
559
- fallback(m, m.passages || [], 'The page reloaded while the AI was writing this (the device likely ran out of memory), so this answer comes from the knowledge base.');
560
- state.current = state.chats[0] || null;
561
- const [kb, device, config] = await Promise.all([
562
- getJson('library/knowledge.json'),
563
- detectDevice().catch(() => ({})),
564
- getJson('app/model-version.json', { cache: 'no-cache' }).catch(() => ({})),
565
- ]);
566
- state.kb = new Knowledge(kb); state.device = await describeComputer(device); state.config = config;
567
- $('aiCardText').textContent = $('aiCardText').textContent.replace('about 1 GB', chatModel().download);
568
- try { state.deviceChoice = localStorage.getItem(DEVICE_CHOICE); } catch { /* ignore */ }
569
- // Coming back from the phone check with a fresh report.
570
- if (location.hash === '#audit') {
571
- try { const ev = JSON.parse(localStorage.getItem('devseis.lastAudit')); if (ev) { attachAudit(ev); localStorage.removeItem('devseis.lastAudit'); } } catch { /* ignore */ }
572
- history.replaceState(null, '', location.pathname);
573
- } else if (location.hash.startsWith('#audit=')) {
574
- // From the old address (apps 0.4.0/0.4.1 via the endpoint-auditor redirect Space). The fragment never reaches a server.
575
- try { attachAudit(JSON.parse(decodeURIComponent(location.hash.slice(7)))); } catch { /* ignore */ }
576
- history.replaceState(null, '', location.pathname);
577
- }
578
- render(); aiChip();
579
- if ('gpu' in navigator) {
580
- let consent = null; try { consent = localStorage.getItem(AI_CONSENT); } catch { /* ignore */ }
581
- let fault = null; try { fault = localStorage.getItem(AI_FAULT); } catch { /* ignore */ }
582
- let crashed = null; try { crashed = localStorage.getItem(AI_RUNNING); } catch { /* ignore */ }
583
- if (crashed) {
584
- aiRunning(false);
585
- if (crashed === faultKey()) { turnOffBrokenAi('the page reloaded while the AI was running (this device ran out of memory)'); fault = faultKey(); }
586
- }
587
- let reason = 'it wrote unreadable text on this device'; try { reason = localStorage.getItem(AI_FAULT_REASON) || reason; } catch { /* ignore */ }
588
- if (fault && fault === faultKey()) { state.kbOnly = true; state.aiFault = true; state.modelLabel = `Turned off: ${reason}`; $('modelName').textContent = 'Knowledge base only'; aiChip(); }
589
- else aiUnsuitable().then(no => no ? (() => { state.engine = false; state.modelLabel = `Not available: ${no}`; $('modelName').textContent = 'Knowledge base only'; aiChip(); render(); updateSend(); })() : modelCached().then(cached => {
590
- if (cached || consent === 'yes') { state.cachedStart = cached; getEngine(); }
591
- else if (consent === 'later') { state.kbOnly = true; aiChip(); }
592
- else {
593
- state.showAiCard = true;
594
- if (navigator.connection && navigator.connection.type === 'cellular') $('aiCardText').textContent += ' You seem to be on mobile data: Wi-Fi is recommended.';
595
- render(); aiChip();
596
- }
597
- }));
598
- } else { state.engine = false; state.modelLabel = 'Not available: this browser has no WebGPU'; $('modelName').textContent = 'Knowledge base only'; aiChip(); }
599
- if (new URLSearchParams(location.search).has('test')) Object.assign(window, { __chat: state, __ask: ask, __enable: enableAi, __rewrite: rewriteWithAi });
600
- }
601
- init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chat/device.js DELETED
@@ -1,48 +0,0 @@
1
- // What the chat knows about the device it runs on: the phone details from mobile/detect.js, plus the computer's OS,
2
- // version and chip, which a browser only partly reveals (Chromium client hints are precise; Safari and Firefox freeze
3
- // the macOS version, and Windows 10 and 11 look the same without hints). Nothing here is uploaded.
4
-
5
- const OS_NAMES = { macos: 'macOS', windows: 'Windows', linux: 'Linux', chromeos: 'ChromeOS', ios: 'iOS', android: 'Android' };
6
- const DEVICE_NAMES = { macos: 'Mac', windows: 'Windows PC', linux: 'Linux computer', chromeos: 'Chromebook', ios: 'iPhone', android: 'Android phone' };
7
- export const OS_CHOICES = [['windows', '🪟 Windows'], ['macos', '💻 macOS'], ['linux', '🐧 Linux'], ['ios', '📱 iPhone / iPad'], ['android', '🤖 Android']];
8
-
9
- // Fill in a computer's details (phones are already done by detectDevice).
10
- export async function describeComputer(device) {
11
- const d = { ...device };
12
- if (!d.desktop) return d;
13
- const ua = navigator.userAgent;
14
- d.os_family = /CrOS/.test(ua) ? 'chromeos' : /Windows/.test(ua) ? 'windows' : /Mac/.test(ua) ? 'macos' : 'linux';
15
- d.os_name = OS_NAMES[d.os_family];
16
- d.device_model = DEVICE_NAMES[d.os_family];
17
- d.os_version = null; d.version_precise = false; d.arch = null;
18
- const uaData = navigator.userAgentData;
19
- if (uaData && uaData.getHighEntropyValues) {
20
- try {
21
- const hi = await Promise.race([uaData.getHighEntropyValues(['platformVersion', 'architecture', 'bitness']), new Promise(r => setTimeout(() => r({}), 1500))]);
22
- const v = hi.platformVersion || '';
23
- if (d.os_family === 'windows' && v) { d.os_version = parseInt(v, 10) >= 13 ? '11' : '10'; d.version_precise = true; }
24
- if (d.os_family === 'macos' && v) { d.os_version = v.replace(/(\.0)+$/, ''); d.version_precise = true; }
25
- if (hi.architecture) d.arch = hi.architecture === 'arm' ? (d.os_family === 'macos' ? 'Apple silicon' : 'ARM') : (d.os_family === 'macos' ? 'Intel' : `${hi.architecture === 'x86' ? 'x86' : hi.architecture}-${hi.bitness || '64'}`);
26
- } catch { /* hints refused */ }
27
- }
28
- if (d.os_family === 'windows' && !d.os_version) d.os_version = '10 or 11';
29
- return d;
30
- }
31
-
32
- // "macOS 15.6", "Windows 10 or 11", "iOS 18.6", "Linux".
33
- export function osLabel(d) { return [d.os_name || OS_NAMES[d.os_family] || 'Unknown system', d.os_version].filter(Boolean).join(' '); }
34
-
35
- export function deviceIcon(d) { return d.os_family === 'ios' || d.os_family === 'android' ? '📱' : '💻'; }
36
-
37
- // A device the visitor picked by hand ("I'm asking about my Windows PC").
38
- export function chosenDevice(family) {
39
- return { os_family: family, os_name: OS_NAMES[family], os_version: null, device_model: DEVICE_NAMES[family], chosen: true };
40
- }
41
-
42
- // The MY DEVICE line in the chat prompt: what the browser saw, and that settings are not checked.
43
- export function deviceBrief(d) {
44
- if (!d || !d.os_family) return null;
45
- if (d.chosen) return `${osLabel(d)} (${d.os_family}), chosen by the user; its security settings have not been checked.`;
46
- const model = d.device_model && d.device_model !== d.os_name ? ` on ${/^[AEIOU]/.test(d.device_model) ? 'an' : 'a'} ${d.device_model}${d.arch ? ` (${d.arch})` : ''}` : '';
47
- return `${osLabel(d)} (${d.os_family})${model}, ${d.browser || 'unknown browser'}; detected by the browser, security settings not checked.`;
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
download.html CHANGED
@@ -1,214 +1,5 @@
1
  <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="description" content="Download the Devseis Endpoint Auditor for Mac, Windows and Linux: a free, read-only security and compliance check (ISO 27001, GDPR, EU AI Act) with an AI auditor that runs on your computer.">
7
- <meta name="theme-color" content="#ED2939">
8
- <title>Download Arx</title>
9
- <style>
10
- :root { --red: #ED2939; --red-dark: #C81E2B; --blue: #00A1DE; --bg: #f6f7fa; --card: #fff; --text: #16181d; --muted: #5b6270; --line: #e2e5eb; --soft: #fff4f4; --ok: #1a7f37; }
11
- @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #101216; --card: #1a1d23; --text: #eef0f4; --muted: #a2a9b6; --line: #2c313a; --soft: #2a1a1c; --ok: #3fb950; } }
12
- :root[data-theme="dark"] { --bg: #101216; --card: #1a1d23; --text: #eef0f4; --muted: #a2a9b6; --line: #2c313a; --soft: #2a1a1c; --ok: #3fb950; }
13
- * { box-sizing: border-box; }
14
- html, body { margin: 0; background: var(--bg); color: var(--text); }
15
- body { font: 17px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; }
16
- a { color: var(--blue); }
17
- .top { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: max(12px, env(safe-area-inset-top)) 16px 10px; border-top: 4px solid var(--red); background: var(--card); border-bottom: 1px solid var(--line); }
18
- .brand { display: flex; align-items: center; gap: 10px; font-size: 16px; } .brand img { width: 30px; height: 30px; }
19
- .top nav a { font-size: 15px; margin-left: 14px; text-decoration: none; }
20
- main { max-width: 860px; margin: 0 auto; padding: 8px 16px 40px; }
21
- h1 { font-size: clamp(28px, 5vw, 40px); line-height: 1.15; margin: 22px 0 8px; }
22
- h2 { font-size: 22px; margin: 32px 0 12px; }
23
- h3 { font-size: 18px; margin: 0 0 4px; }
24
- .lead { font-size: 19px; color: var(--muted); margin: 0 0 18px; }
25
- .muted { color: var(--muted); font-size: 15px; }
26
- .facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin: 18px 0; }
27
- .fact { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 12px 14px; }
28
- .fact b { display: block; font-size: 15px; } .fact span { color: var(--muted); font-size: 14px; }
29
- .tabs { display: flex; gap: 8px; margin: 26px 0 0; position: sticky; top: 0; background: var(--bg); padding: 8px 0; z-index: 2; }
30
- .tab { flex: 1; font: inherit; font-weight: 700; font-size: 17px; padding: 14px 10px; border-radius: 14px; border: 2px solid var(--line); background: var(--card); color: var(--text); cursor: pointer; min-height: 56px; }
31
- .tab[aria-selected="true"] { border-color: var(--red); background: var(--soft); color: var(--red-dark); }
32
- .tab small { display: block; font-weight: 400; font-size: 13px; color: var(--muted); }
33
- .panel { display: none; } .panel.active { display: block; }
34
- .download { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; background: var(--card); border: 2px solid var(--red); border-radius: 18px; padding: 18px; margin: 16px 0 6px; }
35
- .download .big { display: inline-block; background: var(--red); color: #fff; text-decoration: none; font-weight: 800; font-size: 19px; padding: 16px 26px; border-radius: 14px; }
36
- .download .big:hover { background: var(--red-dark); }
37
- .download .alt { font-size: 15px; }
38
- .download .meta { flex: 1 1 260px; color: var(--muted); font-size: 14px; }
39
- .beta { display: inline-block; background: #fff3cd; color: #7a5a00; border-radius: 99px; padding: 1px 10px; font-size: 13px; font-weight: 700; margin-left: 6px; }
40
- ol.steps { list-style: none; padding: 0; margin: 18px 0; counter-reset: s; }
41
- ol.steps > li { counter-increment: s; position: relative; background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 16px 16px 16px 64px; margin: 12px 0; }
42
- ol.steps > li::before { content: counter(s); position: absolute; left: 16px; top: 16px; width: 34px; height: 34px; border-radius: 50%; background: var(--red); color: #fff; font-weight: 800; display: grid; place-items: center; }
43
- ol.steps p { margin: 4px 0; }
44
- .path { display: inline-block; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 1px 8px; font-size: 15px; }
45
- .key { display: inline-block; border: 1px solid var(--line); border-bottom-width: 3px; border-radius: 6px; padding: 0 7px; font-size: 14px; background: var(--card); }
46
- code { font: 14px ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; word-break: break-all; }
47
- /* Pictures of the messages people will see (drawn, not screenshots) */
48
- .shot { margin: 12px 0 4px; max-width: 420px; border-radius: 12px; border: 1px solid var(--line); box-shadow: 0 6px 20px rgba(0,0,0,.08); overflow: hidden; font-size: 14px; background: #fff; color: #1d1d1f; }
49
- .shot .bar { background: #ececec; padding: 6px 10px; font-size: 12px; color: #555; display: flex; gap: 6px; align-items: center; }
50
- .shot .bar i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; background: #ff5f57; } .shot .bar i:nth-child(2) { background: #febc2e; } .shot .bar i:nth-child(3) { background: #28c840; }
51
- .shot .body { padding: 14px 16px; } .shot .title { font-weight: 700; margin-bottom: 6px; }
52
- .shot .btns { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; flex-wrap: wrap; }
53
- .shot .btn { border: 1px solid #c8c8cc; border-radius: 7px; padding: 4px 14px; background: #fff; font-size: 13px; }
54
- .shot .btn.hit { outline: 3px solid var(--red); outline-offset: 2px; font-weight: 700; }
55
- .shot.win { font-family: "Segoe UI", Arial, sans-serif; } .shot.win .body.blue { background: #0b5cad; color: #fff; }
56
- .shot.win .body.blue a { color: #fff; text-decoration: underline; }
57
- .shot.win .btn { border-radius: 3px; }
58
- .shot .row { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid #e5e5e5; }
59
- .shot.linux .bar { background: #2c2c2c; color: #ddd; }
60
- .tip { background: var(--soft); border-left: 4px solid var(--red); border-radius: 0 12px 12px 0; padding: 10px 14px; margin: 10px 0; font-size: 15px; }
61
- details { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 12px 16px; margin: 10px 0; }
62
- summary { font-weight: 700; cursor: pointer; } details p { margin: 8px 0 2px; }
63
- .hash { font: 12px ui-monospace, Menlo, monospace; color: var(--muted); word-break: break-all; }
64
- .foot { text-align: center; font-size: 13px; color: var(--muted); padding: 22px 16px max(22px, env(safe-area-inset-bottom)); }
65
- .top nav a { white-space: nowrap; }
66
- @media (max-width: 560px) { .brand span { font-size: 14px; } .brand b { display: block; } body { font-size: 16px; } ol.steps > li { padding-left: 56px; } .tab { font-size: 15px; padding: 10px 6px; } .top nav a { margin-left: 10px; } }
67
- </style>
68
- </head>
69
- <body>
70
- <header class="top">
71
- <div class="brand"><img src="app/build/icon.png" alt=""><span><b>Arx</b> by Devseis</span></div>
72
- <nav><a href="index.html">Arx</a><a href="phone.html">Phone check</a><a href="about.html">About</a></nav>
73
- </header>
74
-
75
- <main>
76
- <h1>Check your computer with Arx in 15 minutes</h1>
77
- <p class="lead">The desktop app (named <b>Devseis Endpoint Auditor</b> on your computer) runs a free, safe check of your computer's security settings against <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>. An AI auditor writes a clear report with what to fix — <b>on your computer</b>, without sending anything anywhere.</p>
78
-
79
- <div class="facts">
80
- <div class="fact"><b>✅ Only looks, never changes</b><span>It reads settings. Nothing on your computer is changed.</span></div>
81
- <div class="fact"><b>🔒 Private</b><span>Your results stay on your computer. Nothing is uploaded.</span></div>
82
- <div class="fact"><b>⏱ About 15 minutes</b><span>Install in 2 minutes; the first check takes 5–15 minutes.</span></div>
83
- <div class="fact"><b>📄 A PDF report</b><span>A score, the main gaps and simple steps to fix them.</span></div>
84
- </div>
85
-
86
- <div class="tabs" role="tablist">
87
- <button class="tab" role="tab" id="tab-mac" data-os="mac" aria-controls="panel-mac">🍎 Mac<small>macOS 13 or newer</small></button>
88
- <button class="tab" role="tab" id="tab-windows" data-os="windows" aria-controls="panel-windows">🪟 Windows<small>Windows 10 or 11</small></button>
89
- <button class="tab" role="tab" id="tab-linux" data-os="linux" aria-controls="panel-linux">🐧 Linux<small>Ubuntu, Debian and more</small></button>
90
- </div>
91
-
92
- <!-- ===================== MAC ===================== -->
93
- <section class="panel" id="panel-mac" role="tabpanel" aria-labelledby="tab-mac">
94
- <div class="download">
95
- <a class="big" id="dl-dmg" href="#">⬇ Download for Mac</a>
96
- <div class="meta"><span id="size-mac">Works on every Mac from 2017 on (Apple chip or Intel).</span><br><a class="alt" id="dl-zip" href="#">Prefer a .zip file?</a></div>
97
- </div>
98
- <ol class="steps">
99
- <li><h3>Download</h3><p>Click <b>Download for Mac</b> above. The file goes to your <b>Downloads</b> folder.</p></li>
100
- <li><h3>Install</h3><p>Open the downloaded file (it ends in <b>.dmg</b>). A small window opens: <b>drag the red app icon onto the Applications folder</b>. Then you can close that window.</p></li>
101
- <li><h3>Open it — the first time, macOS asks you to confirm</h3>
102
- <p>Open <b>Devseis Endpoint Auditor</b> from your Applications folder (or Launchpad). Because this free app is not yet registered with Apple, you will see this message. Click <b>Done</b>:</p>
103
- <div class="shot"><div class="bar"><i></i><i></i><i></i></div><div class="body"><div class="title">"Devseis Endpoint Auditor" Not Opened</div>Apple could not verify "Devseis Endpoint Auditor" is free of malware that may harm your Mac or compromise your privacy.<div class="btns"><span class="btn hit">Done</span><span class="btn">Move to Bin</span></div></div></div>
104
- </li>
105
- <li><h3>Allow it once</h3>
106
- <p>Click the <b> Apple menu</b> › <b>System Settings</b> › <b>Privacy &amp; Security</b>. Scroll down and click <b>Open Anyway</b>, then type your Mac password:</p>
107
- <div class="shot"><div class="bar"><i></i><i></i><i></i> Privacy &amp; Security</div><div class="body"><div class="row"><span>"Devseis Endpoint Auditor" was blocked to protect your Mac.</span><span class="btn hit">Open Anyway</span></div></div></div>
108
- <p class="muted">You only do this once. From now on the app opens normally.</p>
109
- </li>
110
- <li><h3>Run the check</h3>
111
- <p>The first time, the app downloads its AI auditor (about 1 GB — a few minutes on Wi-Fi). Then click <b>Run audit</b>. Your Mac asks for your password: this only lets the app <i>read</i> the security settings.</p>
112
- <p>After 5–15 minutes your report is ready. Click <b>Save report as PDF</b>. If you are away, it saves itself to <span class="path">Documents › Devseis Endpoint Audit</span>.</p>
113
- </li>
114
- </ol>
115
- <div class="tip"><b>Comfortable with Terminal?</b> Instead of steps 3–4 you can run <code>xattr -dr com.apple.quarantine "/Applications/Devseis Endpoint Auditor.app"</code>.</div>
116
- <p class="hash" id="hash-mac"></p>
117
- </section>
118
-
119
- <!-- ===================== WINDOWS ===================== -->
120
- <section class="panel" id="panel-windows" role="tabpanel" aria-labelledby="tab-windows">
121
- <div class="download">
122
- <a class="big" id="dl-exe" href="#">⬇ Download for Windows</a>
123
- <div class="meta"><span id="size-win">Windows 10 or 11, 64-bit.</span> <span class="beta">BETA</span><br>Built and packaged, not yet tested on many Windows PCs. Tell us if anything does not work.</div>
124
- </div>
125
- <ol class="steps">
126
- <li><h3>Download</h3><p>Click <b>Download for Windows</b> above. If your browser asks, choose <b>Keep</b>. The file goes to your <b>Downloads</b> folder.</p></li>
127
- <li><h3>Start the installer — Windows asks you to confirm</h3>
128
- <p>Open the downloaded file (it ends in <b>.exe</b>). Because this free app is not yet signed, Windows shows a blue box. Click <b>More info</b>, then <b>Run anyway</b>:</p>
129
- <div class="shot win"><div class="body blue"><div class="title" style="font-size:18px">Windows protected your PC</div>Microsoft Defender SmartScreen prevented an unrecognised app from starting.<br><a style="outline:3px solid var(--red);outline-offset:2px">① More info</a><div class="btns"><span class="btn hit" style="background:#fff;color:#000">② Run anyway</span><span class="btn" style="background:transparent;color:#fff;border-color:#fff">Don't run</span></div></div></div>
130
- </li>
131
- <li><h3>Install</h3><p>Click <b>Next</b>, keep the suggested folder, click <b>Install</b> and then <b>Finish</b>. A <b>Devseis Endpoint Auditor</b> shortcut appears on your desktop.</p></li>
132
- <li><h3>Run the check</h3>
133
- <p>Open the app. The first time, it downloads its AI auditor (about 1 GB). Click <b>Run audit</b>. Windows asks <i>"Do you want to allow this app to make changes to your device?"</i> — click <b>Yes</b>. The app only <i>reads</i> settings; it changes nothing.</p>
134
- <div class="shot win"><div class="body"><div class="title">User Account Control</div>Do you want to allow this app to make changes to your device?<div class="btns"><span class="btn hit">Yes</span><span class="btn">No</span></div></div></div>
135
- </li>
136
- <li><h3>Save your report</h3><p>After 5–15 minutes, click <b>Save report as PDF</b>. If you are away, it saves itself to <span class="path">Documents › Devseis Endpoint Audit</span>.</p></li>
137
- </ol>
138
- <p class="hash" id="hash-win"></p>
139
- </section>
140
-
141
- <!-- ===================== LINUX ===================== -->
142
- <section class="panel" id="panel-linux" role="tabpanel" aria-labelledby="tab-linux">
143
- <div class="download">
144
- <a class="big" id="dl-deb" href="#">⬇ Download for Ubuntu / Debian</a>
145
- <div class="meta"><span id="size-linux">For Ubuntu, Debian, Linux Mint, Pop!_OS (64-bit).</span><br><a class="alt" id="dl-appimage" href="#">Other Linux (Fedora, openSUSE, Arch…): download the AppImage</a></div>
146
- </div>
147
- <h3 style="margin-top:18px">Ubuntu, Debian, Linux Mint, Pop!_OS</h3>
148
- <ol class="steps">
149
- <li><h3>Download</h3><p>Click <b>Download for Ubuntu / Debian</b> above. The file (ending in <b>.deb</b>) goes to your <b>Downloads</b> folder.</p></li>
150
- <li><h3>Install</h3><p>Double-click the file. The <b>App Center</b> (or Software) opens: click <b>Install</b> and type your password.</p>
151
- <p class="muted">Or in a terminal: <code>sudo apt install ~/Downloads/Devseis-Endpoint-Auditor-*.deb</code></p></li>
152
- <li><h3>Run the check</h3><p>Open <b>Devseis Endpoint Auditor</b> from your applications menu. The first time, it downloads its AI auditor (about 1 GB). Click <b>Run audit</b> and type your password when asked — the app only <i>reads</i> settings.</p>
153
- <div class="shot linux"><div class="bar">Authentication Required</div><div class="body">Authentication is needed to run "/bin/bash" as the super user<div class="btns"><span class="btn">Cancel</span><span class="btn hit">Authenticate</span></div></div></div>
154
- </li>
155
- <li><h3>Save your report</h3><p>After 5–15 minutes, click <b>Save report as PDF</b> (or find it in <span class="path">Documents › Devseis Endpoint Audit</span>).</p></li>
156
- </ol>
157
- <h3>Other Linux (AppImage)</h3>
158
- <ol class="steps">
159
- <li><h3>Download the AppImage</h3><p>Use the <b>AppImage</b> link above.</p></li>
160
- <li><h3>Make it runnable</h3><p>Right-click the file › <b>Properties</b> › <b>Permissions</b> › tick <b>Allow executing file as program</b>. (Or: <code>chmod +x ~/Downloads/Devseis-Endpoint-Auditor-*.AppImage</code>.)</p></li>
161
- <li><h3>Double-click it and run the check</h3><p>Same as above: click <b>Run audit</b> and save the PDF.</p></li>
162
- </ol>
163
- <p class="hash" id="hash-linux"></p>
164
- </section>
165
-
166
- <h2>Questions</h2>
167
- <details><summary>Is it safe? Why does my computer warn me?</summary><p>The app is open source: anyone can read its code on Hugging Face. Mac and Windows warn about every app that has not paid for a developer certificate yet; the warning is not about anything the app does. You can check that your download is the real file with the SHA-256 code shown under each download.</p></details>
168
- <details><summary>Does it change anything on my computer?</summary><p>No. It only reads settings (like password rules, firewall, disk encryption). It never changes them. The password prompt is needed because some settings can only be read by an administrator.</p></details>
169
- <details><summary>Where do my results go?</summary><p>Nowhere — they stay on your computer. The app downloads two things (the AI auditor and a list of known security problems) but sends nothing about your computer.</p></details>
170
- <details><summary>What is in the report?</summary><p>A score, the most important gaps, and for each problem: what was found, why it matters, how risky it is, and simple steps to fix it — mapped to ISO 27001, GDPR and the EU AI Act.</p></details>
171
- <details><summary>Do I need internet?</summary><p>Only the first time (to download the AI auditor). After that, checks work offline.</p></details>
172
- <details><summary>I'm setting this up for a company</summary><p>Before running, edit <code>baseline.conf</code> (approved admins, approved AI tools, rules) and <code>organisational.conf</code> (8 company questions). They are in <span class="path">~/Library/Application Support/Devseis Endpoint Auditor</span> on Mac, <span class="path">%APPDATA%\Devseis Endpoint Auditor</span> on Windows and <span class="path">~/.config/Devseis Endpoint Auditor</span> on Linux.</p></details>
173
- <details><summary>What about my phone?</summary><p>Open <a href="phone.html">the phone check</a> on your iPhone or Android phone — the phone check runs in the browser, nothing to install.</p></details>
174
- <p class="muted">Not legal advice or a certification. Open source by Devseis: code Apache-2.0, data CC BY 4.0. <a href="https://huggingface.co/spaces/Devseis/Arx/tree/main">Source</a> · <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-releases">All releases</a></p>
175
- </main>
176
- <footer class="foot">Devseis · <a href="index.html">Arx</a> · <a href="phone.html">Phone check</a> · <a href="about.html">About</a></footer>
177
-
178
- <script>
179
- // Pick the visitor's system, and fill in links, sizes and checksums from the releases repository.
180
- const RELEASES = 'https://huggingface.co/datasets/Devseis/endpoint-auditor-releases/resolve/main/';
181
- const ua = navigator.userAgent;
182
- const detected = /Windows/.test(ua) ? 'windows' : (/Linux|X11|CrOS/.test(ua) && !/Android/.test(ua)) ? 'linux' : 'mac';
183
- function select(os) {
184
- document.querySelectorAll('.tab').forEach(t => t.setAttribute('aria-selected', String(t.dataset.os === os)));
185
- document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === `panel-${os}`));
186
- }
187
- document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => { select(t.dataset.os); history.replaceState(null, '', `#${t.dataset.os}`); }));
188
- const fromHash = () => (['mac', 'windows', 'linux'].includes(location.hash.slice(1)) ? location.hash.slice(1) : detected);
189
- select(fromHash());
190
- window.addEventListener('hashchange', () => select(fromHash()));
191
-
192
- const setLink = (id, file) => { const a = document.getElementById(id); a.href = RELEASES + file.file; };
193
- const hash = (id, label, file) => { document.getElementById(id).textContent = `${label}: ${file.file} · SHA-256 ${file.sha256}`; };
194
- fetch(RELEASES + 'latest.json', { cache: 'no-cache' }).then(r => r.json()).then(rel => {
195
- const v = `Version ${rel.version}`;
196
- setLink('dl-dmg', rel.mac.dmg); setLink('dl-zip', rel.mac.zip);
197
- document.getElementById('size-mac').textContent = `${v} · ${rel.mac.dmg.size_mb} MB · works on every Mac with macOS ${rel.mac.min_macos} or newer (Apple chip or Intel).`;
198
- hash('hash-mac', 'Check your download', rel.mac.dmg);
199
- if (rel.windows) {
200
- setLink('dl-exe', rel.windows.exe);
201
- document.getElementById('size-win').textContent = `${v} · ${rel.windows.exe.size_mb} MB · Windows 10 or 11, 64-bit.`;
202
- hash('hash-win', 'Check your download', rel.windows.exe);
203
- }
204
- if (rel.linux) {
205
- setLink('dl-deb', rel.linux.deb); setLink('dl-appimage', rel.linux.appimage);
206
- document.getElementById('size-linux').textContent = `${v} · ${rel.linux.deb.size_mb} MB · for Ubuntu, Debian, Linux Mint, Pop!_OS (64-bit).`;
207
- hash('hash-linux', 'Check your download', rel.linux.deb);
208
- }
209
- }).catch(() => {
210
- document.querySelectorAll('.download .meta').forEach(m => { m.insertAdjacentHTML('beforeend', '<br>Downloads could not be loaded right now: see <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-releases">all releases</a>.'); });
211
- });
212
- </script>
213
- </body>
214
- </html>
 
1
  <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
3
+ <title>Arx</title><meta http-equiv="refresh" content="0; url=index.html#download"><link rel="canonical" href="index.html">
4
+ <style>body{font-family:ui-monospace,Menlo,monospace;background:#f7f7f7;color:#0a0a0a;margin:0;padding:40px 16px;text-align:center}@media (prefers-color-scheme:dark){body{background:#0a0a0a;color:#fafafa}}a{color:inherit}</style></head>
5
+ <body><p>Arx has moved to <a href="index.html#download">the Arx home page</a>.</p></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html CHANGED
@@ -2,99 +2,171 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
6
- <meta name="description" content="Arx by Devseis, your security assistant: ask about securing your computer or phone, ISO 27001, GDPR and the EU AI Act, and run a free audit. The AI runs on your device; nothing is uploaded.">
7
- <meta name="theme-color" content="#ED2939">
8
- <title>Arx · Devseis security assistant</title>
9
  <link rel="icon" href="app/build/icon.png">
10
  <link rel="apple-touch-icon" href="app/build/icon.png">
11
- <link rel="stylesheet" href="chat/chat.css">
 
 
 
 
12
  </head>
13
  <body>
14
- <header>
15
- <div class="header-left">
16
- <button type="button" id="sidebarToggle" class="icon-btn" aria-label="Conversations" title="Conversations">
17
- <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
18
- </button>
19
- <a class="brand" href="index.html"><img src="app/build/icon.png" alt=""><span><b>Arx</b> by Devseis</span></a>
20
- </div>
21
- <div class="header-actions">
22
- <button type="button" id="aiChip" class="ai-chip" data-state="off">⚡ Enable AI</button>
23
- <button type="button" id="newChat" class="icon-btn" aria-label="New chat" title="New chat">
24
- <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
25
- </button>
26
- <button type="button" id="themeToggle" class="icon-btn" aria-label="Light or dark" title="Light or dark">◐</button>
27
- <button type="button" id="auditBtn" class="audit-btn">🛡 Run audit</button>
28
  </div>
29
  </header>
30
- <div class="status-bar hidden" id="statusBar"><div class="status-text" id="statusText"></div><div class="status-progress"><div id="statusFill"></div></div></div>
31
 
32
- <main id="chatMain">
33
- <div class="empty-state" id="emptyState">
34
- <div class="hero-icon"><img src="app/build/icon.png" alt=""></div>
35
- <h1>Hi, I'm Arx, your Devseis security assistant.</h1>
36
- <p class="hero-sub">Ask me how to secure your computer or phone, or about ISO 27001, GDPR and the EU AI Act. I can also <b>audit this device</b> for free. I run on your device: nothing you type is uploaded.</p>
37
- <div class="ai-card hidden" id="aiCard">
38
- <div><b>⚡ Turn on the AI assistant</b><br><span id="aiCardText">One-time download of about 1 GB; it then runs on this device, even offline. The chat opens when it is ready. Choose Not now to get answers from the Devseis knowledge base instead.</span></div>
39
- <div class="btn-row"><button type="button" class="primary-btn" id="aiCardYes">Download now</button><button type="button" class="small-btn" id="aiCardNo">Not now</button></div>
 
 
 
 
 
 
 
40
  </div>
41
- <div class="chips">
42
- <button type="button" class="chip" data-prompt="How do I turn on disk encryption on my computer?">🔒 Turn on disk encryption</button>
43
- <button type="button" class="chip" data-prompt="Is ChatGPT OK to use at work under GDPR?">🤖 Is ChatGPT OK at work?</button>
44
- <button type="button" class="chip" data-prompt="What does ISO 27001 A.8.7 require?">📘 Explain ISO 27001 A.8.7</button>
45
- <button type="button" class="chip" data-action="audit">🛡 Audit this device</button>
46
  </div>
47
- </div>
48
- <div id="thread" aria-live="polite"></div>
49
- </main>
50
 
51
- <div class="context-wrap" id="contextWrap">
52
- <div class="context-bar">
53
- <button type="button" id="deviceBtn" class="device-pill" aria-haspopup="dialog" aria-expanded="false" aria-controls="devicePanel">
54
- <span class="device-ic" id="deviceIcon">💻</span><span class="device-text" id="deviceText">Detecting this device…</span><span class="chev" aria-hidden="true">▾</span>
55
- </button>
56
- <span class="context-note" id="contextNote">Answers are tailored to this device</span>
57
- </div>
58
- <div class="sheet-backdrop hidden" id="deviceBackdrop"></div>
59
- <div class="device-panel hidden" id="devicePanel" role="dialog" aria-modal="false" aria-labelledby="deviceTitle">
60
- <div class="device-head"><h2 id="deviceTitle">This device</h2><button type="button" class="icon-btn" id="deviceClose" aria-label="Close">✕</button></div>
61
- <div id="deviceBody"></div>
62
- </div>
63
- </div>
64
- <form id="composer" autocomplete="off">
65
- <button type="button" id="attachBtn" class="icon-btn attach" title="Add an audit report (.json from the Devseis app)" aria-label="Add an audit report">
66
- <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a5.5 5.5 0 01-7.78-7.78l9.2-9.19a3.5 3.5 0 014.95 4.95l-9.2 9.19a1.5 1.5 0 01-2.12-2.12l8.49-8.48"/></svg>
67
- </button>
68
- <input type="file" id="fileInput" accept=".json,application/json" hidden>
69
- <div class="input-pill" id="inputPill">
70
- <textarea id="input" rows="1" placeholder="Ask Arx about security, or add your audit report…" aria-label="Message"></textarea>
71
- </div>
72
- <button type="submit" id="sendBtn" class="send-btn" aria-label="Send" disabled>
73
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3.4 20.4l17.45-7.48a1 1 0 000-1.84L3.4 3.6a.99.99 0 00-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z"/></svg>
74
- </button>
75
- </form>
76
- <p class="disclaimer">Runs on your device · Answers come from the Devseis knowledge base and can be wrong · Not legal advice</p>
77
 
78
- <div class="overlay hidden" id="overlay"></div>
79
- <aside class="sidebar" id="sidebar" aria-label="Conversations">
80
- <div class="sidebar-head"><h2>Conversations</h2><button type="button" class="icon-btn" id="sidebarClose" aria-label="Close">✕</button></div>
81
- <div class="sidebar-list" id="sidebarList"></div>
82
- <div class="sidebar-foot">
83
- <div class="row"><span>AI model</span><span class="muted" id="modelName">loads on first question</span></div>
84
- <div class="row"><a href="phone.html">📱 Phone check</a><a href="download.html">💻 Desktop app</a></div>
85
- <div class="row"><a href="about.html">About</a><button type="button" class="small-btn" id="clearAll">Clear history</button></div>
86
- <p class="muted tiny">Conversations are stored only in this browser. Open source by Devseis (Apache-2.0).</p>
87
- </div>
88
- </aside>
89
 
90
- <div class="modal hidden" id="auditModal" role="dialog" aria-modal="true" aria-labelledby="auditTitle">
91
- <div class="modal-card">
92
- <h2 id="auditTitle">Audit this device</h2>
93
- <div id="auditOptions"></div>
94
- <button type="button" class="link-btn" id="auditClose">Not now</button>
95
- </div>
96
- </div>
97
 
98
- <script type="module" src="chat/chat.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </body>
100
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <meta name="description" content="Arx by Devseis: a free, read-only security audit for your Mac, Windows or Linux computer. 68 checks in about a minute, offline, mapped to ISO 27001, GDPR and the EU AI Act. Nothing is uploaded.">
7
+ <meta name="theme-color" content="#f7f7f7">
8
+ <title>Arx · Security audit for your computer</title>
9
  <link rel="icon" href="app/build/icon.png">
10
  <link rel="apple-touch-icon" href="app/build/icon.png">
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500;600;700;800&display=swap">
14
+ <link rel="stylesheet" href="site/site.css">
15
+ <script>try { const t = localStorage.getItem('devseis.theme'); if (t) document.documentElement.dataset.theme = t; } catch { /* ignore */ }</script>
16
  </head>
17
  <body>
18
+ <header class="nav">
19
+ <div class="wrap nav-inner">
20
+ <a class="brand" href="index.html"><img src="app/build/icon.png" alt=""><b>Arx</b><span class="muted hide-xs">by Devseis</span></a>
21
+ <nav class="nav-links">
22
+ <a href="#checks">Checks</a><a href="#how">How it works</a><a href="#about">About</a>
23
+ </nav>
24
+ <div class="nav-actions">
25
+ <button type="button" class="icon-btn" id="themeToggle" aria-label="Light or dark" title="Light or dark">◐</button>
26
+ <a class="btn btn-primary btn-sm" href="#download">Download</a>
27
+ </div>
 
 
 
 
28
  </div>
29
  </header>
 
30
 
31
+ <!-- ===== Landing ===== -->
32
+ <main id="landing">
33
+ <section class="wrap hero">
34
+ <p class="eyebrow"><img src="app/build/icon.png" alt="">Arx security audit</p>
35
+ <h1>Is your computer secure?<br><span class="soft">Find out in a minute.</span></h1>
36
+ <p class="lead">A free, read-only audit of your Mac, Windows or Linux computer: <b>68 checks</b> mapped to <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>. It runs offline, and a small AI on your computer writes the report. <u>Nothing is uploaded.</u></p>
37
+
38
+ <div class="device-card" id="deviceCard">
39
+ <span class="device-icon" id="deviceIcon">💻</span>
40
+ <div class="device-text"><span class="tiny muted" id="deviceLabel">Detected</span><b id="deviceName">Your computer</b></div>
41
+ <div class="seg" id="osSeg" aria-label="Choose your system">
42
+ <button type="button" data-os="mac">macOS</button>
43
+ <button type="button" data-os="windows">Windows</button>
44
+ <button type="button" data-os="linux">Linux</button>
45
+ </div>
46
  </div>
47
+
48
+ <div class="phone-note hidden" id="phoneNote">
49
+ <b>📱 Arx checks computers, not phones.</b>
50
+ <span class="muted">Open this page on your Mac, Windows or Linux computer to download it.</span>
51
+ <button type="button" class="btn btn-sm" id="copyLink">Copy link</button>
52
  </div>
 
 
 
53
 
54
+ <div class="cta">
55
+ <a class="btn btn-primary btn-lg" id="heroDownload" href="#download">Download for macOS</a>
56
+ <label class="btn btn-lg" for="reportFile" title="Open the …-evidence.json file Arx saves next to each report">Open a report</label>
57
+ <input type="file" id="reportFile" accept=".json,application/json" hidden>
58
+ </div>
59
+ <p class="tiny muted" id="heroMeta">Free · open source · version 0.4.2</p>
60
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ <section class="wrap">
63
+ <div class="stats">
64
+ <div class="stat"><span class="tiny muted">Checks</span><b>68</b><span class="tiny muted">settings &amp; controls</span></div>
65
+ <div class="stat"><span class="tiny muted">Time</span><b>~1 min</b><span class="tiny muted">for the whole audit</span></div>
66
+ <div class="stat"><span class="tiny muted">Uploaded</span><b>0 bytes</b><span class="tiny muted">runs offline</span></div>
67
+ <div class="stat"><span class="tiny muted">Frameworks</span><b>3</b><span class="tiny muted">ISO · GDPR · AI Act</span></div>
68
+ </div>
69
+ </section>
 
 
 
70
 
71
+ <section class="wrap section" id="checks">
72
+ <div class="section-head"><h2>High-risk checks</h2><a class="tiny muted link" href="#all-checks">See all 68 →</a></div>
73
+ <div class="rail" id="riskRail"></div>
74
+ </section>
 
 
 
75
 
76
+ <section class="wrap section" id="how">
77
+ <div class="section-head"><h2>How it works</h2></div>
78
+ <ol class="steps">
79
+ <li><span class="step-n">1</span><div><b>Download and open Arx</b><p class="muted">One app for your system. Admin rights let it read every setting; without them, a few checks are skipped.</p></div></li>
80
+ <li><span class="step-n">2</span><div><b>Run the audit</b><p class="muted">Arx only reads settings: it never changes anything. The first run downloads the AI once; after that it works offline.</p></div></li>
81
+ <li><span class="step-n">3</span><div><b>Get your report</b><p class="muted">A score, your biggest gaps and simple steps to fix each one, mapped to ISO 27001, GDPR and the EU AI Act. Save it as a PDF.</p></div></li>
82
+ </ol>
83
+ </section>
84
+
85
+ <section class="wrap section" id="all-checks">
86
+ <div class="section-head"><h2>Everything Arx checks</h2><span class="tiny muted" id="checkCount"></span></div>
87
+ <div class="filters" id="filters"></div>
88
+ <div class="table-card">
89
+ <table class="table" id="checkTable">
90
+ <thead><tr><th class="num">#</th><th>Check</th><th>Risk</th><th class="hide-sm">ISO 27001</th><th class="hide-sm">GDPR</th><th class="hide-xs">Systems</th></tr></thead>
91
+ <tbody></tbody>
92
+ </table>
93
+ <button type="button" class="table-more" id="moreChecks">Show more</button>
94
+ </div>
95
+ </section>
96
+
97
+ <section class="wrap section" id="download">
98
+ <div class="section-head"><h2>Download Arx</h2><span class="tiny muted" id="dlVersion">Version 0.4.2</span></div>
99
+ <div class="dl-grid">
100
+ <article class="dl-card" data-os="mac">
101
+ <span class="ribbon ribbon-you hidden">YOUR SYSTEM</span>
102
+ <div class="dl-top"><span class="dl-icon"></span><div><b>macOS</b><span class="tiny muted">macOS 13+ · Apple chip or Intel</span></div></div>
103
+ <div class="kv"><span class="muted">Size</span><b data-size="mac">226 MB</b></div>
104
+ <a class="btn btn-primary" data-link="mac-dmg">Download .dmg</a>
105
+ <a class="tiny muted link" data-link="mac-zip">or .zip</a>
106
+ <details class="hash"><summary>SHA-256</summary><code data-hash="mac">loading…</code></details>
107
+ </article>
108
+ <article class="dl-card" data-os="windows">
109
+ <span class="ribbon ribbon-you hidden">YOUR SYSTEM</span><span class="ribbon ribbon-beta">BETA</span>
110
+ <div class="dl-top"><span class="dl-icon"></span><div><b>Windows</b><span class="tiny muted">Windows 10 or 11 · 64-bit</span></div></div>
111
+ <div class="kv"><span class="muted">Size</span><b data-size="windows">113 MB</b></div>
112
+ <a class="btn btn-primary" data-link="win-exe">Download .exe</a>
113
+ <span class="tiny muted">Not yet tested on real Windows PCs.</span>
114
+ <details class="hash"><summary>SHA-256</summary><code data-hash="windows">loading…</code></details>
115
+ </article>
116
+ <article class="dl-card" data-os="linux">
117
+ <span class="ribbon ribbon-you hidden">YOUR SYSTEM</span>
118
+ <div class="dl-top"><span class="dl-icon"></span><div><b>Linux</b><span class="tiny muted">Ubuntu, Debian, Mint, Pop!_OS · x86-64</span></div></div>
119
+ <div class="kv"><span class="muted">Size</span><b data-size="linux">103 MB</b></div>
120
+ <a class="btn btn-primary" data-link="linux-deb">Download .deb</a>
121
+ <a class="tiny muted link" data-link="linux-appimage">or AppImage (any distribution)</a>
122
+ <details class="hash"><summary>SHA-256</summary><code data-hash="linux">loading…</code></details>
123
+ </article>
124
+ </div>
125
+ <p class="tiny muted">If macOS says the app is from an unidentified developer: right-click it, choose Open, then Open again. All files: <a class="link" href="https://huggingface.co/datasets/Devseis/endpoint-auditor-releases">releases</a>.</p>
126
+ </section>
127
+
128
+ <section class="wrap section" id="about">
129
+ <div class="section-head"><h2>Why trust it</h2></div>
130
+ <div class="trust-grid">
131
+ <div class="card"><b>🔒 Read-only</b><p class="muted">Arx reads settings and never changes them. You decide what to fix.</p></div>
132
+ <div class="card"><b>📴 Offline and private</b><p class="muted">The audit and the AI run on your computer. The report stays there unless you share it.</p></div>
133
+ <div class="card"><b>🧾 Grounded report</b><p class="muted">The AI writes only from what the audit found and the Devseis library, and each answer is checked against both. If a check fails, Arx uses a fixed template instead.</p></div>
134
+ <div class="card"><b>📖 Open source</b><p class="muted">Code Apache-2.0, data CC BY 4.0. <a class="link" href="https://huggingface.co/spaces/Devseis/Arx/tree/main">Read the source</a>.</p></div>
135
+ </div>
136
+ <details class="faq"><summary>Is this a certification?</summary><p class="muted">No. ISO 27001 certification needs an accredited auditor, and much of GDPR and AI Act compliance is organisational. Arx collects evidence from the computer and flags the gaps.</p></details>
137
+ <details class="faq"><summary>I'm setting this up for a company</summary><p class="muted">Before running, edit <code>baseline.conf</code> (approved admins, approved AI tools, limits) and <code>organisational.conf</code> (8 company questions). They are in <code>~/Library/Application Support/Devseis Endpoint Auditor</code> on Mac, <code>%APPDATA%\Devseis Endpoint Auditor</code> on Windows and <code>~/.config/Devseis Endpoint Auditor</code> on Linux.</p></details>
138
+ <details class="faq"><summary>What does "Open a report" do?</summary><p class="muted">Arx saves an <code>…-evidence.json</code> file next to each PDF report. Open it here to see the results and fixes in your browser. The file is read on this page only; it is never uploaded.</p></details>
139
+ </section>
140
+ </main>
141
+
142
+ <!-- ===== Results: from the app's "Ask Arx about this report" (#audit), or an opened evidence file ===== -->
143
+ <main id="results" class="hidden">
144
+ <section class="wrap section">
145
+ <a class="tiny muted link" href="index.html">← Arx home</a>
146
+ <div class="result-head">
147
+ <div><p class="eyebrow" id="resDevice"></p><h1 id="resTitle">Your audit</h1><p class="tiny muted" id="resWhen"></p></div>
148
+ <div class="result-actions"><button type="button" class="btn btn-sm" id="printBtn">Save as PDF</button><label class="btn btn-sm" for="reportFile2">Open another</label><input type="file" id="reportFile2" accept=".json,application/json" hidden></div>
149
+ </div>
150
+ <div class="stats" id="resStats"></div>
151
+ <div class="card summary-card"><span class="tiny muted">Summary</span><p id="resSummary"></p><span class="tiny muted">Do next</span><ol id="resNext"></ol></div>
152
+ </section>
153
+ <section class="wrap section">
154
+ <div class="section-head"><h2>Gaps to fix</h2><span class="tiny muted" id="gapCount"></span></div>
155
+ <div id="gapList" class="findings"></div>
156
+ </section>
157
+ <section class="wrap section">
158
+ <details class="card other-results"><summary><b>Other results</b> <span class="tiny muted" id="otherCount"></span></summary><div id="otherList" class="findings"></div></details>
159
+ <p class="tiny muted">This page reads the report in your browser only; nothing is uploaded. Not legal advice or a certification.</p>
160
+ </section>
161
+ </main>
162
+
163
+ <footer class="foot">
164
+ <div class="wrap foot-inner">
165
+ <div><b>Arx</b> <span class="muted">by Devseis · a free security audit for your computer</span></div>
166
+ <div class="foot-links"><a href="#download">Download</a><a href="https://huggingface.co/spaces/Devseis/Arx/tree/main">Source</a><a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-releases">Releases</a><a href="https://huggingface.co/Devseis/endpoint-auditor-0.5b">AI model</a><a href="CITATION.cff">Cite</a></div>
167
+ </div>
168
+ </footer>
169
+ <div class="toast hidden" id="toast" role="status"></div>
170
+ <script type="module" src="site/site.js"></script>
171
  </body>
172
  </html>
phone.html CHANGED
@@ -1,92 +1,5 @@
1
  <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="description" content="Free, private security and compliance check for your phone (ISO 27001, GDPR, EU AI Act). Runs in your browser; nothing about your phone is uploaded.">
7
- <meta name="theme-color" content="#ED2939">
8
- <title>Arx phone check · Devseis</title>
9
- <link rel="stylesheet" href="mobile/styles.css">
10
- </head>
11
- <body>
12
- <header class="top">
13
- <div class="brand"><span class="mark">D</span><span><b>Arx</b> phone check</span></div>
14
- <div style="display:flex;gap:8px;align-items:center"><div id="model-chip" class="chip hidden"></div><a class="chip" href="index.html">← Arx</a></div>
15
- </header>
16
-
17
- <main>
18
- <!-- 1. Start -->
19
- <section id="s-start" class="screen">
20
- <h1 id="hero-title">Is this phone ready for work?</h1>
21
- <p class="lead" id="hero-lead">A free check against <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>, written by the Devseis AI auditor <b>inside your browser</b>.</p>
22
- <div id="device-card" class="card device"><span class="muted">Detecting this device…</span></div>
23
- <div id="desktop-note" class="card note hidden">
24
- <p><a class="dl-mac" id="dl-desktop" href="download.html">Download the desktop app for Mac</a></p>
25
- <p class="muted" id="desktop-other"></p>
26
- <p class="muted">The phone check runs on phones: open this page on your iPhone or Android. To try it here as a demo, choose a phone type:</p>
27
- <div class="row"><button class="secondary" data-demo="ios">iPhone</button><button class="secondary" data-demo="android">Android</button></div>
28
- </div>
29
- <ul class="facts" id="facts">
30
- <li><b>Private:</b> your answers and the report stay on this phone. Only the AI model and vulnerability data are downloaded.</li>
31
- <li><b>About 5 minutes:</b> the browser detects what it can; you answer the rest, with help on where to look.</li>
32
- <li><b>Honest:</b> every result says whether it was <i>detected</i> or <i>self-reported</i>.</li>
33
- </ul>
34
- <button id="start" class="primary" disabled>Start the check</button>
35
- <p class="muted center"><a href="about.html">About the project</a></p>
36
- </section>
37
-
38
- <!-- 2. Approval -->
39
- <section id="s-consent" class="screen hidden">
40
- <h1>Before we start</h1>
41
- <div class="card">
42
- <p><b>What will be checked</b> (<span id="check-count"></span> checks):</p>
43
- <ul id="plan" class="plan"></ul>
44
- </div>
45
- <label class="card toggle">
46
- <input type="checkbox" id="use-ai">
47
- <span><b>Use the AI auditor</b><br><span id="ai-note" class="muted"></span></span>
48
- </label>
49
- <p class="muted">Nothing on the phone is changed. You can stop at any time.</p>
50
- <div class="row"><button class="secondary" id="consent-back">Back</button><button class="primary" id="consent-ok">I agree, continue</button></div>
51
- </section>
52
-
53
- <!-- 3. Questions -->
54
- <section id="s-question" class="screen hidden">
55
- <div class="progress"><div id="q-bar" class="bar"></div></div>
56
- <p class="muted" id="q-count"></p>
57
- <h2 id="q-check" class="check-name"></h2>
58
- <h1 id="q-text"></h1>
59
- <div class="where"><span class="where-label">Where to look</span><span id="q-where"></span></div>
60
- <div id="q-answers" class="answers"></div>
61
- <div class="row"><button class="secondary" id="q-back">Back</button></div>
62
- </section>
63
-
64
- <!-- 4. Writing the report -->
65
- <section id="s-work" class="screen hidden">
66
- <h1 id="w-title">Writing your report</h1>
67
- <p id="w-step" class="muted"></p>
68
- <div class="progress big"><div id="w-bar" class="bar"></div></div>
69
- <p id="w-current" class="muted"></p>
70
- </section>
71
-
72
- <!-- 5. Result -->
73
- <section id="s-done" class="screen hidden">
74
- <div id="result"></div>
75
- <div class="actions">
76
- <button class="primary" id="share-pdf">Save or share PDF</button>
77
- <button class="secondary" id="view-report">View full report</button>
78
- <button class="secondary" id="save-json">Download evidence (JSON)</button>
79
- </div>
80
- <p id="pdf-note" class="muted"></p>
81
- <button class="link" id="again">Check again</button>
82
- </section>
83
- </main>
84
-
85
- <footer class="foot">
86
- Open source by Devseis · <a href="about.html">about</a> ·
87
- <a href="https://huggingface.co/spaces/Devseis/Arx/tree/main">code (Apache-2.0)</a> ·
88
- <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-synthetic">data (CC BY 4.0)</a> · Not legal advice or a certification.
89
- </footer>
90
- <script type="module" src="mobile/app.js"></script>
91
- </body>
92
- </html>
 
1
  <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
3
+ <title>Arx</title><meta http-equiv="refresh" content="0; url=index.html"><link rel="canonical" href="index.html">
4
+ <style>body{font-family:ui-monospace,Menlo,monospace;background:#f7f7f7;color:#0a0a0a;margin:0;padding:40px 16px;text-align:center}@media (prefers-color-scheme:dark){body{background:#0a0a0a;color:#fafafa}}a{color:inherit}</style></head>
5
+ <body><p>Arx has moved to <a href="index.html">the Arx home page</a>.</p></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
site/site.css ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Arx website: neutral greys, thin-bordered cards and a monospace face (inspired by trustmrr.com). */
2
+ :root {
3
+ --bg: #f7f7f7; --card: #ffffff; --fg: #0a0a0a; --muted: #737373; --soft: #a3a3a3; --border: #e5e5e5;
4
+ --primary: #171717; --primary-fg: #fafafa; --hover: #f2f2f2; --chip: #f2f2f2;
5
+ --high-bg: #fee2e2; --high-fg: #b91c1c; --med-bg: #fef3c7; --med-fg: #b45309; --low-bg: #e0f2fe; --low-fg: #0369a1;
6
+ --ok-bg: #dcfce7; --ok-fg: #15803d; --brand: #ed2939; --radius: 10px;
7
+ --font: 'Inconsolata', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8
+ color-scheme: light;
9
+ }
10
+ @media (prefers-color-scheme: dark) {
11
+ :root:not([data-theme="light"]) {
12
+ --bg: #0a0a0a; --card: #171717; --fg: #fafafa; --muted: #a3a3a3; --soft: #737373; --border: rgba(255,255,255,.1);
13
+ --primary: #e5e5e5; --primary-fg: #171717; --hover: #262626; --chip: #262626;
14
+ --high-bg: rgba(239,68,68,.15); --high-fg: #f87171; --med-bg: rgba(245,158,11,.15); --med-fg: #fbbf24; --low-bg: rgba(14,165,233,.15); --low-fg: #38bdf8;
15
+ --ok-bg: rgba(34,197,94,.15); --ok-fg: #4ade80; color-scheme: dark;
16
+ }
17
+ }
18
+ :root[data-theme="dark"] {
19
+ --bg: #0a0a0a; --card: #171717; --fg: #fafafa; --muted: #a3a3a3; --soft: #737373; --border: rgba(255,255,255,.1);
20
+ --primary: #e5e5e5; --primary-fg: #171717; --hover: #262626; --chip: #262626;
21
+ --high-bg: rgba(239,68,68,.15); --high-fg: #f87171; --med-bg: rgba(245,158,11,.15); --med-fg: #fbbf24; --low-bg: rgba(14,165,233,.15); --low-fg: #38bdf8;
22
+ --ok-bg: rgba(34,197,94,.15); --ok-fg: #4ade80; color-scheme: dark;
23
+ }
24
+
25
+ * { box-sizing: border-box; }
26
+ html { scroll-behavior: smooth; scroll-padding-top: 72px; }
27
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.55 var(--font); -webkit-font-smoothing: antialiased; }
28
+ a { color: inherit; text-decoration: none; }
29
+ code { font-family: var(--font); font-size: .92em; background: var(--chip); padding: 1px 5px; border-radius: 5px; word-break: break-all; }
30
+ h1, h2 { margin: 0; letter-spacing: -0.02em; }
31
+ p { margin: 0; }
32
+ .hidden { display: none !important; }
33
+ .wrap { max-width: 1024px; margin: 0 auto; padding: 0 16px; }
34
+ .muted { color: var(--muted); }
35
+ .soft { color: var(--soft); }
36
+ .tiny { font-size: 13px; }
37
+ .link:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 4px; }
38
+
39
+ /* nav */
40
+ .nav { position: sticky; top: 0; z-index: 20; background: color-mix(in srgb, var(--bg) 85%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border); }
41
+ .nav-inner { display: flex; align-items: center; gap: 16px; height: 56px; }
42
+ .brand { display: flex; align-items: center; gap: 8px; font-size: 17px; }
43
+ .brand img { width: 24px; height: 24px; border-radius: 6px; }
44
+ .nav-links { display: flex; gap: 20px; margin-left: auto; font-size: 14px; color: var(--muted); }
45
+ .nav-links a:hover { color: var(--fg); }
46
+ .nav-actions { display: flex; gap: 8px; align-items: center; }
47
+ .icon-btn { width: 34px; height: 34px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--fg); cursor: pointer; font: inherit; }
48
+ .icon-btn:hover { background: var(--hover); }
49
+
50
+ /* buttons */
51
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 40px; padding: 0 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--fg); font: 600 15px var(--font); cursor: pointer; white-space: nowrap; transition: background .15s, transform .05s; }
52
+ .btn:hover { background: var(--hover); }
53
+ .btn:active { transform: translateY(1px); }
54
+ .btn-primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
55
+ .btn-primary:hover { background: color-mix(in srgb, var(--primary) 88%, var(--bg)); }
56
+ .btn-sm { min-height: 34px; padding: 0 12px; font-size: 14px; }
57
+ .btn-lg { min-height: 48px; padding: 0 22px; font-size: 16px; }
58
+
59
+ /* hero */
60
+ .hero { text-align: center; padding: 56px 16px 32px; }
61
+ .eyebrow { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; color: var(--muted); margin-bottom: 18px; }
62
+ .eyebrow img { width: 22px; height: 22px; border-radius: 5px; }
63
+ .hero h1 { font-size: clamp(32px, 6vw, 54px); line-height: 1.08; font-weight: 800; margin-bottom: 18px; }
64
+ .lead { max-width: 640px; margin: 0 auto 28px; color: var(--muted); font-size: 16px; }
65
+ .lead b { color: var(--fg); font-weight: 600; }
66
+ .lead u { text-underline-offset: 4px; color: var(--fg); }
67
+ .device-card { display: flex; align-items: center; gap: 12px; max-width: 560px; margin: 0 auto 18px; padding: 10px 10px 10px 14px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); text-align: left; }
68
+ .device-icon { font-size: 22px; }
69
+ .device-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
70
+ .device-text b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
71
+ .seg { display: inline-flex; background: var(--chip); border-radius: 8px; padding: 3px; gap: 2px; flex-shrink: 0; }
72
+ .seg button { border: 0; background: transparent; color: var(--muted); font: 600 13px var(--font); padding: 6px 10px; border-radius: 6px; cursor: pointer; }
73
+ .seg button[aria-pressed="true"] { background: var(--card); color: var(--fg); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
74
+ .phone-note { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 12px; max-width: 560px; margin: 0 auto 18px; padding: 12px 14px; background: var(--med-bg); color: var(--fg); border-radius: var(--radius); font-size: 14px; }
75
+ .cta { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px; }
76
+
77
+ /* stats (metric grid) */
78
+ .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
79
+ .stat { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; display: flex; flex-direction: column; gap: 2px; }
80
+ .stat b { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; }
81
+
82
+ /* sections */
83
+ .section { padding-top: 56px; }
84
+ .section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; padding: 0 2px; }
85
+ .section-head h2 { font-size: 20px; font-weight: 700; }
86
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
87
+ .card p { margin-top: 6px; font-size: 14px; }
88
+
89
+ /* rail of check cards */
90
+ .rail { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 8px; scroll-snap-type: x mandatory; scrollbar-width: none; margin: 0 -16px; padding-left: 16px; padding-right: 16px; }
91
+ .rail::-webkit-scrollbar { display: none; }
92
+ .check-card { position: relative; flex: 0 0 236px; scroll-snap-align: start; display: flex; flex-direction: column; gap: 10px; padding: 14px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; transition: border-color .15s; text-align: left; font: inherit; color: inherit; }
93
+ .check-card:hover { border-color: color-mix(in srgb, var(--fg) 30%, transparent); }
94
+ .check-card .name { font-weight: 700; font-size: 15px; line-height: 1.25; padding-right: 40px; }
95
+ .check-card .why { font-size: 13px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; min-height: 3.9em; }
96
+ .mini { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: auto; }
97
+ .mini > span { display: flex; flex-direction: column; font-size: 12px; min-width: 0; }
98
+ .mini > span > span { color: var(--muted); font-size: 11px; }
99
+ .mini b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
100
+ .check-card .name, .check-card .why { display: block; }
101
+ .ribbon { position: absolute; top: 0; right: 0; font-size: 10px; font-weight: 800; padding: 3px 8px; border-bottom-left-radius: 8px; letter-spacing: .04em; }
102
+ .ribbon-high { background: var(--high-bg); color: var(--high-fg); }
103
+ .ribbon-beta { background: var(--med-bg); color: var(--med-fg); }
104
+ .ribbon-you { background: var(--ok-bg); color: var(--ok-fg); right: auto; left: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 8px; }
105
+
106
+ /* steps */
107
+ .steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
108
+ .steps li { display: flex; gap: 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
109
+ .steps p { font-size: 14px; margin-top: 4px; }
110
+ .step-n { flex: 0 0 28px; height: 28px; border-radius: 50%; background: var(--primary); color: var(--primary-fg); display: grid; place-items: center; font-weight: 800; font-size: 14px; }
111
+
112
+ /* table */
113
+ .filters { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
114
+ .pill { border: 1px solid var(--border); background: var(--card); color: var(--muted); border-radius: 999px; padding: 4px 12px; font: 600 13px var(--font); cursor: pointer; }
115
+ .pill[aria-pressed="true"] { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
116
+ .table-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
117
+ .table { width: 100%; border-collapse: collapse; font-size: 14px; }
118
+ .table th { text-align: left; font-size: 12px; font-weight: 600; color: var(--muted); padding: 10px 12px; border-bottom: 1px solid var(--border); }
119
+ .table td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
120
+ .table tr:last-child td { border-bottom: 0; }
121
+ .table tbody tr { cursor: pointer; }
122
+ .table tbody tr:hover td { background: var(--hover); }
123
+ .table .num { width: 44px; color: var(--muted); }
124
+ .table .check-name { font-weight: 600; }
125
+ .table .sub { display: block; color: var(--muted); font-size: 12px; }
126
+ .table tr.detail td { background: var(--bg); cursor: default; font-size: 13px; color: var(--muted); }
127
+ .table tr.detail:hover td { background: var(--bg); }
128
+ .table-more { width: 100%; border: 0; border-top: 1px solid var(--border); background: var(--card); color: var(--muted); padding: 12px; font: 600 14px var(--font); cursor: pointer; }
129
+ .table-more:hover { color: var(--fg); background: var(--hover); }
130
+ .badge { display: inline-block; font-size: 11px; font-weight: 800; padding: 2px 8px; border-radius: 999px; letter-spacing: .02em; white-space: nowrap; }
131
+ .badge-High { background: var(--high-bg); color: var(--high-fg); }
132
+ .badge-Medium { background: var(--med-bg); color: var(--med-fg); }
133
+ .badge-Low { background: var(--low-bg); color: var(--low-fg); }
134
+ .badge-ok { background: var(--ok-bg); color: var(--ok-fg); }
135
+ .badge-grey { background: var(--chip); color: var(--muted); }
136
+ .medal { font-size: 15px; }
137
+
138
+ /* downloads */
139
+ .dl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 12px; }
140
+ .dl-card { position: relative; display: flex; flex-direction: column; gap: 10px; padding: 18px 16px 16px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
141
+ .dl-card.you { border-color: color-mix(in srgb, var(--fg) 35%, transparent); }
142
+ .dl-top { display: flex; gap: 10px; align-items: center; }
143
+ .dl-top div { display: flex; flex-direction: column; }
144
+ .dl-icon { font-size: 26px; }
145
+ .dl-card[data-os="mac"] .dl-icon::before { content: '💻'; }
146
+ .dl-card[data-os="windows"] .dl-icon::before { content: '🪟'; }
147
+ .dl-card[data-os="linux"] .dl-icon::before { content: '🐧'; }
148
+ .kv { display: flex; justify-content: space-between; font-size: 14px; border-top: 1px dashed var(--border); padding-top: 8px; }
149
+ .hash summary { font-size: 12px; color: var(--muted); cursor: pointer; }
150
+ .hash code { display: block; margin-top: 6px; font-size: 11px; }
151
+
152
+ /* trust + faq */
153
+ .trust-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 14px; }
154
+ .faq { border-bottom: 1px solid var(--border); padding: 12px 2px; }
155
+ .faq summary { cursor: pointer; font-weight: 600; }
156
+ .faq p { margin-top: 8px; font-size: 14px; }
157
+
158
+ /* results */
159
+ .result-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; flex-wrap: wrap; margin: 16px 0 18px; }
160
+ .result-head h1 { font-size: clamp(26px, 5vw, 38px); font-weight: 800; }
161
+ .result-head .eyebrow { margin-bottom: 6px; }
162
+ .result-actions { display: flex; gap: 8px; }
163
+ .stat.score b { font-size: 34px; }
164
+ .bar { height: 6px; background: var(--chip); border-radius: 99px; overflow: hidden; margin-top: 6px; }
165
+ .bar i { display: block; height: 100%; background: var(--fg); border-radius: 99px; }
166
+ .summary-card { margin-top: 10px; }
167
+ .summary-card p { font-size: 15px; color: var(--fg); margin: 6px 0 12px; }
168
+ .summary-card ol { margin: 6px 0 0; padding-left: 20px; font-size: 14px; }
169
+ .summary-card li { margin-bottom: 4px; }
170
+ .findings { display: flex; flex-direction: column; gap: 8px; }
171
+ .finding { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); }
172
+ .finding > summary { list-style: none; display: flex; align-items: center; gap: 10px; padding: 12px 14px; cursor: pointer; }
173
+ .finding > summary::-webkit-details-marker { display: none; }
174
+ .finding > summary::after { content: '+'; margin-left: auto; color: var(--muted); font-weight: 700; }
175
+ .finding[open] > summary::after { content: '−'; }
176
+ .finding .f-name { font-weight: 700; }
177
+ .finding > summary > .badge { min-width: 62px; text-align: center; flex-shrink: 0; }
178
+ .finding .f-found { font-size: 13px; color: var(--muted); }
179
+ .f-body { padding: 0 14px 14px; font-size: 14px; display: flex; flex-direction: column; gap: 10px; border-top: 1px dashed var(--border); padding-top: 12px; }
180
+ .f-body h4 { margin: 0 0 4px; font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
181
+ .f-body ol { margin: 0; padding-left: 20px; }
182
+ .f-refs { display: flex; flex-wrap: wrap; gap: 6px; }
183
+ .other-results summary { cursor: pointer; }
184
+ .other-results .findings { margin-top: 12px; }
185
+
186
+ /* footer + toast */
187
+ .foot { margin-top: 72px; border-top: 1px solid var(--border); padding: 24px 0 40px; font-size: 14px; }
188
+ .foot-inner { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
189
+ .foot-links { display: flex; gap: 16px; flex-wrap: wrap; color: var(--muted); }
190
+ .foot-links a:hover { color: var(--fg); }
191
+ .toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); background: var(--primary); color: var(--primary-fg); padding: 10px 16px; border-radius: 8px; font-size: 14px; z-index: 50; max-width: calc(100% - 32px); }
192
+
193
+ @media (max-width: 760px) {
194
+ .nav-links { display: none; }
195
+ .nav-actions { margin-left: auto; }
196
+ .stats { grid-template-columns: repeat(2, 1fr); }
197
+ .steps, .dl-grid, .trust-grid { grid-template-columns: 1fr; }
198
+ .hide-sm { display: none; }
199
+ .hero { padding-top: 36px; }
200
+ }
201
+ @media (max-width: 520px) {
202
+ .device-card { flex-wrap: wrap; }
203
+ .seg { width: 100%; }
204
+ .seg button { flex: 1; }
205
+ .cta .btn { width: 100%; }
206
+ .hide-xs { display: none; }
207
+ .table th, .table td { padding: 9px 8px; }
208
+ }
209
+ @media print {
210
+ .nav, .foot, .result-actions, #landing, .toast { display: none !important; }
211
+ body { background: #fff; color: #000; }
212
+ .finding { break-inside: avoid; }
213
+ details.finding:not([open]) > .f-body, .other-results:not([open]) > .findings { display: flex !important; }
214
+ }
site/site.js ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Arx website: detect the visitor's computer and offer the right download, list every check from the catalog, and
2
+ // show a report from the desktop app (its "Ask Arx about this report" button, or an opened …-evidence.json file).
3
+ // No AI here: the AI runs only inside the desktop app. Reports are written with the app's own templates
4
+ // (app/renderer/auditor-core.js), so they read the same as the app's fallback report. Nothing is uploaded.
5
+ import { Library, findingTemplate, summaryTemplate } from '../app/renderer/auditor-core.js';
6
+
7
+ const $ = id => document.getElementById(id);
8
+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
9
+ const RELEASES = 'https://huggingface.co/datasets/Devseis/endpoint-auditor-releases/resolve/main/';
10
+ const DESKTOP = ['windows', 'macos', 'linux'];
11
+ const OS_LABEL = { mac: 'macOS', windows: 'Windows', linux: 'Linux' };
12
+ const FAMILY = { mac: 'macos', windows: 'windows', linux: 'linux' };
13
+ const SHORT = { windows: 'Win', macos: 'Mac', linux: 'Linux' };
14
+ const RISK_ORDER = { High: 0, Medium: 1, Low: 2 };
15
+ const state = { os: 'mac', lib: null, checks: [], filter: 'all', shown: 15, open: null };
16
+
17
+ function toast(text) {
18
+ const t = $('toast'); t.textContent = text; t.classList.remove('hidden');
19
+ clearTimeout(toast.timer); toast.timer = setTimeout(() => t.classList.add('hidden'), 2600);
20
+ }
21
+
22
+ // ---------- theme ----------
23
+ $('themeToggle').addEventListener('click', () => {
24
+ const root = document.documentElement;
25
+ const dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
26
+ root.dataset.theme = dark ? 'light' : 'dark';
27
+ try { localStorage.setItem('devseis.theme', root.dataset.theme); } catch { /* ignore */ }
28
+ });
29
+
30
+ // ---------- device ----------
31
+ // Computers only. iPadOS reports itself as a Mac, so a touch screen on "Mac" counts as a tablet.
32
+ async function detect() {
33
+ const ua = navigator.userAgent;
34
+ const phone = /iPhone|iPod|Android|Mobile/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1) || /iPad/.test(ua);
35
+ const os = /Windows/.test(ua) ? 'windows' : (/Linux|X11|CrOS/.test(ua) && !/Android/.test(ua)) ? 'linux' : 'mac';
36
+ const browser = (/Edg\/(\d+)/.exec(ua) && `Edge ${/Edg\/(\d+)/.exec(ua)[1]}`) || (/Firefox\/(\d+)/.exec(ua) && `Firefox ${/Firefox\/(\d+)/.exec(ua)[1]}`)
37
+ || (/Chrome\/(\d+)/.exec(ua) && `Chrome ${/Chrome\/(\d+)/.exec(ua)[1]}`) || (/Version\/(\d+[.\d]*).*Safari/.exec(ua) && `Safari ${/Version\/(\d+[.\d]*)/.exec(ua)[1]}`) || 'your browser';
38
+ let version = '', arch = '';
39
+ try {
40
+ const hi = await Promise.race([navigator.userAgentData?.getHighEntropyValues(['platformVersion', 'architecture']), new Promise(r => setTimeout(() => r(null), 1200))]);
41
+ if (hi) {
42
+ if (os === 'windows' && hi.platformVersion) version = parseInt(hi.platformVersion, 10) >= 13 ? '11' : '10';
43
+ if (os === 'mac' && hi.platformVersion) version = hi.platformVersion.replace(/(\.0)+$/, '');
44
+ if (hi.architecture) arch = hi.architecture === 'arm' ? (os === 'mac' ? 'Apple chip' : 'ARM') : (os === 'mac' ? 'Intel' : 'x86-64');
45
+ }
46
+ } catch { /* hints refused */ }
47
+ return { phone, os, browser, version, arch };
48
+ }
49
+
50
+ function chooseOs(os) {
51
+ state.os = os;
52
+ document.querySelectorAll('#osSeg button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.os === os)));
53
+ document.querySelectorAll('.dl-card').forEach(c => {
54
+ c.classList.toggle('you', c.dataset.os === os);
55
+ c.querySelector('.ribbon-you').classList.toggle('hidden', c.dataset.os !== os);
56
+ });
57
+ $('heroDownload').textContent = `Download for ${OS_LABEL[os]}`;
58
+ const main = document.querySelector(`.dl-card[data-os="${os}"] .btn-primary`);
59
+ $('heroDownload').href = main && main.href ? main.href : '#download';
60
+ renderTable();
61
+ }
62
+
63
+ async function initDevice() {
64
+ const d = await detect();
65
+ if (d.phone) {
66
+ $('phoneNote').classList.remove('hidden');
67
+ $('deviceIcon').textContent = '📱'; $('deviceLabel').textContent = 'You are on a phone or tablet'; $('deviceName').textContent = 'Choose the computer to check';
68
+ $('heroDownload').classList.add('hidden');
69
+ document.querySelector('label[for="reportFile"]').classList.add('hidden');
70
+ } else {
71
+ $('deviceName').textContent = [OS_LABEL[d.os] + (d.version ? ` ${d.version}` : ''), d.arch, d.browser].filter(Boolean).join(' · ');
72
+ }
73
+ chooseOs(d.os);
74
+ }
75
+ document.querySelectorAll('#osSeg button').forEach(b => b.addEventListener('click', () => {
76
+ chooseOs(b.dataset.os);
77
+ if (!$('phoneNote').classList.contains('hidden')) $('deviceName').textContent = `${OS_LABEL[b.dataset.os]} (chosen)`;
78
+ }));
79
+ $('copyLink').addEventListener('click', async () => {
80
+ try { await navigator.clipboard.writeText(location.origin + location.pathname); toast('Link copied: open it on your computer'); }
81
+ catch { toast(location.origin + location.pathname); }
82
+ });
83
+
84
+ // ---------- downloads ----------
85
+ async function initDownloads() {
86
+ try {
87
+ const rel = await (await fetch(RELEASES + 'latest.json', { cache: 'no-cache' })).json();
88
+ const set = (key, file) => document.querySelectorAll(`[data-link="${key}"]`).forEach(a => { a.href = RELEASES + file.file; a.setAttribute('download', ''); });
89
+ set('mac-dmg', rel.mac.dmg); set('mac-zip', rel.mac.zip);
90
+ if (rel.windows) set('win-exe', rel.windows.exe);
91
+ if (rel.linux) { set('linux-deb', rel.linux.deb); set('linux-appimage', rel.linux.appimage); }
92
+ const sizes = { mac: rel.mac.dmg, windows: rel.windows?.exe, linux: rel.linux?.deb };
93
+ for (const [os, f] of Object.entries(sizes)) if (f) {
94
+ document.querySelector(`[data-size="${os}"]`).textContent = `${f.size_mb} MB`;
95
+ document.querySelector(`[data-hash="${os}"]`).textContent = `${f.file}\n${f.sha256}`;
96
+ }
97
+ $('dlVersion').textContent = `Version ${rel.version} · ${rel.date}`;
98
+ $('heroMeta').textContent = `Free · open source · version ${rel.version}`;
99
+ chooseOs(state.os);
100
+ } catch {
101
+ document.querySelectorAll('[data-hash]').forEach(c => { c.textContent = 'Could not load: see the releases page.'; });
102
+ document.querySelectorAll('.dl-card .btn-primary').forEach(a => { a.href = 'https://huggingface.co/datasets/Devseis/endpoint-auditor-releases'; });
103
+ }
104
+ }
105
+
106
+ // ---------- checks ----------
107
+ async function loadLibrary() {
108
+ const get = async path => { const r = await fetch(path); if (!r.ok) throw new Error(`${path}: HTTP ${r.status}`); return r.json(); };
109
+ const [catalog, controls, guidance, aiTools] = await Promise.all([get('catalog/checks.json'), get('library/controls.json'), get('library/check_guidance.json'), get('library/ai_tools.json')]);
110
+ state.lib = new Library({ catalog, controls, guidance, aiTools });
111
+ state.checks = catalog.checks.filter(c => !c.os || c.os.some(o => DESKTOP.includes(o))).map(c => {
112
+ const g = state.lib.guide(c.id) || {};
113
+ return { ...c, guide: g, risk: g.risk?.level || 'Low', systems: (c.os || DESKTOP).filter(o => DESKTOP.includes(o)) };
114
+ }).sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]);
115
+ }
116
+
117
+ const refs = (c, key) => (c.controls?.[key] || []).join(', ') || '—';
118
+ const systems = c => c.systems.length === 3 ? 'All' : c.systems.map(o => SHORT[o]).join(' · ');
119
+
120
+ function renderRail() {
121
+ const high = state.checks.filter(c => c.risk === 'High' && c.section === 'baseline').slice(0, 12);
122
+ $('riskRail').innerHTML = high.map(c => `
123
+ <button type="button" class="check-card" data-id="${esc(c.id)}">
124
+ <span class="ribbon ribbon-high">HIGH RISK</span>
125
+ <span class="name">${esc(c.name)}</span>
126
+ <span class="why">${esc(c.guide.why || '')}</span>
127
+ <span class="mini"><span><span>ISO 27001</span><b>${esc(refs(c, 'iso27001').split(', ')[0])}</b></span><span><span>GDPR</span><b>${esc(refs(c, 'gdpr').split(', ')[0])}</b></span><span><span>Systems</span><b>${esc(systems(c))}</b></span></span>
128
+ </button>`).join('');
129
+ $('riskRail').querySelectorAll('.check-card').forEach(b => b.addEventListener('click', () => {
130
+ state.filter = 'all'; state.open = b.dataset.id;
131
+ const i = state.checks.findIndex(c => c.id === b.dataset.id); state.shown = Math.max(state.shown, i + 1);
132
+ renderFilters(); renderTable();
133
+ document.querySelector(`tr[data-id="${CSS.escape(b.dataset.id)}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
134
+ }));
135
+ }
136
+
137
+ const FILTERS = [['all', 'All'], ['High', 'High risk'], ['windows', 'Windows'], ['macos', 'macOS'], ['linux', 'Linux'], ['gdpr', 'GDPR'], ['aiact', 'AI Act'], ['org', 'Company']];
138
+ function matches(c, f) {
139
+ if (f === 'all') return true;
140
+ if (f === 'High') return c.risk === 'High';
141
+ if (DESKTOP.includes(f)) return c.systems.includes(f);
142
+ return c.section === f;
143
+ }
144
+ function renderFilters() {
145
+ $('filters').innerHTML = FILTERS.map(([k, label]) => `<button type="button" class="pill" data-f="${k}" aria-pressed="${state.filter === k}">${label} <span class="soft">${state.checks.filter(c => matches(c, k)).length}</span></button>`).join('');
146
+ $('filters').querySelectorAll('.pill').forEach(p => p.addEventListener('click', () => { state.filter = p.dataset.f; state.shown = 15; state.open = null; renderFilters(); renderTable(); }));
147
+ }
148
+
149
+ function fixFor(c) {
150
+ const fam = c.systems.includes(FAMILY[state.os]) ? FAMILY[state.os] : c.systems[0];
151
+ const steps = c.guide.fix?.[fam] || c.guide.fix?.all || [];
152
+ return { fam, steps: steps.map(s => s.replace(/\{[A-Z_]+\}/g, '…')) };
153
+ }
154
+
155
+ function renderTable() {
156
+ if (!state.checks.length) return;
157
+ const rows = state.checks.filter(c => matches(c, state.filter));
158
+ $('checkCount').textContent = `${rows.length} of ${state.checks.length}`;
159
+ $('checkTable').querySelector('tbody').innerHTML = rows.slice(0, state.shown).map((c, i) => {
160
+ const row = `<tr data-id="${esc(c.id)}"><td class="num">${i + 1}</td><td><span class="check-name">${esc(c.name)}</span><span class="sub">${esc(c.id)}</span></td>
161
+ <td><span class="badge badge-${c.risk}">${c.risk}</span></td><td class="hide-sm">${esc(refs(c, 'iso27001'))}</td><td class="hide-sm">${esc(refs(c, 'gdpr'))}</td><td class="hide-xs">${esc(systems(c))}</td></tr>`;
162
+ if (state.open !== c.id) return row;
163
+ const fix = fixFor(c);
164
+ return row + `<tr class="detail"><td></td><td colspan="5"><b>Why it matters:</b> ${esc(c.guide.why || '')}<br><b>Risk:</b> ${esc(c.guide.risk?.reason || '')}
165
+ ${fix.steps.length ? `<br><b>How to fix on ${esc({ windows: 'Windows', macos: 'macOS', linux: 'Linux' }[fix.fam] || fix.fam)}:</b> ${esc(fix.steps[0])}` : ''}</td></tr>`;
166
+ }).join('');
167
+ $('moreChecks').classList.toggle('hidden', rows.length <= state.shown);
168
+ $('moreChecks').textContent = `Show more (${rows.length - state.shown} more checks)`;
169
+ $('checkTable').querySelectorAll('tbody tr[data-id]').forEach(tr => tr.addEventListener('click', () => { state.open = state.open === tr.dataset.id ? null : tr.dataset.id; renderTable(); }));
170
+ }
171
+ $('moreChecks').addEventListener('click', () => { state.shown += 25; renderTable(); });
172
+
173
+ // ---------- results ----------
174
+ function validEvidence(ev) { return ev && ev.host && Array.isArray(ev.results) && ev.summary && ev.audit; }
175
+
176
+ function findingHtml(record, result, open) {
177
+ let f;
178
+ try { f = findingTemplate(record, result, state.lib); } catch { f = { title: result.name, finding: `${result.actual || ''} (expected ${result.expected || '—'})`, risk: { level: 'Unknown', reason: '' }, recommendation: [], references: [] }; }
179
+ const level = f.risk?.level;
180
+ const badge = result.status === 'Compliant' ? '<span class="badge badge-ok">PASS</span>'
181
+ : result.status === 'Non-Compliant' ? `<span class="badge badge-${esc(level)}">${esc(level)}</span>` : `<span class="badge badge-grey">${esc(result.status.toUpperCase())}</span>`;
182
+ return `<details class="finding"${open ? ' open' : ''}><summary>${badge}<span><span class="f-name">${esc(result.name)}</span><br><span class="f-found">Found: ${esc(result.actual || '—')}${result.expected ? ` · required: ${esc(result.expected)}` : ''}</span></span></summary>
183
+ <div class="f-body"><div><h4>Finding</h4>${esc(f.finding)}</div>
184
+ ${f.risk?.reason && level !== 'None' ? `<div><h4>Risk</h4>${esc(f.risk.reason)}</div>` : ''}
185
+ ${f.recommendation?.length ? `<div><h4>How to fix</h4><ol>${f.recommendation.map(s => `<li>${esc(s)}</li>`).join('')}</ol></div>` : ''}
186
+ ${f.verification ? `<div><h4>Check it worked</h4>${esc(f.verification)}</div>` : ''}
187
+ ${f.references?.length ? `<div class="f-refs">${f.references.map(r => `<span class="badge badge-grey">${esc(r)}</span>`).join('')}</div>` : ''}</div></details>`;
188
+ }
189
+
190
+ function showResults(ev) {
191
+ if (!validEvidence(ev)) { toast('That file is not an Arx report (…-evidence.json).'); return; }
192
+ if (!DESKTOP.includes(ev.host.os_family)) { toast('Arx now shows computer reports only.'); return; }
193
+ const s = summaryTemplate(ev, state.lib);
194
+ $('landing').classList.add('hidden'); $('results').classList.remove('hidden');
195
+ const h = ev.host;
196
+ $('resDevice').textContent = `💻 ${h.hostname || 'Computer'} · ${h.os_name || ''} ${h.os_version || ''}`;
197
+ $('resTitle').textContent = `${s.score.percent}% of checks met`;
198
+ const when = ev.audit.started_at ? new Date(ev.audit.started_at) : null;
199
+ $('resWhen').textContent = `${when && !isNaN(when) ? `Audited ${when.toLocaleString()}` : 'Audit'}${ev.audit.is_admin ? ' · as administrator' : ' · without admin rights (some checks skipped)'}`;
200
+ const c = ev.summary;
201
+ $('resStats').innerHTML = `<div class="stat score"><span class="tiny muted">Score</span><b>${s.score.percent}%</b><div class="bar"><i style="width:${s.score.percent}%"></i></div></div>
202
+ <div class="stat"><span class="tiny muted">Passed</span><b>${c.Compliant}</b><span class="tiny muted">of ${s.score.assessed} assessed</span></div>
203
+ <div class="stat"><span class="tiny muted">Gaps</span><b>${c['Non-Compliant']}</b><span class="tiny muted">${ev.results.filter(r => r.status === 'Non-Compliant' && state.lib.guide(r.id)?.risk?.level === 'High').length} high risk</span></div>
204
+ <div class="stat"><span class="tiny muted">Not checked</span><b>${(c.Error || 0) + (c.Pending || 0)}</b><span class="tiny muted">${c.Error || 0} errors · ${c.Pending || 0} pending</span></div>`;
205
+ $('resSummary').textContent = s.summary;
206
+ $('resNext').innerHTML = s.next_steps.map(x => `<li>${esc(x)}</li>`).join('') || '<li>Nothing urgent: keep the current settings and re-run the audit regularly.</li>';
207
+ const gaps = ev.results.filter(r => r.status === 'Non-Compliant')
208
+ .sort((a, b) => (RISK_ORDER[state.lib.guide(a.id)?.risk?.level] ?? 3) - (RISK_ORDER[state.lib.guide(b.id)?.risk?.level] ?? 3));
209
+ $('gapCount').textContent = `${gaps.length} found`;
210
+ $('gapList').innerHTML = gaps.map((r, i) => findingHtml(ev, r, i === 0)).join('') || '<div class="card">No gaps found. 🎉</div>';
211
+ const other = ev.results.filter(r => r.status !== 'Non-Compliant');
212
+ $('otherCount').textContent = `(${other.length}: passed, pending, errors, not applicable)`;
213
+ $('otherList').innerHTML = other.map(r => findingHtml(ev, r, false)).join('');
214
+ document.title = `Arx · ${s.score.percent}% · ${h.hostname || 'audit'}`;
215
+ scrollTo(0, 0);
216
+ }
217
+
218
+ function openFile(input) {
219
+ const file = input.files && input.files[0];
220
+ if (!file) return;
221
+ file.text().then(t => showResults(JSON.parse(t))).catch(() => toast('Could not read that file.'));
222
+ input.value = '';
223
+ }
224
+ $('reportFile').addEventListener('change', e => openFile(e.target));
225
+ $('reportFile2').addEventListener('change', e => openFile(e.target));
226
+ $('printBtn').addEventListener('click', () => print());
227
+ addEventListener('beforeprint', () => document.querySelectorAll('#results details').forEach(d => { d.dataset.wasOpen = d.open; d.open = true; }));
228
+ addEventListener('afterprint', () => document.querySelectorAll('#results details').forEach(d => { d.open = d.dataset.wasOpen === 'true'; }));
229
+
230
+ // A report handed over by the desktop app (0.4.x: "Ask Arx about this report" stores it, then opens #audit),
231
+ // or by apps 0.4.0/0.4.1 in the address fragment (#audit=…; fragments never reach a server).
232
+ function handedOverReport() {
233
+ try {
234
+ if (location.hash === '#audit') {
235
+ const ev = JSON.parse(localStorage.getItem('devseis.lastAudit'));
236
+ localStorage.removeItem('devseis.lastAudit');
237
+ return ev;
238
+ }
239
+ if (location.hash.startsWith('#audit=')) return JSON.parse(decodeURIComponent(location.hash.slice(7)));
240
+ } catch { /* ignore */ }
241
+ return null;
242
+ }
243
+
244
+ async function init() {
245
+ initDevice(); initDownloads();
246
+ try { await loadLibrary(); } catch (e) { $('checkCount').textContent = 'Could not load the check list.'; console.warn(e); return; }
247
+ renderRail(); renderFilters(); renderTable();
248
+ const showHandedOver = () => { const ev = handedOverReport(); if (ev) { history.replaceState(null, '', location.pathname); showResults(ev); } };
249
+ showHandedOver();
250
+ addEventListener('hashchange', showHandedOver);
251
+ }
252
+ init();
training/build_chat.py CHANGED
@@ -1,12 +1,14 @@
1
  #!/usr/bin/env python3
2
- """Chat training examples for model v0.4 (questions about endpoint security, ISO 27001, GDPR, EU AI Act, the auditor,
3
  and the user's own audit), grounded in library/knowledge.json and cited as [n].
4
 
5
  Answers are composed from the same library facts the passages are built from, so they are grounded by construction;
6
  check_chat() verifies it (citations exist, numbers appear in the prompt, refusals cite nothing). Prompts are built by
7
  training/chat-contexts.mjs with the app's own search, so training matches runtime.
8
 
9
- Usage: python3 training/build_chat.py --evidence synthetic/out-v0.3/evidence.jsonl --base training/out-v0.3 --out training/out-v0.4
 
 
10
  """
11
  import argparse
12
  import json
@@ -20,7 +22,7 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
  sys.path.insert(0, os.path.join(ROOT, "library"))
21
  import build_knowledge as kb_build # noqa: E402
22
 
23
- DATASET_VERSION = "0.4.0"
24
  OS_LABEL = kb_build.OS_LABEL
25
  NUMBER = re.compile(r"(?<![\w.])\d+(?:\.\d+)?(?!\w)")
26
 
@@ -210,6 +212,66 @@ def make_jobs(rng, catalog, guidance, controls, tools, phone, evidences):
210
  return jobs
211
 
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  def audit_brief(ev):
214
  """Python twin of auditBrief() in app/renderer/knowledge.js (tested for identical output)."""
215
  h, s = ev["host"], ev["summary"]
@@ -254,7 +316,8 @@ def main():
254
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
255
  parser.add_argument("--evidence", default=os.path.join(ROOT, "synthetic", "out-v0.3", "evidence.jsonl"))
256
  parser.add_argument("--base", default=os.path.join(ROOT, "training", "out-v0.3"), help="report-task data to include")
257
- parser.add_argument("--out", default=os.path.join(ROOT, "training", "out-v0.4"))
 
258
  parser.add_argument("--seed", type=int, default=11)
259
  args = parser.parse_args()
260
  rng = random.Random(args.seed)
@@ -262,6 +325,8 @@ def main():
262
  tools, phone = load("library", "ai_tools.json"), load("catalog", "mobile_questions.json")
263
  evidences = [json.loads(l) for l in open(args.evidence)][:600]
264
  jobs = make_jobs(rng, catalog, guidance, controls, tools, phone, evidences)
 
 
265
  # A few examples carry an earlier, unrelated turn so the model learns to answer the latest question.
266
  for job, _, _ in jobs:
267
  job["shuffle"] = rng.random() < 0.5
@@ -270,7 +335,11 @@ def main():
270
  built = [json.loads(l) for l in proc.stdout.splitlines() if l.strip()]
271
  examples, bad = [], 0
272
  for (job, answer, (kind, os_family)), b in zip(jobs, built):
273
- msgs = b["messages"] + [{"role": "assistant", "content": answer(b["order"])}]
 
 
 
 
274
  ex = {"messages": msgs, "meta": {"task": "chat", "os": os_family, "status": kind, "dataset_version": DATASET_VERSION}}
275
  if check_chat(ex):
276
  bad += 1
 
1
  #!/usr/bin/env python3
2
+ """Chat training examples for models v0.4/v0.5 (questions about endpoint security, ISO 27001, GDPR, EU AI Act, the auditor,
3
  and the user's own audit), grounded in library/knowledge.json and cited as [n].
4
 
5
  Answers are composed from the same library facts the passages are built from, so they are grounded by construction;
6
  check_chat() verifies it (citations exist, numbers appear in the prompt, refusals cite nothing). Prompts are built by
7
  training/chat-contexts.mjs with the app's own search, so training matches runtime.
8
 
9
+ v0.5 adds MY DEVICE (what the browser detected, as deviceBrief() in the former chat/device.js wrote it) to most prompts without an audit.
10
+
11
+ Usage: python3 training/build_chat.py --evidence synthetic/out-v0.3/evidence.jsonl --base training/out-v0.3 --out training/out-v0.5
12
  """
13
  import argparse
14
  import json
 
22
  sys.path.insert(0, os.path.join(ROOT, "library"))
23
  import build_knowledge as kb_build # noqa: E402
24
 
25
+ DATASET_VERSION = "0.5.0"
26
  OS_LABEL = kb_build.OS_LABEL
27
  NUMBER = re.compile(r"(?<![\w.])\d+(?:\.\d+)?(?!\w)")
28
 
 
212
  return jobs
213
 
214
 
215
+ # What browsers report (the former chat/device.js). arch only for computers with Chromium client hints.
216
+ DEVICES = {
217
+ "macos": {"model": "Mac", "versions": ["13.6", "14.5", "14.7.1", "15.1", "15.6", "26.0.1", "26.5.2", None], "arch": ["Intel", "Apple silicon", None],
218
+ "browsers": ["Chrome 141", "Chrome 152", "Edge 140", "Safari 18.6", "Firefox 143"]},
219
+ "windows": {"model": "Windows PC", "versions": ["10", "11", "10 or 11"], "arch": ["x86-64", "ARM", None],
220
+ "browsers": ["Chrome 141", "Chrome 152", "Edge 141", "Firefox 143"]},
221
+ "linux": {"model": "Linux computer", "versions": [None], "arch": ["x86-64", None], "browsers": ["Chrome 141", "Firefox 143", "Chrome 152"]},
222
+ "ios": {"model": "iPhone", "versions": ["17.6", "18.5", "18.6", "26.0", "26.1"], "arch": [None], "browsers": ["Safari 18.6", "Safari 26.0", "Chrome 141 (iOS)"]},
223
+ "android": {"model": "Android phone", "versions": ["13", "14", "15", "16"], "arch": [None], "browsers": ["Chrome 141", "Samsung Internet 28.0", "Firefox 143"]},
224
+ }
225
+ OS_NAMES = {"macos": "macOS", "windows": "Windows", "linux": "Linux", "ios": "iOS", "android": "Android"}
226
+
227
+
228
+ def make_device(rng, family):
229
+ """A detected device: (MY DEVICE line, short label for answers, browser). Python twin of deviceBrief() in the former chat/device.js."""
230
+ d = DEVICES[family]
231
+ version, arch, browser = rng.choice(d["versions"]), rng.choice(d["arch"]), rng.choice(d["browsers"])
232
+ model = d["model"]
233
+ if family == "android" and rng.random() < 0.5:
234
+ model = rng.choice(["Pixel 8", "SM-S921B", "Pixel 9a", "moto g84"])
235
+ os_label = " ".join(x for x in (OS_NAMES[family], version) if x)
236
+ article = "an" if model[0].lower() in "aeiou" else "a"
237
+ on = f" on {article} {model}{f' ({arch})' if arch else ''}"
238
+ return f"{os_label} ({family}){on}, {browser}; detected by the browser, security settings not checked.", f"{os_label}{on}", browser
239
+
240
+
241
+ def add_devices(rng, jobs, share):
242
+ """MY DEVICE on most prompts without an audit, as at runtime; the search then filters by that system, as the chat does."""
243
+ for job, answer, (kind, os_family) in jobs:
244
+ if job.get("audit") or rng.random() >= share:
245
+ continue
246
+ family = job["os"] or (None if kind == "noaudit" else rng.choice(list(DEVICES)))
247
+ if not family:
248
+ continue
249
+ job["os"] = family
250
+ job["device"], job["device_label"], _ = make_device(rng, family)
251
+
252
+
253
+ DEVICE_QUESTIONS = ["Which system am I using?", "What device am I on?", "Which operating system do I have?",
254
+ "What version of my operating system do I have?", "Can you see my device?", "What do you know about my computer?"]
255
+
256
+
257
+ def device_jobs(rng):
258
+ """Questions about the device itself: say what the browser saw, and that settings need an audit."""
259
+ jobs = []
260
+ for q in DEVICE_QUESTIONS:
261
+ for family in ("macos", "windows", "linux", "ios", "android"):
262
+ line, label, browser = make_device(rng, family)
263
+ phone = family in ("ios", "android")
264
+ gold = "faq:7" if phone else "faq:3"
265
+ jobs.append(({"question": q, "os": family, "gold": [gold], "device": line, "device_label": label},
266
+ lambda o, label=label, browser=browser, phone=phone, gold=gold: (
267
+ f"This browser reports {label}, using {browser}. That is all a web page can see: it can't read security settings "
268
+ "such as encryption, the firewall or the screen lock. "
269
+ + ("To check those, run the phone check: open this page on the phone and tap Start the check " if phone
270
+ else "To check those, run an audit with the free Devseis desktop app ") + f"{cite(o, gold)}."),
271
+ ("device", family)))
272
+ return jobs
273
+
274
+
275
  def audit_brief(ev):
276
  """Python twin of auditBrief() in app/renderer/knowledge.js (tested for identical output)."""
277
  h, s = ev["host"], ev["summary"]
 
316
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
317
  parser.add_argument("--evidence", default=os.path.join(ROOT, "synthetic", "out-v0.3", "evidence.jsonl"))
318
  parser.add_argument("--base", default=os.path.join(ROOT, "training", "out-v0.3"), help="report-task data to include")
319
+ parser.add_argument("--out", default=os.path.join(ROOT, "training", "out-v0.5"))
320
+ parser.add_argument("--device-share", type=float, default=0.6, help="share of prompts without an audit that carry MY DEVICE")
321
  parser.add_argument("--seed", type=int, default=11)
322
  args = parser.parse_args()
323
  rng = random.Random(args.seed)
 
325
  tools, phone = load("library", "ai_tools.json"), load("catalog", "mobile_questions.json")
326
  evidences = [json.loads(l) for l in open(args.evidence)][:600]
327
  jobs = make_jobs(rng, catalog, guidance, controls, tools, phone, evidences)
328
+ add_devices(rng, jobs, args.device_share)
329
+ jobs += device_jobs(rng)
330
  # A few examples carry an earlier, unrelated turn so the model learns to answer the latest question.
331
  for job, _, _ in jobs:
332
  job["shuffle"] = rng.random() < 0.5
 
335
  built = [json.loads(l) for l in proc.stdout.splitlines() if l.strip()]
336
  examples, bad = [], 0
337
  for (job, answer, (kind, os_family)), b in zip(jobs, built):
338
+ text = answer(b["order"])
339
+ if kind == "noaudit" and job.get("device"):
340
+ text = text.replace("I can't see your device from here, so I can't tell you yet.",
341
+ f"I can see this is {job['device_label']}, but a web page can't read its security settings, so I can't tell you yet.")
342
+ msgs = b["messages"] + [{"role": "assistant", "content": text}]
343
  ex = {"messages": msgs, "meta": {"task": "chat", "os": os_family, "status": kind, "dataset_version": DATASET_VERSION}}
344
  if check_chat(ex):
345
  bad += 1