← Commit history

demo: fluid layout (was pinned 1000px, wasting ~840px on a wide window), scrollbar to the true right edge (stable not both-edges), progress bars land on a final frame instead of freezing mid-run, and no orange permission alarm when a standing grant means no prompt ever appears

John Lauer ·ce02a4a225 ·2mo ago ·parent 185cd74
1 file changed +84−13
web-control-demo.html+84−13
@@ -9,7 +9,21 @@ <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; }+  /* Border-box everywhere so padding can never push a child past the viewport+     and create a page-level horizontal scrollbar (always a bug, never a feature). */+  *, *::before, *::after { box-sizing: border-box; }++  /* FLUID column. It was pinned at 1000px, which on a 1900px window left ~420px+     of dead space on EACH side and made the page look like it was ignoring the+     window. It now grows with the viewport up to a readable ceiling, and the+     .cards grid (auto-fill minmax) turns that extra width into more columns+     instead of longer lines. Padding scales with the viewport too. */+  .wrap {+    width: 100%;+    max-width: 1760px;+    margin: 0 auto;+    padding: 24px clamp(16px, 2.5vw, 40px);+  }    /* ── Scrollbars: the Adom pretty one, per adom-ui-design/skills/scrollbars ──      A default OS scrollbar in an Adom app is hideous (John, 2026-08-18). The@@ -18,10 +32,12 @@   /* Firefox + the standard property */   * { scrollbar-width: thin; scrollbar-color: rgba(139,148,158,.38) transparent; } -  /* The centered .wrap is exactly the case the skill calls out: reserve the-     gutter on BOTH edges so the column does not jump sideways the moment a-     scrollbar appears. */-  html { scrollbar-gutter: stable both-edges; }+  /* `stable` (NOT `both-edges`): stable alone reserves the scrollbar's own gutter+     so the layout never jumps when it appears. `both-edges` additionally reserves+     a MATCHING gutter on the opposite side, which holds the scrollbar visibly off+     the right edge of the window - John caught that immediately. The centering+     argument for both-edges does not apply once the column is fluid. */+  html { scrollbar-gutter: stable; }    /* Chromium / WebKit */   ::-webkit-scrollbar { width: 10px; height: 10px; }@@ -73,6 +89,7 @@   .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.running { color: #8b949e; }   /* neutral: started, nothing to worry about */   .res.pending { color: #d29922; }   .res.ok { color: #3fb950; }   .res.err { color: #f85149; }@@ -302,9 +319,41 @@ for (const w of mine) {   }   function selfLabel() { return selfName || "this machine"; }   function setRes(el, cls, tag, detail) {+    // Any result CANCELS a queued "asking permission" escalation (see setAwaiting).+    el._awaitToken = (el._awaitToken || 0) + 1;+    // If this run never had to ask, say so. A standing grant is invisible+    // otherwise, and the user is left guessing whether approval happened.+    if (cls === "ok" && el._promptShown === false) {+      detail = (detail ? detail + " - " : "") + "auto-approved (no prompt needed)";+    }     el.className = "res " + cls;     el.innerHTML = "<span class='tag'>" + tag + "</span>" + (detail ? " <small>" + detail + "</small>" : "");   }++  // Start a run NEUTRALLY, and only claim we are waiting on a human if we still+  // are after a beat.+  //+  // Every card used to announce "asking permission (choose Allow 1 hr)" in ORANGE+  // the instant it started - even when the origin was already on Auto mode, where+  // no prompt ever appears and the call returns in milliseconds. That reads as a+  // permission PROBLEM when there is none (John, 2026-08-20). The page cannot+  // simply read the grant list: a cross-origin /status deliberately projects it+  // away (that disclosure was fixed in ab 2.0.65), so the honest signal available+  // here is TIME - if the call has not come back, a human is being asked.+  function setAwaiting(el, promptTag, promptDetail, runningDetail) {+    el._promptShown = false;+    el.className = "res running";+    el.innerHTML = "<span class='tag'>running ...</span>" ++      (runningDetail ? " <small>" + runningDetail + "</small>" : "");+    var token = (el._awaitToken = (el._awaitToken || 0) + 1);+    setTimeout(function () {+      if (el._awaitToken !== token) return;           // already resolved - no prompt happened+      el._promptShown = true;+      el.className = "res pending";+      el.innerHTML = "<span class='tag'>" + promptTag + "</span>" ++        (promptDetail ? " <small>" + promptDetail + "</small>" : "");+    }, 1200);+  }   // Most desktop verbs return {success, output:"<stringified json>"}. Parse output up.   function flat(j) {     if (j && typeof j.output === "string" && j.output.charAt(0) === "{") {@@ -326,7 +375,7 @@ for (const w of mine) {     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 (no banner? Windows may have tucked it in the notification center - press Win+N)");+    setAwaiting(el, "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast (no banner? Windows may have tucked it in the notification center - press Win+N)", "on " + esc(who));     logLine("POST /command " + JSON.stringify(body) + "   [on " + who + "]");     postRaw(body)       .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })@@ -683,7 +732,7 @@ 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”) ...", "runs on " + esc(who) + " - the demo runs many steps; a 1-hour grant keeps the progress silent");+    setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...", "runs on " + esc(who) + " - the demo runs many steps; a 1-hour grant keeps the progress silent", "runs on " + esc(who));     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).@@ -910,11 +959,12 @@ for (const w of mine) {       // The card disables its button for the whole run and re-enables it on       // EVERY exit path, so that flag is the one stop condition that cannot       // leave this loop polling forever.-      if (!c.btn.disabled) return;+      if (!c.btn.disabled) { finishSurfaceBar(c); return; }       postVerb(pollCmd, {}).then(function (res) {         if (!c.btn.disabled) return;         var block = readProgressBlock(res.j);         if (block) {+          c._sawProgress = true;           renderProgressBlock(c.card, block);           var ss = readStepShot(block);           var img = ss && (ss.image || ss.base64);@@ -931,11 +981,31 @@ for (const w of mine) {     })();   } +  // Land the bar on a FINAL frame when the run ends.+  //+  // The poll loop stops the moment the card re-enables its button, and it used to+  // just return - leaving the bar frozen wherever the last poll happened to catch+  // it. That produced the thing John caught: a card reading "Done - the surface is+  // below" in green above a bar still saying "show_footprint starting - 23% - 3s+  // left". The verb finished between polls, so the last frame was mid-flight and+  // nothing ever superseded it.+  //+  // The outcome is read from the card's own result class rather than assumed, so a+  // FAILED run does not get a triumphant 100% - it lands red and says stopped.+  function finishSurfaceBar(c) {+    if (!c._sawProgress) return;               // no bar was ever drawn; nothing to land+    c._sawProgress = false;                    // once per run+    var ok = c.res && c.res.classList && c.res.classList.contains("ok");+    renderProgressBlock(c.card, ok+      ? { percent: 100, stepLabel: "done", etaSec: 0 }+      : { percent: 100, stepLabel: "stopped", failed: true });+  }+   function showSurface(c, cfg) {     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”) ...", "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));+    setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...", "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"), "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));     clog(c, "Target machine: " + selfLabel());     startSurfaceProgress(c, cfg.showCmd);     var staged = cfg.stage ? cfg.stage(c) : Promise.resolve();@@ -1089,7 +1159,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”) ...",+    setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...",+      "on " + esc(who) + " - one slow verb, animated purely from its progress block",       "on " + esc(who) + " - one slow verb, animated purely from its progress block");      var finished = false, frames = 0, sawMoving = false, lastLabel = null;@@ -1397,7 +1468,7 @@ for (const w of mine) {   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");+    setAwaiting(cShot.res, "asking permission...", "approve the toast", "capturing the screen");     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");@@ -1416,7 +1487,7 @@ for (const w of mine) {   var notifyId = null;   cNotify.btn.onclick = function () {     notifyId = "webdemo-" + (Math.floor(Math.random() * 1e9));-    setRes(cNotify.res, "pending", "asking permission...", "approve the toast");+    setAwaiting(cNotify.res, "asking permission...", "approve the toast", "sending the toast");     postVerb("notify_user", { title: "Hello from a web page", body: "Sticky toast. Click Got it or Dismiss, then Check toast state.", scenario: "reminder", id: notifyId, buttons: [{ label: "Got it" }, { label: "Dismiss" }] }, chosenTarget()).then(function (res) {       var act = res.j && res.j.action;       if (res.s === 200 && act && String(act).toLowerCase().indexOf("error") === 0) { setRes(cNotify.res, "err", "notify rejected", String(act).slice(0, 140)); return; }@@ -1451,7 +1522,7 @@ for (const w of mine) {   cShotSel.btn.onclick = function () {     var sb = cShotSel.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";     var who = machineLabel();-    setRes(cShotSel.res, "pending", "asking permission...", "capturing " + who);+    setAwaiting(cShotSel.res, "asking permission...", "capturing " + who, "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");