fix: serve cached server data before background sync

This commit is contained in:
Luca Sacchi Ricciardi
2026-04-25 15:57:37 +02:00
parent f60781bd7f
commit 9649f2ccfb
4 changed files with 306 additions and 46 deletions
+64 -25
View File
@@ -26,6 +26,9 @@ class LLMMonitorApp {
this.updateServerContextUI(); this.updateServerContextUI();
// Caricare dati da localStorage prima di qualsiasi sync di rete.
this.loadFromLocalStorage();
// Inizializzare il Web Worker // Inizializzare il Web Worker
if (typeof Worker !== 'undefined') { if (typeof Worker !== 'undefined') {
this.worker = new Worker('/static/js/data-sync.worker.js'); this.worker = new Worker('/static/js/data-sync.worker.js');
@@ -35,19 +38,22 @@ class LLMMonitorApp {
// Fallback: sincronizzazione nel main thread // Fallback: sincronizzazione nel main thread
this.syncDataInMainThread(); this.syncDataInMainThread();
}; };
const shouldSyncImmediately = this.shouldSyncImmediately();
this.worker.postMessage({ this.worker.postMessage({
type: "SET_SERVER", type: "SET_SERVER",
serverId: this.activeServer.id, 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"); console.warn("Web Workers not supported, using main thread");
this.syncDataInMainThread(); this.syncDataInMainThread();
} }
// Caricare dati da localStorage
this.loadFromLocalStorage();
// Listener al pulsante manuale // Listener al pulsante manuale
document.getElementById("refresh-btn")?.addEventListener("click", () => { document.getElementById("refresh-btn")?.addEventListener("click", () => {
this.requestSync(); this.requestSync();
@@ -73,31 +79,23 @@ class LLMMonitorApp {
// Caricare dati da localStorage // Caricare dati da localStorage
loadFromLocalStorage() { loadFromLocalStorage() {
const healthStr = localStorage.getItem(this.getStorageKey("health")); const health = readServerCache(this.activeServer.id, "health");
const modelsStr = localStorage.getItem(this.getStorageKey("models")); const models = readServerCache(this.activeServer.id, "models");
if (healthStr) { if (health) {
try { this.lastData.health = health;
this.lastData.health = JSON.parse(healthStr);
this.renderHealth(this.lastData.health); this.renderHealth(this.lastData.health);
} catch (e) {
console.error("Error parsing health data:", e);
}
} }
if (modelsStr) { if (models) {
try { this.lastData.models = models;
this.lastData.models = JSON.parse(modelsStr);
this.renderModels(this.lastData.models); this.renderModels(this.lastData.models);
} catch (e) {
console.error("Error parsing models data:", e);
}
} }
} }
// Gestire messaggi dal Worker // Gestire messaggi dal Worker
handleWorkerMessage(event) { 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) { if (serverId && this.activeServer && serverId !== this.activeServer.id) {
return; return;
@@ -107,7 +105,7 @@ class LLMMonitorApp {
if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) { if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) {
this.lastData.health = health; this.lastData.health = health;
try { try {
localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health)); writeServerCache(this.activeServer.id, "health", health);
} catch (error) { } catch (error) {
console.warn("Cannot persist health in localStorage:", 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)) { if (modelsData && JSON.stringify(this.lastData.models) !== JSON.stringify(modelsData)) {
this.lastData.models = modelsData; this.lastData.models = modelsData;
try { try {
localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData)); writeServerCache(this.activeServer.id, "models", modelsData);
} catch (error) { } catch (error) {
console.warn("Cannot persist models in localStorage:", error); console.warn("Cannot persist models in localStorage:", error);
} }
@@ -126,6 +124,14 @@ class LLMMonitorApp {
this.showModelDetails(this.selectedModelName); 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) { if (response.ok) {
const health = await response.json(); const health = await response.json();
this.lastData.health = health; this.lastData.health = health;
localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health)); writeServerCache(this.activeServer.id, "health", health);
this.renderHealth(health); this.renderHealth(health);
} }
} catch (error) { } catch (error) {
@@ -396,7 +402,7 @@ class LLMMonitorApp {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}; };
this.lastData.models = modelsData; this.lastData.models = modelsData;
localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData)); writeServerCache(this.activeServer.id, "models", modelsData);
this.renderModels(modelsData); this.renderModels(modelsData);
if (this.selectedModelName) { if (this.selectedModelName) {
this.showModelDetails(this.selectedModelName); this.showModelDetails(this.selectedModelName);
@@ -408,7 +414,22 @@ class LLMMonitorApp {
} }
getStorageKey(suffix) { 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) { 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 = `
<div class="text-center py-8">
<div class="animate-spin inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full"></div>
<p class="text-gray-400 mt-4">Loading models...</p>
</div>
`;
}
} }
// Inizializzare l'app quando il DOM è pronto // Inizializzare l'app quando il DOM è pronto
+54 -4
View File
@@ -7,6 +7,7 @@ const API_BASE = "/api/v1";
const REFRESH_INTERVAL = 30000; // 30 secondi const REFRESH_INTERVAL = 30000; // 30 secondi
let activeServerId = null; let activeServerId = null;
let activeHost = null; let activeHost = null;
let nextSyncTimeout = null;
// Formattare bytes // Formattare bytes
function formatBytes(bytes) { function formatBytes(bytes) {
@@ -99,6 +100,30 @@ async function fetchAllModelsShow(models) {
return showByModel; 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 // Sincronizzare i dati
async function syncData() { async function syncData() {
if (!activeHost) { if (!activeHost) {
@@ -113,6 +138,7 @@ async function syncData() {
const health = await fetchHealth(); const health = await fetchHealth();
const modelsData = await fetchModels(); const modelsData = await fetchModels();
const runningData = await fetchRunningModels();
if (modelsData && modelsData.models.length > 0) { if (modelsData && modelsData.models.length > 0) {
modelsData.showByModel = await fetchAllModelsShow(modelsData.models); modelsData.showByModel = await fetchAllModelsShow(modelsData.models);
@@ -126,6 +152,7 @@ async function syncData() {
type: "DATA_UPDATED", type: "DATA_UPDATED",
health, health,
modelsData, modelsData,
runningData,
serverId: activeServerId serverId: activeServerId
}); });
} }
@@ -136,18 +163,41 @@ function buildApiUrl(path) {
return `${url.pathname}${url.search}`; return `${url.pathname}${url.search}`;
} }
// Pianificare aggiornamenti periodici function clearNextSync() {
setInterval(syncData, REFRESH_INTERVAL); 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 // Gestire messaggi dal main thread
self.onmessage = (event) => { self.onmessage = (event) => {
if (event.data.type === "SET_SERVER") { if (event.data.type === "SET_SERVER") {
activeServerId = event.data.serverId || null; activeServerId = event.data.serverId || null;
activeHost = event.data.host || 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") { if (event.data.type === "SYNC_NOW") {
syncData(); syncData().finally(() => scheduleNextSync(Date.now()));
} }
}; };
+132 -15
View File
@@ -1,6 +1,8 @@
class RunningModelsPage { class RunningModelsPage {
constructor() { constructor() {
this.activeServer = getActiveServer(); this.activeServer = getActiveServer();
this.worker = null;
this.lastRunningData = null;
this.init(); this.init();
} }
@@ -12,25 +14,105 @@ class RunningModelsPage {
return; return;
} }
document.getElementById("refresh-btn")?.addEventListener("click", () => { this.loadFromLocalStorage();
this.loadRunningModels();
});
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);
} }
async loadRunningModels() { document.getElementById("refresh-btn")?.addEventListener("click", () => {
if (this.worker) {
this.worker.postMessage({ type: "SYNC_NOW" });
} else {
this.loadRunningModels(true);
}
});
}
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"); const container = document.getElementById("running-models");
if (!container) { if (!container) {
return; return;
} }
container.innerHTML = ` if (!forceNetwork && this.lastRunningData) {
<div class="text-center py-8"> this.renderStats(this.lastRunningData.models || [], this.lastRunningData.timestamp);
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div> this.renderRunningModels(this.lastRunningData.models || []);
<p class="text-gray-400 mt-4">Refreshing data...</p> return;
</div> }
`;
this.renderLoadingState();
try { try {
const response = await fetch(this.buildApiUrl("/api/v1/models/running")); const response = await fetch(this.buildApiUrl("/api/v1/models/running"));
@@ -40,8 +122,16 @@ class RunningModelsPage {
const data = await response.json(); const data = await response.json();
const models = data.models || []; 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); this.renderRunningModels(models);
} catch (error) { } catch (error) {
container.innerHTML = ` container.innerHTML = `
@@ -49,11 +139,24 @@ class RunningModelsPage {
<p>Failed to load ollama ps output</p> <p>Failed to load ollama ps output</p>
</div> </div>
`; `;
this.renderStats([]); this.renderStats([], null);
console.error(error); 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) { buildApiUrl(path) {
const url = new URL(path, window.location.origin); const url = new URL(path, window.location.origin);
url.searchParams.set("host", this.activeServer.host); 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 runningCountEl = document.getElementById("running-count");
const vramTotalEl = document.getElementById("vram-total"); const vramTotalEl = document.getElementById("vram-total");
const lastRefreshEl = document.getElementById("last-refresh"); const lastRefreshEl = document.getElementById("last-refresh");
@@ -117,7 +220,7 @@ class RunningModelsPage {
vramTotalEl.textContent = this.formatBytes(totalVram); vramTotalEl.textContent = this.formatBytes(totalVram);
} }
if (lastRefreshEl) { 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); div.textContent = String(text);
return div.innerHTML; return div.innerHTML;
} }
renderLoadingState() {
const container = document.getElementById("running-models");
if (!container || this.lastRunningData) {
return;
}
container.innerHTML = `
<div class="text-center py-8">
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div>
<p class="text-gray-400 mt-4">Refreshing data...</p>
</div>
`;
}
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
+54
View File
@@ -1,5 +1,6 @@
const SERVER_STORAGE_KEY = "llm_monitor_servers"; const SERVER_STORAGE_KEY = "llm_monitor_servers";
const ACTIVE_SERVER_KEY = "llm_monitor_active_server"; const ACTIVE_SERVER_KEY = "llm_monitor_active_server";
const DATA_REFRESH_INTERVAL_MS = 30000;
function normalizeHost(host) { function normalizeHost(host) {
if (!host) { if (!host) {
@@ -97,3 +98,56 @@ function buildServerUrl(path, serverId) {
} }
return `${url.pathname}${url.search}`; 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;
}