From 40ec12cc4eca199f85b11091e2547b4d6f32aae4 Mon Sep 17 00:00:00 2001 From: voidReq <100312344+voidReq@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:58:11 -0400 Subject: [PATCH] =?UTF-8?q?Assistant=20starts=20on=20the=20homelab=20show?= =?UTF-8?q?=20at=20once:=20fast=20reads=20for=2045=20s,=20the=20button=20s?= =?UTF-8?q?tays=20Starting=E2=80=A6=20until=20its=20session=20shows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/REMOTE.md | 3 ++- src/nuauto/gui.py | 9 ++++++--- src/nuauto/gui_static/app.js | 25 +++++++++++++++++++------ tests/test_remote.py | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/REMOTE.md b/docs/REMOTE.md index 1d233ea..df430ce 100644 --- a/docs/REMOTE.md +++ b/docs/REMOTE.md @@ -239,7 +239,8 @@ THE LAPTOP'S LINK (link.py, 2026-10-15, my request: "as much support and smooth page only ever talks to the laptop's server (/api/homelab/...). What the homelab's page refuses, it refuses here too, with the same words: the same rules, checked in one place. - Linked, the laptop's app also: reads the homelab's sessions, queue and running task through it every 20 s (ssh every - 60 s without it); opens a homelab session's screen inside this window (#/assistant/h: approve its Submit, reply, + 60 s without it; every 3 s for 45 s after a start or an answer there, gui.homelab_soon, so a new session shows at once + and its row's button says "Starting…" until it does); opens a homelab session's screen inside this window (#/assistant/h: approve its Submit, reply, end it); shows each company-site row's place in the homelab's queue, with "Start on the homelab" next to "Start here" (greyed out with the queue's reason when it can't start there now); Apply's "Run on: This computer / The homelab" (the homelab's page runs Apply on its virtual screen, one NUworks machine at a time as always; its log, screenshots, diff --git a/src/nuauto/gui.py b/src/nuauto/gui.py index 54cc245..2e40795 100644 --- a/src/nuauto/gui.py +++ b/src/nuauto/gui.py @@ -1413,7 +1413,9 @@ def homelab_review(): HOMELAB_EVERY = 60 # seconds between reads of the homelab's sessions over ssh (in the background) LINK_EVERY = 20 # the same through the app's link (sessions, queue, its running task: one cheap request each) -_homelab = {"at": 0.0, "sessions": [], "busy": False, "queue": None, "task": None, "commit": None, "error": None} +SOON_EVERY, SOON_FOR = 3, 45 # after a start or an answer there: every 3 s for 45 s (a new session takes a few seconds to show) +_homelab = {"at": 0.0, "sessions": [], "busy": False, "queue": None, "task": None, "commit": None, "error": None, + "soon_until": 0.0} def homelab_assistants(): @@ -1423,7 +1425,7 @@ def homelab_assistants(): if VIEW or config.DEMO and not config.DEMO_LINK or not config.REMOTE_APP or config.IS_SERVER: return [] from nuauto import link - every = LINK_EVERY if link.linked() else HOMELAB_EVERY + every = SOON_EVERY if time.time() < _homelab["soon_until"] else LINK_EVERY if link.linked() else HOMELAB_EVERY if time.time() - _homelab["at"] > every and not _homelab["busy"]: _homelab["busy"] = True threading.Thread(target=homelab_read, daemon=True).start() @@ -1449,8 +1451,9 @@ def homelab_read(): def homelab_soon(): - """Read the homelab again at the next chance (something was started or answered there).""" + """Read the homelab again at the next chance, and often for a while (something was started or answered there).""" _homelab["at"] = 0 + _homelab["soon_until"] = time.time() + SOON_FOR def link_state(): diff --git a/src/nuauto/gui_static/app.js b/src/nuauto/gui_static/app.js index 753670a..5aeac80 100644 --- a/src/nuauto/gui_static/app.js +++ b/src/nuauto/gui_static/app.js @@ -1558,8 +1558,14 @@ function assistantStart(d, r, tab, testid, args) { return assistantStartButton(d, r, tab, testid, args); } +// Rows started on the homelab whose session doesn't show yet ("tab-row" -> until when): their button stays "Starting…" +// through every redraw until it shows (the lists reload by themselves then), or 45 s pass +const starting = {}; + function assistantStartButton(d, r, tab, testid, args) { + const key = `${tab}-${r.row}`; const remote = (d.homelab || []).find((s) => s.tab === tab && s.row === r.row); + if (remote || (d.sessions || []).some((s) => s.tab === tab && s.row === r.row)) delete starting[key]; if (remote) { // the homelab's assistant has this row: its screen, in this app (linked) or in NUauto on the homelab const wait = remote.attention && remote.attention.kind; const text = wait ? "assistant on the homelab: waiting for you" : "assistant running on the homelab"; @@ -1590,14 +1596,20 @@ function assistantStartButton(d, r, tab, testid, args) { // Apply, the same button as a NUworks row's: on the homelab when this is its page, or Run on says the homelab (the // laptop's app, through its link; the queue says when it can't); otherwise here const there = onHomelab() || d.linked && S.queue; - if (there && (onHomelab() || runOn() === "homelab")) return el("span", { class: "row" }, chip, el("button", { class: "btn small", - testid: onHomelab() ? testid : `${testid}-homelab-start`, text: "Apply", disabled: args.disabled || !!(q && !q.start), - title: q && !q.start ? q.text : "Starts the assistant on the homelab now, for just this job (you answer it on its screen)", onclick: async (e) => { + const pending = starting[key] > Date.now(); + if (there && (onHomelab() || runOn() === "homelab")) return el("span", { class: "row" }, pending ? null : chip, el("button", { class: "btn small", + testid: onHomelab() ? testid : `${testid}-homelab-start`, text: pending ? "Starting…" : "Apply", disabled: pending || args.disabled || !!(q && !q.start), + title: pending ? "Its session shows up here in a few seconds" : q && !q.start ? q.text : + "Starts the assistant on the homelab now, for just this job (you answer it on its screen)", onclick: async (e) => { const btn = e.currentTarget; btn.disabled = true; btn.textContent = "Starting…"; + starting[key] = Date.now() + 45000; const ok = await action("assist", onHomelab() ? args.body : { ...args.body, on: "homelab" }); - setTimeout(() => { if (S.screen && S.screen.reload) S.screen.reload(); }, ok ? 9000 : 0); // its session shows up in a few seconds + if (!ok) delete starting[key]; + const redraw = () => { if (S.screen && S.screen.reload) S.screen.reload(); }; + redraw(); // the queue says it runs now; the button stays "Starting…" until its session shows + if (ok) setTimeout(() => { if (starting[key]) { delete starting[key]; redraw(); } }, 45500); // it never showed: Apply again } })); const full = (d.sessions || []).length >= (d.slots || 1); const startHere = el("button", { class: "btn small", text: "Apply", testid, disabled: full || (args.disabled || false), @@ -1621,8 +1633,9 @@ function assistantsBar(d) { const here = (d.sessions || []).length, there = (d.homelab || []).length; if (!here && !there && !onHomelab()) return null; const waits = [...(d.sessions || []), ...(d.homelab || [])].filter((s) => s.attention && s.attention.kind).length; - const parts = [`Assistants: ${here} of ${d.slots} running${onHomelab() ? "" : " here"}`]; - if (there) parts.push(`${there} on the homelab`); + // (laptop) with every one on the homelab, the line says that, not "0 of 3 running here" + const parts = here || !there ? [`Assistants: ${here} of ${d.slots} running${onHomelab() ? "" : " here"}`] : []; + if (there) parts.push(`${parts.length ? "" : "Assistants: "}${there} running on the homelab`); if (waits) parts.push(`${waits} ${waits === 1 ? "waits" : "wait"} for you (Today lists ${waits === 1 ? "it" : "them"})`); if (here >= d.slots) parts.push(onHomelab() ? "end one (on its screen) to start another" : "finish one (type /exit in its window) to start another"); const q = d.queue || {}; diff --git a/tests/test_remote.py b/tests/test_remote.py index b51ff5d..4040ad5 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1107,6 +1107,21 @@ def boom(): gui.homelab_assistants() wait_idle() assert gui._homelab["busy"] is False and gui._homelab["at"] > 0 + # something was started there: read again now, then every SOON_EVERY seconds for a while (its session shows late) + remote.homelab_sessions = lambda: [] + gui.homelab_soon() + gui.homelab_assistants() + wait_idle() + gui._homelab["at"] -= gui.SOON_EVERY + 1 + reads_before = gui._homelab["at"] + gui.homelab_assistants() + wait_idle() + assert gui._homelab["at"] > reads_before # read again after SOON_EVERY, not HOMELAB_EVERY + gui._homelab["soon_until"] = 0 + gui._homelab["at"] -= gui.SOON_EVERY + 1 + reads_before = gui._homelab["at"] + gui.homelab_assistants() + assert gui._homelab["at"] == reads_before and not gui._homelab["busy"] # the while is over: HOMELAB_EVERY again finally: gui.VIEW, config.DEMO, config.REMOTE_ASSIST, config.IS_SERVER, config.REMOTE_APP, config.HAS_SERVER = real_flags threading.excepthook = threading.__excepthook__