diff --git a/app/web/static/js/app.js b/app/web/static/js/app.js index 328a8c9..9f4fdef 100644 --- a/app/web/static/js/app.js +++ b/app/web/static/js/app.js @@ -296,13 +296,43 @@ class LLMMonitorApp { detailsModal.setAttribute("aria-hidden", "false"); if (!showData) { - detailsContent.textContent = "Show details are not available for this model."; + detailsContent.textContent = "Loading show details..."; + this.loadModelShowDetails(modelName, detailsContent); return; } detailsContent.textContent = JSON.stringify(showData, null, 2); } + async loadModelShowDetails(modelName, detailsContent) { + try { + const response = await fetch(this.buildApiUrl(`/api/v1/models/${encodeURIComponent(modelName)}/show`)); + if (!response.ok) { + throw new Error(`Failed to load show details for ${modelName}`); + } + + const showData = await response.json(); + if (!this.lastData.models) { + return; + } + + if (!this.lastData.models.showByModel) { + this.lastData.models.showByModel = {}; + } + + this.lastData.models.showByModel[modelName] = showData; + + if (this.selectedModelName === modelName) { + detailsContent.textContent = JSON.stringify(showData, null, 2); + } + } catch (error) { + console.error(error); + if (this.selectedModelName === modelName) { + detailsContent.textContent = "Show details are not available for this model."; + } + } + } + hideModelDetails() { const detailsModal = document.getElementById("model-details-modal"); const detailsDialog = document.getElementById("model-details-dialog"); diff --git a/app/web/static/js/server-config.js b/app/web/static/js/server-config.js index ad1d7ae..068d79b 100644 --- a/app/web/static/js/server-config.js +++ b/app/web/static/js/server-config.js @@ -1,6 +1,7 @@ const SERVER_STORAGE_KEY = "llm_monitor_servers"; const ACTIVE_SERVER_KEY = "llm_monitor_active_server"; const DATA_REFRESH_INTERVAL_MS = 30000; +const SERVER_CACHE_SUFFIXES = ["health", "models", "running"]; function normalizeHost(host) { if (!host) { @@ -41,6 +42,7 @@ function loadServers() { function saveServers(servers) { localStorage.setItem(SERVER_STORAGE_KEY, JSON.stringify(servers)); + cleanupOrphanedServerCaches(servers); } function generateServerId() { @@ -121,11 +123,108 @@ function readServerCache(serverId, suffix) { } function writeServerCache(serverId, suffix, value) { + if (!serverId) { + return value; + } + + const storageKey = getServerStorageKey(serverId, suffix); + const candidates = [value]; + + cleanupOrphanedServerCaches(); + + if (suffix === "models") { + candidates.push(createSlimModelsCache(value)); + } + + for (const candidate of candidates) { + try { + localStorage.setItem(storageKey, JSON.stringify(candidate)); + return candidate; + } catch (error) { + if (!isQuotaExceededError(error)) { + throw error; + } + } + } + + // Last resort: free stale server caches and retry with the smallest payload. + cleanupOrphanedServerCaches(loadServers()); + + if (suffix === "models") { + const slimValue = createSlimModelsCache(value); + try { + localStorage.setItem(storageKey, JSON.stringify(slimValue)); + return slimValue; + } catch (error) { + if (!isQuotaExceededError(error)) { + throw error; + } + console.warn(`Cache quota exceeded for ${storageKey}; using in-memory models data only.`); + return null; + } + } + + console.warn(`Cache quota exceeded for ${storageKey}; skipping persistence for this payload.`); + return null; +} + +function createSlimModelsCache(value) { + if (!value || typeof value !== "object") { + return value; + } + + const slimValue = { ...value }; + if (slimValue.showByModel) { + delete slimValue.showByModel; + slimValue.showDetailsDeferred = true; + } + + return slimValue; +} + +function isQuotaExceededError(error) { + return error instanceof DOMException && ( + error.code === 22 || + error.code === 1014 || + error.name === "QuotaExceededError" || + error.name === "NS_ERROR_DOM_QUOTA_REACHED" + ); +} + +function cleanupOrphanedServerCaches(servers = loadServers()) { + const validServerIds = new Set(servers.map((server) => server.id)); + const keysToRemove = []; + + for (let index = 0; index < localStorage.length; index += 1) { + const key = localStorage.key(index); + if (!key) { + continue; + } + + for (const suffix of SERVER_CACHE_SUFFIXES) { + const prefix = `llm_monitor_${suffix}_`; + if (!key.startsWith(prefix)) { + continue; + } + + const serverId = key.slice(prefix.length); + if (!validServerIds.has(serverId)) { + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)); +} + +function clearServerCaches(serverId) { if (!serverId) { return; } - localStorage.setItem(getServerStorageKey(serverId, suffix), JSON.stringify(value)); + SERVER_CACHE_SUFFIXES.forEach((suffix) => { + localStorage.removeItem(getServerStorageKey(serverId, suffix)); + }); } function getCacheTimestamp(cacheValue) { diff --git a/app/web/static/js/servers.js b/app/web/static/js/servers.js index fbcc516..b9b4d35 100644 --- a/app/web/static/js/servers.js +++ b/app/web/static/js/servers.js @@ -68,6 +68,7 @@ class ServersPage { deleteServer(serverId) { const servers = loadServers().filter((server) => server.id !== serverId); saveServers(servers); + clearServerCaches(serverId); const activeServerId = getActiveServerId(); if (activeServerId === serverId) {