← Commit history

picker-targeted demos + live per-step proof + log dedup

John Lauer ·0f667ee0bd ·2mo ago ·parent fa2f5fb
1 file changed +50−12
web-control-demo.html+50−12
@@ -227,6 +227,7 @@ 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;+    if (target) { body.args = body.args || {}; if (!body.args.caller) body.args.caller = { aiThread: "web-control demo page", containerName: selfLabel() }; }     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 + "]");@@ -243,6 +244,9 @@ for (const w of mine) {   // 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;+    // A relayed verb arrives at the OTHER machine via_relay, where ab requires+    // caller identity - name this page as the thread so the run is attributed.+    if (target) { body.args = body.args || {}; if (!body.args.caller) body.args.caller = { aiThread: "web-control demo page", containerName: selfLabel() }; }     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) }; }); });@@ -407,15 +411,22 @@ for (const w of mine) {   // Drive a bridge's demo verb: start (background) -> poll progress -> render the   // returned window array. cfg = { startCmd, startArgs, pollArgs, stateCmd }.   function runBridgeDemo(c, cfg) {+    // The demo HONORS the machine picker: pick ConfRoomROG and the tour runs+    // THERE (this page's ab relays the verbs to that machine). Captured ONCE+    // at click so changing the picker mid-run cannot split the run.+    var target = chosenTarget();+    if (target === "all") { setRes(c.res, "err", "pick ONE machine", "a demo streams per-machine progress - fan-out is not supported here"); return; }+    var who = machineLabel(target);+    c._who = who;     c.btn.disabled = true;     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”) ...", "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)");+    setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "runs on " + esc(who) + " - the demo runs many steps; a 1-hour grant keeps the progress silent");+    clog(c, "Target machine: " + who + (target ? " (relayed through this machine's Adom Bridge)" : " (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) {+    postVerb(cfg.startCmd, cfg.startArgs || {}, target).then(function (res) {       if (res.s !== 200 || !res.j || (res.j.started !== true && res.j.success !== true)) {         if (res.j && res.j.errorCode === "web_control_disabled") { setRes(c.res, "err", "Refused: web control OFF", "re-enable in Settings → Web Control"); }         else { setRes(c.res, "err", "could not start", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s)); }@@ -434,26 +445,51 @@ for (const w of mine) {       clog(c, "Started (bridge estimates ~" + est + "s). Polling for live progress ...");       setProgress(c.card, res.j.percent || 2, "preparing ...", 0, 0, 0, est);       setRes(c.res, "pending", "running the demo ...", "usually ~" + est + "s - watch the bar");+      // LIVE PROOF: at every step transition, snap the app's current state via+      // the bridge's state verb and pin it in the card - the user SEES each+      // stage of the tour as it happens, not just text + a final gallery+      // (John, 2026-08-16). One snap in flight at a time; a slow snap skips+      // steps rather than queueing behind the tour.+      var lastStep = null, snapBusy = false;+      function maybeSnap(label) {+        if (!cfg.snap || snapBusy) return;+        snapBusy = true;+        postVerb(cfg.snap.cmd, Object.assign({}, cfg.snap.args || {}), target).then(function (sr) {+          snapBusy = false;+          if (!(sr.s === 200 && sr.j)) return;+          var withImg = findShots(sr.j).filter(function (s) { return s && (s.image || s.base64 || s.png); });+          if (!withImg.length) return;+          var s = withImg[withImg.length - 1]; // the newest window is the step's surface+          addShot(c.card, s.image || s.base64 || s.png, "live: " + label);+          clog(c, "  [proof] captured \"" + (s.title || label) + "\"");+        }).catch(function () { snapBusy = false; });+      }       // 2) Poll progress every 2.5s (the Allow-1hr grant makes these silent).       var polls = 0, maxPolls = 240; // ~10 min ceiling       function poll() {         polls++;         if (polls > maxPolls) { setRes(c.res, "err", "timed out", "the demo did not finish in time"); c.btn.disabled = false; return; }-        postVerb(cfg.pollCmd || cfg.startCmd, pollArgs).then(function (pr) {+        postVerb(cfg.pollCmd || cfg.startCmd, pollArgs, target).then(function (pr) {           if (pr.s !== 200 || !pr.j) { return new Promise(function (r) { setTimeout(r, 2500); }).then(poll); }           var d = pr.j;           if (d.error && d.active === undefined && d.done === undefined) {             // job already gone - treat as done, go fetch the final state             clog(c, "  progress job ended; fetching final state ...");-            return finishDemo(c, cfg, est);+            return finishDemo(c, cfg, est, target);           }           setProgress(c.card, d.percent, d.stepLabel || d.step, d.beatIndex, d.beatCount, d.elapsedSec, d.estimatedSec || est);-          if (d.stepLabel) clog(c, "  " + (d.percent != null ? d.percent + "% " : "") + d.stepLabel + (d.beatCount ? " (step " + ((d.beatIndex || 0) + 1) + "/" + d.beatCount + ")" : ""));+          // Log + snap only on CHANGE - a 2.5s poll of a 30s step used to spam+          // the same line eight times.+          if (d.stepLabel && d.stepLabel !== lastStep) {+            lastStep = d.stepLabel;+            clog(c, "  " + (d.percent != null ? d.percent + "% " : "") + d.stepLabel + (d.beatCount ? " (step " + ((d.beatIndex || 0) + 1) + "/" + d.beatCount + ")" : ""));+            maybeSnap(d.stepLabel);+          }           if (d.done === true || d.active === false) {             setProgress(c.card, 100, "done", d.beatCount, d.beatCount, d.elapsedSec, d.estimatedSec || est);             // The bridge rides its final state on the last poll (result.state.shots).             // Render it if present; otherwise ask the state verb.-            if (!renderShots(c, d)) { return finishDemo(c, cfg, est); }+            if (!renderShots(c, d)) { return finishDemo(c, cfg, est, target); }             return;           }           return new Promise(function (r) { setTimeout(r, 2500); }).then(poll);@@ -463,10 +499,10 @@ for (const w of mine) {     }).catch(function (e) { setRes(c.res, "err", "blocked", String(e)); c.btn.disabled = false; });   }   // Pull the bridge's full state (the window array) and render it.-  function finishDemo(c, cfg, est) {+  function finishDemo(c, cfg, est, target) {     if (!cfg.stateCmd) { setRes(c.res, "ok", "Demo finished", "the bridge closed its windows"); c.btn.disabled = false; return; }     clog(c, "Fetching the bridge's full window state: " + cfg.stateCmd);-    return postVerb(cfg.stateCmd, {}).then(function (sr) {+    return postVerb(cfg.stateCmd, {}, target).then(function (sr) {       if (!(sr.s === 200 && sr.j && renderShots(c, sr.j))) {         setRes(c.res, "ok", "Demo finished", "the bridge already closed its windows - nothing to show");         clog(c, "DONE - the demo completed; the bridge closed its windows before we captured state.");@@ -524,7 +560,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) on " + esc(selfLabel()));+    setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s) on " + esc(c._who || selfLabel()));     c.btn.disabled = false;     return true;   }@@ -691,7 +727,8 @@ for (const w of mine) {     runBridgeDemo(cKicad, {       startCmd: "kicad_demo", startArgs: { all: true, background: true },       pollCmd: "kicad_demo", pollArgs: { progress: true },-      stateCmd: "kicad_state"+      stateCmd: "kicad_state",+      snap: { cmd: "kicad_state", args: { maxWidth: 640 } }     });   };   var cFusion = card("grp-do", "verb", "Run the Fusion demo (bridge-driven, live progress)");@@ -705,7 +742,8 @@ for (const w of mine) {       startCmd: "fusion_demo", startArgs: { mode: "tour" },       pollCmd: "fusion_demo_progress", pollArgs: {},       idFrom: "demoId", idAs: "demoId",-      stateCmd: "fusion_state"+      stateCmd: "fusion_state",+      snap: { cmd: "fusion_state", args: { screenshot: true, imageAs: "base64", maxWidth: 640 } }     });   };   // ---- The wiki part every surface button shows: Inolux IN-S42ATR ----