From 3ba6a9a41cce697cc02dacb4e45df354aca0fa41 Mon Sep 17 00:00:00 2001 From: Luca Sacchi Ricciardi Date: Sat, 25 Apr 2026 15:32:10 +0200 Subject: [PATCH] Translate UI to English and add PWA support --- app/web/static/js/app.js | 14 +++--- app/web/static/js/data-sync.worker.js | 2 +- app/web/static/js/models-running.js | 20 ++++---- app/web/static/js/pwa-register.js | 13 +++++ app/web/static/js/service-worker.js | 72 +++++++++++++++++++++++++++ app/web/static/manifest.webmanifest | 18 +++++++ app/web/templates/index.html | 37 ++++++++------ app/web/templates/models_running.html | 29 ++++++----- main.py | 50 ++++++++++++------- 9 files changed, 190 insertions(+), 65 deletions(-) create mode 100644 app/web/static/js/pwa-register.js create mode 100644 app/web/static/js/service-worker.js create mode 100644 app/web/static/manifest.webmanifest diff --git a/app/web/static/js/app.js b/app/web/static/js/app.js index 0d04d86..70dbc56 100644 --- a/app/web/static/js/app.js +++ b/app/web/static/js/app.js @@ -156,7 +156,7 @@ class LLMMonitorApp { if (models.length === 0) { container.innerHTML = `
-

Nessun modello caricato

+

No models loaded

`; return; @@ -229,15 +229,15 @@ class LLMMonitorApp {

${modelName}

- Caricato + Loaded
-

Dimensione

+

Size

${this.formatBytes(model.size)}

-

Ultimo aggiornamento

+

Last Updated

${formattedDate}

@@ -245,7 +245,7 @@ class LLMMonitorApp {

Digest

${this.escapeHtml(model.digest.substring(0, 64))}...

-

Passa il mouse o clicca per vedere dettagli show

+

Hover or click to view show details

`; } @@ -273,7 +273,7 @@ class LLMMonitorApp { detailsModal.setAttribute("aria-hidden", "false"); if (!showData) { - detailsContent.textContent = "Dettagli show non disponibili per questo modello."; + detailsContent.textContent = "Show details are not available for this model."; return; } @@ -308,7 +308,7 @@ class LLMMonitorApp { // Formattare data formatDate(dateString) { const date = new Date(dateString); - return date.toLocaleDateString("it-IT", { + return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", diff --git a/app/web/static/js/data-sync.worker.js b/app/web/static/js/data-sync.worker.js index 7aaa4bb..98fbc12 100644 --- a/app/web/static/js/data-sync.worker.js +++ b/app/web/static/js/data-sync.worker.js @@ -37,7 +37,7 @@ async function fetchHealth() { async function fetchModels() { try { 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 models = data.models || []; diff --git a/app/web/static/js/models-running.js b/app/web/static/js/models-running.js index f5cf950..b4bc5c2 100644 --- a/app/web/static/js/models-running.js +++ b/app/web/static/js/models-running.js @@ -20,14 +20,14 @@ class RunningModelsPage { container.innerHTML = `
-

Aggiornamento in corso...

+

Refreshing data...

`; try { const response = await fetch("/api/v1/models/running"); 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(); @@ -38,7 +38,7 @@ class RunningModelsPage { } catch (error) { container.innerHTML = `
-

Errore nel caricamento di ollama ps

+

Failed to load ollama ps output

`; this.renderStats([]); @@ -60,7 +60,7 @@ class RunningModelsPage { vramTotalEl.textContent = this.formatBytes(totalVram); } 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) { container.innerHTML = `
-

Nessun modello residente in memoria al momento.

+

No models are currently loaded in memory.

`; return; @@ -99,15 +99,15 @@ class RunningModelsPage {

${name}

${modelId}

- Pronto + Ready
-

Dimensione modello

+

Model Size

${size}

-

VRAM usata

+

VRAM Used

${sizeVram}

@@ -115,7 +115,7 @@ class RunningModelsPage {

${processor}

-

Scarico previsto

+

Unload Time

${expiresAt}

@@ -139,7 +139,7 @@ class RunningModelsPage { return "-"; } - return date.toLocaleString("it-IT", { + return date.toLocaleString("en-US", { year: "numeric", month: "short", day: "2-digit", diff --git a/app/web/static/js/pwa-register.js b/app/web/static/js/pwa-register.js new file mode 100644 index 0000000..e46f76c --- /dev/null +++ b/app/web/static/js/pwa-register.js @@ -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); + } + }); +})(); diff --git a/app/web/static/js/service-worker.js b/app/web/static/js/service-worker.js new file mode 100644 index 0000000..045ad62 --- /dev/null +++ b/app/web/static/js/service-worker.js @@ -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")); + }) + ); +}); diff --git a/app/web/static/manifest.webmanifest b/app/web/static/manifest.webmanifest new file mode 100644 index 0000000..75e42db --- /dev/null +++ b/app/web/static/manifest.webmanifest @@ -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" + } + ] +} diff --git a/app/web/templates/index.html b/app/web/templates/index.html index ac90806..88c3775 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -1,10 +1,14 @@ - + - LLM Monitor - Dashboard Ollama + LLM Monitor - Ollama Dashboard + + + +