diff --git a/public/index.html b/public/index.html index c948359..ba271e7 100644 --- a/public/index.html +++ b/public/index.html @@ -129,10 +129,10 @@ ? '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')">
+ :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')">
+ x-text="$store.data.connectionStatus === 'connected' ? $store.global.t('online') : ($store.data.connectionStatus === 'disconnected' ? $store.global.t('offline') : $store.global.t('connecting'))">
diff --git a/public/js/app-init.js b/public/js/app-init.js index c2b4593..407d48d 100644 --- a/public/js/app-init.js +++ b/public/js/app-init.js @@ -34,7 +34,7 @@ document.addEventListener('alpine:init', () => { this.startAutoRefresh(); document.addEventListener('refresh-interval-changed', () => this.startAutoRefresh()); - // Initial Fetch + // Initial Data Fetch (separate from health check) Alpine.store('data').fetchData(); }, diff --git a/public/js/data-store.js b/public/js/data-store.js index 4bcb24d..c1332e7 100644 --- a/public/js/data-store.js +++ b/public/js/data-store.js @@ -16,6 +16,7 @@ document.addEventListener('alpine:init', () => { loading: false, connectionStatus: 'connecting', lastUpdated: '-', + healthCheckTimer: null, // Filters state filters: { @@ -30,9 +31,8 @@ document.addEventListener('alpine:init', () => { // For simplicity, let's keep relevant filters here. init() { - // Watch filters to recompute - // Alpine stores don't have $watch automatically unless inside a component? - // We can manually call compute when filters change. + // Start health check monitoring + this.startHealthCheck(); }, async fetchData() { @@ -63,11 +63,9 @@ document.addEventListener('alpine:init', () => { this.computeQuotaRows(); - this.connectionStatus = 'connected'; this.lastUpdated = new Date().toLocaleTimeString(); } catch (error) { console.error('Fetch error:', error); - this.connectionStatus = 'disconnected'; const store = Alpine.store('global'); store.showToast(store.t('connectionLost'), 'error'); } 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() { const models = this.models || []; const rows = []; @@ -209,6 +267,10 @@ document.addEventListener('alpine:init', () => { }); return rows; + }, + + destroy() { + this.stopHealthCheck(); } }); });