mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Bug/connection mode fixes (#5998)
This commit is contained in:
@@ -10,4 +10,4 @@ VITE_SAAS_BACKEND_API_URL=https://api2.stirling.com
|
||||
|
||||
# Dev only: set to true to mimic an expired access token (no valid JWT for API/auth checks).
|
||||
# Production builds ignore this. Restart tauri-dev after changing.
|
||||
VITE_DEV_SIMULATE_EXPIRED_JWT=false
|
||||
VITE_DEV_SIMULATE_EXPIRED_JWT=
|
||||
|
||||
@@ -6817,10 +6817,13 @@ title = "Sign in to Stirling"
|
||||
[setup.selfhosted]
|
||||
link = "or connect to a self-hosted account"
|
||||
subtitle = "Enter your server credentials"
|
||||
changeServerLocked = "Your organisation has restricted this app to a specific server"
|
||||
switchToLocal = "Use local tools instead"
|
||||
title = "Sign in to Server"
|
||||
|
||||
[setup.selfhosted.unreachable]
|
||||
changeServer = "Connect to a different server"
|
||||
changeServerLocked = "Your organisation has restricted this app to a specific server"
|
||||
continueOffline = "Use local tools instead"
|
||||
message = "Could not reach {{url}}. Check that the server is running and accessible."
|
||||
retry = "Retry"
|
||||
|
||||
@@ -72,6 +72,28 @@ pub async fn set_connection_mode(
|
||||
) -> Result<(), String> {
|
||||
log::info!("Setting connection mode: {:?}", mode);
|
||||
|
||||
let store = app_handle
|
||||
.store(STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access store: {}", e))?;
|
||||
|
||||
// If the store is already locked, protect connection_mode, server_config, and the lock
|
||||
// flag from being overwritten by any JS-side call.
|
||||
// Only allow marking setup_completed and updating auth-related fields.
|
||||
let already_locked = store
|
||||
.get(LOCK_CONNECTION_KEY)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if already_locked {
|
||||
log::warn!("set_connection_mode called while lock_connection_mode=true — preserving connection settings, but marking setup as completed");
|
||||
// Still allow setup_completed to be written so the onboarding doesn't repeat.
|
||||
store.set(FIRST_LAUNCH_KEY, serde_json::json!(true));
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save store: {}", e))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Update in-memory state
|
||||
if let Ok(mut conn_state) = state.0.lock() {
|
||||
conn_state.mode = mode.clone();
|
||||
@@ -81,11 +103,6 @@ pub async fn set_connection_mode(
|
||||
}
|
||||
}
|
||||
|
||||
// Save to store
|
||||
let store = app_handle
|
||||
.store(STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access store: {}", e))?;
|
||||
|
||||
store.set(
|
||||
CONNECTION_MODE_KEY,
|
||||
serde_json::to_value(&mode).map_err(|e| format!("Failed to serialize mode: {}", e))?,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ToolActionsContext } from '@app/contexts/ToolActionsContext';
|
||||
import { useFirstLaunchCheck } from '@app/hooks/useFirstLaunchCheck';
|
||||
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
|
||||
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { connectionModeService, JWT_EXPIRED_PROMPTED_KEY } from '@app/services/connectionModeService';
|
||||
import { STIRLING_SAAS_URL } from '@app/constants/connection';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor';
|
||||
@@ -47,9 +47,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
const { isFirstLaunch, setupComplete } = useFirstLaunchCheck();
|
||||
const [connectionMode, setConnectionMode] = useState<'saas' | 'selfhosted' | 'local' | null>(null);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
// When auth check finds no valid session, record the sign-in detail here so the
|
||||
// dispatch useEffect below can fire it only after SignInModal has mounted.
|
||||
const [pendingSignIn, setPendingSignIn] = useState<{ locked: boolean } | null>(null);
|
||||
const [pendingSignIn, setPendingSignIn] = useState(false);
|
||||
// Prevent first-launch setup from running twice when connectionMode state update re-triggers the effect
|
||||
const firstLaunchInitiated = useRef(false);
|
||||
// Key incremented on every connection mode change after initial load — forces SaaS provider
|
||||
@@ -99,69 +97,73 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
})
|
||||
.finally(() => setAuthChecked(true));
|
||||
} else {
|
||||
let pendingDetail: { locked: boolean } | null = null;
|
||||
authService.isAuthenticated()
|
||||
.then(async (isAuth) => {
|
||||
if (!isAuth) {
|
||||
const cfg = await connectionModeService.getCurrentConfig().catch(() => null);
|
||||
if (cfg?.lock_connection_mode) {
|
||||
// Provisioned deployment — stay in the configured mode and prompt for credentials.
|
||||
// Don't fall back to local; the admin has locked the connection mode.
|
||||
pendingDetail = { locked: true };
|
||||
} else {
|
||||
// JWT expired — fall back to local so local tools still work, then prompt
|
||||
// for re-authentication via the sign-in modal.
|
||||
if (!cfg?.lock_connection_mode) {
|
||||
// JWT expired — fall back to local so local tools still work.
|
||||
await connectionModeService.switchToLocal().catch(console.error);
|
||||
setConnectionMode('local');
|
||||
pendingDetail = { locked: false };
|
||||
// Show sign-in modal once per expiry cycle. If the user dismisses
|
||||
// without signing in the flag stays set and we won't prompt again
|
||||
// until they successfully sign in (which clears the flag).
|
||||
if (!localStorage.getItem(JWT_EXPIRED_PROMPTED_KEY)) {
|
||||
localStorage.setItem(JWT_EXPIRED_PROMPTED_KEY, 'true');
|
||||
setPendingSignIn(true);
|
||||
}
|
||||
}
|
||||
// Locked deployments stay in their configured mode — user can sign in
|
||||
// via Settings when they're ready.
|
||||
}
|
||||
})
|
||||
.catch(async () => {
|
||||
const cfg = await connectionModeService.getCurrentConfig().catch(() => null);
|
||||
if (cfg?.lock_connection_mode) {
|
||||
// Auth check threw (e.g. network error) but mode is locked — still prompt for
|
||||
// credentials so the user can sign in when connectivity is restored.
|
||||
pendingDetail = { locked: true };
|
||||
} else {
|
||||
if (!cfg?.lock_connection_mode) {
|
||||
await connectionModeService.switchToLocal().catch(console.error);
|
||||
setConnectionMode('local');
|
||||
pendingDetail = { locked: false };
|
||||
if (!localStorage.getItem(JWT_EXPIRED_PROMPTED_KEY)) {
|
||||
localStorage.setItem(JWT_EXPIRED_PROMPTED_KEY, 'true');
|
||||
setPendingSignIn(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setAuthChecked(true);
|
||||
// Schedule sign-in via state so the dispatch useEffect fires AFTER
|
||||
// SignInModal mounts (children effects run before parent effects).
|
||||
if (pendingDetail) {
|
||||
setPendingSignIn(pendingDetail);
|
||||
}
|
||||
});
|
||||
.finally(() => setAuthChecked(true));
|
||||
}
|
||||
} else if (isFirstLaunch && !setupComplete) {
|
||||
// Auto-enter local mode on first launch — skip the setup wizard entirely.
|
||||
// The onboarding carousel + sign-in toast will be shown inside the main app.
|
||||
// Start the backend explicitly here because shouldMonitorBackend relies on
|
||||
// setupComplete (still false from the hook), so useBackendInitializer won't fire.
|
||||
// Guard against re-running when setConnectionMode('local') below triggers this effect.
|
||||
// Guard against re-running when setConnectionMode triggers this effect.
|
||||
if (firstLaunchInitiated.current) return;
|
||||
firstLaunchInitiated.current = true;
|
||||
connectionModeService.switchToLocal()
|
||||
.then(() => tauriBackendService.startBackend())
|
||||
connectionModeService.getCurrentConfig()
|
||||
.then(async (cfg) => {
|
||||
if (cfg.lock_connection_mode && cfg.server_config?.url) {
|
||||
// Locked provisioned deployment — do NOT switch to local (would clear server_config
|
||||
// from the store). Show onboarding normally; the sign-in slide handles locked auth.
|
||||
// Still start the local backend so local tools work while the user signs in.
|
||||
await tauriBackendService.startBackend().catch(console.error);
|
||||
setConnectionMode('selfhosted');
|
||||
} else {
|
||||
// Normal first launch — auto-enter local mode.
|
||||
// The onboarding carousel + sign-in slide will be shown inside the main app.
|
||||
await connectionModeService.switchToLocal();
|
||||
await tauriBackendService.startBackend();
|
||||
setConnectionMode('local');
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setConnectionMode('local');
|
||||
setAuthChecked(true);
|
||||
});
|
||||
.finally(() => setAuthChecked(true));
|
||||
}
|
||||
}, [isFirstLaunch, setupComplete, connectionMode]);
|
||||
|
||||
// Initialize backend health monitoring for self-hosted mode
|
||||
useEffect(() => {
|
||||
if (setupComplete && !isFirstLaunch && connectionMode === 'selfhosted') {
|
||||
if (connectionMode !== 'selfhosted') {
|
||||
// Stop the monitor whenever we leave selfhosted mode so the dot resets.
|
||||
selfHostedServerMonitor.stop();
|
||||
return;
|
||||
}
|
||||
if (setupComplete && !isFirstLaunch) {
|
||||
void tauriBackendService.initializeExternalBackend();
|
||||
// Also start the self-hosted server monitor so the operation router and UI
|
||||
// can detect when the remote server goes offline and fall back to local backend.
|
||||
connectionModeService.getServerConfig().then(cfg => {
|
||||
if (cfg?.url) {
|
||||
selfHostedServerMonitor.start(cfg.url);
|
||||
@@ -205,13 +207,16 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
return unsubscribe;
|
||||
}, [shouldPreloadLocalEndpoints, connectionMode]);
|
||||
|
||||
// Dispatch sign-in event only after authChecked=true so SignInModal is mounted.
|
||||
// Using useEffect (not setTimeout) guarantees child effects (SignInModal's listener
|
||||
// registration) run before this parent effect fires the event.
|
||||
|
||||
// Dispatch sign-in modal after authChecked so SignInModal's listener is registered.
|
||||
// (Child effects run before parent effects, so this fires after SignInModal mounts.)
|
||||
// detail.locked is always false here: setPendingSignIn(true) is only called inside
|
||||
// `if (!cfg?.lock_connection_mode)` branches above, so locked deployments never set
|
||||
// pendingSignIn and therefore never reach this dispatch.
|
||||
useEffect(() => {
|
||||
if (!authChecked || !pendingSignIn) return;
|
||||
window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT, { detail: pendingSignIn }));
|
||||
setPendingSignIn(null);
|
||||
window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT, { detail: { locked: false } }));
|
||||
setPendingSignIn(false);
|
||||
}, [authChecked, pendingSignIn]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Modal, Stack, Group, Button, ActionIcon } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
@@ -24,20 +24,13 @@ export function DesktopOnboardingModal() {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(() => !localStorage.getItem(ONBOARDING_KEY));
|
||||
const [step, setStep] = useState(0);
|
||||
// null = still checking, true = locked (suppress modal), false = not locked (show modal)
|
||||
const [isLocked, setIsLocked] = useState<boolean | null>(null);
|
||||
|
||||
// Provisioned (locked) deployments skip the onboarding entirely — the non-dismissible
|
||||
// SignInModal handles authentication and shows the correct self-hosted login flow.
|
||||
useEffect(() => {
|
||||
connectionModeService.getCurrentConfig().then((cfg) => {
|
||||
setIsLocked(cfg.lock_connection_mode && !!cfg.server_config?.url);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dismissFinal = () => {
|
||||
localStorage.setItem(ONBOARDING_KEY, 'true');
|
||||
setVisible(false);
|
||||
// If the user dismissed the sign-in slide without authenticating, fall back to local mode
|
||||
// so the app is usable without a server connection.
|
||||
connectionModeService.switchToLocal().catch(console.error);
|
||||
};
|
||||
|
||||
// X on slide 0 advances to sign-in slide rather than dismissing entirely
|
||||
@@ -63,7 +56,7 @@ export function DesktopOnboardingModal() {
|
||||
const welcomeSlide = useMemo(() => WelcomeSlide(), []);
|
||||
const totalSteps = 2;
|
||||
|
||||
if (!visible || isLocked === null || isLocked) return null;
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -12,6 +12,7 @@ import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
import { STIRLING_SAAS_URL } from '@app/constants/connection';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import '@app/routes/authShared/auth.css';
|
||||
import { DisabledButtonWithTooltip } from '@app/components/shared/DisabledButtonWithTooltip';
|
||||
|
||||
enum SetupStep {
|
||||
SaaSLogin,
|
||||
@@ -28,6 +29,7 @@ interface SetupWizardProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete, noLayout = false, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeStep, setActiveStep] = useState<SetupStep>(SetupStep.SaaSLogin);
|
||||
@@ -313,10 +315,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete, noLayout =
|
||||
const loadLockedConfig = useCallback(async () => {
|
||||
const currentConfig = await connectionModeService.getCurrentConfig();
|
||||
if (!currentConfig.lock_connection_mode) return;
|
||||
// server_config may be null when the user switched to local mode from a locked deployment.
|
||||
// Fall back to the URL saved by switchToLocal() so the wizard still shows locked login.
|
||||
const serverUrl = currentConfig.server_config?.url
|
||||
|| localStorage.getItem('stirling-provisioned-server-url');
|
||||
const serverUrl = currentConfig.server_config?.url;
|
||||
if (!serverUrl) return;
|
||||
|
||||
setLockConnectionMode(true);
|
||||
@@ -433,6 +432,26 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete, noLayout =
|
||||
>
|
||||
{t('setup.selfhosted.unreachable.retry', 'Retry')}
|
||||
</Button>
|
||||
{lockConnectionMode ? (
|
||||
<DisabledButtonWithTooltip
|
||||
tooltip={t('setup.selfhosted.changeServerLocked', 'Your organisation has restricted this app to a specific server')}
|
||||
>
|
||||
{t('setup.selfhosted.unreachable.changeServer', 'Connect to a different server')}
|
||||
</DisabledButtonWithTooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setLockedServerUnreachable(false);
|
||||
setActiveStep(SetupStep.ServerSelection);
|
||||
}}
|
||||
>
|
||||
{t('setup.selfhosted.unreachable.changeServer', 'Connect to a different server')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="white"
|
||||
@@ -458,18 +477,16 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete, noLayout =
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
{lockConnectionMode && (
|
||||
<div className="navigation-link-container" style={{ marginTop: '1.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLocalMode}
|
||||
className="navigation-link-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{t('setup.selfhosted.switchToLocal', 'Use local tools instead')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="navigation-link-container" style={{ marginTop: '1.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLocalMode}
|
||||
className="navigation-link-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{t('setup.selfhosted.switchToLocal', 'Use local tools instead')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
.locked-button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
color: var(--mantine-color-dimmed);
|
||||
font-size: var(--mantine-font-size-md);
|
||||
background-color: var(--mantine-color-blue-light);
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.locked-button-tooltip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: calc(100% + 8px);
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: var(--mantine-z-index-popover);
|
||||
box-shadow: var(--mantine-shadow-lg);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
padding: 6px var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.locked-button-tooltip-arrow {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid var(--mantine-color-dark-7);
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import '@app/components/shared/DisabledButtonWithTooltip.css';
|
||||
|
||||
interface DisabledButtonWithTooltipProps {
|
||||
/** Tooltip text shown on hover */
|
||||
tooltip: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visually disabled button that still responds to hover (showing a tooltip).
|
||||
* Mantine's disabled prop prevents pointer events entirely, so this is a plain
|
||||
* div styled to match a disabled button with a custom hover tooltip.
|
||||
*/
|
||||
export function DisabledButtonWithTooltip({ tooltip, children, className, style }: DisabledButtonWithTooltipProps) {
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
return (
|
||||
<div
|
||||
className="relative w-full"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div className={`locked-button${className ? ` ${className}` : ''}`} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
{hovered && (
|
||||
<div className="locked-button-tooltip">
|
||||
{tooltip}
|
||||
<div className="locked-button-tooltip-arrow" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { endpointAvailabilityService } from '@app/services/endpointAvailabilityService';
|
||||
import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor';
|
||||
|
||||
export type ConnectionMode = 'saas' | 'selfhosted' | 'local';
|
||||
|
||||
@@ -37,6 +38,7 @@ export interface ConnectionTestResult {
|
||||
}
|
||||
|
||||
export const LOCAL_MODE_STORAGE_KEY = 'stirling-local-mode';
|
||||
export const JWT_EXPIRED_PROMPTED_KEY = 'stirling-jwt-expired-prompted';
|
||||
|
||||
export class ConnectionModeService {
|
||||
private static instance: ConnectionModeService;
|
||||
@@ -87,8 +89,11 @@ export class ConnectionModeService {
|
||||
|
||||
const localFlag = localStorage.getItem(LOCAL_MODE_STORAGE_KEY);
|
||||
|
||||
if (config.mode === 'saas' && localFlag === 'true') {
|
||||
// User previously chose local-only mode.
|
||||
if (localFlag === 'true') {
|
||||
// User previously chose local-only mode (signed out or explicitly went offline).
|
||||
// Applies to both 'saas' and 'selfhosted' store modes — the Rust guard on locked
|
||||
// deployments can't change 'selfhosted' to 'saas' in the store, so we check the
|
||||
// flag regardless of what the store says.
|
||||
config.mode = 'local';
|
||||
} else if (
|
||||
config.mode === 'saas' &&
|
||||
@@ -122,8 +127,9 @@ export class ConnectionModeService {
|
||||
throw new Error('Connection mode is locked by provisioning');
|
||||
}
|
||||
|
||||
// Clear local-only flag if switching to a real account
|
||||
// Clear local-only flag and expiry-prompted flag when signing in
|
||||
localStorage.removeItem(LOCAL_MODE_STORAGE_KEY);
|
||||
localStorage.removeItem(JWT_EXPIRED_PROMPTED_KEY);
|
||||
|
||||
console.log('Switching to SaaS mode');
|
||||
|
||||
@@ -153,18 +159,17 @@ export class ConnectionModeService {
|
||||
// the 'local' distinction purely on the TypeScript side.
|
||||
localStorage.setItem(LOCAL_MODE_STORAGE_KEY, 'true');
|
||||
|
||||
// When a locked provisioned deployment falls back to local, preserve the server URL
|
||||
// so the SetupWizard can pre-fill it if the user tries to sign in again.
|
||||
if (this.currentConfig?.lock_connection_mode && this.currentConfig.server_config?.url) {
|
||||
localStorage.setItem('stirling-provisioned-server-url', this.currentConfig.server_config.url);
|
||||
}
|
||||
|
||||
await invoke('set_connection_mode', {
|
||||
mode: 'saas',
|
||||
serverConfig: null,
|
||||
});
|
||||
|
||||
this.currentConfig = { mode: 'local', server_config: null, lock_connection_mode: this.currentConfig?.lock_connection_mode ?? false };
|
||||
// For locked deployments, preserve server_config so the sign-in form can still
|
||||
// show the correct server URL if the user wants to sign in later.
|
||||
const isLocked = this.currentConfig?.lock_connection_mode ?? false;
|
||||
const preservedServerConfig = isLocked ? (this.currentConfig?.server_config ?? null) : null;
|
||||
|
||||
this.currentConfig = { mode: 'local', server_config: preservedServerConfig, lock_connection_mode: isLocked };
|
||||
|
||||
// Clear endpoint availability cache when mode changes
|
||||
endpointAvailabilityService.clearCache();
|
||||
@@ -173,8 +178,9 @@ export class ConnectionModeService {
|
||||
}
|
||||
|
||||
async switchToSelfHosted(serverConfig: ServerConfig): Promise<void> {
|
||||
// Clear local-only flag if switching to a real account
|
||||
// Clear local-only flag and expiry-prompted flag when signing in
|
||||
localStorage.removeItem(LOCAL_MODE_STORAGE_KEY);
|
||||
localStorage.removeItem(JWT_EXPIRED_PROMPTED_KEY);
|
||||
|
||||
console.log('Switching to self-hosted mode:', serverConfig);
|
||||
|
||||
@@ -189,6 +195,10 @@ export class ConnectionModeService {
|
||||
endpointAvailabilityService.clearCache();
|
||||
console.log('Cleared endpoint availability cache due to connection mode change');
|
||||
|
||||
// Single authoritative calling point for health monitoring — every path that
|
||||
// switches to self-hosted mode funnels through here.
|
||||
selfHostedServerMonitor.start(serverConfig.url);
|
||||
|
||||
this.notifyListeners();
|
||||
|
||||
console.log('Switched to self-hosted mode successfully');
|
||||
|
||||
@@ -441,7 +441,7 @@ const licenseService = {
|
||||
*/
|
||||
async getLicenseInfo(): Promise<LicenseInfo> {
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/admin/license-info');
|
||||
const response = await apiClient.get('/api/v1/admin/license-info', { suppressErrorToast: true });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching license info:', error);
|
||||
|
||||
Reference in New Issue
Block a user