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 0acbb8f..5a981b9 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: { @@ -39,6 +40,9 @@ document.addEventListener('alpine: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(); }, loadFromCache() { @@ -111,7 +115,6 @@ document.addEventListener('alpine:init', () => { this.saveToCache(); // Save fresh data this.computeQuotaRows(); - this.connectionStatus = 'connected'; this.lastUpdated = new Date().toLocaleTimeString(); // Fetch version from config endpoint if not already loaded @@ -120,7 +123,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 { @@ -143,6 +145,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 = []; @@ -312,6 +375,13 @@ document.addEventListener('alpine:init', () => { }); return rows; + }, + + destroy() { + this.stopHealthCheck(); + if (this._visibilityHandler) { + document.removeEventListener('visibilitychange', this._visibilityHandler); + } } }); });