mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Fix backend issues in desktop app (#4995)
# Description of Changes Fixes two distinct but related issues in the backend of the desktop app: - Correctly shows tools as unavaialable when the backend doesn't have the dependencies or has disabled them etc. (same as web version - this primarily didn't work on desktop because the app spawns before the backend is running) - Fixes infinite re-rendering issues caused by the app polling whether the backend is healthy or not
This commit is contained in:
@@ -25,6 +25,7 @@ public class ExternalAppDepConfig {
|
|||||||
private final String weasyprintPath;
|
private final String weasyprintPath;
|
||||||
private final String unoconvPath;
|
private final String unoconvPath;
|
||||||
private final Map<String, List<String>> commandToGroupMapping;
|
private final Map<String, List<String>> commandToGroupMapping;
|
||||||
|
private volatile boolean dependenciesChecked = false;
|
||||||
|
|
||||||
public ExternalAppDepConfig(
|
public ExternalAppDepConfig(
|
||||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||||
@@ -111,6 +112,10 @@ public class ExternalAppDepConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDependenciesChecked() {
|
||||||
|
return dependenciesChecked;
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void checkDependencies() {
|
public void checkDependencies() {
|
||||||
// Check core dependencies
|
// Check core dependencies
|
||||||
@@ -162,5 +167,7 @@ public class ExternalAppDepConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
endpointConfiguration.logDisabledEndpointsSummary();
|
endpointConfiguration.logDisabledEndpointsSummary();
|
||||||
|
dependenciesChecked = true;
|
||||||
|
log.info("Dependency checks completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -35,6 +35,7 @@ public class ConfigController {
|
|||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
private final ServerCertificateServiceInterface serverCertificateService;
|
private final ServerCertificateServiceInterface serverCertificateService;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
private final stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig;
|
||||||
|
|
||||||
public ConfigController(
|
public ConfigController(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@@ -43,12 +44,14 @@ public class ConfigController {
|
|||||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||||
ServerCertificateServiceInterface serverCertificateService,
|
ServerCertificateServiceInterface serverCertificateService,
|
||||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||||
UserServiceInterface userService) {
|
UserServiceInterface userService,
|
||||||
|
stirling.software.SPDF.config.ExternalAppDepConfig externalAppDepConfig) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
this.serverCertificateService = serverCertificateService;
|
this.serverCertificateService = serverCertificateService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.externalAppDepConfig = externalAppDepConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/app-config")
|
@GetMapping("/app-config")
|
||||||
@@ -56,6 +59,9 @@ public class ConfigController {
|
|||||||
Map<String, Object> configData = new HashMap<>();
|
Map<String, Object> configData = new HashMap<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Add dependency check status
|
||||||
|
configData.put("dependenciesReady", externalAppDepConfig.isDependenciesChecked());
|
||||||
|
|
||||||
// Get AppConfig bean
|
// Get AppConfig bean
|
||||||
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
|
AppConfig appConfig = applicationContext.getBean(AppConfig.class);
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
use reqwest;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn check_backend_health(port: u16) -> Result<bool, String> {
|
|
||||||
let url = format!("http://localhost:{}/api/v1/info/status", port);
|
|
||||||
|
|
||||||
match reqwest::Client::new()
|
|
||||||
.get(&url)
|
|
||||||
.timeout(std::time::Duration::from_secs(5))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(response) => Ok(response.status().is_success()),
|
|
||||||
Err(_) => Ok(false), // Return false instead of error for connection failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ pub mod files;
|
|||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod default_app;
|
pub mod default_app;
|
||||||
pub mod health;
|
|
||||||
|
|
||||||
pub use backend::{cleanup_backend, get_backend_port, start_backend};
|
pub use backend::{cleanup_backend, get_backend_port, start_backend};
|
||||||
pub use files::{add_opened_file, clear_opened_files, get_opened_files};
|
pub use files::{add_opened_file, clear_opened_files, get_opened_files};
|
||||||
@@ -24,4 +23,3 @@ pub use auth::{
|
|||||||
start_oauth_login,
|
start_oauth_login,
|
||||||
};
|
};
|
||||||
pub use default_app::{is_default_pdf_handler, set_as_default_pdf_handler};
|
pub use default_app::{is_default_pdf_handler, set_as_default_pdf_handler};
|
||||||
pub use health::check_backend_health;
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ mod state;
|
|||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
add_opened_file,
|
add_opened_file,
|
||||||
check_backend_health,
|
|
||||||
cleanup_backend,
|
cleanup_backend,
|
||||||
clear_auth_token,
|
clear_auth_token,
|
||||||
clear_opened_files,
|
clear_opened_files,
|
||||||
@@ -94,7 +93,6 @@ pub fn run() {
|
|||||||
set_as_default_pdf_handler,
|
set_as_default_pdf_handler,
|
||||||
is_first_launch,
|
is_first_launch,
|
||||||
reset_setup_completion,
|
reset_setup_completion,
|
||||||
check_backend_health,
|
|
||||||
login,
|
login,
|
||||||
save_auth_token,
|
save_auth_token,
|
||||||
get_auth_token,
|
get_auth_token,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface AppConfig {
|
|||||||
appVersion?: string;
|
appVersion?: string;
|
||||||
machineType?: string;
|
machineType?: string;
|
||||||
activeSecurity?: boolean;
|
activeSecurity?: boolean;
|
||||||
|
dependenciesReady?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export function useBackendHealth(): BackendHealthState {
|
|||||||
return {
|
return {
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
message: null,
|
message: null,
|
||||||
isChecking: false,
|
|
||||||
lastChecked: Date.now(),
|
|
||||||
error: null,
|
error: null,
|
||||||
isHealthy: true,
|
isHealthy: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ export type BackendStatus = 'stopped' | 'starting' | 'healthy' | 'unhealthy';
|
|||||||
export interface BackendHealthState {
|
export interface BackendHealthState {
|
||||||
status: BackendStatus;
|
status: BackendStatus;
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
lastChecked?: number;
|
|
||||||
isChecking: boolean;
|
|
||||||
error: string | null;
|
error: string | null;
|
||||||
isHealthy: boolean;
|
isHealthy: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const colorScheme = useComputedColorScheme('light');
|
const colorScheme = useComputedColorScheme('light');
|
||||||
const { isHealthy, isChecking, checkHealth } = useBackendHealth();
|
const { status, isHealthy, checkHealth } = useBackendHealth();
|
||||||
|
|
||||||
const label = useMemo(() => {
|
const label = useMemo(() => {
|
||||||
if (isChecking) {
|
if (status === 'starting') {
|
||||||
return t('backendHealth.checking', 'Checking backend status...');
|
return t('backendHealth.checking', 'Checking backend status...');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,17 +25,17 @@ export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return t('backendHealth.offline', 'Backend Offline');
|
return t('backendHealth.offline', 'Backend Offline');
|
||||||
}, [isChecking, isHealthy, t]);
|
}, [status, isHealthy, t]);
|
||||||
|
|
||||||
const dotColor = useMemo(() => {
|
const dotColor = useMemo(() => {
|
||||||
if (isChecking) {
|
if (status === 'starting') {
|
||||||
return theme.colors.yellow?.[5] ?? '#fcc419';
|
return theme.colors.yellow?.[5] ?? '#fcc419';
|
||||||
}
|
}
|
||||||
if (isHealthy) {
|
if (isHealthy) {
|
||||||
return theme.colors.green?.[5] ?? '#37b24d';
|
return theme.colors.green?.[5] ?? '#37b24d';
|
||||||
}
|
}
|
||||||
return theme.colors.red?.[6] ?? '#e03131';
|
return theme.colors.red?.[6] ?? '#e03131';
|
||||||
}, [isChecking, isHealthy, theme.colors.green, theme.colors.red, theme.colors.yellow]);
|
}, [status, isHealthy, theme.colors.green, theme.colors.red, theme.colors.yellow]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLSpanElement>) => {
|
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { tauriBackendService } from '@app/services/tauriBackendService';
|
|||||||
import { isBackendNotReadyError } from '@app/constants/backendErrors';
|
import { isBackendNotReadyError } from '@app/constants/backendErrors';
|
||||||
import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
|
import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
|
||||||
import { connectionModeService } from '@desktop/services/connectionModeService';
|
import { connectionModeService } from '@desktop/services/connectionModeService';
|
||||||
|
import type { AppConfig } from '@app/contexts/AppConfigContext';
|
||||||
|
|
||||||
|
|
||||||
interface EndpointConfig {
|
interface EndpointConfig {
|
||||||
@@ -28,6 +29,17 @@ function getErrorMessage(err: unknown): string {
|
|||||||
return 'Unknown error occurred';
|
return 'Unknown error occurred';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkDependenciesReady(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config', {
|
||||||
|
suppressErrorToast: true,
|
||||||
|
});
|
||||||
|
return response.data?.dependenciesReady ?? false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop-specific endpoint checker that hits the backend directly via axios.
|
* Desktop-specific endpoint checker that hits the backend directly via axios.
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +50,7 @@ export function useEndpointEnabled(endpoint: string): {
|
|||||||
refetch: () => Promise<void>;
|
refetch: () => Promise<void>;
|
||||||
} {
|
} {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [enabled, setEnabled] = useState<boolean | null>(() => (endpoint ? true : null));
|
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
@@ -68,6 +80,11 @@ export function useEndpointEnabled(endpoint: string): {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dependenciesReady = await checkDependenciesReady();
|
||||||
|
if (!dependenciesReady) {
|
||||||
|
return; // Health monitor will trigger retry when truly ready
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -76,27 +93,27 @@ export function useEndpointEnabled(endpoint: string): {
|
|||||||
suppressErrorToast: true,
|
suppressErrorToast: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isMountedRef.current) return;
|
|
||||||
setEnabled(response.data);
|
setEnabled(response.data);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const isBackendStarting = isBackendNotReadyError(err);
|
const isBackendStarting = isBackendNotReadyError(err);
|
||||||
const message = getErrorMessage(err);
|
const message = getErrorMessage(err);
|
||||||
if (!isMountedRef.current) return;
|
|
||||||
setError(isBackendStarting ? t('backendHealth.starting', 'Backend starting up...') : message);
|
|
||||||
setEnabled(true);
|
|
||||||
|
|
||||||
if (!retryTimeoutRef.current) {
|
if (isBackendStarting) {
|
||||||
retryTimeoutRef.current = setTimeout(() => {
|
setError(t('backendHealth.starting', 'Backend starting up...'));
|
||||||
retryTimeoutRef.current = null;
|
if (!retryTimeoutRef.current) {
|
||||||
fetchEndpointStatus();
|
retryTimeoutRef.current = setTimeout(() => {
|
||||||
}, RETRY_DELAY_MS);
|
retryTimeoutRef.current = null;
|
||||||
|
fetchEndpointStatus();
|
||||||
|
}, RETRY_DELAY_MS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(message);
|
||||||
|
setEnabled(false);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isMountedRef.current) {
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [endpoint, clearRetryTimeout]);
|
}, [endpoint, clearRetryTimeout, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
@@ -136,15 +153,9 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
|||||||
refetch: () => Promise<void>;
|
refetch: () => Promise<void>;
|
||||||
} {
|
} {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>(() => {
|
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>({});
|
||||||
if (!endpoints || endpoints.length === 0) return {};
|
|
||||||
return endpoints.reduce((acc, endpointName) => {
|
|
||||||
acc[endpointName] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, boolean>);
|
|
||||||
});
|
|
||||||
const [endpointDetails, setEndpointDetails] = useState<Record<string, EndpointAvailabilityDetails>>({});
|
const [endpointDetails, setEndpointDetails] = useState<Record<string, EndpointAvailabilityDetails>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -167,26 +178,31 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
|||||||
clearRetryTimeout();
|
clearRetryTimeout();
|
||||||
|
|
||||||
if (!endpoints || endpoints.length === 0) {
|
if (!endpoints || endpoints.length === 0) {
|
||||||
if (!isMountedRef.current) return;
|
|
||||||
setEndpointStatus({});
|
setEndpointStatus({});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dependenciesReady = await checkDependenciesReady();
|
||||||
|
if (!dependenciesReady) {
|
||||||
|
return; // Health monitor will trigger retry when truly ready
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const endpointsParam = endpoints.join(',');
|
const endpointsParam = endpoints.join(',');
|
||||||
|
|
||||||
const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>('/api/v1/config/endpoints-availability', {
|
const response = await apiClient.get<Record<string, EndpointAvailabilityDetails>>(
|
||||||
params: { endpoints: endpointsParam },
|
`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`,
|
||||||
suppressErrorToast: true,
|
{
|
||||||
});
|
suppressErrorToast: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!isMountedRef.current) return;
|
|
||||||
const details = Object.entries(response.data).reduce((acc, [endpointName, detail]) => {
|
const details = Object.entries(response.data).reduce((acc, [endpointName, detail]) => {
|
||||||
acc[endpointName] = {
|
acc[endpointName] = {
|
||||||
enabled: detail?.enabled ?? true,
|
enabled: detail?.enabled ?? false,
|
||||||
reason: detail?.reason ?? null,
|
reason: detail?.reason ?? null,
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
@@ -198,34 +214,34 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
|||||||
}, {} as Record<string, boolean>);
|
}, {} as Record<string, boolean>);
|
||||||
|
|
||||||
setEndpointDetails(prev => ({ ...prev, ...details }));
|
setEndpointDetails(prev => ({ ...prev, ...details }));
|
||||||
setEndpointStatus(statusMap);
|
setEndpointStatus(prev => ({ ...prev, ...statusMap }));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const isBackendStarting = isBackendNotReadyError(err);
|
const isBackendStarting = isBackendNotReadyError(err);
|
||||||
const message = getErrorMessage(err);
|
const message = getErrorMessage(err);
|
||||||
if (!isMountedRef.current) return;
|
|
||||||
setError(isBackendStarting ? t('backendHealth.starting', 'Backend starting up...') : message);
|
|
||||||
|
|
||||||
const fallbackStatus = endpoints.reduce((acc, endpointName) => {
|
if (isBackendStarting) {
|
||||||
const fallbackDetail: EndpointAvailabilityDetails = { enabled: true, reason: null };
|
setError(t('backendHealth.starting', 'Backend starting up...'));
|
||||||
acc.status[endpointName] = true;
|
if (!retryTimeoutRef.current) {
|
||||||
acc.details[endpointName] = fallbackDetail;
|
retryTimeoutRef.current = setTimeout(() => {
|
||||||
return acc;
|
retryTimeoutRef.current = null;
|
||||||
}, { status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> });
|
fetchAllEndpointStatuses();
|
||||||
setEndpointStatus(fallbackStatus.status);
|
}, RETRY_DELAY_MS);
|
||||||
setEndpointDetails(prev => ({ ...prev, ...fallbackStatus.details }));
|
}
|
||||||
|
} else {
|
||||||
if (!retryTimeoutRef.current) {
|
setError(message);
|
||||||
retryTimeoutRef.current = setTimeout(() => {
|
const fallbackStatus = endpoints.reduce((acc, endpointName) => {
|
||||||
retryTimeoutRef.current = null;
|
const fallbackDetail: EndpointAvailabilityDetails = { enabled: false, reason: 'UNKNOWN' };
|
||||||
fetchAllEndpointStatuses();
|
acc.status[endpointName] = false;
|
||||||
}, RETRY_DELAY_MS);
|
acc.details[endpointName] = fallbackDetail;
|
||||||
|
return acc;
|
||||||
|
}, { status: {} as Record<string, boolean>, details: {} as Record<string, EndpointAvailabilityDetails> });
|
||||||
|
setEndpointStatus(fallbackStatus.status);
|
||||||
|
setEndpointDetails(prev => ({ ...prev, ...fallbackStatus.details }));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isMountedRef.current) {
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [endpoints, clearRetryTimeout]);
|
}, [endpoints, clearRetryTimeout, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!endpoints || endpoints.length === 0) {
|
if (!endpoints || endpoints.length === 0) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class BackendHealthMonitor {
|
|||||||
private readonly intervalMs: number;
|
private readonly intervalMs: number;
|
||||||
private state: BackendHealthState = {
|
private state: BackendHealthState = {
|
||||||
status: tauriBackendService.getBackendStatus(),
|
status: tauriBackendService.getBackendStatus(),
|
||||||
isChecking: false,
|
|
||||||
error: null,
|
error: null,
|
||||||
isHealthy: tauriBackendService.getBackendStatus() === 'healthy',
|
isHealthy: tauriBackendService.getBackendStatus() === 'healthy',
|
||||||
};
|
};
|
||||||
@@ -26,20 +25,30 @@ class BackendHealthMonitor {
|
|||||||
message: status === 'healthy'
|
message: status === 'healthy'
|
||||||
? i18n.t('backendHealth.online', 'Backend Online')
|
? i18n.t('backendHealth.online', 'Backend Online')
|
||||||
: this.state.message ?? i18n.t('backendHealth.offline', 'Backend Offline'),
|
: this.state.message ?? i18n.t('backendHealth.offline', 'Backend Offline'),
|
||||||
isChecking: status === 'healthy' ? false : this.state.isChecking,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateState(partial: Partial<BackendHealthState>) {
|
private updateState(partial: Partial<BackendHealthState>) {
|
||||||
const nextStatus = partial.status ?? this.state.status;
|
const nextStatus = partial.status ?? this.state.status;
|
||||||
this.state = {
|
const nextState = {
|
||||||
...this.state,
|
...this.state,
|
||||||
...partial,
|
...partial,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
isHealthy: nextStatus === 'healthy',
|
isHealthy: nextStatus === 'healthy',
|
||||||
};
|
};
|
||||||
this.listeners.forEach((listener) => listener(this.state));
|
|
||||||
|
// Only notify listeners if meaningful state changed
|
||||||
|
const meaningfulChange =
|
||||||
|
this.state.status !== nextState.status ||
|
||||||
|
this.state.error !== nextState.error ||
|
||||||
|
this.state.message !== nextState.message;
|
||||||
|
|
||||||
|
this.state = nextState;
|
||||||
|
|
||||||
|
if (meaningfulChange) {
|
||||||
|
this.listeners.forEach((listener) => listener(this.state));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensurePolling() {
|
private ensurePolling() {
|
||||||
@@ -60,29 +69,19 @@ class BackendHealthMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pollOnce(): Promise<boolean> {
|
private async pollOnce(): Promise<boolean> {
|
||||||
this.updateState({
|
|
||||||
isChecking: true,
|
|
||||||
lastChecked: Date.now(),
|
|
||||||
error: this.state.error ?? 'Backend offline',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const healthy = await tauriBackendService.checkBackendHealth();
|
const healthy = await tauriBackendService.checkBackendHealth();
|
||||||
if (healthy) {
|
if (healthy) {
|
||||||
this.updateState({
|
this.updateState({
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
isChecking: false,
|
|
||||||
message: i18n.t('backendHealth.online', 'Backend Online'),
|
message: i18n.t('backendHealth.online', 'Backend Online'),
|
||||||
error: null,
|
error: null,
|
||||||
lastChecked: Date.now(),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.updateState({
|
this.updateState({
|
||||||
status: 'unhealthy',
|
status: 'unhealthy',
|
||||||
isChecking: false,
|
|
||||||
message: i18n.t('backendHealth.offline', 'Backend Offline'),
|
message: i18n.t('backendHealth.offline', 'Backend Offline'),
|
||||||
error: i18n.t('backendHealth.offline', 'Backend Offline'),
|
error: i18n.t('backendHealth.offline', 'Backend Offline'),
|
||||||
lastChecked: Date.now(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return healthy;
|
return healthy;
|
||||||
@@ -90,10 +89,8 @@ class BackendHealthMonitor {
|
|||||||
console.error('[BackendHealthMonitor] Health check failed:', error);
|
console.error('[BackendHealthMonitor] Health check failed:', error);
|
||||||
this.updateState({
|
this.updateState({
|
||||||
status: 'unhealthy',
|
status: 'unhealthy',
|
||||||
isChecking: false,
|
|
||||||
message: 'Backend is unavailable',
|
message: 'Backend is unavailable',
|
||||||
error: 'Backend offline',
|
error: 'Backend offline',
|
||||||
lastChecked: Date.now(),
|
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ export class TauriBackendService {
|
|||||||
async checkBackendHealth(): Promise<boolean> {
|
async checkBackendHealth(): Promise<boolean> {
|
||||||
const mode = await connectionModeService.getCurrentMode();
|
const mode = await connectionModeService.getCurrentMode();
|
||||||
|
|
||||||
// For self-hosted mode, check the configured remote server
|
// Determine base URL based on mode
|
||||||
|
let baseUrl: string;
|
||||||
if (mode === 'selfhosted') {
|
if (mode === 'selfhosted') {
|
||||||
const serverConfig = await connectionModeService.getServerConfig();
|
const serverConfig = await connectionModeService.getServerConfig();
|
||||||
if (!serverConfig) {
|
if (!serverConfig) {
|
||||||
@@ -141,47 +142,37 @@ export class TauriBackendService {
|
|||||||
this.setStatus('unhealthy');
|
this.setStatus('unhealthy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
baseUrl = serverConfig.url.replace(/\/$/, '');
|
||||||
|
} else {
|
||||||
|
// SaaS mode - check bundled local backend
|
||||||
|
if (!this.backendStarted) {
|
||||||
|
this.setStatus('stopped');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.backendPort) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
baseUrl = `http://localhost:${this.backendPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Check if backend is ready (dependencies checked)
|
||||||
const baseUrl = serverConfig.url.replace(/\/$/, '');
|
try {
|
||||||
const healthUrl = `${baseUrl}/api/v1/info/status`;
|
const configUrl = `${baseUrl}/api/v1/config/app-config`;
|
||||||
const response = await fetch(healthUrl, {
|
const response = await fetch(configUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
connectTimeout: 5000,
|
connectTimeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isHealthy = response.ok;
|
if (!response.ok) {
|
||||||
this.setStatus(isHealthy ? 'healthy' : 'unhealthy');
|
|
||||||
return isHealthy;
|
|
||||||
} catch (error) {
|
|
||||||
const errorStr = String(error);
|
|
||||||
if (!errorStr.includes('connection refused') && !errorStr.includes('No connection could be made')) {
|
|
||||||
console.error('[TauriBackendService] Self-hosted server health check failed:', error);
|
|
||||||
}
|
|
||||||
this.setStatus('unhealthy');
|
this.setStatus('unhealthy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// For SaaS mode, check the bundled local backend via Rust
|
const data = await response.json();
|
||||||
if (!this.backendStarted) {
|
const dependenciesReady = data.dependenciesReady === true;
|
||||||
this.setStatus('stopped');
|
this.setStatus(dependenciesReady ? 'healthy' : 'starting');
|
||||||
return false;
|
return dependenciesReady;
|
||||||
}
|
} catch {
|
||||||
|
|
||||||
if (!this.backendPort) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isHealthy = await invoke<boolean>('check_backend_health', { port: this.backendPort });
|
|
||||||
this.setStatus(isHealthy ? 'healthy' : 'unhealthy');
|
|
||||||
return isHealthy;
|
|
||||||
} catch (error) {
|
|
||||||
const errorStr = String(error);
|
|
||||||
if (!errorStr.includes('connection refused') && !errorStr.includes('No connection could be made')) {
|
|
||||||
console.error('[TauriBackendService] Bundled backend health check failed:', error);
|
|
||||||
}
|
|
||||||
this.setStatus('unhealthy');
|
this.setStatus('unhealthy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user