@@ -2,14 +2,16 @@ import './style.scss';
22
33import { useQuery , useQueryClient } from '@tanstack/react-query' ;
44import { listen } from '@tauri-apps/api/event' ;
5- import { useEffect } from 'react' ;
5+ import { error } from '@tauri-apps/plugin-log' ;
6+ import { useEffect , useRef } from 'react' ;
67import { Outlet , useLocation , useNavigate } from 'react-router-dom' ;
78import { shallow } from 'zustand/shallow' ;
89import AutoProvisioningManager from '../../components/AutoProvisioningManager' ;
910import { useI18nContext } from '../../i18n/i18n-react' ;
1011import { DeepLinkProvider } from '../../shared/components/providers/DeepLinkProvider' ;
1112import { useToaster } from '../../shared/defguard-ui/hooks/toasts/useToaster' ;
1213import { routes } from '../../shared/routes' ;
14+ import { errorDetail } from '../../shared/utils/errorDetail' ;
1315import { clientApi } from './clientAPI/clientApi' ;
1416import { ClientSideBar } from './components/ClientSideBar/ClientSideBar' ;
1517import { MfaModalProvider } from './components/MfaModalProvider' ;
@@ -23,10 +25,11 @@ import {
2325 ClientConnectionType ,
2426 type CommonWireguardFields ,
2527 type DeadConDroppedPayload ,
28+ LocationMfaType ,
2629 TauriEventKey ,
2730} from './types' ;
2831
29- const { getInstances, getTunnels, getAppConfig } = clientApi ;
32+ const { getInstances, getTunnels, getAppConfig, getLocations , connect } = clientApi ;
3033
3134export const ClientPage = ( ) => {
3235 const queryClient = useQueryClient ( ) ;
@@ -40,6 +43,8 @@ export const ClientPage = () => {
4043 state . listChecked ,
4144 state . setListChecked ,
4245 ] ) ;
46+ // Ref (not state) so the flag persists across re-renders without triggering them.
47+ const autoConnectAttempted = useRef ( false ) ;
4348 const location = useLocation ( ) ;
4449 const toaster = useToaster ( ) ;
4550 const openDeadConDroppedModal = useDeadConDroppedModal ( ( s ) => s . open ) ;
@@ -221,6 +226,47 @@ export const ClientPage = () => {
221226 // eslint-disable-next-line react-hooks/exhaustive-deps
222227 } , [ appConfig ] ) ;
223228
229+ // Auto-connect the configured default instance once on startup.
230+ useEffect ( ( ) => {
231+ if ( autoConnectAttempted . current || ! instances || ! appConfig ) return ;
232+ const defaultInstanceId = appConfig . default_instance ;
233+ if ( defaultInstanceId === null ) return ;
234+ const instance = instances . find ( ( i ) => i . id === defaultInstanceId ) ;
235+ if ( ! instance ) return ;
236+ autoConnectAttempted . current = true ;
237+ setClientState ( {
238+ selectedInstance : { id : instance . id , type : ClientConnectionType . LOCATION } ,
239+ } ) ;
240+ getLocations ( { instanceId : instance . id } )
241+ . then ( ( locations ) => {
242+ for ( const loc of locations ) {
243+ const mfaEnabled =
244+ loc . location_mfa_mode === LocationMfaType . INTERNAL ||
245+ loc . location_mfa_mode === LocationMfaType . EXTERNAL ;
246+ // MFA locations must go through the modal flow which collects
247+ // credentials before calling connect — calling connect directly
248+ // would mark the location active in the DB without a tunnel.
249+ if ( mfaEnabled ) {
250+ if ( appConfig . auto_connect_mfa ) openMFAModal ( loc , true ) ;
251+ } else {
252+ connect ( { locationId : loc . id , connectionType : ClientConnectionType . LOCATION } ) . catch (
253+ ( e ) => {
254+ const detail = errorDetail ( e ) ;
255+ error ( `Auto-connect failed for location ${ loc . id } : ${ detail } ` ) ;
256+ toaster . error ( LL . common . messages . errorWithMessage ( { message : detail } ) ) ;
257+ } ,
258+ ) ;
259+ }
260+ }
261+ } )
262+ . catch ( ( e ) => {
263+ const detail = errorDetail ( e ) ;
264+ error ( `Auto-connect failed to fetch locations for instance ${ instance . id } : ${ detail } ` ) ;
265+ toaster . error ( LL . common . messages . errorWithMessage ( { message : detail } ) ) ;
266+ } ) ;
267+ // eslint-disable-next-line react-hooks/exhaustive-deps
268+ } , [ instances , appConfig ] ) ;
269+
224270 // navigate to carousel on first app Launch
225271 useEffect ( ( ) => {
226272 if ( ! location . pathname . includes ( routes . client . carousel ) && firstLaunch ) {
0 commit comments