fix: serve cached server data before background sync
This commit is contained in:
+66
-27
@@ -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 = `
|
||||
<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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 = `
|
||||
<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>
|
||||
`;
|
||||
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 {
|
||||
<p>Failed to load ollama ps output</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user