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:
James Brunton
2025-11-25 13:15:30 +00:00
committed by GitHub
parent 2d8b0ff08c
commit 80f2980755
12 changed files with 126 additions and 132 deletions
@@ -10,7 +10,6 @@ class BackendHealthMonitor {
private readonly intervalMs: number;
private state: BackendHealthState = {
status: tauriBackendService.getBackendStatus(),
isChecking: false,
error: null,
isHealthy: tauriBackendService.getBackendStatus() === 'healthy',
};
@@ -26,20 +25,30 @@ class BackendHealthMonitor {
message: status === 'healthy'
? i18n.t('backendHealth.online', 'Backend Online')
: this.state.message ?? i18n.t('backendHealth.offline', 'Backend Offline'),
isChecking: status === 'healthy' ? false : this.state.isChecking,
});
});
}
private updateState(partial: Partial<BackendHealthState>) {
const nextStatus = partial.status ?? this.state.status;
this.state = {
const nextState = {
...this.state,
...partial,
status: nextStatus,
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() {
@@ -60,29 +69,19 @@ class BackendHealthMonitor {
}
private async pollOnce(): Promise<boolean> {
this.updateState({
isChecking: true,
lastChecked: Date.now(),
error: this.state.error ?? 'Backend offline',
});
try {
const healthy = await tauriBackendService.checkBackendHealth();
if (healthy) {
this.updateState({
status: 'healthy',
isChecking: false,
message: i18n.t('backendHealth.online', 'Backend Online'),
error: null,
lastChecked: Date.now(),
});
} else {
this.updateState({
status: 'unhealthy',
isChecking: false,
message: i18n.t('backendHealth.offline', 'Backend Offline'),
error: i18n.t('backendHealth.offline', 'Backend Offline'),
lastChecked: Date.now(),
});
}
return healthy;
@@ -90,10 +89,8 @@ class BackendHealthMonitor {
console.error('[BackendHealthMonitor] Health check failed:', error);
this.updateState({
status: 'unhealthy',
isChecking: false,
message: 'Backend is unavailable',
error: 'Backend offline',
lastChecked: Date.now(),
});
return false;
}
@@ -133,7 +133,8 @@ export class TauriBackendService {
async checkBackendHealth(): Promise<boolean> {
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') {
const serverConfig = await connectionModeService.getServerConfig();
if (!serverConfig) {
@@ -141,47 +142,37 @@ export class TauriBackendService {
this.setStatus('unhealthy');
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 {
const baseUrl = serverConfig.url.replace(/\/$/, '');
const healthUrl = `${baseUrl}/api/v1/info/status`;
const response = await fetch(healthUrl, {
method: 'GET',
connectTimeout: 5000,
});
// Check if backend is ready (dependencies checked)
try {
const configUrl = `${baseUrl}/api/v1/config/app-config`;
const response = await fetch(configUrl, {
method: 'GET',
connectTimeout: 5000,
});
const isHealthy = 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);
}
if (!response.ok) {
this.setStatus('unhealthy');
return false;
}
}
// For SaaS mode, check the bundled local backend via Rust
if (!this.backendStarted) {
this.setStatus('stopped');
return false;
}
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);
}
const data = await response.json();
const dependenciesReady = data.dependenciesReady === true;
this.setStatus(dependenciesReady ? 'healthy' : 'starting');
return dependenciesReady;
} catch {
this.setStatus('unhealthy');
return false;
}