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) { return ""; } const trimmed = host.trim(); if (!trimmed) { return ""; } return trimmed.replace(/\/+$/, ""); } function loadServers() { const raw = localStorage.getItem(SERVER_STORAGE_KEY); if (!raw) { return []; } try { const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) { return []; } return parsed .map((item) => ({ id: String(item.id || ""), name: String(item.name || "").trim(), host: normalizeHost(item.host || "") })) .filter((item) => item.id && item.name && item.host); } catch { return []; } } function saveServers(servers) { localStorage.setItem(SERVER_STORAGE_KEY, JSON.stringify(servers)); cleanupOrphanedServerCaches(servers); } function generateServerId() { return `srv_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; } function getActiveServerId() { return localStorage.getItem(ACTIVE_SERVER_KEY); } function setActiveServerId(serverId) { localStorage.setItem(ACTIVE_SERVER_KEY, serverId); } function getServerById(serverId) { return loadServers().find((server) => server.id === serverId) || null; } function getServerIdFromQuery() { const params = new URLSearchParams(window.location.search); return params.get("server") || ""; } function getActiveServer() { const queryServerId = getServerIdFromQuery(); if (queryServerId) { const fromQuery = getServerById(queryServerId); if (fromQuery) { setActiveServerId(fromQuery.id); return fromQuery; } } const activeServerId = getActiveServerId(); if (activeServerId) { const activeServer = getServerById(activeServerId); if (activeServer) { return activeServer; } } const servers = loadServers(); if (servers.length > 0) { setActiveServerId(servers[0].id); return servers[0]; } return null; } function buildServerUrl(path, serverId) { const url = new URL(path, window.location.origin); if (serverId) { url.searchParams.set("server", 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 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; } SERVER_CACHE_SUFFIXES.forEach((suffix) => { localStorage.removeItem(getServerStorageKey(serverId, suffix)); }); } 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; } function hasDeferredShowDetails(cacheValue) { return Boolean(cacheValue && cacheValue.showDetailsDeferred); }