diff --git a/public/index.html b/public/index.html index 1533120..dd4467b 100644 --- a/public/index.html +++ b/public/index.html @@ -82,10 +82,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 === 'connecting' ? $store.global.t('connecting') : $store.global.t('offline'))">
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 4b5262c..dbc65ec 100644 --- a/public/js/data-store.js +++ b/public/js/data-store.js @@ -17,6 +17,7 @@ document.addEventListener('alpine:init', () => { initialLoad: true, // Track first load for skeleton screen connectionStatus: 'connecting', lastUpdated: '-', + healthCheckTimer: null, // Filters state filters: { @@ -31,9 +32,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() { @@ -67,7 +67,6 @@ document.addEventListener('alpine:init', () => { this.computeQuotaRows(); - this.connectionStatus = 'connected'; this.lastUpdated = new Date().toLocaleTimeString(); // Fetch version from config endpoint if not already loaded @@ -76,7 +75,6 @@ document.addEventListener('alpine:init', () => { } } catch (error) { console.error('Fetch error:', error); - this.connectionStatus = 'disconnected'; const store = Alpine.store('global'); store.showToast(store.t('connectionLost'), 'error'); } finally { @@ -99,6 +97,67 @@ document.addEventListener('alpine:init', () => { } }, + async performHealthCheck() { + try { + // Get password from global store + const password = Alpine.store('global').webuiPassword; + + // Use lightweight endpoint (no quota fetching) + const { response, newPassword } = await window.utils.request('/api/config', {}, 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 (only once) + if (!this._healthVisibilitySetup) { + this._healthVisibilitySetup = true; + this._visibilityHandler = () => { + if (document.hidden) { + // Tab hidden - stop health checks + this.stopHealthCheck(); + } else { + // Tab visible - restart health checks + this.startHealthCheck(); + } + }; + document.addEventListener('visibilitychange', this._visibilityHandler); + } + + // 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 = []; @@ -233,6 +292,13 @@ document.addEventListener('alpine:init', () => { }); return rows; + }, + + destroy() { + this.stopHealthCheck(); + if (this._visibilityHandler) { + document.removeEventListener('visibilitychange', this._visibilityHandler); + } } }); });