New website: computers only, download + check list + report viewer, no AI in the browser (chat and phone check removed)
Browse files- README.md +15 -29
- about.html +4 -160
- app/model-version.json +1 -7
- app/renderer/knowledge.js +1 -1
- chat/chat.css +0 -155
- chat/chat.js +0 -601
- chat/device.js +0 -48
- download.html +4 -213
- index.html +153 -81
- phone.html +4 -91
- site/site.css +214 -0
- site/site.js +252 -0
- 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 |
-
##
|
| 155 |
-
|
| 156 |
-
The Space's front page
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
(`
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
|
| 167 |
## Install on a Mac
|
| 168 |
|
| 169 |
-
Download from **https://devseis-arx.static.hf.space/download
|
| 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 |
-
|
|
| 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
|
| 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 |
-
<
|
| 4 |
-
<
|
| 5 |
-
<
|
| 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
|
| 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/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 |
-
<
|
| 4 |
-
|
| 5 |
-
|
| 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 & 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 & 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
|
| 6 |
-
<meta name="description" content="Arx by Devseis,
|
| 7 |
-
<meta name="theme-color" content="#
|
| 8 |
-
<title>Arx ·
|
| 9 |
<link rel="icon" href="app/build/icon.png">
|
| 10 |
<link rel="apple-touch-icon" href="app/build/icon.png">
|
| 11 |
-
<link rel="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
-
<header>
|
| 15 |
-
<div class="
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
<
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
<
|
| 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 |
-
<
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
<
|
| 36 |
-
<
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</div>
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
<
|
| 44 |
-
<
|
| 45 |
-
<button type="button" class="
|
| 46 |
</div>
|
| 47 |
-
</div>
|
| 48 |
-
<div id="thread" aria-live="polite"></div>
|
| 49 |
-
</main>
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
<
|
| 55 |
-
</
|
| 56 |
-
<
|
| 57 |
-
</
|
| 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 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
<
|
| 85 |
-
|
| 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 |
-
|
| 91 |
-
|
| 92 |
-
<
|
| 93 |
-
|
| 94 |
-
<button type="button" class="link-btn" id="auditClose">Not now</button>
|
| 95 |
-
</div>
|
| 96 |
-
</div>
|
| 97 |
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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 |
-
<
|
| 4 |
-
|
| 5 |
-
|
| 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
| 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 |
-
|
|
|
|
|
|
|
| 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.
|
| 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.
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|