Compare commits

...
Author SHA1 Message Date
Mike Schwarz 02e7fd1083 fix(tui): keep a daemon-only start on its default bind, and point at the way back to the kernel (#3181)
A daemon started from the TUI's crash cart or start-and-return page used to run `rig daemon start --no-kernel --host 127.0.0.1 --port <port>`. `--host` is an explicit bind, so the daemon listened on 127.0.0.1 alone and other machines on the tailnet couldn't reach it until it was restarted. For the default `http://127.0.0.1:<port>` target the TUI now passes only `--port`, so the daemon uses its default bind (127.0.0.1, plus the Tailscale address when one is present). A `localhost` or `[::1]` target still names its host, a configured bind host is honoured as it is by a plain `rig daemon start`, and `--no-kernel`, the port and the local-target check are unchanged.

After a daemon-only start, which leaves every seat as it was, the start-and-return page says no seats were started and points at the kernel, when one is listed, to see its seats and resume one; with no kernel rig it says to choose a rig or press `k`. The crash cart's notice points at that page with `S`; its mention of the kernel isn't checked against the listed rigs.

A configured non-loopback bind host still can't be reached from the TUI's default loopback target, as with a daemon started any other way.
2026-11-13 16:49:49 -08:00
5 changed files with 29 additions and 4 deletions

No files matched your search

+4 -2
View File
@@ -6,6 +6,8 @@ export function daemonStartArgs(target: string): string[] {
|| url.username || url.password || url.search || url.hash || url.pathname !== "/") {
throw new Error("This target is not a local daemon endpoint. Reconnect after its owner starts it; no local daemon was started.");
}
return ["daemon", "start", "--no-kernel", "--host", url.hostname.replace(/^\[|\]$/g, ""),
"--port", url.port || "80"];
// The daemon's default bind already listens on 127.0.0.1, with Tailscale beside it when present; naming that host would
// make it the only listener. Another loopback spelling isn't in the default bind, so it's still named.
const host = url.hostname.replace(/^\[|\]$/g, "");
return ["daemon", "start", "--no-kernel", ...(host === "127.0.0.1" ? [] : ["--host", host]), "--port", url.port || "80"];
}
+2
View File
@@ -389,6 +389,8 @@ async function run(): Promise<void> {
} else {
void refreshCrashCart();
void live?.refresh();
// Daemon only: no seat was started. Start and return lists the rigs, the kernel first, to bring one back.
if (startup) view.dispatch({ type: "notice", message: "Daemon started; this start didn't start any seats. Press S to choose seats to bring back, the kernel's first." });
}
});
return;
+10
View File
@@ -163,6 +163,8 @@ export class StartupController {
const failure = s.notice;
await this.refresh();
if (!confirmed) { s.notice = failure; s.detail = failure; this.changed(); }
// refresh() moves the page on; read it again rather than through the narrowed `s.page`.
else if (this.state.page === "rigs") { s.notice = daemonOnlyNotice(s.rigs); this.changed(); }
return;
}
if (s.page === "rigs" && key === "k" && !s.rigs.some((r) => r.name === "kernel")) {
@@ -244,6 +246,14 @@ export class StartupController {
}
}
/** After a daemon-only start: this start brought back no seat, and the existing way to bring one back is the rig list
* here, where the kernel comes first. It says nothing about why any seat isn't running. */
export function daemonOnlyNotice(rigs: Array<{ name: string }>): string {
return rigs.some((r) => r.name === "kernel")
? "Daemon started; this start didn't start any seats. The kernel is listed first: choose it to see its seats and resume one."
: "Daemon started; this start didn't start any seats. Choose a rig, or press k to set up a kernel.";
}
export function startupLines(s: StartupState): Array<{ text: string; action?: Action }> {
const button = (text: string, key: string) => ({ text, action: { type: "startup" as const, key } });
const lines: Array<{ text: string; action?: Action }> = [
@@ -6,10 +6,16 @@ import { resolveCrashCartKey } from "../src/crash-cart/keys.js";
describe("local startup target", () => {
it("starts only the intended local daemon, leaving occupant selection to the user", () => {
// The default bind is 127.0.0.1 plus Tailscale when present, so naming 127.0.0.1 would drop the Tailscale listener.
expect(daemonStartArgs("http://127.0.0.1:17433")).toEqual([
"daemon", "start", "--no-kernel", "--host", "127.0.0.1", "--port", "17433",
"daemon", "start", "--no-kernel", "--port", "17433",
]);
});
it.each([["http://localhost:7433", "localhost"], ["http://[::1]:7433", "::1"]])(
"names %s's host, which the default bind doesn't listen on", (target, host) => {
expect(daemonStartArgs(target)).toEqual(["daemon", "start", "--no-kernel", "--host", host, "--port", "7433"]);
},
);
it.each(["https://example.test", "http://10.0.0.2:7433", "http://user:password@localhost:7433", "http://localhost:7433/other"])(
"does not turn %s into local startup authority", (target) => {
expect(() => daemonStartArgs(target)).toThrow("no local daemon was started");
+6 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { DaemonClient, launchNodeNotice } from "../src/daemon-client.js";
import { StartupController, startupLines, type StartupSeat } from "../src/startup.js";
import { StartupController, daemonOnlyNotice, startupLines, type StartupSeat } from "../src/startup.js";
import { createViewState, emptySnapshot } from "../src/state.js";
import { renderScreen } from "../src/render.js";
@@ -118,6 +118,11 @@ describe("TUI startup choices", () => {
expect(f.startDaemon).toHaveBeenCalledTimes(1);
expect(f.posts).toEqual([]);
expect(f.controller.state.page).toBe("rigs");
// It names the existing way back for the kernel, and claims nothing about why it isn't running.
expect(f.controller.state.notice).toBe("Daemon started; this start didn't start any seats. The kernel is listed first: choose it to see its seats and resume one.");
});
it("with no kernel rig, a daemon-only start points at choosing a rig or setting up a kernel", () => {
expect(daemonOnlyNotice([{ name: "work" }])).toBe("Daemon started; this start didn't start any seats. Choose a rig, or press k to set up a kernel.");
});
it("decline and refresh invalidate fresh consent with no launch effect", async () => {
const f = fixture(); await chooseOperator(f);