app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
Web-control test harness (served directly; drives ab's loopback API)
1 file changed
+451
web-control-demo.htmladded+451@@ -0,0 +1,451 @@+<!doctype html>+<html lang="en">+<head>+<meta charset="utf-8" />+<meta name="viewport" content="width=device-width, initial-scale=1" />+<title>Adom Bridge web control - local test harness</title>+<style>+ :root { color-scheme: dark; }+ html, body { margin: 0; padding: 0; background: #0d1117; color: #e6edf3; font-family: system-ui, "Segoe UI", sans-serif; }+ .wrap { max-width: 1000px; margin: 0 auto; padding: 24px; }+ h1 { font-size: 20px; margin: 0 0 4px; }+ .sub { font-size: 12px; color: #8b949e; margin-bottom: 18px; }+ .sub code { color: #79c0ff; }+ .bar { display: flex; align-items: center; gap: 10px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px 14px; margin-bottom: 18px; flex-wrap: wrap; }+ .bar label { font-size: 12px; color: #8b949e; }+ select { background: #0d1117; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px; padding: 6px 8px; font-size: 13px; }+ #dot { width: 11px; height: 11px; border-radius: 50%; background: #484f58; display: inline-block; }+ .grp { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #6e7681; margin: 18px 0 8px; }+ .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; align-items: start; }+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px; display: flex; flex-direction: column; gap: 8px; }+ .card button { padding: 8px 10px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e6edf3; cursor: pointer; font-size: 13px; text-align: left; transition: opacity .12s, border-color .12s; }+ .card.read button { background: #12181f; }+ .card.verb button { background: #1a2230; border-color: #2b3a55; }+ .card.multi button { background: #24202e; border-color: #4a3a63; }+ .card button:hover:not(:disabled) { border-color: #8b949e; }+ .card button:disabled { opacity: .5; cursor: default; }+ .res { font-size: 12px; line-height: 1.35; min-height: 16px; color: #8b949e; word-break: break-word; }+ .res .tag { font-weight: 600; }+ .res.idle { color: #6e7681; }+ .res.pending { color: #d29922; }+ .res.ok { color: #3fb950; }+ .res.err { color: #f85149; }+ .res small { color: #8b949e; font-weight: 400; }+ .slog { margin: 4px 0 0; padding: 8px; background: #010409; border: 1px solid #21262d; border-radius: 4px; font-size: 11px; max-height: 180px; overflow: auto; white-space: pre-wrap; color: #9aa4af; line-height: 1.45; }+ .shots { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }+ .shots .cap { font-size: 10px; color: #8b949e; margin-bottom: 3px; }+ .shots img { max-width: 100%; border: 1px solid #30363d; border-radius: 4px; display: block; }+ .pill { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid #30363d; color: #8b949e; }+ details { margin-top: 20px; } summary { cursor: pointer; color: #8b949e; font-size: 12px; }+ pre#log { background: #010409; border: 1px solid #21262d; border-radius: 6px; padding: 12px; font-size: 11px; max-height: 240px; overflow: auto; white-space: pre-wrap; margin: 8px 0 0; color: #c9d1d9; }+ .doc { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px 18px; font-size: 13px; line-height: 1.55; color: #c9d1d9; }+ .doc h3 { font-size: 13px; margin: 18px 0 6px; color: #e6edf3; }+ .doc h3:first-of-type { margin-top: 4px; }+ .doc p { margin: 0 0 10px; }+ .doc code { background: #0d1117; border: 1px solid #21262d; border-radius: 4px; padding: 1px 5px; font-size: 12px; color: #79c0ff; }+ .doc pre { background: #010409; border: 1px solid #21262d; border-radius: 6px; padding: 12px; overflow: auto; margin: 0 0 10px; }+ .doc pre code { background: none; border: none; color: #c9d1d9; padding: 0; font-size: 12px; line-height: 1.55; display: block; white-space: pre; }+ .doc ul { margin: 6px 0 0; padding-left: 20px; } .doc li { margin: 5px 0; }+ .doc .warn { color: #d29922; }+</style>+</head>+<body>+<div class="wrap">+ <h1><span id="dot"></span> Adom Bridge web control - local test harness</h1>+ <div class="sub">+ Page origin: <code id="origin">...</code> · API: <code>http://127.0.0.1:24680</code>+ · ab: <span id="ver" class="pill">unknown</span>+ · web control: <span id="wc" class="pill">?</span>+ </div>++ <div class="bar">+ <label for="target">Run on machine:</label>+ <select id="target"><option value="">this machine (self)</option></select>+ <button id="refresh-targets" style="background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:13px">Detect my computers</button>+ <span class="pill" id="target-count">1 known</span>+ </div>++ <div class="grp">Detect - open to any page, no permission prompt</div>+ <div class="cards" id="grp-read"></div>++ <div class="grp">Info verbs - each asks your permission</div>+ <div class="cards" id="grp-info"></div>++ <div class="grp">Do something - each asks permission, streams live status, then captures the app's FULL window state</div>+ <div class="cards" id="grp-do"></div>++ <div class="grp">Multi-machine - fan a verb across every connected computer</div>+ <div class="cards" id="grp-multi"></div>++ <div class="grp">How a web page drives Adom Bridge (the embed pattern + sample code)</div>+ <div class="doc">+ <p>There is <b>no SDK and no library</b>. A web page drives the machine with a plain <code>fetch()</code> (AJAX) to Adom Bridge's loopback HTTP API at <code>http://127.0.0.1:24680</code>. That is the whole integration: the wiki, or any other Adom surface, embeds a page whose buttons do the fetches below. Every state-changing call pops an Allow / Deny prompt in the app before anything runs.</p>++ <h3>1. Detect it (open to any page, no prompt)</h3>+ <pre><code>const r = await fetch("http://127.0.0.1:24680/health");+const h = await r.json(); // { ok: true, service: "adom-bridge" }+const running = h.service === "adom-bridge";</code></pre>++ <h3>2. Run one verb (pops Allow / Deny in the app, then runs)</h3>+ <pre><code>const r = await fetch("http://127.0.0.1:24680/command", {+ method: "POST",+ headers: { "Content-Type": "application/json" },+ body: JSON.stringify({ command: "notify_user", args: { title: "Hi", body: "from a web page" } })+});+const out = await r.json();+// 200 = ran. 403 { errorCode: "web_denied" } = the user clicked Deny.+// A screenshot comes back as base64 you render inline:+// { command: "desktop_screenshot_base64", args: { hwnd: 12345, maxWidth: 900 } } -> out.image</code></pre>++ <h3>3. Stream a long op (open app → wait for its window → screenshot), live status over ONE connection</h3>+ <pre><code>const resp = await fetch("http://127.0.0.1:24680/command/stream", {+ method: "POST",+ headers: { "Content-Type": "application/json" },+ body: JSON.stringify({ label: "Open KiCad", steps: [+ { command: "kicad_open_symbol_editor", args: {}, label: "Launching KiCad", usuallySec: 30 },+ { waitForWindow: { titleContains: "Symbol Editor", timeoutSec: 90 }, label: "Waiting for the window" }+ ]})+});+// Read the Server-Sent-Events stream:+const reader = resp.body.getReader(), dec = new TextDecoder(); let buf = "";+for (;;) {+ const { value, done } = await reader.read(); if (done) break;+ buf += dec.decode(value, { stream: true }); let i;+ while ((i = buf.indexOf("\n\n")) >= 0) {+ const block = buf.slice(0, i); buf = buf.slice(i + 2);+ let ev = "message", data = "";+ block.split("\n").forEach(l => {+ if (l.startsWith("event:")) ev = l.slice(6).trim();+ else if (l.startsWith("data:")) data += l.slice(5).trim();+ });+ console.log(ev, JSON.parse(data)); // ev = start | progress | image | done+ }+}</code></pre>++ <h3>To capture an app's FULL state, screenshot every one of its windows</h3>+ <pre><code>// 1) list windows, 2) keep the app's, 3) screenshot EACH by hwnd (the parent/child array).+const wins = JSON.parse((await (await fetch(API + "/command", { method: "POST",+ headers: { "Content-Type": "application/json" },+ body: JSON.stringify({ command: "desktop_list_windows" }) })).json()).output).data.windows;+const mine = wins.filter(w => /kicad|symbol editor|pcb editor/i.test(w.title));+for (const w of mine) {+ const shot = await (await fetch(API + "/command", { method: "POST",+ headers: { "Content-Type": "application/json" },+ body: JSON.stringify({ command: "desktop_screenshot_base64", args: { hwnd: w.hwnd, maxWidth: 900 } }) })).json();+ render(shot.image, w.title); // shot.image is base64 PNG+}</code></pre>++ <h3>Security and embedding notes</h3>+ <ul>+ <li>Adom Bridge binds <b>127.0.0.1 only</b> and runs the human Allow / Deny gate on every command. Detection (<code>/health</code>, <code>/status</code>) is open; commands are gated. It also supports an optional cloud-K-V handshake so only the real signed-in wiki can even reach the prompt.</li>+ <li><code>http://127.0.0.1</code> is a browser <b>secure context</b>, so an HTTPS page reaching it is <b>not</b> blocked as mixed content, and Adom Bridge returns the CORS + Private-Network-Access headers a public page needs.</li>+ <li><span class="warn">Caveat - the wiki readme iframe cannot do this yet.</span> The wiki serves a readme.html inside a sandboxed, null-origin iframe with <code>Content-Security-Policy: ... connect-src 'self'</code>, which blocks the fetch to loopback. A normally-served page (like this one) works fine; the wiki-embedded case needs the wiki to allow <code>connect-src http://127.0.0.1:*</code> (filed as a wiki issue). Until then, embed this on a normally-served Adom page, not inside the sandboxed readme iframe.</li>+ </ul>+ </div>++ <details><summary>Full request/response log</summary><pre id="log">Ready.</pre></details>+</div>++<script>+(function () {+ var AB = "http://127.0.0.1:24680";+ var log = document.getElementById("log");+ var dot = document.getElementById("dot");+ var sel = document.getElementById("target");+ document.getElementById("origin").textContent = location.origin || "(null)";+ function logLine(m) { log.textContent += "\n" + m; log.scrollTop = log.scrollHeight; }+ function chosenTarget() { return sel.value || null; }+ function setRes(el, cls, tag, detail) {+ el.className = "res " + cls;+ el.innerHTML = "<span class='tag'>" + tag + "</span>" + (detail ? " <small>" + detail + "</small>" : "");+ }+ // Most desktop verbs return {success, output:"<stringified json>"}. Parse output up.+ function flat(j) {+ if (j && typeof j.output === "string" && j.output.charAt(0) === "{") {+ try { return Object.assign({}, j, JSON.parse(j.output)); } catch (e) {}+ }+ return j;+ }+ function esc(s) { return (s || "").replace(/[<>&]/g, function (c) { return { "<": "<", ">": ">", "&": "&" }[c]; }); }++ function doGet(el, path, summarize) {+ setRes(el, "pending", "checking...", "");+ logLine("GET " + AB + path);+ fetch(AB + path).then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })+ .then(function (res) { logLine(" <- HTTP " + res.s); if (res.s === 200) setRes(el, "ok", "OK", summarize ? summarize(res.j) : ""); else setRes(el, "err", "HTTP " + res.s, ""); })+ .catch(function (e) { setRes(el, "err", "unreachable", String(e)); });+ }+ function doVerb(el, command, args, target, summarize) {+ var body = { command: command }; if (args) body.args = args;+ if (target === undefined) target = chosenTarget(); if (target) body.target = target;+ setRes(el, "pending", "asking permission...", "approve the Adom Bridge toast");+ logLine("POST /command " + JSON.stringify(body));+ fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })+ .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })+ .then(function (res) {+ logLine(" <- HTTP " + res.s + " " + JSON.stringify(res.j).slice(0, 300));+ if (res.s === 200) setRes(el, "ok", "Allowed & ran", summarize ? summarize(flat(res.j)) : "");+ else if (res.j && res.j.errorCode === "web_control_disabled") setRes(el, "err", "Refused: web control OFF", "re-enable in Settings → Web Control");+ else setRes(el, "err", "Not approved", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s));+ })+ .catch(function (e) { setRes(el, "err", "blocked", String(e)); });+ }+ // POST that returns the flattened JSON. Optional target.+ function postVerb(command, args, target) {+ var body = { command: command }; if (args) body.args = args; if (target) body.target = target;+ logLine("POST /command " + JSON.stringify(body));+ return fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })+ .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: flat(j) }; }); });+ }+ // Loopback self POST (the window array + per-window screenshots run on THIS machine).+ function postSelf(command, args) {+ return fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: command, args: args || {} }) })+ .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: flat(j) }; }); });+ }+ function windowsList() {+ return postSelf("desktop_list_windows", {}).then(function (res) {+ var j = res.j, out = j.output; try { if (typeof out === "string") out = JSON.parse(out); } catch (e) {}+ return (out && out.data && out.data.windows) || (out && out.windows) || j.windows || [];+ });+ }++ function card(groupId, cls, label) {+ var c = document.createElement("div"); c.className = "card " + cls;+ var b = document.createElement("button"); b.textContent = label;+ var r = document.createElement("div"); r.className = "res idle"; r.textContent = "not run yet";+ c.appendChild(b); c.appendChild(r); document.getElementById(groupId).appendChild(c);+ return { btn: b, res: r, card: c };+ }+ // Per-card live stream log (right under the button).+ function cardLog(card) {+ var el = card.querySelector("pre.slog");+ if (!el) { el = document.createElement("pre"); el.className = "slog"; card.appendChild(el); }+ return el;+ }+ function clog(c, msg) { var el = cardLog(c.card); el.textContent += (el.textContent ? "\n" : "") + msg; el.scrollTop = el.scrollHeight; }+ // Per-card gallery of screenshots (the app's window array).+ function shotsBox(card) { var b = card.querySelector("div.shots"); if (!b) { b = document.createElement("div"); b.className = "shots"; card.appendChild(b); } return b; }+ function addShot(card, b64, label) {+ var box = shotsBox(card);+ var wrap = document.createElement("div");+ var cap = document.createElement("div"); cap.className = "cap"; cap.textContent = label;+ var img = document.createElement("img"); img.src = "data:image/png;base64," + b64;+ wrap.appendChild(cap); wrap.appendChild(img); box.appendChild(wrap);+ }++ // Capture the app's FULL window state: list windows, keep the ones matching this+ // app (include patterns, minus exclude patterns), and screenshot EACH by hwnd.+ // This is the parent/child window array - the real "state of the union" of the app.+ function captureWindows(c, spec) {+ clog(c, "Capturing the app's full window state (all its windows) ...");+ return windowsList().then(function (wins) {+ var inc = (spec.include || []).map(function (s) { return s.toLowerCase(); });+ var exc = (spec.exclude || []).map(function (s) { return s.toLowerCase(); });+ var match = wins.filter(function (w) { var t = (w.title || "").toLowerCase(); return inc.some(function (p) { return t.indexOf(p) >= 0; }) && !exc.some(function (p) { return t.indexOf(p) >= 0; }); });+ if (!match.length) { clog(c, " no windows matched - is the app actually open?"); setRes(c.res, "err", "no windows to capture", "the app may not have opened"); return; }+ clog(c, " found " + match.length + " window(s): " + match.map(function (w) { return '"' + (w.title || "").slice(0, 42) + '"'; }).join(", "));+ var i = 0;+ function next() {+ if (i >= match.length) { setRes(c.res, "ok", "Done - full state below", match.length + " window(s)"); clog(c, "DONE - captured the app's full window state (" + match.length + " windows)."); return; }+ var w = match[i++];+ return postSelf("desktop_screenshot_base64", { hwnd: w.hwnd, maxWidth: 900 }).then(function (sr) {+ if (sr.s === 200 && sr.j.image) { clog(c, " captured window " + i + "/" + match.length + ': "' + (w.title || "").slice(0, 50) + '" ' + sr.j.width + "x" + sr.j.height); addShot(c.card, sr.j.image, w.title || "window"); }+ else { clog(c, " could not capture: \"" + (w.title || "") + "\""); }+ return next();+ });+ }+ return next();+ });+ }++ // ---- SSE streaming (open -> wait for window), then capture the window array ----+ function onStream(c, ev, dataStr) {+ var d = {}; try { d = JSON.parse(dataStr); } catch (e) {}+ var t = Math.round((d.elapsedMs || 0) / 1000);+ var u = (d.usuallySec != null) ? (" (usually ~" + d.usuallySec + "s)") : "";+ var k = (d.tookMs != null) ? (" in " + (d.tookMs / 1000).toFixed(1) + "s") : "";+ var w = (d.waitedSec != null ? d.waitedSec : t);+ if (ev === "start") { clog(c, "Adom Bridge accepted the recipe (" + d.steps + " steps). Streaming live:"); return; }+ if (ev === "progress") {+ if (d.state === "running") { setRes(c.res, "pending", d.label + " ...", "running" + u); clog(c, "> step " + d.step + "/" + d.of + ": " + d.label + u + " ..."); }+ else if (d.state === "working") { setRes(c.res, "pending", d.label, w + "s - still working" + u); clog(c, " [" + w + "s] still working" + u + " ..."); }+ else if (d.state === "waiting") { setRes(c.res, "pending", "waiting for the window", w + "s" + u); clog(c, " [" + w + "s] waiting for the window" + u + " ..."); }+ else if (d.state === "appeared") { setRes(c.res, "pending", "window appeared" + k, d.window || ""); clog(c, ' window appeared: "' + (d.window || "") + '"' + k); }+ else if (d.state === "ok") { clog(c, " step " + d.step + " done" + k); }+ else if (d.state === "failed") { setRes(c.res, "err", "step " + d.step + " failed", JSON.stringify(d.result || {}).slice(0, 120)); clog(c, " step " + d.step + " FAILED" + k); }+ return;+ }+ if (ev === "error") { setRes(c.res, "err", "error", d.error || ""); clog(c, " ERROR: " + (d.error || "")); return; }+ if (ev === "done") {+ if (d.ok) { clog(c, " launch confirmed (total " + t + "s). Now grabbing the full window state ..."); if (c._capture) captureWindows(c, c._capture); }+ else { setRes(c.res, "err", "did not complete", d.reason || ("failed step " + d.failedStep)); clog(c, "DONE - did not complete."); }+ return;+ }+ }+ function streamRecipe(c, steps, capture) {+ c._capture = capture;+ c.btn.disabled = true;+ cardLog(c.card).textContent = "";+ var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";+ setRes(c.res, "pending", "opening a live connection ...", "approve the toast, then watch");+ logLine("POST /command/stream " + c.btn.textContent);+ function finish() { c.btn.disabled = false; }+ fetch(AB + "/command/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label: c.btn.textContent, steps: steps }) })+ .then(function (resp) {+ if (!resp.ok) { return resp.json().catch(function () { return {}; }).then(function (j) { setRes(c.res, "err", (j.errorCode === "web_control_disabled" ? "web control OFF" : "refused"), j.error || j.errorCode || ("HTTP " + resp.status)); finish(); }); }+ var reader = resp.body.getReader(), dec = new TextDecoder(), buf = "";+ function pump() {+ return reader.read().then(function (rr) {+ if (rr.done) { finish(); return; }+ buf += dec.decode(rr.value, { stream: true }); var idx;+ while ((idx = buf.indexOf("\n\n")) >= 0) {+ var raw = buf.slice(0, idx); buf = buf.slice(idx + 2);+ var ev = "message", data = ""; raw.split("\n").forEach(function (l) { if (l.indexOf("event:") === 0) ev = l.slice(6).trim(); else if (l.indexOf("data:") === 0) data += l.slice(5).trim(); });+ if (data) onStream(c, ev, data);+ }+ return pump();+ });+ }+ return pump();+ })+ .catch(function (e) { setRes(c.res, "err", "blocked", String(e)); finish(); });+ }++ // summaries+ function n(x) { return (x === undefined || x === null) ? 0 : x; }+ function bridgesSummary(j) { var d = j.data || j; var a = d.bridges || []; return a.length + " bridge(s): " + a.map(function (b) { return b.name; }).slice(0, 6).join(", "); }+ function targetsSummary(j) { var d = j.data || j; var a = d.targets || []; return (a.length || 1) + " machine(s): " + a.map(function (t) { return t.name || t.hostname; }).join(", "); }+ function windowsSummary(j) { var d = j.data || j; var a = d.windows || []; return a.length + " open. Top 3: " + a.slice(0, 3).map(function (w) { return '"' + (w.title || "").slice(0, 26) + '"'; }).join(", "); }+ function browsersSummary(j) { var d = j.data || j; var a = d.browsers || (Array.isArray(d) ? d : []); if (!Array.isArray(a)) a = []; return a.length + " installed. Top 3: " + a.slice(0, 3).map(function (b) { return b.name || b.browser || b.displayName || b.id || "browser"; }).join(", "); }++ // ---- Detect (reads) ----+ var cDetect = card("grp-read", "read", "Detect Adom Bridge");+ cDetect.btn.onclick = function () {+ setRes(cDetect.res, "pending", "checking...", "");+ fetch(AB + "/health").then(function (r) { return r.json(); }).then(function (h) {+ dot.style.background = (h && h.service === "adom-bridge") ? "#2ea043" : "#d29922";+ return fetch(AB + "/status").then(function (r) { return r.json(); }).then(function (s) {+ document.getElementById("ver").textContent = "v" + (s.version || "?");+ var wc = (s && s.webControl) || {}; var el = document.getElementById("wc");+ var on = wc.enabled !== false; el.textContent = on ? "ON" : "OFF"; el.style.color = on ? "#2ea043" : "#f85149";+ setRes(cDetect.res, "ok", "Running: v" + (s.version || "?"), "web control " + (on ? "ON" : "OFF"));+ });+ }).catch(function (e) { dot.style.background = "#484f58"; setRes(cDetect.res, "err", "not reachable", String(e)); });+ };+ var cStatus = card("grp-read", "read", "Read status");+ cStatus.btn.onclick = function () { doGet(cStatus.res, "/status", function (s) { var st = n(s.bridgeUpdates && s.bridgeUpdates.staleCount); return "v" + s.version + " - web control " + ((s.webControl && s.webControl.enabled === false) ? "OFF" : "ON") + (st ? (" - " + st + " bridge update(s)") : ""); }); };++ // ---- Info verbs (with top-3 lists) ----+ var cBridges = card("grp-info", "verb", "List installed bridges");+ cBridges.btn.onclick = function () { doVerb(cBridges.res, "bridge_list", null, undefined, bridgesSummary); };+ var cTargets = card("grp-info", "verb", "List my computers (targets)");+ cTargets.btn.onclick = function () { doVerb(cTargets.res, "targets", null, null, targetsSummary); };+ var cWindows = card("grp-info", "verb", "List open windows (top 3)");+ cWindows.btn.onclick = function () { doVerb(cWindows.res, "desktop_list_windows", null, undefined, windowsSummary); };+ var cApps = card("grp-info", "verb", "What apps are installed (top 3)");+ cApps.btn.onclick = function () { doVerb(cApps.res, "desktop_list_browsers", null, undefined, browsersSummary); };++ // ---- Do something: stream the launch, then capture the FULL window array ----+ var cKicad = card("grp-do", "verb", "Open KiCad (watched)");+ cKicad.btn.onclick = function () {+ streamRecipe(cKicad, [+ { command: "kicad_open_symbol_editor", args: {}, label: "Launching KiCad", usuallySec: 30 },+ { waitForWindow: { titleContains: "Symbol Editor", timeoutSec: 90 }, label: "Waiting for KiCad to come up", usuallySec: 8 }+ ], { include: ["kicad", "symbol editor", "pcb editor", "schematic editor", "footprint editor"], exclude: ["for testing", "harness", "adom bridge web", "adom wiki"] });+ };+ var cFusion = card("grp-do", "verb", "Open Fusion 360");+ cFusion.btn.onclick = function () {+ streamRecipe(cFusion, [+ { command: "fusion_start", args: {}, label: "Launching Autodesk Fusion", usuallySec: 90 },+ { waitForWindow: { titleContains: "Fusion", timeoutSec: 280 }, label: "Waiting for Fusion to come up", usuallySec: 120 }+ ], { include: ["fusion", "autodesk"], exclude: ["for testing", "harness"] });+ };+ var cSym = card("grp-do", "verb", "Open KiCad (Symbol Editor)");+ cSym.btn.onclick = function () {+ streamRecipe(cSym, [+ { command: "kicad_open_symbol_editor", args: {}, label: "Opening the KiCad Symbol Editor", usuallySec: 30 },+ { waitForWindow: { titleContains: "Symbol Editor", timeoutSec: 90 }, label: "Waiting for the Symbol Editor", usuallySec: 8 }+ ], { include: ["kicad", "symbol editor", "pcb editor", "schematic editor"], exclude: ["for testing", "harness", "adom bridge web", "adom wiki"] });+ };+ // Single full-screen shot (keep the simple case too).+ var cShot = card("grp-do", "verb", "Take a screenshot (whole screen)");+ cShot.btn.onclick = function () {+ var sb = cShot.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";+ setRes(cShot.res, "pending", "asking permission...", "approve the toast");+ postVerb("desktop_screenshot_base64", { maxWidth: 900 }, chosenTarget()).then(function (res) {+ if (res.s === 200 && res.j.image) { setRes(cShot.res, "ok", "Captured " + res.j.width + "x" + res.j.height, ""); addShot(cShot.card, res.j.image, "whole screen"); }+ else if (res.j && res.j.errorCode === "web_control_disabled") setRes(cShot.res, "err", "Refused: web control OFF", "re-enable in Settings");+ else setRes(cShot.res, "err", "no image", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s));+ }).catch(function (e) { setRes(cShot.res, "err", "blocked", String(e)); });+ };+ var cUrl = card("grp-do", "verb", "Open a URL (then screenshot the REAL browser)");+ cUrl.btn.onclick = function () {+ streamRecipe(cUrl, [+ { command: "desktop_open_url", args: { url: "https://wiki.adom.inc" }, label: "Opening wiki.adom.inc in your default browser", usuallySec: 2 },+ { waitForWindow: { titleContains: "Adom Wiki", timeoutSec: 30 }, label: "Waiting for the page to load", usuallySec: 4 }+ ], { include: ["adom wiki", "wiki.adom.inc"], exclude: ["for testing", "127.0.0.1", "harness", "puppeteer"] });+ };+ // Notify + VISIBLE state re-query.+ var cNotify = card("grp-do", "verb", "Notify me (sticky + query state)");+ var notifyId = null;+ cNotify.btn.onclick = function () {+ notifyId = "webdemo-" + (Math.floor(Math.random() * 1e9));+ setRes(cNotify.res, "pending", "asking permission...", "approve the toast");+ postVerb("notify_user", { title: "Hello from a web page", body: "Sticky toast. Click a button on it (or ignore it), then hit 'Check toast state'.", scenario: "reminder", id: notifyId, buttons: [{ text: "Got it", id: "ack" }, { text: "Dismiss", id: "dismiss" }] }, chosenTarget()).then(function (res) {+ if (res.s === 200) {+ if (res.j.id) notifyId = res.j.id;+ setRes(cNotify.res, "ok", "Toast shown (id " + notifyId + ")", "click a button on it, then Check toast state");+ if (!cNotify.card.querySelector("button.chk")) {+ var cb = document.createElement("button"); cb.className = "chk"; cb.textContent = "Check toast state";+ cb.style.cssText = "margin-top:6px;background:#12181f";+ cb.onclick = function () {+ setRes(cNotify.res, "pending", "re-querying toast state ...", "GET notify_state id=" + notifyId);+ postVerb("notify_state", { id: notifyId }, chosenTarget()).then(function (sr) {+ if (sr.s === 200) {+ var st = sr.j.state || "unknown";+ var extra = sr.j.action ? (" (you clicked: " + sr.j.action + ")") : (sr.j.onScreen ? " (still on screen)" : "");+ setRes(cNotify.res, st === "responded" ? "ok" : "pending", "state: " + st + extra, sr.j.note || "click a toast button, then check again");+ logLine(" notify_state -> " + JSON.stringify(sr.j).slice(0, 200));+ } else setRes(cNotify.res, "err", "state query failed", (sr.j && (sr.j.error || sr.j.errorCode)) || ("HTTP " + sr.s));+ }).catch(function (e) { setRes(cNotify.res, "err", "state query error", String(e)); });+ };+ cNotify.card.appendChild(cb);+ }+ } else if (res.j && res.j.errorCode === "web_control_disabled") setRes(cNotify.res, "err", "Refused: web control OFF", "re-enable in Settings");+ else setRes(cNotify.res, "err", "not approved", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s));+ }).catch(function (e) { setRes(cNotify.res, "err", "blocked", String(e)); });+ };++ // ---- Multi-machine ----+ var cNotifyAll = card("grp-multi", "multi", "Notify ALL my computers");+ cNotifyAll.btn.onclick = function () { doVerb(cNotifyAll.res, "notify_user", { title: "Fan-out from the web", body: "Sent to every connected Adom Bridge." }, "all", function (j) { var s = j.summary || (j.data && j.data.summary); return s ? (s.ok + "/" + s.total + " machines notified") : "sent to all machines"; }); };+ var cShotSel = card("grp-multi", "multi", "Screenshot the SELECTED machine");+ cShotSel.btn.onclick = function () {+ var sb = cShotSel.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";+ var who = chosenTarget() || "this machine";+ setRes(cShotSel.res, "pending", "asking permission...", "capturing " + who);+ postVerb("desktop_screenshot_base64", { maxWidth: 900 }, chosenTarget()).then(function (res) {+ if (res.s === 200 && res.j.image) { setRes(cShotSel.res, "ok", "Captured " + res.j.width + "x" + res.j.height, "on " + who); addShot(cShotSel.card, res.j.image, who); }+ else if (res.j && res.j.errorCode === "web_control_disabled") setRes(cShotSel.res, "err", "Refused: web control OFF", "re-enable in Settings");+ else setRes(cShotSel.res, "err", "no image", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s));+ }).catch(function (e) { setRes(cShotSel.res, "err", "blocked", String(e)); });+ };++ // ---- target picker ----+ document.getElementById("refresh-targets").onclick = function () {+ logLine("POST /command {targets}");+ fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "targets" }) })+ .then(function (r) { return r.json(); })+ .then(function (j) {+ var d = j.data || j; var list = d.targets || [];+ sel.innerHTML = '<option value="">this machine (self)</option>';+ list.forEach(function (t) { var o = document.createElement("option"); o.value = t.name || t.hostname; o.textContent = (t.name || t.hostname) + (t.version ? " (v" + t.version + ")" : ""); sel.appendChild(o); });+ if (list.length > 1) { var a = document.createElement("option"); a.value = "all"; a.textContent = "ALL machines (fan out)"; sel.appendChild(a); }+ document.getElementById("target-count").textContent = (list.length || 1) + " known";+ })+ .catch(function (e) { logLine("targets failed: " + e); });+ };+})();+</script>+</body>+</html>