← Commit history

web-control demo 2.0.53: machine labels on every run (resolved self name), fusion_state single-window rendering, picker self-dedupe

John Lauer ·a66fb85b77 ·2mo ago ·parent cbf8fc3
1 file changed +57−16
web-control-demo.html+57−16
@@ -66,7 +66,7 @@     <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>+    <span class="pill" id="target-count">self only until detected</span>   </div>    <div class="grp">Detect - open to any page, no permission prompt</div>@@ -165,6 +165,25 @@ for (const w of mine) {   document.getElementById("origin").textContent = location.origin || "(null)";   function logLine(m) { log.textContent += "\n" + m; log.scrollTop = log.scrollHeight; }   function chosenTarget() { return sel.value || null; }+  // WHO is "this machine": ab 2.0.53+ reports its relay client name at+  // /status.machine, so every card can say "on AdomLapper" instead of a vague+  // "this machine" (John: "always tell me which machine this demo is running on").+  var selfName = null;+  function loadSelfName() {+    return fetch(AB + "/status").then(function (r) { return r.json(); }).then(function (s) {+      var m = s && s.machine;+      if (m && (m.clientName || m.hostname)) selfName = m.clientName || m.hostname;+      return selfName;+    }).catch(function () { return null; });+  }+  loadSelfName();+  // The label for where a run lands. Verbs that pass no target run on SELF.+  function machineLabel(target) {+    if (target === undefined) target = chosenTarget();+    if (target === "all") return "ALL machines";+    return target || selfName || "this machine";+  }+  function selfLabel() { return selfName || "this machine"; }   function setRes(el, cls, tag, detail) {     el.className = "res " + cls;     el.innerHTML = "<span class='tag'>" + tag + "</span>" + (detail ? " <small>" + detail + "</small>" : "");@@ -188,13 +207,14 @@ for (const w of mine) {   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));+    var who = machineLabel(target || null);+    setRes(el, "pending", "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast");+    logLine("POST /command " + JSON.stringify(body) + "   [on " + who + "]");     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 &amp; ran", summarize ? summarize(flat(res.j)) : "");+        if (res.s === 200) { var sum = summarize ? summarize(flat(res.j)) : ""; setRes(el, "ok", "Allowed &amp; ran", "on " + esc(who) + (sum ? " - " + sum : "")); }         else if (res.j && res.j.errorCode === "web_control_disabled") setRes(el, "err", "Refused: web control OFF", "re-enable in Settings &rarr; Web Control");         else setRes(el, "err", "Not approved", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s));       })@@ -371,7 +391,8 @@ for (const w of mine) {     cardLog(c.card).textContent = "";     var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";     setProgress(c.card, 0, "starting ...");-    setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "the demo runs many steps - a 1-hour grant keeps the progress silent");+    setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "runs on " + esc(selfLabel()) + " - the demo runs many steps; a 1-hour grant keeps the progress silent");+    clog(c, "Target machine: " + selfLabel() + " (demos always run on the machine serving this page)");     clog(c, "Starting the bridge's own demo verb: " + cfg.startCmd + " " + JSON.stringify(cfg.startArgs || {}));     // 1) Start the background tour (cross-origin -> ONE consent prompt).     postVerb(cfg.startCmd, cfg.startArgs || {}).then(function (res) {@@ -451,6 +472,14 @@ for (const w of mine) {     for (var i = 0; i < cands.length; i++) {       if (Array.isArray(cands[i]) && cands[i].length) return cands[i];     }+    // fusion_state (1.9.42+) is a SINGLE-window server-side render: a top-level+    // base64 `image` (with imageAs:"base64") plus title/hwnd. Wrap it so the+    // one-window shape renders through the same path as the arrays.+    var dd = d.data || d;+    var single = dd.image || dd.imageBase64 || dd.screenshotBase64;+    if (typeof single === "string" && single.length > 100) {+      return [{ title: dd.title || "app window", image: single }];+    }     return [];   }   // Render a bridge state payload's screenshots (each item: {title, image, ...}).@@ -475,7 +504,7 @@ for (const w of mine) {     }     clog(c, "Rendering " + withImg.length + " window(s).");     withImg.forEach(function (s) { addShot(c.card, s.image || s.base64 || s.png, s.title || s.label || s.window || "window"); });-    setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s)");+    setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s) on " + esc(selfLabel()));     c.btn.disabled = false;     return true;   }@@ -498,7 +527,8 @@ for (const w of mine) {     c.btn.disabled = true;     cardLog(c.card).textContent = "";     var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";-    setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", cfg.note || "opens in the background, then we screenshot it");+    setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));+    clog(c, "Target machine: " + selfLabel());     var staged = cfg.stage ? cfg.stage(c) : Promise.resolve();     staged.then(function () {       var args = (typeof cfg.showArgs === "function") ? cfg.showArgs() : (cfg.showArgs || {});@@ -600,7 +630,7 @@ for (const w of mine) {   // 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 targetsSummary(j) { var d = j.data || j; var a = d.targets || d.clients || []; 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(", "); } @@ -611,7 +641,8 @@ for (const w of mine) {     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 || "?");+        if (s.machine && (s.machine.clientName || s.machine.hostname)) selfName = s.machine.clientName || s.machine.hostname;+        document.getElementById("ver").textContent = "v" + (s.version || "?") + (selfName ? " on " + selfName : "");         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"));@@ -894,7 +925,7 @@ for (const w of mine) {   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";+    var who = machineLabel();     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); }@@ -906,14 +937,24 @@ for (const w of mine) {   // ---- 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" }) })+    // Resolve the self name first so the dedupe + "self" label are right even+    // when Detect was never clicked (an old ab just returns no machine block).+    (selfName ? Promise.resolve(selfName) : loadSelfName())+      .then(function () { return 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";+        // The direct API answers with clients[]; the relay-CLI shape is targets[].+        var d = j.data || j; var list = d.targets || d.clients || [];+        // The relay lists THIS machine by name too - that row is redundant with+        // the "self" option, so fold it in (it names the self option instead).+        var peers = list.filter(function (t) {+          var n = (t.name || t.hostname || "").toLowerCase();+          return !(selfName && n === selfName.toLowerCase());+        });+        sel.innerHTML = '<option value="">this machine (' + esc(selfLabel()) + ')</option>';+        peers.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 (peers.length >= 1) { var a = document.createElement("option"); a.value = "all"; a.textContent = "ALL machines (fan out)"; sel.appendChild(a); }+        document.getElementById("target-count").textContent = selfLabel() + (peers.length ? " + " + peers.length + " more" : " only");       })       .catch(function (e) { logLine("targets failed: " + e); });   };