Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<slot>: 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<slot>: 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,
Expand Down
9 changes: 6 additions & 3 deletions src/nuauto/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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()
Expand All @@ -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():
Expand Down
25 changes: 19 additions & 6 deletions src/nuauto/gui_static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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),
Expand All @@ -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 || {};
Expand Down
15 changes: 15 additions & 0 deletions tests/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
Loading