fix(app): remove test connection button that broke host status
The Test button in settings was creating a separate DaemonClientV2 instance without the Tauri WebSocket transport. In the desktop app, this caused origin rejection (tauri://localhost) which then overwrote the connection status to offline, even though the main session connection was working fine. Removed the redundant test functionality since: - The daemon client auto-reconnects - Connection status is already tracked and displayed via SessionProvider - The test was actively harmful by creating a rogue client Also simplified waitForDaemonRestart to rely on auto-reconnect instead of polling with a separate connection.
This commit is contained in:
@@ -22,8 +22,7 @@ import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons
|
|||||||
import { theme as defaultTheme } from "@/styles/theme";
|
import { theme as defaultTheme } from "@/styles/theme";
|
||||||
import { MenuHeader } from "@/components/headers/menu-header";
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { DaemonClientV2 } from "@server/client/daemon-client-v2";
|
import { normalizeHostPort } from "@/utils/daemon-endpoints";
|
||||||
import { buildDaemonWebSocketUrl, normalizeHostPort } from "@/utils/daemon-endpoints";
|
|
||||||
|
|
||||||
const delay = (ms: number) =>
|
const delay = (ms: number) =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
@@ -374,10 +373,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type DaemonTestState = {
|
|
||||||
status: "idle" | "testing" | "success" | "error";
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const { settings, isLoading: settingsLoading, updateSettings, resetSettings } = useAppSettings();
|
const { settings, isLoading: settingsLoading, updateSettings, resetSettings } = useAppSettings();
|
||||||
@@ -400,7 +395,6 @@ export default function SettingsScreen() {
|
|||||||
offerUrl: string;
|
offerUrl: string;
|
||||||
}>({ id: null, label: "", endpoint: "", offerUrl: "" });
|
}>({ id: null, label: "", endpoint: "", offerUrl: "" });
|
||||||
const [isSavingDaemon, setIsSavingDaemon] = useState(false);
|
const [isSavingDaemon, setIsSavingDaemon] = useState(false);
|
||||||
const [daemonTestStates, setDaemonTestStates] = useState<Map<string, DaemonTestState>>(() => new Map());
|
|
||||||
const isLoading = settingsLoading || daemonLoading;
|
const isLoading = settingsLoading || daemonLoading;
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
@@ -427,40 +421,6 @@ export default function SettingsScreen() {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const testServerConnection = useCallback(async (url: string, timeoutMs = 5000) => {
|
|
||||||
const client = new DaemonClientV2({
|
|
||||||
url,
|
|
||||||
suppressSendErrors: true,
|
|
||||||
reconnect: { enabled: false },
|
|
||||||
});
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
reject(new Error("Connection timeout - server did not respond"));
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
client
|
|
||||||
.connect()
|
|
||||||
.then(resolve)
|
|
||||||
.catch((error) => {
|
|
||||||
const message =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Connection failed - check URL and network";
|
|
||||||
console.error("[Settings] Daemon test error", { url, message });
|
|
||||||
reject(new Error(message));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
if (timeoutId !== null) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOpenDaemonForm = useCallback((profile?: DaemonProfile) => {
|
const handleOpenDaemonForm = useCallback((profile?: DaemonProfile) => {
|
||||||
if (profile) {
|
if (profile) {
|
||||||
setDaemonFormMode("manual");
|
setDaemonFormMode("manual");
|
||||||
@@ -591,37 +551,6 @@ export default function SettingsScreen() {
|
|||||||
[removeDaemon]
|
[removeDaemon]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateDaemonTestState = useCallback((daemonId: string, state: { status: "idle" | "testing" | "success" | "error"; message?: string }) => {
|
|
||||||
setDaemonTestStates((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(daemonId, state);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleTestDaemonConnection = useCallback(
|
|
||||||
async (profile: DaemonProfile) => {
|
|
||||||
const endpoint = profile.endpoints?.[0] ?? null;
|
|
||||||
if (!endpoint) {
|
|
||||||
Alert.alert("Missing host", "This host has no endpoints configured.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = buildDaemonWebSocketUrl(endpoint);
|
|
||||||
updateDaemonTestState(profile.id, { status: "testing" });
|
|
||||||
updateConnectionStatus(profile.id, { status: "connecting" });
|
|
||||||
try {
|
|
||||||
await testServerConnection(url, 4000);
|
|
||||||
updateDaemonTestState(profile.id, { status: "success", message: "Reachable" });
|
|
||||||
updateConnectionStatus(profile.id, { status: "online", lastOnlineAt: new Date().toISOString() });
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : "Connection failed";
|
|
||||||
updateDaemonTestState(profile.id, { status: "error", message });
|
|
||||||
updateConnectionStatus(profile.id, { status: "offline", lastError: message });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[testServerConnection, updateConnectionStatus, updateDaemonTestState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleUseSpeaker = useCallback(
|
const handleToggleUseSpeaker = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
void updateSettings({ ...settings, useSpeaker: value });
|
void updateSettings({ ...settings, useSpeaker: value });
|
||||||
@@ -707,20 +636,16 @@ export default function SettingsScreen() {
|
|||||||
const connection = connectionStates.get(daemon.id);
|
const connection = connectionStates.get(daemon.id);
|
||||||
const connectionStatus = connection?.status ?? "idle";
|
const connectionStatus = connection?.status ?? "idle";
|
||||||
const lastConnectionError = connection?.lastError ?? null;
|
const lastConnectionError = connection?.lastError ?? null;
|
||||||
const testState = daemonTestStates.get(daemon.id);
|
|
||||||
return (
|
return (
|
||||||
<DaemonCard
|
<DaemonCard
|
||||||
key={daemon.id}
|
key={daemon.id}
|
||||||
daemon={daemon}
|
daemon={daemon}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
lastError={lastConnectionError}
|
lastError={lastConnectionError}
|
||||||
testState={testState}
|
|
||||||
onTestConnection={handleTestDaemonConnection}
|
|
||||||
onEdit={handleOpenDaemonForm}
|
onEdit={handleOpenDaemonForm}
|
||||||
onRemove={handleRemoveDaemon}
|
onRemove={handleRemoveDaemon}
|
||||||
restartConfirmationMessage={restartConfirmationMessage}
|
restartConfirmationMessage={restartConfirmationMessage}
|
||||||
waitForCondition={waitForCondition}
|
waitForCondition={waitForCondition}
|
||||||
testServerConnection={testServerConnection}
|
|
||||||
isScreenMountedRef={isMountedRef}
|
isScreenMountedRef={isMountedRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -969,13 +894,10 @@ interface DaemonCardProps {
|
|||||||
daemon: DaemonProfile;
|
daemon: DaemonProfile;
|
||||||
connectionStatus: ConnectionStatus;
|
connectionStatus: ConnectionStatus;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
testState?: DaemonTestState;
|
|
||||||
onTestConnection: (daemon: DaemonProfile) => void;
|
|
||||||
onEdit: (daemon: DaemonProfile) => void;
|
onEdit: (daemon: DaemonProfile) => void;
|
||||||
onRemove: (daemon: DaemonProfile) => void;
|
onRemove: (daemon: DaemonProfile) => void;
|
||||||
restartConfirmationMessage: string;
|
restartConfirmationMessage: string;
|
||||||
waitForCondition: (predicate: () => boolean, timeoutMs: number, intervalMs?: number) => Promise<boolean>;
|
waitForCondition: (predicate: () => boolean, timeoutMs: number, intervalMs?: number) => Promise<boolean>;
|
||||||
testServerConnection: (url: string, timeoutMs?: number) => Promise<void>;
|
|
||||||
isScreenMountedRef: MutableRefObject<boolean>;
|
isScreenMountedRef: MutableRefObject<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,17 +905,13 @@ function DaemonCard({
|
|||||||
daemon,
|
daemon,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
lastError,
|
lastError,
|
||||||
testState,
|
|
||||||
onTestConnection,
|
|
||||||
onEdit,
|
onEdit,
|
||||||
onRemove,
|
onRemove,
|
||||||
restartConfirmationMessage,
|
restartConfirmationMessage,
|
||||||
waitForCondition,
|
waitForCondition,
|
||||||
testServerConnection,
|
|
||||||
isScreenMountedRef,
|
isScreenMountedRef,
|
||||||
}: DaemonCardProps) {
|
}: DaemonCardProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const directWsUrl = buildDaemonWebSocketUrl(daemon.endpoints?.[0] ?? "localhost:6767");
|
|
||||||
const statusLabel = formatConnectionStatus(connectionStatus);
|
const statusLabel = formatConnectionStatus(connectionStatus);
|
||||||
const statusTone = getConnectionStatusTone(connectionStatus);
|
const statusTone = getConnectionStatusTone(connectionStatus);
|
||||||
const statusColor =
|
const statusColor =
|
||||||
@@ -1013,56 +931,33 @@ function DaemonCard({
|
|||||||
const [isRestarting, setIsRestarting] = useState(false);
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
const isConnected = daemonConnection?.isConnected ?? false;
|
const isConnected = daemonConnection?.isConnected ?? false;
|
||||||
const isConnectedRef = useRef(isConnected);
|
const isConnectedRef = useRef(isConnected);
|
||||||
const isTesting = testState?.status === "testing";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isConnectedRef.current = isConnected;
|
isConnectedRef.current = isConnected;
|
||||||
}, [isConnected]);
|
}, [isConnected]);
|
||||||
|
|
||||||
const waitForDaemonRestart = useCallback(async () => {
|
const waitForDaemonRestart = useCallback(async () => {
|
||||||
const maxAttempts = 12;
|
|
||||||
const retryDelayMs = 2500;
|
|
||||||
const disconnectTimeoutMs = 7000;
|
const disconnectTimeoutMs = 7000;
|
||||||
const reconnectTimeoutMs = 10000;
|
const reconnectTimeoutMs = 30000;
|
||||||
|
|
||||||
|
// Wait for disconnect first
|
||||||
if (isConnectedRef.current) {
|
if (isConnectedRef.current) {
|
||||||
await waitForCondition(() => !isConnectedRef.current, disconnectTimeoutMs);
|
await waitForCondition(() => !isConnectedRef.current, disconnectTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
// Wait for auto-reconnect
|
||||||
try {
|
const reconnected = await waitForCondition(() => isConnectedRef.current, reconnectTimeoutMs);
|
||||||
await testServerConnection(directWsUrl);
|
|
||||||
const reconnected = await waitForCondition(() => isConnectedRef.current, reconnectTimeoutMs);
|
|
||||||
|
|
||||||
if (isScreenMountedRef.current) {
|
if (isScreenMountedRef.current) {
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
if (!reconnected) {
|
if (!reconnected) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Host reachable",
|
"Unable to reconnect",
|
||||||
`${daemon.label} came back online but Paseo has not reconnected yet.`
|
`${daemon.label} did not come back online. Please verify it restarted.`
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`[Settings] Restart poll attempt ${attempt}/${maxAttempts} failed for ${daemon.label}`,
|
|
||||||
error
|
|
||||||
);
|
);
|
||||||
if (attempt === maxAttempts) {
|
|
||||||
if (isScreenMountedRef.current) {
|
|
||||||
setIsRestarting(false);
|
|
||||||
Alert.alert(
|
|
||||||
"Unable to reconnect",
|
|
||||||
`${daemon.label} did not come back online. Please verify it restarted.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await delay(retryDelayMs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [daemon.label, directWsUrl, isScreenMountedRef, testServerConnection, waitForCondition]);
|
}, [daemon.label, isScreenMountedRef, waitForCondition]);
|
||||||
|
|
||||||
const beginServerRestart = useCallback(() => {
|
const beginServerRestart = useCallback(() => {
|
||||||
if (!restartServerFn) {
|
if (!restartServerFn) {
|
||||||
@@ -1141,13 +1036,6 @@ function DaemonCard({
|
|||||||
? "rgba(248, 113, 113, 0.1)"
|
? "rgba(248, 113, 113, 0.1)"
|
||||||
: "rgba(161, 161, 170, 0.1)";
|
: "rgba(161, 161, 170, 0.1)";
|
||||||
|
|
||||||
const testResultColor =
|
|
||||||
testState?.status === "success"
|
|
||||||
? theme.colors.palette.green[400]
|
|
||||||
: testState?.status === "error"
|
|
||||||
? theme.colors.palette.red[300]
|
|
||||||
: theme.colors.foregroundMuted;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.hostCard}>
|
<View style={styles.hostCard}>
|
||||||
<View style={styles.hostCardContent}>
|
<View style={styles.hostCardContent}>
|
||||||
@@ -1160,27 +1048,8 @@ function DaemonCard({
|
|||||||
</View>
|
</View>
|
||||||
<Text style={styles.hostUrl}>{daemon.endpoints?.[0] ?? ""}</Text>
|
<Text style={styles.hostUrl}>{daemon.endpoints?.[0] ?? ""}</Text>
|
||||||
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}
|
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}
|
||||||
{testState && testState.status !== "idle" ? (
|
|
||||||
<Text style={[styles.testResultText, { color: testResultColor }]}>
|
|
||||||
{testState.message ?? (testState.status === "success" ? "Reachable" : "Testing...")}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.hostActionsRow}>
|
<View style={styles.hostActionsRow}>
|
||||||
<Pressable
|
|
||||||
style={[
|
|
||||||
styles.hostActionButton,
|
|
||||||
styles.hostActionPrimary,
|
|
||||||
isTesting && styles.hostActionDisabled,
|
|
||||||
]}
|
|
||||||
onPress={() => onTestConnection(daemon)}
|
|
||||||
disabled={isTesting}
|
|
||||||
>
|
|
||||||
<Text style={[styles.hostActionText, styles.hostActionPrimaryText]}>
|
|
||||||
{isTesting ? "Testing..." : "Test"}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
<View style={styles.hostActionSeparator} />
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.hostActionButton, isRestarting && styles.hostActionDisabled]}
|
style={[styles.hostActionButton, isRestarting && styles.hostActionDisabled]}
|
||||||
onPress={handleRestartPress}
|
onPress={handleRestartPress}
|
||||||
|
|||||||
Reference in New Issue
Block a user