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 = `
+
+ `;
+ }
}
// 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 = `
-
- `;
+ 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 = `
+
+ `;
+ }
}
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;
+}