diff --git a/app/web/static/js/app.js b/app/web/static/js/app.js index 94fd3d9..328a8c9 100644 --- a/app/web/static/js/app.js +++ b/app/web/static/js/app.js @@ -26,6 +26,9 @@ class LLMMonitorApp { this.updateServerContextUI(); + // Caricare dati da localStorage prima di qualsiasi sync di rete. + this.loadFromLocalStorage(); + // Inizializzare il Web Worker if (typeof Worker !== 'undefined') { this.worker = new Worker('/static/js/data-sync.worker.js'); @@ -35,19 +38,22 @@ class LLMMonitorApp { // Fallback: sincronizzazione nel main thread this.syncDataInMainThread(); }; + const shouldSyncImmediately = this.shouldSyncImmediately(); this.worker.postMessage({ type: "SET_SERVER", serverId: this.activeServer.id, - host: this.activeServer.host + host: this.activeServer.host, + syncImmediately: shouldSyncImmediately, + lastSyncTimestamp: this.getLatestCacheTimestamp() }); - } else { + if (shouldSyncImmediately) { + this.renderLoadingState(); + } + } else if (this.shouldSyncImmediately()) { console.warn("Web Workers not supported, using main thread"); this.syncDataInMainThread(); } - // Caricare dati da localStorage - this.loadFromLocalStorage(); - // Listener al pulsante manuale document.getElementById("refresh-btn")?.addEventListener("click", () => { this.requestSync(); @@ -73,31 +79,23 @@ class LLMMonitorApp { // Caricare dati da localStorage loadFromLocalStorage() { - const healthStr = localStorage.getItem(this.getStorageKey("health")); - const modelsStr = localStorage.getItem(this.getStorageKey("models")); + const health = readServerCache(this.activeServer.id, "health"); + const models = readServerCache(this.activeServer.id, "models"); - if (healthStr) { - try { - this.lastData.health = JSON.parse(healthStr); - this.renderHealth(this.lastData.health); - } catch (e) { - console.error("Error parsing health data:", e); - } + if (health) { + this.lastData.health = health; + this.renderHealth(this.lastData.health); } - if (modelsStr) { - try { - this.lastData.models = JSON.parse(modelsStr); - this.renderModels(this.lastData.models); - } catch (e) { - console.error("Error parsing models data:", e); - } + if (models) { + this.lastData.models = models; + this.renderModels(this.lastData.models); } } // Gestire messaggi dal Worker handleWorkerMessage(event) { - const { type, health, modelsData, serverId } = event.data; + const { type, health, modelsData, runningData, serverId } = event.data; if (serverId && this.activeServer && serverId !== this.activeServer.id) { return; @@ -107,7 +105,7 @@ class LLMMonitorApp { if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) { this.lastData.health = health; try { - localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health)); + writeServerCache(this.activeServer.id, "health", health); } catch (error) { console.warn("Cannot persist health in localStorage:", error); } @@ -117,7 +115,7 @@ class LLMMonitorApp { if (modelsData && JSON.stringify(this.lastData.models) !== JSON.stringify(modelsData)) { this.lastData.models = modelsData; try { - localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData)); + writeServerCache(this.activeServer.id, "models", modelsData); } catch (error) { console.warn("Cannot persist models in localStorage:", error); } @@ -126,6 +124,14 @@ class LLMMonitorApp { this.showModelDetails(this.selectedModelName); } } + + if (runningData) { + try { + writeServerCache(this.activeServer.id, "running", runningData); + } catch (error) { + console.warn("Cannot persist running models in localStorage:", error); + } + } } } @@ -365,7 +371,7 @@ class LLMMonitorApp { if (response.ok) { const health = await response.json(); this.lastData.health = health; - localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health)); + writeServerCache(this.activeServer.id, "health", health); this.renderHealth(health); } } catch (error) { @@ -396,7 +402,7 @@ class LLMMonitorApp { timestamp: new Date().toISOString() }; this.lastData.models = modelsData; - localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData)); + writeServerCache(this.activeServer.id, "models", modelsData); this.renderModels(modelsData); if (this.selectedModelName) { this.showModelDetails(this.selectedModelName); @@ -408,7 +414,22 @@ class LLMMonitorApp { } getStorageKey(suffix) { - return `llm_monitor_${suffix}_${this.activeServer.id}`; + return getServerStorageKey(this.activeServer.id, suffix); + } + + shouldSyncImmediately() { + const health = readServerCache(this.activeServer.id, "health"); + const models = readServerCache(this.activeServer.id, "models"); + + if (!health || !models) { + return true; + } + + return isCacheStale(this.getLatestCacheTimestamp()); + } + + getLatestCacheTimestamp() { + return getLatestServerCacheTimestamp(this.activeServer.id, ["health", "models", "running"]); } buildApiUrl(path) { @@ -463,6 +484,24 @@ class LLMMonitorApp { `; } } + + renderLoadingState() { + if (this.lastData.models) { + return; + } + + const container = document.getElementById("models-container"); + if (!container) { + return; + } + + container.innerHTML = ` +
+
+

Loading models...

+
+ `; + } } // Inizializzare l'app quando il DOM รจ pronto diff --git a/app/web/static/js/data-sync.worker.js b/app/web/static/js/data-sync.worker.js index d8d73df..ecb6a07 100644 --- a/app/web/static/js/data-sync.worker.js +++ b/app/web/static/js/data-sync.worker.js @@ -7,6 +7,7 @@ const API_BASE = "/api/v1"; const REFRESH_INTERVAL = 30000; // 30 secondi let activeServerId = null; let activeHost = null; +let nextSyncTimeout = null; // Formattare bytes function formatBytes(bytes) { @@ -99,6 +100,30 @@ async function fetchAllModelsShow(models) { return showByModel; } +async function fetchRunningModels() { + if (!activeHost) { + return null; + } + + try { + const response = await fetch(buildApiUrl(`${API_BASE}/models/running`)); + if (!response.ok) { + throw new Error("Failed to load running models"); + } + + const data = await response.json(); + return { + models: data.models || [], + total: data.total || (data.models || []).length, + timestamp: new Date().toISOString(), + serverId: activeServerId + }; + } catch (error) { + console.error("Error loading running models:", error); + return null; + } +} + // Sincronizzare i dati async function syncData() { if (!activeHost) { @@ -113,6 +138,7 @@ async function syncData() { const health = await fetchHealth(); const modelsData = await fetchModels(); + const runningData = await fetchRunningModels(); if (modelsData && modelsData.models.length > 0) { modelsData.showByModel = await fetchAllModelsShow(modelsData.models); @@ -126,6 +152,7 @@ async function syncData() { type: "DATA_UPDATED", health, modelsData, + runningData, serverId: activeServerId }); } @@ -136,18 +163,41 @@ function buildApiUrl(path) { return `${url.pathname}${url.search}`; } -// Pianificare aggiornamenti periodici -setInterval(syncData, REFRESH_INTERVAL); +function clearNextSync() { + if (nextSyncTimeout) { + clearTimeout(nextSyncTimeout); + nextSyncTimeout = null; + } +} + +function scheduleNextSync(lastSyncTimestamp = 0) { + clearNextSync(); + + const ageMs = lastSyncTimestamp ? Math.max(0, Date.now() - lastSyncTimestamp) : REFRESH_INTERVAL; + const delayMs = Math.max(0, REFRESH_INTERVAL - ageMs); + + nextSyncTimeout = setTimeout(async () => { + await syncData(); + scheduleNextSync(Date.now()); + }, delayMs); +} // Gestire messaggi dal main thread self.onmessage = (event) => { if (event.data.type === "SET_SERVER") { activeServerId = event.data.serverId || null; activeHost = event.data.host || null; - syncData(); + const lastSyncTimestamp = Number(event.data.lastSyncTimestamp || 0); + + if (event.data.syncImmediately) { + syncData().finally(() => scheduleNextSync(Date.now())); + return; + } + + scheduleNextSync(lastSyncTimestamp); } if (event.data.type === "SYNC_NOW") { - syncData(); + syncData().finally(() => scheduleNextSync(Date.now())); } }; diff --git a/app/web/static/js/models-running.js b/app/web/static/js/models-running.js index 52a27fe..538282a 100644 --- a/app/web/static/js/models-running.js +++ b/app/web/static/js/models-running.js @@ -1,6 +1,8 @@ class RunningModelsPage { constructor() { this.activeServer = getActiveServer(); + this.worker = null; + this.lastRunningData = null; this.init(); } @@ -12,25 +14,105 @@ class RunningModelsPage { return; } - document.getElementById("refresh-btn")?.addEventListener("click", () => { - this.loadRunningModels(); - }); + this.loadFromLocalStorage(); - this.loadRunningModels(); + if (typeof Worker !== "undefined") { + this.worker = new Worker("/static/js/data-sync.worker.js"); + this.worker.onmessage = (event) => this.handleWorkerMessage(event); + this.worker.onerror = (error) => { + console.error("Worker error:", error); + this.loadRunningModels(true); + }; + const shouldSyncImmediately = this.shouldSyncImmediately(); + this.worker.postMessage({ + type: "SET_SERVER", + serverId: this.activeServer.id, + host: this.activeServer.host, + syncImmediately: shouldSyncImmediately, + lastSyncTimestamp: this.getLatestCacheTimestamp() + }); + if (shouldSyncImmediately && !this.lastRunningData) { + this.renderLoadingState(); + } + } else if (this.shouldSyncImmediately()) { + this.loadRunningModels(true); + } + + document.getElementById("refresh-btn")?.addEventListener("click", () => { + if (this.worker) { + this.worker.postMessage({ type: "SYNC_NOW" }); + } else { + this.loadRunningModels(true); + } + }); } - async loadRunningModels() { + loadFromLocalStorage() { + const runningData = readServerCache(this.activeServer.id, "running"); + if (!runningData) { + return; + } + + this.lastRunningData = runningData; + this.renderStats(runningData.models || [], runningData.timestamp); + this.renderRunningModels(runningData.models || []); + } + + handleWorkerMessage(event) { + const { type, health, modelsData, runningData, serverId } = event.data; + + if (type !== "DATA_UPDATED") { + return; + } + + if (serverId && serverId !== this.activeServer.id) { + return; + } + + if (health) { + try { + writeServerCache(this.activeServer.id, "health", health); + } catch (error) { + console.warn("Cannot persist health in localStorage:", error); + } + } + + if (modelsData) { + try { + writeServerCache(this.activeServer.id, "models", modelsData); + } catch (error) { + console.warn("Cannot persist models in localStorage:", error); + } + } + + if (!runningData) { + return; + } + + this.lastRunningData = runningData; + try { + writeServerCache(this.activeServer.id, "running", runningData); + } catch (error) { + console.warn("Cannot persist running models in localStorage:", error); + } + + this.renderStats(runningData.models || [], runningData.timestamp); + this.renderRunningModels(runningData.models || []); + } + + async loadRunningModels(forceNetwork = false) { const container = document.getElementById("running-models"); if (!container) { return; } - container.innerHTML = ` -
-
-

Refreshing data...

-
- `; + if (!forceNetwork && this.lastRunningData) { + this.renderStats(this.lastRunningData.models || [], this.lastRunningData.timestamp); + this.renderRunningModels(this.lastRunningData.models || []); + return; + } + + this.renderLoadingState(); try { const response = await fetch(this.buildApiUrl("/api/v1/models/running")); @@ -40,8 +122,16 @@ class RunningModelsPage { const data = await response.json(); const models = data.models || []; + const runningData = { + models, + total: data.total || models.length, + timestamp: new Date().toISOString(), + serverId: this.activeServer.id + }; - this.renderStats(models); + this.lastRunningData = runningData; + writeServerCache(this.activeServer.id, "running", runningData); + this.renderStats(models, runningData.timestamp); this.renderRunningModels(models); } catch (error) { container.innerHTML = ` @@ -49,11 +139,24 @@ class RunningModelsPage {

Failed to load ollama ps output

`; - this.renderStats([]); + this.renderStats([], null); console.error(error); } } + shouldSyncImmediately() { + const running = readServerCache(this.activeServer.id, "running"); + if (!running) { + return true; + } + + return isCacheStale(this.getLatestCacheTimestamp()); + } + + getLatestCacheTimestamp() { + return getLatestServerCacheTimestamp(this.activeServer.id, ["health", "models", "running"]); + } + buildApiUrl(path) { const url = new URL(path, window.location.origin); url.searchParams.set("host", this.activeServer.host); @@ -103,7 +206,7 @@ class RunningModelsPage { } } - renderStats(models) { + renderStats(models, timestamp = null) { const runningCountEl = document.getElementById("running-count"); const vramTotalEl = document.getElementById("vram-total"); const lastRefreshEl = document.getElementById("last-refresh"); @@ -117,7 +220,7 @@ class RunningModelsPage { vramTotalEl.textContent = this.formatBytes(totalVram); } if (lastRefreshEl) { - lastRefreshEl.textContent = new Date().toLocaleString("en-US"); + lastRefreshEl.textContent = timestamp ? this.formatDateTime(timestamp) : "-"; } } @@ -210,6 +313,20 @@ class RunningModelsPage { div.textContent = String(text); return div.innerHTML; } + + renderLoadingState() { + const container = document.getElementById("running-models"); + if (!container || this.lastRunningData) { + return; + } + + container.innerHTML = ` +
+
+

Refreshing data...

+
+ `; + } } document.addEventListener("DOMContentLoaded", () => { diff --git a/app/web/static/js/server-config.js b/app/web/static/js/server-config.js index ef07e98..ad1d7ae 100644 --- a/app/web/static/js/server-config.js +++ b/app/web/static/js/server-config.js @@ -1,5 +1,6 @@ const SERVER_STORAGE_KEY = "llm_monitor_servers"; const ACTIVE_SERVER_KEY = "llm_monitor_active_server"; +const DATA_REFRESH_INTERVAL_MS = 30000; function normalizeHost(host) { if (!host) { @@ -97,3 +98,56 @@ function buildServerUrl(path, serverId) { } return `${url.pathname}${url.search}`; } + +function getServerStorageKey(serverId, suffix) { + return `llm_monitor_${suffix}_${serverId}`; +} + +function readServerCache(serverId, suffix) { + if (!serverId) { + return null; + } + + const raw = localStorage.getItem(getServerStorageKey(serverId, suffix)); + if (!raw) { + return null; + } + + try { + return JSON.parse(raw); + } catch { + return null; + } +} + +function writeServerCache(serverId, suffix, value) { + if (!serverId) { + return; + } + + localStorage.setItem(getServerStorageKey(serverId, suffix), JSON.stringify(value)); +} + +function getCacheTimestamp(cacheValue) { + if (!cacheValue || !cacheValue.timestamp) { + return 0; + } + + const parsed = Date.parse(cacheValue.timestamp); + return Number.isNaN(parsed) ? 0 : parsed; +} + +function getLatestServerCacheTimestamp(serverId, suffixes) { + return suffixes.reduce((latest, suffix) => { + const value = readServerCache(serverId, suffix); + return Math.max(latest, getCacheTimestamp(value)); + }, 0); +} + +function isCacheStale(timestamp, maxAgeMs = DATA_REFRESH_INTERVAL_MS) { + if (!timestamp) { + return true; + } + + return (Date.now() - timestamp) >= maxAgeMs; +}