257 lines
5.9 KiB
JavaScript
257 lines
5.9 KiB
JavaScript
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);
|
|
}
|