Translate UI to English and add PWA support
This commit is contained in:
@@ -156,7 +156,7 @@ class LLMMonitorApp {
|
|||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="text-center py-8 text-gray-400">
|
<div class="text-center py-8 text-gray-400">
|
||||||
<p>Nessun modello caricato</p>
|
<p>No models loaded</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -229,15 +229,15 @@ class LLMMonitorApp {
|
|||||||
<div data-model-key="${modelKey}" class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 hover:-translate-y-0.5 transition cursor-pointer h-full">
|
<div data-model-key="${modelKey}" class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 hover:-translate-y-0.5 transition cursor-pointer h-full">
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<h3 class="text-lg font-semibold">${modelName}</h3>
|
<h3 class="text-lg font-semibold">${modelName}</h3>
|
||||||
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Caricato</span>
|
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Loaded</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">Dimensione</p>
|
<p class="text-gray-400">Size</p>
|
||||||
<p class="font-semibold">${this.formatBytes(model.size)}</p>
|
<p class="font-semibold">${this.formatBytes(model.size)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">Ultimo aggiornamento</p>
|
<p class="text-gray-400">Last Updated</p>
|
||||||
<p class="font-semibold">${formattedDate}</p>
|
<p class="font-semibold">${formattedDate}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +245,7 @@ class LLMMonitorApp {
|
|||||||
<p class="text-gray-400 text-xs">Digest</p>
|
<p class="text-gray-400 text-xs">Digest</p>
|
||||||
<p class="font-mono text-xs bg-gray-800 p-2 rounded mt-1 break-all">${this.escapeHtml(model.digest.substring(0, 64))}...</p>
|
<p class="font-mono text-xs bg-gray-800 p-2 rounded mt-1 break-all">${this.escapeHtml(model.digest.substring(0, 64))}...</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-purple-300 mt-3">Passa il mouse o clicca per vedere dettagli show</p>
|
<p class="text-xs text-purple-300 mt-3">Hover or click to view show details</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,7 @@ class LLMMonitorApp {
|
|||||||
detailsModal.setAttribute("aria-hidden", "false");
|
detailsModal.setAttribute("aria-hidden", "false");
|
||||||
|
|
||||||
if (!showData) {
|
if (!showData) {
|
||||||
detailsContent.textContent = "Dettagli show non disponibili per questo modello.";
|
detailsContent.textContent = "Show details are not available for this model.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ class LLMMonitorApp {
|
|||||||
// Formattare data
|
// Formattare data
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString("it-IT", {
|
return date.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function fetchHealth() {
|
|||||||
async function fetchModels() {
|
async function fetchModels() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/models`);
|
const response = await fetch(`${API_BASE}/models`);
|
||||||
if (!response.ok) throw new Error("Errore nel caricamento");
|
if (!response.ok) throw new Error("Failed to load models");
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const models = data.models || [];
|
const models = data.models || [];
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ class RunningModelsPage {
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div>
|
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div>
|
||||||
<p class="text-gray-400 mt-4">Aggiornamento in corso...</p>
|
<p class="text-gray-400 mt-4">Refreshing data...</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/v1/models/running");
|
const response = await fetch("/api/v1/models/running");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Errore nel caricamento dei modelli residenti");
|
throw new Error("Failed to load running models");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -38,7 +38,7 @@ class RunningModelsPage {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="text-center py-8 text-red-400">
|
<div class="text-center py-8 text-red-400">
|
||||||
<p>Errore nel caricamento di ollama ps</p>
|
<p>Failed to load ollama ps output</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.renderStats([]);
|
this.renderStats([]);
|
||||||
@@ -60,7 +60,7 @@ class RunningModelsPage {
|
|||||||
vramTotalEl.textContent = this.formatBytes(totalVram);
|
vramTotalEl.textContent = this.formatBytes(totalVram);
|
||||||
}
|
}
|
||||||
if (lastRefreshEl) {
|
if (lastRefreshEl) {
|
||||||
lastRefreshEl.textContent = new Date().toLocaleString("it-IT");
|
lastRefreshEl.textContent = new Date().toLocaleString("en-US");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class RunningModelsPage {
|
|||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="text-center py-8 text-gray-400">
|
<div class="text-center py-8 text-gray-400">
|
||||||
<p>Nessun modello residente in memoria al momento.</p>
|
<p>No models are currently loaded in memory.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -99,15 +99,15 @@ class RunningModelsPage {
|
|||||||
<h3 class="text-lg font-semibold">${name}</h3>
|
<h3 class="text-lg font-semibold">${name}</h3>
|
||||||
<p class="text-xs text-gray-400 mt-1">${modelId}</p>
|
<p class="text-xs text-gray-400 mt-1">${modelId}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="bg-green-700 text-green-100 text-xs px-2 py-1 rounded">Pronto</span>
|
<span class="bg-green-700 text-green-100 text-xs px-2 py-1 rounded">Ready</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-sm">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-sm">
|
||||||
<div class="bg-gray-800 rounded p-3">
|
<div class="bg-gray-800 rounded p-3">
|
||||||
<p class="text-gray-400 text-xs">Dimensione modello</p>
|
<p class="text-gray-400 text-xs">Model Size</p>
|
||||||
<p class="font-semibold mt-1">${size}</p>
|
<p class="font-semibold mt-1">${size}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded p-3">
|
<div class="bg-gray-800 rounded p-3">
|
||||||
<p class="text-gray-400 text-xs">VRAM usata</p>
|
<p class="text-gray-400 text-xs">VRAM Used</p>
|
||||||
<p class="font-semibold mt-1">${sizeVram}</p>
|
<p class="font-semibold mt-1">${sizeVram}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded p-3">
|
<div class="bg-gray-800 rounded p-3">
|
||||||
@@ -115,7 +115,7 @@ class RunningModelsPage {
|
|||||||
<p class="font-semibold mt-1">${processor}</p>
|
<p class="font-semibold mt-1">${processor}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded p-3">
|
<div class="bg-gray-800 rounded p-3">
|
||||||
<p class="text-gray-400 text-xs">Scarico previsto</p>
|
<p class="text-gray-400 text-xs">Unload Time</p>
|
||||||
<p class="font-semibold mt-1">${expiresAt}</p>
|
<p class="font-semibold mt-1">${expiresAt}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@ class RunningModelsPage {
|
|||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toLocaleString("it-IT", {
|
return date.toLocaleString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
(() => {
|
||||||
|
if (!("serviceWorker" in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.serviceWorker.register("/service-worker.js", { scope: "/" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Service worker registration failed:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const CACHE_NAME = "llm-monitor-v1";
|
||||||
|
const APP_SHELL = [
|
||||||
|
"/",
|
||||||
|
"/models-running",
|
||||||
|
"/models-available",
|
||||||
|
"/static/css/output.css",
|
||||||
|
"/static/js/app.js",
|
||||||
|
"/static/js/models-running.js",
|
||||||
|
"/static/js/data-sync.worker.js",
|
||||||
|
"/static/js/pwa-register.js",
|
||||||
|
"/manifest.webmanifest",
|
||||||
|
"/favicon.ico"
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.method !== "GET") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUrl = new URL(event.request.url);
|
||||||
|
const isApiRequest = requestUrl.pathname.startsWith("/api/");
|
||||||
|
|
||||||
|
if (isApiRequest) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request).catch(() =>
|
||||||
|
new Response(JSON.stringify({ detail: "Offline" }), {
|
||||||
|
status: 503,
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((cached) => {
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response || response.status !== 200 || response.type !== "basic") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseClone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match("/models-running"));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "LLM Monitor",
|
||||||
|
"short_name": "LLM Monitor",
|
||||||
|
"description": "Monitor available and running Ollama models.",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#111827",
|
||||||
|
"theme_color": "#111827",
|
||||||
|
"lang": "en",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/favicon.ico",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="it">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LLM Monitor - Dashboard Ollama</title>
|
<title>LLM Monitor - Ollama Dashboard</title>
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#111827">
|
||||||
|
<meta name="application-name" content="LLM Monitor">
|
||||||
|
<meta name="description" content="Monitor available and running Ollama models.">
|
||||||
<!-- Tailwind CSS (compiled for production) -->
|
<!-- Tailwind CSS (compiled for production) -->
|
||||||
<link rel="stylesheet" href="/static/css/output.css">
|
<link rel="stylesheet" href="/static/css/output.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -47,10 +51,10 @@
|
|||||||
<h1 class="text-2xl font-bold">LLM Monitor</h1>
|
<h1 class="text-2xl font-bold">LLM Monitor</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/models-running" class="text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg transition">Modelli in Memoria</a>
|
<a href="/models-running" class="text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg transition">Running Models</a>
|
||||||
<div id="health-status" class="flex items-center gap-2">
|
<div id="health-status" class="flex items-center gap-2">
|
||||||
<div id="status-indicator" class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
<div id="status-indicator" class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
||||||
<span id="status-text" class="text-sm text-gray-400">Controllo...</span>
|
<span id="status-text" class="text-sm text-gray-400">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,15 +67,15 @@
|
|||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">Modelli Caricati</div>
|
<div class="text-gray-400 text-sm font-medium">Loaded Models</div>
|
||||||
<div id="models-count" class="text-4xl font-bold mt-2">-</div>
|
<div id="models-count" class="text-4xl font-bold mt-2">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">Spazio Totale</div>
|
<div class="text-gray-400 text-sm font-medium">Total Size</div>
|
||||||
<div id="total-size" class="text-4xl font-bold mt-2">-</div>
|
<div id="total-size" class="text-4xl font-bold mt-2">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">Status Ollama</div>
|
<div class="text-gray-400 text-sm font-medium">Ollama Status</div>
|
||||||
<div id="ollama-status" class="text-4xl font-bold mt-2">-</div>
|
<div id="ollama-status" class="text-4xl font-bold mt-2">-</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,11 +84,11 @@
|
|||||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Modelli Disponibili</h2>
|
<h2 class="text-xl font-bold">Available Models</h2>
|
||||||
<p class="text-xs text-gray-400 mt-1">Passa il mouse o clicca una card per aprire la modal dettagli.</p>
|
<p class="text-xs text-gray-400 mt-1">Hover or click a card to open the details modal.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="refresh-btn" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
<button id="refresh-btn" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
||||||
🔄 Aggiorna
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,15 +96,15 @@
|
|||||||
<div id="models-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div id="models-container" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<div class="animate-spin inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full"></div>
|
<div class="animate-spin inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full"></div>
|
||||||
<p class="text-gray-400 mt-4">Caricamento modelli...</p>
|
<p class="text-gray-400 mt-4">Loading models...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Documentation Section -->
|
<!-- API Documentation Section -->
|
||||||
<div class="mt-8 bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-6">
|
<div class="mt-8 bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-6">
|
||||||
<h3 class="text-lg font-bold mb-4">📚 Documentazione API</h3>
|
<h3 class="text-lg font-bold mb-4">API Documentation</h3>
|
||||||
<p class="text-gray-300 mb-4">La API è documentata e testabile direttamente da:</p>
|
<p class="text-gray-300 mb-4">The API is documented and testable from:</p>
|
||||||
<div class="flex gap-3 flex-wrap">
|
<div class="flex gap-3 flex-wrap">
|
||||||
<a href="/docs" target="_blank" class="inline-block bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
<a href="/docs" target="_blank" class="inline-block bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition">
|
||||||
Swagger UI
|
Swagger UI
|
||||||
@@ -116,7 +120,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-gray-800 border-t border-gray-700 mt-12">
|
<footer class="bg-gray-800 border-t border-gray-700 mt-12">
|
||||||
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-400 text-sm">
|
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-400 text-sm">
|
||||||
<p>LLM Monitor v1.0.0 • Fatto con ❤️ da <a href="https://lucasacchi.net" target="_blank" class="text-purple-400 hover:text-purple-300">LucaSacchi.Net</a></p>
|
<p>LLM Monitor v1.0.0 • Built by <a href="https://lucasacchi.net" target="_blank" class="text-purple-400 hover:text-purple-300">LucaSacchi.Net</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,10 +132,10 @@
|
|||||||
<div id="model-details-section" class="w-full max-w-4xl bg-gray-800 rounded-lg border border-gray-700 p-6 shadow-xl">
|
<div id="model-details-section" class="w-full max-w-4xl bg-gray-800 rounded-lg border border-gray-700 p-6 shadow-xl">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold">Dettagli Modello</h3>
|
<h3 class="text-lg font-bold">Model Details</h3>
|
||||||
<span id="model-details-name" class="text-sm text-purple-300 font-medium"></span>
|
<span id="model-details-name" class="text-sm text-purple-300 font-medium"></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="model-details-close" type="button" class="text-gray-300 hover:text-white text-2xl leading-none px-2" aria-label="Chiudi modal">×</button>
|
<button id="model-details-close" type="button" class="text-gray-300 hover:text-white text-2xl leading-none px-2" aria-label="Close modal">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pre id="model-details-content" class="bg-gray-900 text-gray-200 text-xs p-4 rounded-lg whitespace-pre-wrap"></pre>
|
<pre id="model-details-content" class="bg-gray-900 text-gray-200 text-xs p-4 rounded-lg whitespace-pre-wrap"></pre>
|
||||||
@@ -144,5 +148,6 @@
|
|||||||
<!-- Web Worker for background data sync -->
|
<!-- Web Worker for background data sync -->
|
||||||
<!-- localStorage for client-side persistence -->
|
<!-- localStorage for client-side persistence -->
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js"></script>
|
||||||
|
<script src="/static/js/pwa-register.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="it">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LLM Monitor - Modelli in Memoria</title>
|
<title>LLM Monitor - Running Models</title>
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#111827">
|
||||||
|
<meta name="application-name" content="LLM Monitor">
|
||||||
|
<meta name="description" content="View models currently loaded in Ollama memory.">
|
||||||
<link rel="stylesheet" href="/static/css/output.css">
|
<link rel="stylesheet" href="/static/css/output.css">
|
||||||
<style>
|
<style>
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -25,13 +29,13 @@
|
|||||||
🧠
|
🧠
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">Modelli in Memoria</h1>
|
<h1 class="text-2xl font-bold">Running Models</h1>
|
||||||
<p class="text-xs text-gray-400">Vista dedicata all'output di ollama ps</p>
|
<p class="text-xs text-gray-400">Dedicated view for ollama ps output</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="/models-available" class="text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg transition">Modelli Disponibili</a>
|
<a href="/models-available" class="text-sm bg-gray-700 hover:bg-gray-600 px-3 py-2 rounded-lg transition">Available Models</a>
|
||||||
<button id="refresh-btn" class="text-sm bg-purple-600 hover:bg-purple-700 px-3 py-2 rounded-lg transition">Aggiorna</button>
|
<button id="refresh-btn" class="text-sm bg-purple-600 hover:bg-purple-700 px-3 py-2 rounded-lg transition">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,25 +45,25 @@
|
|||||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">Modelli Residenti</div>
|
<div class="text-gray-400 text-sm font-medium">Loaded in Memory</div>
|
||||||
<div id="running-count" class="text-4xl font-bold mt-2">-</div>
|
<div id="running-count" class="text-4xl font-bold mt-2">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">VRAM Totale Stimata</div>
|
<div class="text-gray-400 text-sm font-medium">Estimated Total VRAM</div>
|
||||||
<div id="vram-total" class="text-4xl font-bold mt-2">-</div>
|
<div id="vram-total" class="text-4xl font-bold mt-2">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
<div class="text-gray-400 text-sm font-medium">Ultimo Refresh</div>
|
<div class="text-gray-400 text-sm font-medium">Last Refresh</div>
|
||||||
<div id="last-refresh" class="text-base font-semibold mt-3">-</div>
|
<div id="last-refresh" class="text-base font-semibold mt-3">-</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||||
<h2 class="text-xl font-bold mb-4">Output Ollama PS</h2>
|
<h2 class="text-xl font-bold mb-4">Ollama PS Output</h2>
|
||||||
<div id="running-models" class="space-y-3">
|
<div id="running-models" class="space-y-3">
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div>
|
<div class="inline-block w-8 h-8 border-4 border-gray-600 border-t-purple-500 rounded-full animate-spin"></div>
|
||||||
<p class="text-gray-400 mt-4">Caricamento modelli residenti...</p>
|
<p class="text-gray-400 mt-4">Loading running models...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,11 +72,12 @@
|
|||||||
|
|
||||||
<footer class="bg-gray-800 border-t border-gray-700 mt-12">
|
<footer class="bg-gray-800 border-t border-gray-700 mt-12">
|
||||||
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-400 text-sm">
|
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-gray-400 text-sm">
|
||||||
<p>LLM Monitor v1.0.0 • Modelli residenti in memoria (ollama ps)</p>
|
<p>LLM Monitor v1.0.0 • Models currently loaded in memory (ollama ps)</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/models-running.js"></script>
|
<script src="/static/js/models-running.js"></script>
|
||||||
|
<script src="/static/js/pwa-register.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
LLM Monitor - Dashboard per controllare i modelli caricati in Ollama
|
LLM Monitor - Dashboard to monitor Ollama models.
|
||||||
Entry point dell'applicazione FastAPI
|
FastAPI application entry point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,26 +12,26 @@ from fastapi.openapi.docs import get_redoc_html
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Configurazione logging
|
# Logging configuration
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Importare le rotte
|
# Import API routes
|
||||||
from app.api.health import router as health_router
|
from app.api.health import router as health_router
|
||||||
from app.api.models import router as models_router
|
from app.api.models import router as models_router
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
# Creare l'app FastAPI
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="LLM Monitor API",
|
title="LLM Monitor API",
|
||||||
description="Dashboard per il monitoraggio dei modelli LLM in Ollama",
|
description="Dashboard and API for monitoring Ollama LLM models",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
openapi_url="/openapi.json"
|
openapi_url="/openapi.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configurare CORS
|
# Configure CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.CORS_ORIGINS.split(","),
|
allow_origins=settings.CORS_ORIGINS.split(","),
|
||||||
@@ -40,44 +40,56 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Registrare le rotte API
|
# Register API routes
|
||||||
app.include_router(health_router, prefix="/api/v1", tags=["health"])
|
app.include_router(health_router, prefix="/api/v1", tags=["health"])
|
||||||
app.include_router(models_router, prefix="/api/v1", tags=["models"])
|
app.include_router(models_router, prefix="/api/v1", tags=["models"])
|
||||||
|
|
||||||
# Servire i file statici
|
# Serve static files
|
||||||
static_path = Path(__file__).parent / "app" / "web" / "static"
|
static_path = Path(__file__).parent / "app" / "web" / "static"
|
||||||
if static_path.exists():
|
if static_path.exists():
|
||||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||||
|
|
||||||
# Servire la dashboard web
|
# Serve web pages
|
||||||
templates_path = Path(__file__).parent / "app" / "web" / "templates"
|
templates_path = Path(__file__).parent / "app" / "web" / "templates"
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Pagina principale: modelli residenti in memoria (ollama ps)."""
|
"""Primary page: models currently loaded in memory (ollama ps)."""
|
||||||
return FileResponse(templates_path / "models_running.html")
|
return FileResponse(templates_path / "models_running.html")
|
||||||
|
|
||||||
@app.get("/dashboard")
|
@app.get("/dashboard")
|
||||||
async def dashboard():
|
async def dashboard():
|
||||||
"""Alias legacy della pagina principale."""
|
"""Legacy alias for the primary page."""
|
||||||
return FileResponse(templates_path / "models_running.html")
|
return FileResponse(templates_path / "models_running.html")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/models-available")
|
@app.get("/models-available")
|
||||||
async def models_available_page():
|
async def models_available_page():
|
||||||
"""Pagina modelli disponibili su disco."""
|
"""Page listing models available on disk."""
|
||||||
return FileResponse(templates_path / "index.html")
|
return FileResponse(templates_path / "index.html")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/models-running")
|
@app.get("/models-running")
|
||||||
async def models_running_page():
|
async def models_running_page():
|
||||||
"""Pagina dedicata ai modelli residenti in memoria (ollama ps)."""
|
"""Page dedicated to models resident in memory (ollama ps)."""
|
||||||
return FileResponse(templates_path / "models_running.html")
|
return FileResponse(templates_path / "models_running.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/manifest.webmanifest", include_in_schema=False)
|
||||||
|
async def web_manifest():
|
||||||
|
"""PWA web manifest."""
|
||||||
|
return FileResponse(static_path / "manifest.webmanifest", media_type="application/manifest+json")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/service-worker.js", include_in_schema=False)
|
||||||
|
async def service_worker():
|
||||||
|
"""PWA service worker with root scope."""
|
||||||
|
return FileResponse(static_path / "js" / "service-worker.js", media_type="application/javascript")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/redoc", include_in_schema=False)
|
@app.get("/redoc", include_in_schema=False)
|
||||||
async def redoc_html():
|
async def redoc_html():
|
||||||
"""Documentazione ReDoc con bundle stabile (evita dipendenza da redoc@next)."""
|
"""ReDoc documentation using a stable bundle."""
|
||||||
return get_redoc_html(
|
return get_redoc_html(
|
||||||
openapi_url=app.openapi_url,
|
openapi_url=app.openapi_url,
|
||||||
title=f"{app.title} - ReDoc",
|
title=f"{app.title} - ReDoc",
|
||||||
@@ -88,18 +100,18 @@ async def redoc_html():
|
|||||||
|
|
||||||
@app.get("/favicon.ico", include_in_schema=False)
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
"""Favicon dell'applicazione."""
|
"""Application favicon."""
|
||||||
return FileResponse(static_path / "favicon.ico")
|
return FileResponse(static_path / "favicon.ico")
|
||||||
|
|
||||||
# Event hooks
|
# Event hooks
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
logger.info("🚀 LLM Monitor avviato")
|
logger.info("LLM Monitor started")
|
||||||
logger.info(f"📊 Ollama host: {settings.OLLAMA_HOST}")
|
logger.info(f"Ollama host: {settings.OLLAMA_HOST}")
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
logger.info("🛑 LLM Monitor arrestato")
|
logger.info("LLM Monitor stopped")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
Reference in New Issue
Block a user