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 `
`;
}
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 = `
`;
}
}
document.addEventListener("DOMContentLoaded", () => {
window.runningModelsPage = new RunningModelsPage();
});