← Commit history

Embed live web-control demo panel in the page README

John Lauer ·b5b50139d2 ·2mo ago ·parent 9b52e08
1 file changed +60−1
README.md+60−1
@@ -290,7 +290,66 @@ Adom Bridge serves a couple of small pages on its loopback HTTP API (the stable  - **`http://127.0.0.1:24680/verbs`** is a browsable API Explorer: every verb, grouped into tabs by category and with one tab per installed bridge, each with a plain-English description and a copy-to-clipboard curl command. The footer tooltip in the laptop GUI links straight to it. `GET /health`, `/status`, and `/commands` return the same information as JSON. -- **Web integration (detection).** A page served from `https://wiki.adom.inc` (and only that origin) can `fetch('http://127.0.0.1:24680/health')` to detect whether Adom Bridge is running, so the wiki can light up actions like "open this component in KiCad." The security is deliberately tight: Adom Bridge returns CORS headers only for the exact origin `https://wiki.adom.inc` (never a wildcard, never a user-provisionable `*.adom.cloud` host) with a loopback-Host check to defeat DNS rebinding, and it handles the Private-Network-Access preflight. A web origin may reach only the read-only detection endpoints; `POST /command` is refused for every web origin, so no verb can change the machine from a web page. The planned launch and send actions will each require a native Adom Bridge consent dialog. **Try it live:** [talking to Adom Bridge from a web page](docs/web-control.md) is a working harness that detects your running Adom Bridge, reads its status, and shows the "open KiCad" wall refusing a machine-changing call. Full model: the `adom-bridge-web-integration` skill.+- **Web integration (detection).** A page served from `https://wiki.adom.inc` (and only that origin) can `fetch('http://127.0.0.1:24680/health')` to detect whether Adom Bridge is running, so the wiki can light up actions like "open this component in KiCad." The security is deliberately tight: Adom Bridge returns CORS headers only for the exact origin `https://wiki.adom.inc` (never a wildcard, never a user-provisionable `*.adom.cloud` host) with a loopback-Host check to defeat DNS rebinding, and it handles the Private-Network-Access preflight. A web origin may reach only the read-only detection endpoints; `POST /command` is refused for every web origin, so no verb can change the machine from a web page. The planned launch and send actions will each require a native Adom Bridge consent dialog. Full model: the `adom-bridge-web-integration` skill; the mechanism and the security layers, with diagrams: [talking to Adom Bridge from a web page](docs/web-control.md).++**Try it live.** If you are reading this on `https://wiki.adom.inc` on a machine running Adom Bridge, the panel below actually reaches into it. The buttons make real `fetch()` calls to `http://127.0.0.1:24680`: it detects your running Adom Bridge, reads its status, and hits `POST /command` so you can watch the "open KiCad" wall refuse a machine-changing call. It also prints this frame's own origin, so you can see for yourself that only `https://wiki.adom.inc` is allowed in.++<div id="ab-demo" style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;border:1px solid #30363d;border-radius:10px;background:#0d1117;color:#e6edf3;padding:18px;margin:18px 0;max-width:760px">+  <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">+    <span id="ab-dot" style="width:12px;height:12px;border-radius:50%;background:#484f58;display:inline-block"></span>+    <strong style="font-size:16px">Adom Bridge web control demo</strong>+  </div>+  <div style="font-size:12px;color:#8b949e;margin-bottom:14px">+    This frame's origin: <code id="ab-origin" style="color:#00e6dc">checking…</code>+    &nbsp;·&nbsp; API: <code style="color:#8b949e">http://127.0.0.1:24680</code>+  </div>+  <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px">+    <button id="ab-detect" style="background:#00b8b0;color:#04211f;border:0;border-radius:6px;padding:8px 14px;font-weight:600;cursor:pointer">Detect Adom Bridge</button>+    <button id="ab-status" style="background:#1c2128;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:8px 14px;cursor:pointer">Read status</button>+    <button id="ab-kicad" style="background:#1c2128;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:8px 14px;cursor:pointer">Try to open KiCad (should be blocked)</button>+  </div>+  <pre id="ab-log" style="background:#010409;border:1px solid #21262d;border-radius:6px;padding:12px;font-size:12px;line-height:1.5;max-height:300px;overflow:auto;white-space:pre-wrap;margin:0;color:#c9d1d9">Ready. Click "Detect Adom Bridge" to begin.</pre>+  <script>+  (function () {+    var AB = "http://127.0.0.1:24680";+    var log = document.getElementById("ab-log");+    var dot = document.getElementById("ab-dot");+    document.getElementById("ab-origin").textContent = location.origin || "(null: sandboxed frame)";+    function line(msg, color) {+      var t = new Date().toLocaleTimeString();+      log.textContent += "\n[" + t + "] " + msg;+      if (color) { log.style.color = color; }+      log.scrollTop = log.scrollHeight;+    }+    function reset() { log.textContent = "-"; log.style.color = "#c9d1d9"; }+    document.getElementById("ab-detect").onclick = function () {+      reset(); line("GET " + AB + "/health ...");+      fetch(AB + "/health").then(function (r) { return r.ok ? r.json() : Promise.reject("HTTP " + r.status); })+        .then(function (j) {+          if (j && j.service === "adom-bridge") { dot.style.background = "#00b8b0"; line("Adom Bridge is RUNNING on this machine. service=" + j.service, "#7ee787"); line(JSON.stringify(j, null, 2)); }+          else { line("Reached something, but it is not Adom Bridge: " + JSON.stringify(j), "#e6c86c"); }+        })+        .catch(function (e) { dot.style.background = "#484f58"; line("No Adom Bridge reachable (" + e + ").", "#8b949e"); line("Either it is not installed/running here, or this frame's origin is not https://wiki.adom.inc (the only origin it answers)."); });+    };+    document.getElementById("ab-status").onclick = function () {+      reset(); line("GET " + AB + "/status ...");+      fetch(AB + "/status").then(function (r) { return r.ok ? r.json() : Promise.reject("HTTP " + r.status); })+        .then(function (j) { dot.style.background = "#00b8b0"; line("Status (read-only, allowed from the web):", "#7ee787"); line(JSON.stringify(j, null, 2)); })+        .catch(function (e) { line("status failed: " + e, "#f0883e"); });+    };+    document.getElementById("ab-kicad").onclick = function () {+      reset(); line("POST " + AB + "/command {kicad_open_board} - expecting REFUSED ...");+      fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ app: "kicad", command: "kicad_open_board", args: {} }) })+        .then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })+        .then(function (res) {+          if (res.status === 403 || (res.body && res.body.errorCode === "web_forbidden")) { line("REFUSED, as designed. HTTP " + res.status + " errorCode=" + (res.body && res.body.errorCode), "#7ee787"); line("This is the safety wall: a web page cannot run a machine-changing verb. The consent-gated action phase will change this to 'ask the human in Adom Bridge first'."); line(JSON.stringify(res.body, null, 2)); }+          else { line("UNEXPECTED: POST /command was NOT refused (HTTP " + res.status + "). Please report this.", "#f85149"); line(JSON.stringify(res.body, null, 2)); }+        })+        .catch(function (e) { line("The browser blocked the POST before Adom Bridge answered (" + e + ").", "#e6c86c"); line("That is also a refusal: with no CORS grant for a non-allowlisted origin, page JS cannot read the response."); });+    };+  })();+  </script>+</div>  ## Versioning