2026-04-15 01:32:59 +08:00
|
|
|
import React from 'react';
|
2026-06-03 02:42:00 +03:00
|
|
|
import { isDesktopShell, restartDesktopApp } from '@/lib/desktop';
|
2026-04-15 01:32:59 +08:00
|
|
|
import { DesktopConnectionRecovery, type RecoveryVariant } from './DesktopConnectionRecovery';
|
|
|
|
|
import { RemoteConnectionForm } from './RemoteConnectionForm';
|
|
|
|
|
import { resolveRecoveryNextStep } from './desktopRecoveryRouting';
|
|
|
|
|
import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts';
|
2026-06-02 00:43:05 +03:00
|
|
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
type RecoveryScreenProps = {
|
|
|
|
|
/** Recovery variant */
|
|
|
|
|
variant: RecoveryVariant;
|
|
|
|
|
/** Host URL for recovery context */
|
|
|
|
|
hostUrl?: string;
|
|
|
|
|
/** Host label for recovery context */
|
|
|
|
|
hostLabel?: string;
|
|
|
|
|
/** Callback when user wants to retry */
|
|
|
|
|
onRetry?: () => void;
|
|
|
|
|
/** Callback when user chooses remote */
|
|
|
|
|
onChooseRemote?: () => void;
|
|
|
|
|
/** Whether to show the remote connection form */
|
|
|
|
|
showRemoteForm?: boolean;
|
|
|
|
|
/** Callback when closing remote form */
|
|
|
|
|
onCloseRemoteForm?: () => void;
|
|
|
|
|
/** Callback when switching to local from remote form */
|
|
|
|
|
onSwitchToLocalFromRemote?: () => void;
|
|
|
|
|
/** Callback when entering local setup */
|
|
|
|
|
onEnterLocalSetup?: () => void;
|
|
|
|
|
/** Whether retry action is in progress */
|
|
|
|
|
isRetrying?: boolean;
|
2026-07-21 21:11:13 +03:00
|
|
|
localAvailable?: boolean;
|
2026-04-15 01:32:59 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function RecoveryScreen({
|
|
|
|
|
variant,
|
|
|
|
|
hostUrl,
|
|
|
|
|
hostLabel,
|
|
|
|
|
onRetry,
|
|
|
|
|
onChooseRemote,
|
|
|
|
|
showRemoteForm = false,
|
|
|
|
|
onCloseRemoteForm,
|
|
|
|
|
onSwitchToLocalFromRemote,
|
|
|
|
|
onEnterLocalSetup,
|
|
|
|
|
isRetrying = false,
|
2026-07-21 21:11:13 +03:00
|
|
|
localAvailable = true,
|
2026-04-15 01:32:59 +08:00
|
|
|
}: RecoveryScreenProps) {
|
|
|
|
|
// Persist the user's first choice (local or remote)
|
|
|
|
|
const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => {
|
2026-06-03 02:42:00 +03:00
|
|
|
if (!isDesktopShell()) return;
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
const config = await desktopHostsGet();
|
|
|
|
|
await desktopHostsSet({
|
|
|
|
|
...config,
|
|
|
|
|
// Only change defaultHostId when switching to local; remote keeps
|
|
|
|
|
// whatever was there (or null) until a successful connect.
|
|
|
|
|
...(choice === 'local' ? { defaultHostId: 'local' } : {}),
|
|
|
|
|
initialHostChoiceCompleted: true,
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleRecoveryRetry = React.useCallback(async () => {
|
2026-06-03 02:42:00 +03:00
|
|
|
// In desktop boot flow, restart the app so the native host can
|
|
|
|
|
// re-evaluate the boot outcome.
|
|
|
|
|
if (isDesktopShell()) {
|
2026-04-15 01:32:59 +08:00
|
|
|
await restartDesktopApp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 00:43:05 +03:00
|
|
|
await runtimeFetch('/api/config/reload', { method: 'POST' });
|
2026-04-15 01:32:59 +08:00
|
|
|
onRetry?.();
|
|
|
|
|
}, [onRetry]);
|
|
|
|
|
|
|
|
|
|
const handleRecoveryUseLocal = React.useCallback(async () => {
|
|
|
|
|
const step = resolveRecoveryNextStep(variant, 'use-local');
|
|
|
|
|
if (step.kind === 'local-setup') {
|
|
|
|
|
// local-unavailable + local → enter local-setup subflow without reload
|
|
|
|
|
onEnterLocalSetup?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// switch-default-to-local → persist local choice and restart
|
|
|
|
|
await persistFirstChoice('local');
|
|
|
|
|
|
2026-06-03 02:42:00 +03:00
|
|
|
if (isDesktopShell()) {
|
2026-04-15 01:32:59 +08:00
|
|
|
await restartDesktopApp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}, [variant, persistFirstChoice, onEnterLocalSetup]);
|
|
|
|
|
|
|
|
|
|
const handleRecoveryUseRemote = React.useCallback(() => {
|
|
|
|
|
const step = resolveRecoveryNextStep(variant, 'use-remote');
|
|
|
|
|
if (step.kind === 'remote-form') {
|
|
|
|
|
onChooseRemote?.();
|
|
|
|
|
}
|
|
|
|
|
}, [variant, onChooseRemote]);
|
|
|
|
|
|
|
|
|
|
// Recovery mode — show recovery component first; only switch to remote form on explicit user action
|
|
|
|
|
if (showRemoteForm) {
|
|
|
|
|
// For remote-wrong-service, do NOT auto-populate the known bad URL
|
|
|
|
|
const prefillUrl = variant === 'remote-wrong-service' ? '' : (hostUrl || '');
|
|
|
|
|
const prefillLabel = variant === 'remote-wrong-service' ? '' : (hostLabel || '');
|
|
|
|
|
return (
|
|
|
|
|
<RemoteConnectionForm
|
|
|
|
|
onBack={onCloseRemoteForm || (() => onChooseRemote?.())}
|
|
|
|
|
initialUrl={prefillUrl}
|
|
|
|
|
initialLabel={prefillLabel}
|
|
|
|
|
isRecoveryMode={true}
|
2026-07-21 21:11:13 +03:00
|
|
|
showInstancePicker={!localAvailable}
|
|
|
|
|
onSwitchToLocal={localAvailable ? (onSwitchToLocalFromRemote || (() => {
|
2026-04-15 01:32:59 +08:00
|
|
|
persistFirstChoice('local').then(() => {
|
2026-06-03 02:42:00 +03:00
|
|
|
if (isDesktopShell()) {
|
2026-04-15 01:32:59 +08:00
|
|
|
restartDesktopApp();
|
|
|
|
|
} else {
|
|
|
|
|
onEnterLocalSetup?.();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-07-21 21:11:13 +03:00
|
|
|
})) : undefined}
|
2026-04-15 01:32:59 +08:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<DesktopConnectionRecovery
|
|
|
|
|
variant={variant}
|
|
|
|
|
hostLabel={hostLabel}
|
|
|
|
|
hostUrl={hostUrl}
|
|
|
|
|
onRetry={handleRecoveryRetry}
|
2026-07-21 21:11:13 +03:00
|
|
|
onUseLocal={localAvailable ? handleRecoveryUseLocal : undefined}
|
2026-04-15 01:32:59 +08:00
|
|
|
onUseRemote={handleRecoveryUseRemote}
|
|
|
|
|
isRetrying={isRetrying}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|