feat: add multi-server control panel and host-aware sync

This commit is contained in:
Luca Sacchi Ricciardi
2026-04-25 15:40:20 +02:00
parent 3ba6a9a41c
commit f60781bd7f
13 changed files with 641 additions and 41 deletions
+92 -10
View File
@@ -6,6 +6,7 @@
class LLMMonitorApp {
constructor() {
this.worker = null;
this.activeServer = getActiveServer();
this.selectedModelName = null;
this.isModalOpen = false;
this.hoverOpenDelayMs = 180;
@@ -18,6 +19,13 @@ class LLMMonitorApp {
}
init() {
if (!this.activeServer) {
this.renderNoServerState();
return;
}
this.updateServerContextUI();
// Inizializzare il Web Worker
if (typeof Worker !== 'undefined') {
this.worker = new Worker('/static/js/data-sync.worker.js');
@@ -27,6 +35,11 @@ class LLMMonitorApp {
// Fallback: sincronizzazione nel main thread
this.syncDataInMainThread();
};
this.worker.postMessage({
type: "SET_SERVER",
serverId: this.activeServer.id,
host: this.activeServer.host
});
} else {
console.warn("Web Workers not supported, using main thread");
this.syncDataInMainThread();
@@ -60,8 +73,8 @@ class LLMMonitorApp {
// Caricare dati da localStorage
loadFromLocalStorage() {
const healthStr = localStorage.getItem("llm_monitor_health");
const modelsStr = localStorage.getItem("llm_monitor_models");
const healthStr = localStorage.getItem(this.getStorageKey("health"));
const modelsStr = localStorage.getItem(this.getStorageKey("models"));
if (healthStr) {
try {
@@ -84,13 +97,17 @@ class LLMMonitorApp {
// Gestire messaggi dal Worker
handleWorkerMessage(event) {
const { type, health, modelsData } = event.data;
const { type, health, modelsData, serverId } = event.data;
if (serverId && this.activeServer && serverId !== this.activeServer.id) {
return;
}
if (type === "DATA_UPDATED") {
if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) {
this.lastData.health = health;
try {
localStorage.setItem("llm_monitor_health", JSON.stringify(health));
localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health));
} catch (error) {
console.warn("Cannot persist health in localStorage:", error);
}
@@ -100,7 +117,7 @@ class LLMMonitorApp {
if (modelsData && JSON.stringify(this.lastData.models) !== JSON.stringify(modelsData)) {
this.lastData.models = modelsData;
try {
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData));
} catch (error) {
console.warn("Cannot persist models in localStorage:", error);
}
@@ -326,6 +343,10 @@ class LLMMonitorApp {
// Chiedere sincronizzazione manuale al Worker
requestSync() {
if (!this.activeServer) {
return;
}
if (this.worker) {
this.worker.postMessage({ type: "SYNC_NOW" });
} else {
@@ -335,12 +356,16 @@ class LLMMonitorApp {
// Fallback: sincronizzazione nel main thread
async syncDataInMainThread() {
if (!this.activeServer) {
return;
}
try {
const response = await fetch("/api/v1/health");
const response = await fetch(this.buildApiUrl("/api/v1/health"));
if (response.ok) {
const health = await response.json();
this.lastData.health = health;
localStorage.setItem("llm_monitor_health", JSON.stringify(health));
localStorage.setItem(this.getStorageKey("health"), JSON.stringify(health));
this.renderHealth(health);
}
} catch (error) {
@@ -348,7 +373,7 @@ class LLMMonitorApp {
}
try {
const response = await fetch("/api/v1/models");
const response = await fetch(this.buildApiUrl("/api/v1/models"));
if (response.ok) {
const data = await response.json();
const models = data.models || [];
@@ -356,7 +381,7 @@ class LLMMonitorApp {
const showByModel = {};
await Promise.allSettled(
models.map(async (model) => {
const showResponse = await fetch(`/api/v1/models/${encodeURIComponent(model.name)}/show`);
const showResponse = await fetch(this.buildApiUrl(`/api/v1/models/${encodeURIComponent(model.name)}/show`));
if (showResponse.ok) {
showByModel[model.name] = await showResponse.json();
}
@@ -371,7 +396,7 @@ class LLMMonitorApp {
timestamp: new Date().toISOString()
};
this.lastData.models = modelsData;
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
localStorage.setItem(this.getStorageKey("models"), JSON.stringify(modelsData));
this.renderModels(modelsData);
if (this.selectedModelName) {
this.showModelDetails(this.selectedModelName);
@@ -381,6 +406,63 @@ class LLMMonitorApp {
console.error("Models loading error:", error);
}
}
getStorageKey(suffix) {
return `llm_monitor_${suffix}_${this.activeServer.id}`;
}
buildApiUrl(path) {
const url = new URL(path, window.location.origin);
url.searchParams.set("host", this.activeServer.host);
return `${url.pathname}${url.search}`;
}
updateServerContextUI() {
const serverLabel = document.getElementById("active-server-label");
if (serverLabel) {
serverLabel.textContent = `Server: ${this.activeServer.name}`;
serverLabel.classList.remove("hidden");
}
const runningLink = document.getElementById("running-link");
if (runningLink) {
runningLink.href = buildServerUrl("/models-running", this.activeServer.id);
}
const serversLink = document.getElementById("servers-link");
if (serversLink) {
serversLink.href = "/servers";
}
}
renderNoServerState() {
const container = document.getElementById("models-container");
const count = document.getElementById("models-count");
const totalSize = document.getElementById("total-size");
const statusIndicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
const ollamaStatus = document.getElementById("ollama-status");
if (count) count.textContent = "0";
if (totalSize) totalSize.textContent = "0 B";
if (statusIndicator) statusIndicator.className = "w-3 h-3 bg-yellow-500 rounded-full";
if (statusText) {
statusText.className = "text-sm text-yellow-300";
statusText.textContent = "No server selected";
}
if (ollamaStatus) {
ollamaStatus.innerHTML = "🟡 Not configured";
}
if (container) {
container.innerHTML = `
<div class="text-center py-10 text-gray-300">
<p class="text-lg font-semibold">No server selected</p>
<p class="text-sm text-gray-400 mt-2">Configure or select a server from the control panel.</p>
<a href="/servers" class="inline-block mt-4 bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded">Open Servers Control Panel</a>
</div>
`;
}
}
}
// Inizializzare l'app quando il DOM è pronto