← Commit history

2.0.39: KiCad button drives kicad_demo (progress bar + shots)

John Lauer ·8fe5b7fbf5 ·2mo ago ·parent ed6f883
1 file changed +130−5
web-control-demo.html+130−5
@@ -35,6 +35,10 @@   .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; }+  .pbar { margin: 8px 0 2px; height: 16px; background: #010409; border: 1px solid #21262d; border-radius: 8px; overflow: hidden; position: relative; }+  .pbar-fill { height: 100%; width: 0; background: linear-gradient(90deg, #1f6feb, #2ea043); transition: width .35s ease; }+  .pbar-meta { font-size: 11px; color: #9aa4af; margin-bottom: 4px; }+  .pbar-meta .beat { color: #6e7681; }   .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; }@@ -326,6 +330,123 @@ for (const w of mine) {       .catch(function (e) { setRes(c.res, "err", "blocked", String(e)); finish(); });   } +  // ---- Bridge-OWNED demo verb: one long-running call, real progress bar ----+  // This is the modern pattern: the BRIDGE owns a "demo" verb that runs the whole+  // tour, reports structured progress (percent/stepLabel/beat/elapsed/estimated),+  // and returns its full window state as inline screenshots. The web page just+  // starts it and polls - no piecing bridge commands together, no title-guessing.+  // (The KiCad bridge shipped kicad_demo in 0.9.92; Fusion's fusion_demo is next.)+  function progressBar(card) {+    var box = card.querySelector("div.pbar");+    if (!box) {+      var meta = document.createElement("div"); meta.className = "pbar-meta"; meta.textContent = "starting ...";+      box = document.createElement("div"); box.className = "pbar";+      var fill = document.createElement("div"); fill.className = "pbar-fill"; box.appendChild(fill);+      // insert right after the .res line+      var res = card.querySelector("div.res");+      if (res && res.nextSibling) { card.insertBefore(meta, res.nextSibling); card.insertBefore(box, meta.nextSibling); }+      else { card.appendChild(meta); card.appendChild(box); }+    }+    return { fill: box.querySelector(".pbar-fill"), meta: card.querySelector(".pbar-meta") };+  }+  function setProgress(card, pct, label, beatIdx, beatCount, elapsed, estimate) {+    var p = progressBar(card);+    p.fill.style.width = Math.max(0, Math.min(100, pct || 0)) + "%";+    var beat = (beatCount ? ' <span class="beat">- step ' + Math.min((beatIdx || 0) + 1, beatCount) + "/" + beatCount + "</span>" : "");+    var timing = (elapsed != null && estimate != null) ? ' <span class="beat">- ' + elapsed + "s of ~" + estimate + "s</span>" : "";+    p.meta.innerHTML = (pct != null ? pct + "% " : "") + esc(label || "") + beat + timing;+  }+  function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]; }); }++  // Drive a bridge's demo verb: start (background) -> poll progress -> render the+  // returned window array. cfg = { startCmd, startArgs, pollArgs, stateCmd }.+  function runBridgeDemo(c, cfg) {+    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”) ...", "the demo runs many steps - a 1-hour grant keeps the progress silent");+    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) {+      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)); }+        c.btn.disabled = false; return;+      }+      var est = res.j.estimatedSec || 100;+      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");+      // 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, cfg.pollArgs || { progress: true }).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);+          }+          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 + ")" : ""));+          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); }+            return;+          }+          return new Promise(function (r) { setTimeout(r, 2500); }).then(poll);+        }).catch(function (e) { return new Promise(function (r) { setTimeout(r, 2500); }).then(poll); });+      }+      poll();+    }).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) {+    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) {+      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.");+        c.btn.disabled = false;+      }+    }).catch(function (e) { setRes(c.res, "err", "state fetch failed", String(e)); c.btn.disabled = false; });+  }+  // Find the bridge's window array wherever it rides: the final poll embeds it at+  // result.state.shots; kicad_state returns it at shots. Handle every known path.+  function findShots(d) {+    var cands = [d.shots, d.screenshots, d.windows,+      d.data && d.data.shots, d.state && d.state.shots,+      d.result && d.result.state && d.result.state.shots,+      d.result && d.result.shots];+    for (var i = 0; i < cands.length; i++) {+      if (Array.isArray(cands[i]) && cands[i].length) return cands[i];+    }+    return [];+  }+  // Render a bridge state payload's screenshots (each item: {title, image, ...}).+  // Returns true if it rendered any window, false if there was nothing to show.+  function renderShots(c, d) {+    var narrate = (d.result && d.result.narrate) || d.narrate;+    var failed = (d.result && d.result.failedSteps) || d.failedSteps;+    if (narrate) clog(c, "  bridge says: " + narrate);+    if (Array.isArray(failed) && failed.length) clog(c, "  note: the bridge could not open: " + failed.join(", "));+    var shots = findShots(d);+    var withImg = shots.filter(function (s) { return s && (s.image || s.base64 || s.png); });+    if (!withImg.length) return false;+    clog(c, "Captured the bridge's full window state: " + 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", "Demo done - full state below", withImg.length + " window(s)");+    c.btn.disabled = false;+    return true;+  }+   // 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(", "); }@@ -361,12 +482,16 @@ for (const w of mine) {   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)");+  var cKicad = card("grp-do", "verb", "Run the KiCad demo (bridge-driven, live progress)");   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"] });+    // The KiCad bridge OWNS this demo (kicad_demo, 0.9.92+): it builds a sample+    // project, opens the symbol/footprint/3D/board/schematic views, reports real+    // progress, and returns every window as a screenshot. We just start + poll.+    runBridgeDemo(cKicad, {+      startCmd: "kicad_demo", startArgs: { all: true, background: true },+      pollCmd: "kicad_demo", pollArgs: { progress: true },+      stateCmd: "kicad_state"+    });   };   var cFusion = card("grp-do", "verb", "Open Fusion 360");   cFusion.btn.onclick = function () {