Headless windows installer (#5664)

This commit is contained in:
Anthony Stirling
2026-02-06 18:06:01 +00:00
committed by GitHub
parent 94e517df3c
commit ba72a2a623
21 changed files with 557 additions and 34 deletions

View File

@@ -9,6 +9,8 @@ import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
import { connectionModeService } from '@app/services/connectionModeService';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { authService } from '@app/services/authService';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { isTauri } from '@tauri-apps/api/core';
/**
* Desktop application providers
@@ -21,7 +23,6 @@ export function AppProviders({ children }: { children: ReactNode }) {
const [connectionMode, setConnectionMode] = useState<'saas' | 'selfhosted' | null>(null);
const [authChecked, setAuthChecked] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Load connection mode on mount
useEffect(() => {
void connectionModeService.getCurrentMode().then(setConnectionMode);
@@ -51,6 +52,42 @@ export function AppProviders({ children }: { children: ReactNode }) {
const shouldMonitorBackend = setupComplete && !isFirstLaunch && connectionMode === 'saas';
useBackendInitializer(shouldMonitorBackend);
useEffect(() => {
if (!authChecked) {
return;
}
if (!isTauri()) {
return;
}
const currentWindow = getCurrentWindow();
currentWindow
.show()
.then(() => currentWindow.unminimize().catch(() => {}))
.then(() => currentWindow.setFocus().catch(() => {}))
.then(() => currentWindow.requestUserAttention(1).catch(() => {}))
.catch(() => {});
}, [authChecked]);
if (!authChecked) {
return (
<ProprietaryAppProviders
appConfigRetryOptions={{
maxRetries: 5,
initialDelay: 1000,
}}
appConfigProviderProps={{
initialConfig: DESKTOP_DEFAULT_APP_CONFIG,
bootstrapMode: 'non-blocking',
autoFetch: false,
}}
>
<div style={{ minHeight: '100vh' }} />
</ProprietaryAppProviders>
);
}
// Show setup wizard on first launch
if (isFirstLaunch && !setupComplete) {
return (

View File

@@ -31,11 +31,13 @@ export const ConnectionSettings: React.FC = () => {
setLoading(true);
await authService.logout();
// Switch to SaaS mode
await connectionModeService.switchToSaaS(STIRLING_SAAS_URL);
if (!config?.lock_connection_mode) {
// Switch to SaaS mode
await connectionModeService.switchToSaaS(STIRLING_SAAS_URL);
// Reset setup completion to force login screen on reload
await connectionModeService.resetSetupCompletion();
// Reset setup completion to force login screen on reload
await connectionModeService.resetSetupCompletion();
}
// Reload config
const newConfig = await connectionModeService.getCurrentConfig();

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout';
import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen';
@@ -10,7 +10,6 @@ import { AuthServiceError, authService, UserInfo } from '@app/services/authServi
import { tauriBackendService } from '@app/services/tauriBackendService';
import { STIRLING_SAAS_URL } from '@app/constants/connection';
import { listen } from '@tauri-apps/api/event';
import { useEffect } from 'react';
import '@app/routes/authShared/auth.css';
enum SetupStep {
@@ -32,6 +31,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
const [error, setError] = useState<string | null>(null);
const [selfHostedMfaCode, setSelfHostedMfaCode] = useState('');
const [selfHostedMfaRequired, setSelfHostedMfaRequired] = useState(false);
const [lockConnectionMode, setLockConnectionMode] = useState(false);
const handleSaaSLogin = async (username: string, password: string) => {
if (!serverConfig) {
@@ -82,6 +82,9 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
};
const handleSelfHostedClick = () => {
if (lockConnectionMode) {
return;
}
setError(null);
setActiveStep(SetupStep.ServerSelection);
};
@@ -259,6 +262,9 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
}, [onComplete, serverConfig?.url]);
const handleBack = () => {
if (lockConnectionMode) {
return;
}
setError(null);
if (activeStep === SetupStep.SelfHostedLogin) {
setSelfHostedMfaCode('');
@@ -272,10 +278,23 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
}
};
useEffect(() => {
const loadConfig = async () => {
const currentConfig = await connectionModeService.getCurrentConfig();
if (currentConfig.lock_connection_mode && currentConfig.server_config?.url) {
setLockConnectionMode(true);
setServerConfig(currentConfig.server_config);
setActiveStep(SetupStep.SelfHostedLogin);
}
};
void loadConfig();
}, []);
return (
<DesktopAuthLayout>
{/* Step Content */}
{activeStep === SetupStep.SaaSLogin && (
{!lockConnectionMode && activeStep === SetupStep.SaaSLogin && (
<SaaSLoginScreen
serverUrl={serverConfig?.url || STIRLING_SAAS_URL}
onLogin={handleSaaSLogin}
@@ -287,7 +306,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
/>
)}
{activeStep === SetupStep.SaaSSignup && (
{!lockConnectionMode && activeStep === SetupStep.SaaSSignup && (
<SaaSSignupScreen
loading={loading}
error={error}
@@ -296,7 +315,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
/>
)}
{activeStep === SetupStep.ServerSelection && (
{!lockConnectionMode && activeStep === SetupStep.ServerSelection && (
<ServerSelectionScreen
onSelect={handleServerSelection}
loading={loading}
@@ -319,7 +338,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
)}
{/* Back Button */}
{activeStep > SetupStep.SaaSLogin && !loading && (
{!lockConnectionMode && activeStep > SetupStep.SaaSLogin && !loading && (
<div className="navigation-link-container" style={{ marginTop: '1.5rem' }}>
<button
type="button"

View File

@@ -16,8 +16,11 @@ export function useAccountLogout() {
try {
await signOut();
await connectionModeService.switchToSaaS(STIRLING_SAAS_URL);
await connectionModeService.resetSetupCompletion().catch(() => {});
const currentConfig = await connectionModeService.getCurrentConfig();
if (!currentConfig.lock_connection_mode) {
await connectionModeService.switchToSaaS(STIRLING_SAAS_URL);
await connectionModeService.resetSetupCompletion().catch(() => {});
}
window.history.replaceState({}, '', '/');
window.location.reload();

View File

@@ -107,6 +107,9 @@ export function setupApiInterceptors(client: AxiosInstance): void {
// Handle 401 Unauthorized - try to refresh token
if (error.response?.status === 401 && !originalRequest._retry) {
if (typeof window !== 'undefined') {
console.warn('[apiClientSetup] 401 on path:', window.location.pathname, 'url:', originalRequest.url);
}
if (originalRequest.skipAuthRedirect) {
return Promise.reject(error);
}

View File

@@ -17,6 +17,7 @@ export interface ServerConfig {
export interface ConnectionConfig {
mode: ConnectionMode;
server_config: ServerConfig | null;
lock_connection_mode: boolean;
}
export interface DiagnosticResult {
@@ -50,7 +51,7 @@ export class ConnectionModeService {
if (!this.configLoadedOnce) {
await this.loadConfig();
}
return this.currentConfig || { mode: 'saas', server_config: null };
return this.currentConfig || { mode: 'saas', server_config: null, lock_connection_mode: false };
}
async getCurrentMode(): Promise<ConnectionMode> {
@@ -84,12 +85,16 @@ export class ConnectionModeService {
} catch (error) {
console.error('Failed to load connection config:', error);
// Default to SaaS mode on error
this.currentConfig = { mode: 'saas', server_config: null };
this.currentConfig = { mode: 'saas', server_config: null, lock_connection_mode: false };
this.configLoadedOnce = true;
}
}
async switchToSaaS(saasServerUrl: string): Promise<void> {
if (this.currentConfig?.lock_connection_mode) {
throw new Error('Connection mode is locked by provisioning');
}
console.log('Switching to SaaS mode');
const serverConfig: ServerConfig = { url: saasServerUrl };
@@ -99,7 +104,7 @@ export class ConnectionModeService {
serverConfig,
});
this.currentConfig = { mode: 'saas', server_config: serverConfig };
this.currentConfig = { mode: 'saas', server_config: serverConfig, lock_connection_mode: this.currentConfig?.lock_connection_mode ?? false };
this.notifyListeners();
console.log('Switched to SaaS mode successfully');
@@ -113,7 +118,7 @@ export class ConnectionModeService {
serverConfig,
});
this.currentConfig = { mode: 'selfhosted', server_config: serverConfig };
this.currentConfig = { mode: 'selfhosted', server_config: serverConfig, lock_connection_mode: this.currentConfig?.lock_connection_mode ?? false };
this.notifyListeners();
console.log('Switched to self-hosted mode successfully');
@@ -901,6 +906,9 @@ export class ConnectionModeService {
}
async resetSetupCompletion(): Promise<void> {
if (this.currentConfig?.lock_connection_mode) {
return;
}
try {
await invoke('reset_setup_completion');
console.log('Setup completion flag reset successfully');

View File

@@ -171,6 +171,12 @@ export function ServerExperienceProvider({ children }: { children: ReactNode })
if (!config) {
return;
}
if (!config.appVersion) {
return;
}
if (loginEnabled && !isAuthenticated) {
return;
}
const shouldUseAdminData = (config.enableLogin ?? true) && config.isAdmin;
// Use WAU estimate for no-login scenarios OR for login non-admin users
@@ -353,4 +359,3 @@ export function useServerExperienceContext() {
}
return context;
}