mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix settings host sections showing "host not found" when the local daemon is stopped (#1749)
The local daemon's serverId persists while the daemon is stopped, so it isn't among the connected hosts. The settings host-section resolver fell back to it without checking it was connected, resolving the section to an unknown id and rendering "host not found". Extract the fallback into resolveActiveHostServerId, which only uses a serverId when it names a currently connected host (covering both the picker selection and the local daemon), and add regression tests. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,11 @@ import {
|
||||
} from "@/hooks/use-settings";
|
||||
import { useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { orderHostsLocalFirst, type HostProfile } from "@/types/host-connection";
|
||||
import {
|
||||
orderHostsLocalFirst,
|
||||
resolveActiveHostServerId,
|
||||
type HostProfile,
|
||||
} from "@/types/host-connection";
|
||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { WindowChromeRegion, WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
@@ -1144,15 +1148,6 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
||||
const [selectedSettingsHostServerId, setSelectedSettingsHostServerId] = useState<string | null>(
|
||||
view.kind === "host" ? view.serverId : null,
|
||||
);
|
||||
const knownSelectedSettingsHostServerId = useMemo(() => {
|
||||
if (!selectedSettingsHostServerId) {
|
||||
return null;
|
||||
}
|
||||
return hosts.some((host) => host.serverId === selectedSettingsHostServerId)
|
||||
? selectedSettingsHostServerId
|
||||
: null;
|
||||
}, [hosts, selectedSettingsHostServerId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view.kind === "host") {
|
||||
setSelectedSettingsHostServerId(view.serverId);
|
||||
@@ -1160,11 +1155,16 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
||||
}, [view]);
|
||||
|
||||
// The host the four sections scope to: the host on the active view, otherwise
|
||||
// the picker choice, otherwise the local daemon, otherwise the first host.
|
||||
// the picker choice, otherwise the connected local daemon, otherwise the first host.
|
||||
const activeHostServerId = useMemo(() => {
|
||||
if (view.kind === "host") return view.serverId;
|
||||
return knownSelectedSettingsHostServerId ?? localServerId ?? sortedHosts[0]?.serverId ?? null;
|
||||
}, [view, knownSelectedSettingsHostServerId, localServerId, sortedHosts]);
|
||||
return resolveActiveHostServerId({
|
||||
selectedServerId: selectedSettingsHostServerId,
|
||||
localServerId,
|
||||
hosts,
|
||||
orderedHosts: sortedHosts,
|
||||
});
|
||||
}, [view, selectedSettingsHostServerId, localServerId, hosts, sortedHosts]);
|
||||
|
||||
const handleSendBehaviorChange = useCallback(
|
||||
(behavior: SendBehavior) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeStoredHostProfile,
|
||||
orderHostsLocalFirst,
|
||||
resolveActiveHostServerId,
|
||||
type HostProfile,
|
||||
} from "./host-connection";
|
||||
|
||||
@@ -114,3 +115,75 @@ describe("normalizeStoredHostProfile", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveActiveHostServerId", () => {
|
||||
it("uses the selected host when one is set", () => {
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: "srv_selected",
|
||||
localServerId: "srv_local",
|
||||
hosts: [makeHost("srv_local"), makeHost("srv_selected")],
|
||||
orderedHosts: [makeHost("srv_local"), makeHost("srv_selected")],
|
||||
}),
|
||||
).toBe("srv_selected");
|
||||
});
|
||||
|
||||
it("falls back to the local host when it is connected", () => {
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: null,
|
||||
localServerId: "srv_local",
|
||||
hosts: [makeHost("srv_local"), makeHost("srv_remote")],
|
||||
orderedHosts: [makeHost("srv_local"), makeHost("srv_remote")],
|
||||
}),
|
||||
).toBe("srv_local");
|
||||
});
|
||||
|
||||
it("skips a stopped local daemon and uses the first connected host", () => {
|
||||
// Regression: a stopped local daemon's serverId persists but isn't in `hosts`.
|
||||
// Falling back to it would resolve the section to an unknown id ("host not found").
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: null,
|
||||
localServerId: "srv_local_stopped",
|
||||
hosts: [makeHost("srv_remote")],
|
||||
orderedHosts: [makeHost("srv_remote")],
|
||||
}),
|
||||
).toBe("srv_remote");
|
||||
});
|
||||
|
||||
it("returns null when no hosts are connected", () => {
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: null,
|
||||
localServerId: "srv_local_stopped",
|
||||
hosts: [],
|
||||
orderedHosts: [],
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores a selected host that is not connected", () => {
|
||||
// A stale selection (e.g. the host was removed) must not be used unless it is
|
||||
// currently connected, or the section resolves to an unknown id ("host not found").
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: "srv_stale_selection",
|
||||
localServerId: null,
|
||||
hosts: [makeHost("srv_remote")],
|
||||
orderedHosts: [makeHost("srv_remote")],
|
||||
}),
|
||||
).toBe("srv_remote");
|
||||
});
|
||||
|
||||
it("falls through a disconnected selection to the connected local host", () => {
|
||||
expect(
|
||||
resolveActiveHostServerId({
|
||||
selectedServerId: "srv_stale_selection",
|
||||
localServerId: "srv_local",
|
||||
hosts: [makeHost("srv_local"), makeHost("srv_remote")],
|
||||
orderedHosts: [makeHost("srv_local"), makeHost("srv_remote")],
|
||||
}),
|
||||
).toBe("srv_local");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +75,29 @@ export function orderHostsLocalFirst<T extends { serverId: string }>(
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which host a settings host section should target: the picker
|
||||
* selection, else the local daemon, else the first connected host.
|
||||
*
|
||||
* Only a serverId that names a currently connected host is used. Both the
|
||||
* selection and the local daemon can name a host that isn't connected (a stale
|
||||
* selection, or a local daemon whose id persists in storage while it's stopped);
|
||||
* using one would resolve the section to an unknown id and render "host not found".
|
||||
*/
|
||||
export function resolveActiveHostServerId(params: {
|
||||
selectedServerId: string | null;
|
||||
localServerId: string | null;
|
||||
hosts: readonly { serverId: string }[];
|
||||
orderedHosts: readonly { serverId: string }[];
|
||||
}): string | null {
|
||||
const { selectedServerId, localServerId, hosts, orderedHosts } = params;
|
||||
const connected = (serverId: string | null): string | null =>
|
||||
serverId && hosts.some((host) => host.serverId === serverId) ? serverId : null;
|
||||
return (
|
||||
connected(selectedServerId) ?? connected(localServerId) ?? orderedHosts[0]?.serverId ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function hostConnectionEquals(left: HostConnection, right: HostConnection): boolean {
|
||||
if (left.type !== right.type || left.id !== right.id) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user