feat(webui): add health check monitoring for connection status

Implement periodic health checks every 15 seconds to monitor connection status,
pausing when the tab is hidden and resuming on visibility. Update UI bindings
to use data store for connection status instead of global store. Add destroy
method to clean up timers on component teardown.
This commit is contained in:
jgor20
2026-01-10 22:41:15 +00:00
parent 6868bf217c
commit 69440584fd
3 changed files with 70 additions and 8 deletions

View File

@@ -129,10 +129,10 @@
? 'bg-neon-green/10 border-neon-green/20 text-neon-green' ? 'bg-neon-green/10 border-neon-green/20 text-neon-green'
: (connectionStatus === 'connecting' ? 'bg-yellow-500/10 border-yellow-500/20 text-yellow-500' : 'bg-red-500/10 border-red-500/20 text-red-500')"> : (connectionStatus === 'connecting' ? 'bg-yellow-500/10 border-yellow-500/20 text-yellow-500' : 'bg-red-500/10 border-red-500/20 text-red-500')">
<div class="w-1.5 h-1.5 rounded-full" <div class="w-1.5 h-1.5 rounded-full"
:class="connectionStatus === 'connected' ? 'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)]' : (connectionStatus === 'connecting' ? 'bg-yellow-500 animate-pulse' : 'bg-red-500')"> :class="$store.data.connectionStatus === 'connected' ? 'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)]' : ($store.data.connectionStatus === 'connecting' ? 'bg-yellow-500 animate-pulse' : 'bg-red-500')">
</div> </div>
<span <span
x-text="$store.global.connectionStatus === 'connected' ? $store.global.t('online') : ($store.global.connectionStatus === 'disconnected' ? $store.global.t('offline') : $store.global.t('connecting'))"></span> x-text="$store.data.connectionStatus === 'connected' ? $store.global.t('online') : ($store.data.connectionStatus === 'disconnected' ? $store.global.t('offline') : $store.global.t('connecting'))"></span>
</div> </div>
<div class="h-4 w-px bg-space-border"></div> <div class="h-4 w-px bg-space-border"></div>

View File

@@ -34,7 +34,7 @@ document.addEventListener('alpine:init', () => {
this.startAutoRefresh(); this.startAutoRefresh();
document.addEventListener('refresh-interval-changed', () => this.startAutoRefresh()); document.addEventListener('refresh-interval-changed', () => this.startAutoRefresh());
// Initial Fetch // Initial Data Fetch (separate from health check)
Alpine.store('data').fetchData(); Alpine.store('data').fetchData();
}, },

View File

@@ -16,6 +16,7 @@ document.addEventListener('alpine:init', () => {
loading: false, loading: false,
connectionStatus: 'connecting', connectionStatus: 'connecting',
lastUpdated: '-', lastUpdated: '-',
healthCheckTimer: null,
// Filters state // Filters state
filters: { filters: {
@@ -30,9 +31,8 @@ document.addEventListener('alpine:init', () => {
// For simplicity, let's keep relevant filters here. // For simplicity, let's keep relevant filters here.
init() { init() {
// Watch filters to recompute // Start health check monitoring
// Alpine stores don't have $watch automatically unless inside a component? this.startHealthCheck();
// We can manually call compute when filters change.
}, },
async fetchData() { async fetchData() {
@@ -63,11 +63,9 @@ document.addEventListener('alpine:init', () => {
this.computeQuotaRows(); this.computeQuotaRows();
this.connectionStatus = 'connected';
this.lastUpdated = new Date().toLocaleTimeString(); this.lastUpdated = new Date().toLocaleTimeString();
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
this.connectionStatus = 'disconnected';
const store = Alpine.store('global'); const store = Alpine.store('global');
store.showToast(store.t('connectionLost'), 'error'); store.showToast(store.t('connectionLost'), 'error');
} finally { } finally {
@@ -75,6 +73,66 @@ document.addEventListener('alpine:init', () => {
} }
}, },
async performHealthCheck() {
try {
// Get password from global store
const password = Alpine.store('global').webuiPassword;
// Use lightweight health endpoint
const { response, newPassword } = await window.utils.request('/health', {}, password);
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
if (response.ok) {
this.connectionStatus = 'connected';
} else {
this.connectionStatus = 'disconnected';
}
} catch (error) {
console.error('Health check error:', error);
this.connectionStatus = 'disconnected';
}
},
startHealthCheck() {
// Clear existing timer
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
}
// Setup visibility change listener
if (!this._healthVisibilitySetup) {
this._healthVisibilitySetup = true;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Tab hidden - stop health checks
this.stopHealthCheck();
} else {
// Tab visible - restart health checks
this.startHealthCheck();
}
});
}
// Perform immediate health check
this.performHealthCheck();
// Schedule regular health checks every 15 seconds
this.healthCheckTimer = setInterval(() => {
// Only perform health check if tab is visible
if (!document.hidden) {
this.performHealthCheck();
}
}, 15000);
},
stopHealthCheck() {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = null;
}
},
computeQuotaRows() { computeQuotaRows() {
const models = this.models || []; const models = this.models || [];
const rows = []; const rows = [];
@@ -209,6 +267,10 @@ document.addEventListener('alpine:init', () => {
}); });
return rows; return rows;
},
destroy() {
this.stopHealthCheck();
} }
}); });
}); });