fix: handle local storage quota for model cache

This commit is contained in:
Luca Sacchi Ricciardi
2026-04-25 16:11:12 +02:00
parent ac2089f921
commit 165ad9c02b
3 changed files with 132 additions and 2 deletions
+31 -1
View File
@@ -296,13 +296,43 @@ class LLMMonitorApp {
detailsModal.setAttribute("aria-hidden", "false"); detailsModal.setAttribute("aria-hidden", "false");
if (!showData) { if (!showData) {
detailsContent.textContent = "Show details are not available for this model."; detailsContent.textContent = "Loading show details...";
this.loadModelShowDetails(modelName, detailsContent);
return; return;
} }
detailsContent.textContent = JSON.stringify(showData, null, 2); 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() { hideModelDetails() {
const detailsModal = document.getElementById("model-details-modal"); const detailsModal = document.getElementById("model-details-modal");
const detailsDialog = document.getElementById("model-details-dialog"); const detailsDialog = document.getElementById("model-details-dialog");
+100 -1
View File
@@ -1,6 +1,7 @@
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; const DATA_REFRESH_INTERVAL_MS = 30000;
const SERVER_CACHE_SUFFIXES = ["health", "models", "running"];
function normalizeHost(host) { function normalizeHost(host) {
if (!host) { if (!host) {
@@ -41,6 +42,7 @@ function loadServers() {
function saveServers(servers) { function saveServers(servers) {
localStorage.setItem(SERVER_STORAGE_KEY, JSON.stringify(servers)); localStorage.setItem(SERVER_STORAGE_KEY, JSON.stringify(servers));
cleanupOrphanedServerCaches(servers);
} }
function generateServerId() { function generateServerId() {
@@ -121,11 +123,108 @@ function readServerCache(serverId, suffix) {
} }
function writeServerCache(serverId, suffix, value) { 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) { if (!serverId) {
return; return;
} }
localStorage.setItem(getServerStorageKey(serverId, suffix), JSON.stringify(value)); SERVER_CACHE_SUFFIXES.forEach((suffix) => {
localStorage.removeItem(getServerStorageKey(serverId, suffix));
});
} }
function getCacheTimestamp(cacheValue) { function getCacheTimestamp(cacheValue) {
+1
View File
@@ -68,6 +68,7 @@ class ServersPage {
deleteServer(serverId) { deleteServer(serverId) {
const servers = loadServers().filter((server) => server.id !== serverId); const servers = loadServers().filter((server) => server.id !== serverId);
saveServers(servers); saveServers(servers);
clearServerCaches(serverId);
const activeServerId = getActiveServerId(); const activeServerId = getActiveServerId();
if (activeServerId === serverId) { if (activeServerId === serverId) {