class RunningModelsPage { constructor() { this.activeServer = getActiveServer(); this.worker = null; this.lastRunningData = null; this.init(); } init() { this.updateServerContextUI(); if (!this.activeServer) { this.renderNoServerState(); return; } this.loadFromLocalStorage(); 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); } }); } 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; } 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")); if (!response.ok) { throw new Error("Failed to load running models"); } 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.lastRunningData = runningData; writeServerCache(this.activeServer.id, "running", runningData); this.renderStats(models, runningData.timestamp); this.renderRunningModels(models); } catch (error) { container.innerHTML = `

Failed to load ollama ps output

`; 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); return `${url.pathname}${url.search}`; } updateServerContextUI() { if (!this.activeServer) { return; } const serverLabel = document.getElementById("active-server-label"); if (serverLabel) { serverLabel.textContent = `Server: ${this.activeServer.name}`; serverLabel.classList.remove("hidden"); } const availableLink = document.getElementById("available-link"); if (availableLink) { availableLink.href = buildServerUrl("/models-available", this.activeServer.id); } const serversLink = document.getElementById("servers-link"); if (serversLink) { serversLink.href = "/servers"; } } renderNoServerState() { const container = document.getElementById("running-models"); const runningCountEl = document.getElementById("running-count"); const vramTotalEl = document.getElementById("vram-total"); const lastRefreshEl = document.getElementById("last-refresh"); if (runningCountEl) runningCountEl.textContent = "0"; if (vramTotalEl) vramTotalEl.textContent = "0 B"; if (lastRefreshEl) lastRefreshEl.textContent = "-"; if (container) { container.innerHTML = `

No server selected

Select a server in the control panel to load ollama ps data.

Open Servers Control Panel
`; } } renderStats(models, timestamp = null) { const runningCountEl = document.getElementById("running-count"); const vramTotalEl = document.getElementById("vram-total"); const lastRefreshEl = document.getElementById("last-refresh"); const totalVram = models.reduce((sum, model) => sum + (model.size_vram || 0), 0); if (runningCountEl) { runningCountEl.textContent = String(models.length); } if (vramTotalEl) { vramTotalEl.textContent = this.formatBytes(totalVram); } if (lastRefreshEl) { lastRefreshEl.textContent = timestamp ? this.formatDateTime(timestamp) : "-"; } } renderRunningModels(models) { const container = document.getElementById("running-models"); if (!container) { return; } if (models.length === 0) { container.innerHTML = `

No models are currently loaded in memory.

`; return; } container.innerHTML = models .map((model) => this.renderModelCard(model)) .join(""); } renderModelCard(model) { const name = this.escapeHtml(model.name || "unknown"); const modelId = this.escapeHtml(model.model || "-"); const size = this.formatBytes(model.size || 0); const sizeVram = this.formatBytes(model.size_vram || 0); const processor = this.escapeHtml(model.details?.processor || "-"); const expiresAt = model.expires_at ? this.formatDateTime(model.expires_at) : "-"; return `

${name}

${modelId}

Ready

Model Size

${size}

VRAM Used

${sizeVram}

Processor

${processor}

Unload Time

${expiresAt}

`; } formatBytes(bytes) { if (!bytes || bytes <= 0) { return "0 B"; } const units = ["B", "KB", "MB", "GB", "TB"]; const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, index); return `${value.toFixed(2)} ${units[index]}`; } formatDateTime(isoDate) { const date = new Date(isoDate); if (Number.isNaN(date.getTime())) { return "-"; } return date.toLocaleString("en-US", { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } escapeHtml(text) { const div = document.createElement("div"); div.textContent = String(text); return div.innerHTML; } renderLoadingState() { const container = document.getElementById("running-models"); if (!container || this.lastRunningData) { return; } container.innerHTML = `

Refreshing data...

`; } } document.addEventListener("DOMContentLoaded", () => { window.runningModelsPage = new RunningModelsPage(); });