Compare commits
3 Commits
4b782ffdc8
...
b3beb525ad
| Author | SHA1 | Date | |
|---|---|---|---|
| b3beb525ad | |||
| 40d8ae9f52 | |||
| 9a6f835ddf |
@@ -0,0 +1,671 @@
|
|||||||
|
# LLM Monitor - Product Requirements Document (PRD)
|
||||||
|
|
||||||
|
**Versione:** 1.0.0
|
||||||
|
**Data:** Aprile 2024
|
||||||
|
**Autore:** Luca Sacchi
|
||||||
|
**Status:** Active Development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Indice
|
||||||
|
|
||||||
|
1. [Executive Summary](#executive-summary)
|
||||||
|
2. [Vision & Obiettivi](#vision--obiettivi)
|
||||||
|
3. [Problema & Soluzione](#problema--soluzione)
|
||||||
|
4. [Utenti Target](#utenti-target)
|
||||||
|
5. [Feature Principali](#feature-principali)
|
||||||
|
6. [Requisiti Tecnici](#requisiti-tecnici)
|
||||||
|
7. [Architettura](#architettura)
|
||||||
|
8. [User Stories](#user-stories)
|
||||||
|
9. [Acceptance Criteria](#acceptance-criteria)
|
||||||
|
10. [Timeline & Roadmap](#timeline--roadmap)
|
||||||
|
11. [Success Metrics](#success-metrics)
|
||||||
|
12. [Constraints & Assumptions](#constraints--assumptions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Executive Summary
|
||||||
|
|
||||||
|
**LLM Monitor** è una **dashboard web moderna** per il monitoraggio in tempo reale dei modelli LLM caricati in **Ollama**. L'applicazione fornisce una visualizzazione intuitiva dello stato dei modelli, delle risorse utilizzate e dell'accesso ai dati via API REST documentata con Swagger/OpenAPI.
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- ✅ Dashboard reattiva senza page reload
|
||||||
|
- ✅ Web Worker per sincronizzazione dati in background
|
||||||
|
- ✅ localStorage per cache locale e offline support
|
||||||
|
- ✅ API REST completamente documentata
|
||||||
|
- ✅ Containerizzata con Docker
|
||||||
|
- ✅ Architettura server-client moderna
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Vision & Obiettivi
|
||||||
|
|
||||||
|
### Vision
|
||||||
|
Fornire ai developer e ai DevOps una **visibilità completa e in tempo reale** dei modelli LLM disponibili in Ollama, eliminando la necessità di comandi CLI per il monitoraggio.
|
||||||
|
|
||||||
|
### Obiettivi Primari
|
||||||
|
1. **Visualizzare modelli** caricati in Ollama senza comandi CLI
|
||||||
|
2. **Monitorare risorse** (dimensione, memoria, stato)
|
||||||
|
3. **Accedere all'API** via dashboard intuitiva
|
||||||
|
4. **Documentare API** con Swagger per integrazioni esterne
|
||||||
|
5. **Deployare facilmente** con Docker/Docker Compose
|
||||||
|
6. **Aggiornamenti in tempo reale** senza page reload
|
||||||
|
|
||||||
|
### Obiettivi Secondari
|
||||||
|
1. Supporto offline via localStorage
|
||||||
|
2. Performance ottimale con Web Workers
|
||||||
|
3. UI moderna e responsive
|
||||||
|
4. Facilità di installazione e configurazione
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Problema & Soluzione
|
||||||
|
|
||||||
|
### Problema
|
||||||
|
Attualmente, per verificare i modelli LLM in Ollama, è necessario:
|
||||||
|
- Usare comandi CLI (`ollama list`)
|
||||||
|
- Fare chiamate API manuali con curl/Postman
|
||||||
|
- Non c'è una dashboard visuale dedicata
|
||||||
|
- Difficile monitoraggio per non-developer
|
||||||
|
|
||||||
|
### Soluzione Proposta
|
||||||
|
**LLM Monitor** fornisce:
|
||||||
|
- ✅ Dashboard web intuitiva e moderna
|
||||||
|
- ✅ Aggiornamenti automatici ogni 30 secondi
|
||||||
|
- ✅ Nessun page reload grazie ai Web Workers
|
||||||
|
- ✅ API documentata e testabile direttamente
|
||||||
|
- ✅ Deployment semplice con Docker
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Utenti Target
|
||||||
|
|
||||||
|
### Primary Users
|
||||||
|
1. **DevOps Engineers** - Monitorare modelli in produzione
|
||||||
|
2. **ML Engineers** - Verificare disponibilità modelli
|
||||||
|
3. **Developers** - Integrazioni via API
|
||||||
|
|
||||||
|
### Secondary Users
|
||||||
|
1. **System Administrators** - Overview dell'infrastruttura
|
||||||
|
2. **Project Managers** - Status modelli disponibili
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
#### UC1: Verificare Modelli Caricati
|
||||||
|
- **Actor:** Developer
|
||||||
|
- **Goal:** Vedere quali modelli sono disponibili
|
||||||
|
- **Flow:** Apri dashboard → visualizza elenco modelli con dettagli
|
||||||
|
- **Benefit:** Non usare CLI, visione immediata
|
||||||
|
|
||||||
|
#### UC2: Monitorare Spazio Disco
|
||||||
|
- **Actor:** DevOps
|
||||||
|
- **Goal:** Tracciare consumo spazio dei modelli
|
||||||
|
- **Flow:** Dashboard → visualizza spazio totale e per modello
|
||||||
|
- **Benefit:** Pianificare cleanup e capacity planning
|
||||||
|
|
||||||
|
#### UC3: Integrare via API
|
||||||
|
- **Actor:** Developer
|
||||||
|
- **Goal:** Automatizzare script che consumano dati modelli
|
||||||
|
- **Flow:** Consulta Swagger → crea script che chiama endpoint API
|
||||||
|
- **Benefit:** Automazione e integrazione con altri sistemi
|
||||||
|
|
||||||
|
#### UC4: Offline Mode
|
||||||
|
- **Actor:** Developer (senza connessione)
|
||||||
|
- **Goal:** Accedere ai dati modelli salvati
|
||||||
|
- **Flow:** localStorage fornisce ultimo stato noto
|
||||||
|
- **Benefit:** Accesso parziale anche offline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Feature Principali
|
||||||
|
|
||||||
|
### 1. Dashboard Principale
|
||||||
|
**Descrizione:** Homepage con overview dei modelli
|
||||||
|
|
||||||
|
**Componenti:**
|
||||||
|
- Header con logo e status Ollama
|
||||||
|
- Stat cards: Modelli caricati, Spazio totale, Status
|
||||||
|
- Lista modelli con:
|
||||||
|
- Nome modello
|
||||||
|
- Dimensione
|
||||||
|
- Data ultimo aggiornamento
|
||||||
|
- Digest (hash univoco)
|
||||||
|
- Pulsante refresh manuale
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Auto-refresh ogni 30 secondi
|
||||||
|
- Aggiorna solo elementi cambiati (no full re-render)
|
||||||
|
- Mostra loading state durante fetch
|
||||||
|
- Error handling con messaggi chiari
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. API REST Documentata
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
|
||||||
|
#### `GET /api/v1/health`
|
||||||
|
Verifica lo stato dell'API e di Ollama
|
||||||
|
|
||||||
|
**Risposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"ollama_status": "online",
|
||||||
|
"timestamp": "2024-04-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/v1/models`
|
||||||
|
Recupera elenco di tutti i modelli
|
||||||
|
|
||||||
|
**Risposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-04-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/v1/models/{model_name}`
|
||||||
|
Dettagli di un modello specifico
|
||||||
|
|
||||||
|
**Risposta:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-04-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `POST /api/v1/models/{model_name}/pull`
|
||||||
|
Scarica/carica un modello
|
||||||
|
|
||||||
|
#### `DELETE /api/v1/models/{model_name}`
|
||||||
|
Elimina un modello
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Web Worker per Sincronizzazione
|
||||||
|
**Descrizione:** Thread separato per aggiornamenti dati
|
||||||
|
|
||||||
|
**Feature:**
|
||||||
|
- Esegue richieste HTTP senza bloccare UI
|
||||||
|
- Aggiorna localStorage ogni 30 secondi
|
||||||
|
- Notifica main thread con nuovi dati
|
||||||
|
- Fallback per browser senza Web Worker support
|
||||||
|
|
||||||
|
**Vantaggi:**
|
||||||
|
- UI sempre responsiva (60 FPS)
|
||||||
|
- Niente lag durante fetch
|
||||||
|
- Scalabilità migliore
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. LocalStorage Persistence
|
||||||
|
**Descrizione:** Cache locale dei dati
|
||||||
|
|
||||||
|
**Dati Salvati:**
|
||||||
|
- `llm_monitor_health` - Status health
|
||||||
|
- `llm_monitor_models` - Elenco modelli
|
||||||
|
|
||||||
|
**Benefit:**
|
||||||
|
- Offline support
|
||||||
|
- Caricamento istantaneo
|
||||||
|
- Ripristino ultimo stato noto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Swagger/OpenAPI Documentation
|
||||||
|
**Descrizione:** Documentazione interattiva API
|
||||||
|
|
||||||
|
**URL:**
|
||||||
|
- Swagger UI: `/docs`
|
||||||
|
- ReDoc: `/redoc`
|
||||||
|
|
||||||
|
**Feature:**
|
||||||
|
- Testa endpoint direttamente
|
||||||
|
- Visualizza schemi
|
||||||
|
- Genera client code (curl, Python, JS, ecc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Docker Support
|
||||||
|
**Descrizione:** Containerizzazione dell'applicazione
|
||||||
|
|
||||||
|
**Componenti:**
|
||||||
|
- Dockerfile multi-stage ottimizzato
|
||||||
|
- docker-compose.yml con Ollama incluso
|
||||||
|
- Health checks configurati
|
||||||
|
- Sempre acceso fino all'arresto manuale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Requisiti Tecnici
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Linguaggio:** Python 3.10+
|
||||||
|
- **Framework:** FastAPI
|
||||||
|
- **Server:** uVicorn
|
||||||
|
- **Validation:** Pydantic
|
||||||
|
- **HTTP Client:** requests/httpx
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **HTML5** - Template base
|
||||||
|
- **CSS:** TailwindCSS (utility-first)
|
||||||
|
- **JavaScript:** Vanilla JS (no frameworks)
|
||||||
|
- **Web APIs:**
|
||||||
|
- Fetch API per HTTP
|
||||||
|
- Web Workers per threading
|
||||||
|
- localStorage per persistence
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Container:** Docker
|
||||||
|
- **Orchestration:** Docker Compose
|
||||||
|
- **Network:** HTTP/HTTPS
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- Nessuno (stateless)
|
||||||
|
- localStorage nel browser (client-side only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏛️ Architettura
|
||||||
|
|
||||||
|
### Componenti Principali
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Client (Browser) │
|
||||||
|
│ ┌────────────────────────────────────────────────┐ │
|
||||||
|
│ │ index.html + app.js (Main Thread) │ │
|
||||||
|
│ │ - Renderizza UI │ │
|
||||||
|
│ │ - Legge localStorage │ │
|
||||||
|
│ │ - Aggiorna DOM granularmente │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────┐ │
|
||||||
|
│ │ data-sync.worker.js (Web Worker Thread) │ │
|
||||||
|
│ │ - Fetch /api/v1/health │ │
|
||||||
|
│ │ - Fetch /api/v1/models │ │
|
||||||
|
│ │ - Aggiorna localStorage │ │
|
||||||
|
│ │ - Comunica con main thread │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────┬────────────────────────────┘
|
||||||
|
│ HTTP REST API
|
||||||
|
│
|
||||||
|
┌────────────────────────▼────────────────────────────┐
|
||||||
|
│ FastAPI Server (Python) │
|
||||||
|
│ ┌────────────────────────────────────────────────┐ │
|
||||||
|
│ │ main.py (Entry Point) │ │
|
||||||
|
│ │ - CORS middleware │ │
|
||||||
|
│ │ - Route setup │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────┐ │
|
||||||
|
│ │ app/api/ (Endpoints) │ │
|
||||||
|
│ │ - health.py │ │
|
||||||
|
│ │ - models.py │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────┐ │
|
||||||
|
│ │ app/services/ (Business Logic) │ │
|
||||||
|
│ │ - ollama.py (OllamaClient) │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────┬────────────────────────────┘
|
||||||
|
│ HTTP API
|
||||||
|
│
|
||||||
|
┌────────────────────────▼────────────────────────────┐
|
||||||
|
│ Ollama Server (LLM Models) │
|
||||||
|
│ - API Port: 11434 │
|
||||||
|
│ - Gestisce modelli LLM │
|
||||||
|
│ - Endpoint: /api/tags │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Inizializzazione:**
|
||||||
|
- Main thread carica localStorage
|
||||||
|
- Renderizza UI con dati cached
|
||||||
|
- Avvia Web Worker
|
||||||
|
|
||||||
|
2. **Sincronizzazione Periodica (ogni 30s):**
|
||||||
|
- Worker fetch `/api/v1/health`
|
||||||
|
- Worker fetch `/api/v1/models`
|
||||||
|
- Worker aggiorna localStorage
|
||||||
|
- Worker invia messaggio a main thread
|
||||||
|
|
||||||
|
3. **Aggiornamento UI:**
|
||||||
|
- Main thread riceve messaggio dal Worker
|
||||||
|
- Confronta dati vecchi vs nuovi
|
||||||
|
- Aggiorna solo elementi cambiati
|
||||||
|
|
||||||
|
4. **Refresh Manuale:**
|
||||||
|
- Utente clicca pulsante 🔄
|
||||||
|
- Main thread chiama `worker.postMessage({ type: "SYNC_NOW" })`
|
||||||
|
- Worker esegue sincronizzazione immediata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 User Stories
|
||||||
|
|
||||||
|
### US1: Visualizzare Modelli Disponibili
|
||||||
|
```
|
||||||
|
Come: Developer
|
||||||
|
Voglio: Vedere lista di modelli caricati in Ollama
|
||||||
|
Affinché: Sapere quali modelli sono disponibili
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- Dashboard mostra elenco modelli
|
||||||
|
- Per ogni modello: nome, dimensione, data aggiornamento
|
||||||
|
- Totale modelli visualizzato in stat card
|
||||||
|
- Dati aggiornati ogni 30 secondi
|
||||||
|
```
|
||||||
|
|
||||||
|
### US2: Monitorare Consumo Spazio
|
||||||
|
```
|
||||||
|
Come: DevOps Engineer
|
||||||
|
Voglio: Verificare quanto spazio occupano i modelli
|
||||||
|
Affinché: Pianificare capacity planning e cleanup
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- Stat card mostra spazio totale
|
||||||
|
- Ogni modello mostra dimensione
|
||||||
|
- Formato leggibile (GB, MB, etc)
|
||||||
|
- Aggiornamenti automatici
|
||||||
|
```
|
||||||
|
|
||||||
|
### US3: Verificare Status Ollama
|
||||||
|
```
|
||||||
|
Come: System Admin
|
||||||
|
Voglio: Sapere se Ollama è online
|
||||||
|
Affinché: Identificare problemi rapidamente
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- Status indicator nel header (verde/rosso)
|
||||||
|
- Testo descrittivo ("Online/Offline")
|
||||||
|
- Health check ogni 30 secondi
|
||||||
|
```
|
||||||
|
|
||||||
|
### US4: Accedere alla API Documentata
|
||||||
|
```
|
||||||
|
Come: Developer
|
||||||
|
Voglio: Consultare documentazione API con esempi
|
||||||
|
Affinché: Integrare i dati in miei script/app
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- Swagger UI disponibile su /docs
|
||||||
|
- ReDoc disponibile su /redoc
|
||||||
|
- Tutti gli endpoint documentati
|
||||||
|
- Possibile testare endpoint dal browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### US5: Usare Dashboard Offline
|
||||||
|
```
|
||||||
|
Come: Developer
|
||||||
|
Voglio: Visualizzare ultimi dati anche offline
|
||||||
|
Affinché: Accedere all'info anche senza connessione
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- localStorage persiste dati
|
||||||
|
- Dashboard carica senza server
|
||||||
|
- Mostra timestamp ultimo aggiornamento
|
||||||
|
- Warning se dati non aggiornati
|
||||||
|
```
|
||||||
|
|
||||||
|
### US6: Refresh Manuale
|
||||||
|
```
|
||||||
|
Come: User
|
||||||
|
Voglio: Aggiornare i dati immediatamente
|
||||||
|
Affinché: Ottenere le informazioni più recenti
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
- Pulsante 🔄 presente nella dashboard
|
||||||
|
- Clicco aggiorna immediatamente i dati
|
||||||
|
- Loading state durante fetch
|
||||||
|
- Nessun page reload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Acceptance Criteria
|
||||||
|
|
||||||
|
### Funzionalità
|
||||||
|
|
||||||
|
| # | Feature | Accettazione |
|
||||||
|
|---|---------|--------------|
|
||||||
|
| 1 | Dashboard carica modelli | ✅ Elenco visibile entro 2 secondi |
|
||||||
|
| 2 | Auto-refresh ogni 30s | ✅ Nessun page reload, solo DOM update |
|
||||||
|
| 3 | Status Ollama | ✅ Indicatore verde/rosso corretto |
|
||||||
|
| 4 | localStorage sincronizzato | ✅ Dati persistenti tra session |
|
||||||
|
| 5 | Web Worker attivo | ✅ Main thread mai bloccato |
|
||||||
|
| 6 | API Swagger disponibile | ✅ Endpoint testabili su /docs |
|
||||||
|
| 7 | Docker container | ✅ Avvia e rimane acceso |
|
||||||
|
| 8 | Offline mode | ✅ Carica con localStorage |
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
| # | Metrica | Target |
|
||||||
|
|---|---------|--------|
|
||||||
|
| 1 | FCP (First Contentful Paint) | < 1s |
|
||||||
|
| 2 | LCP (Largest Contentful Paint) | < 2s |
|
||||||
|
| 3 | TTI (Time to Interactive) | < 3s |
|
||||||
|
| 4 | API response time | < 200ms |
|
||||||
|
| 5 | Dashboard refresh FPS | 60 FPS |
|
||||||
|
| 6 | Memory usage | < 50MB |
|
||||||
|
|
||||||
|
### Compatibilità Browser
|
||||||
|
|
||||||
|
| Browser | Versione Minima | Status |
|
||||||
|
|---------|-----------------|--------|
|
||||||
|
| Chrome | 70+ | ✅ Supportato |
|
||||||
|
| Firefox | 65+ | ✅ Supportato |
|
||||||
|
| Safari | 12+ | ✅ Supportato |
|
||||||
|
| Edge | 79+ | ✅ Supportato |
|
||||||
|
| Opera | 57+ | ✅ Supportato |
|
||||||
|
| IE11 | - | ❌ Non supportato (no Web Workers) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Timeline & Roadmap
|
||||||
|
|
||||||
|
### Phase 1: MVP (In Development - Completato ✅)
|
||||||
|
**Durata:** 2 settimane
|
||||||
|
**Feature:**
|
||||||
|
- [x] Dashboard base con elenco modelli
|
||||||
|
- [x] API REST con 3 endpoint
|
||||||
|
- [x] Swagger documentation
|
||||||
|
- [x] Docker setup
|
||||||
|
- [x] Web Worker architettura
|
||||||
|
- [x] localStorage integration
|
||||||
|
|
||||||
|
**Release:** v1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Enhancement (Pianificato 🔄)
|
||||||
|
**Durata:** 2 settimane
|
||||||
|
**Feature:**
|
||||||
|
- [ ] Statistiche storiche (grafici)
|
||||||
|
- [ ] Ricerca e filtri modelli
|
||||||
|
- [ ] Dark/Light theme toggle
|
||||||
|
- [ ] Configurazione refresh rate
|
||||||
|
- [ ] Export dati (CSV/JSON)
|
||||||
|
- [ ] Notifiche cambio status
|
||||||
|
|
||||||
|
**Release:** v1.1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Advanced (Futuro 🚀)
|
||||||
|
**Durata:** 3+ settimane
|
||||||
|
**Feature:**
|
||||||
|
- [ ] Multi-tenant support
|
||||||
|
- [ ] Authentication & Authorization
|
||||||
|
- [ ] User preferences storage
|
||||||
|
- [ ] Service Worker per PWA
|
||||||
|
- [ ] Real-time updates (WebSocket)
|
||||||
|
- [ ] Model versioning
|
||||||
|
- [ ] Pull/Delete confirmation modal
|
||||||
|
- [ ] Advanced error handling
|
||||||
|
|
||||||
|
**Release:** v2.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Production (Futuro 🏆)
|
||||||
|
**Durata:** Ongoing
|
||||||
|
**Feature:**
|
||||||
|
- [ ] Monitoring & Alerting
|
||||||
|
- [ ] Analytics dashboard
|
||||||
|
- [ ] Performance optimization
|
||||||
|
- [ ] Load testing & benchmarks
|
||||||
|
- [ ] Security audit
|
||||||
|
- [ ] GDPR compliance
|
||||||
|
|
||||||
|
**Release:** v2.1.0+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Success Metrics
|
||||||
|
|
||||||
|
### Technical Metrics
|
||||||
|
|
||||||
|
| Metrica | Target | Misura |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| Uptime | 99%+ | Monitoring |
|
||||||
|
| API latency | < 200ms | New Relic/DataDog |
|
||||||
|
| Error rate | < 0.1% | Logs |
|
||||||
|
| Test coverage | 80%+ | pytest coverage |
|
||||||
|
| Bundle size | < 100KB | webpack-bundle-analyzer |
|
||||||
|
|
||||||
|
### Business Metrics
|
||||||
|
|
||||||
|
| Metrica | Target | Misura |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| Time to load | < 2s | Lighthouse |
|
||||||
|
| Page interactions/sec | 100+ | App metrics |
|
||||||
|
| User satisfaction | 4.5/5 | Feedback form |
|
||||||
|
| DevOps adoption | 70%+ | Usage analytics |
|
||||||
|
| Automation enabled | 50%+ | Script integrations |
|
||||||
|
|
||||||
|
### User Engagement
|
||||||
|
|
||||||
|
| Metrica | Target | Misura |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| Monthly active users | 100+ | Analytics |
|
||||||
|
| Dashboard views/month | 1000+ | Google Analytics |
|
||||||
|
| API calls/day | 500+ | API logs |
|
||||||
|
| Feature usage rate | 80%+ | Telemetry |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Constraints & Assumptions
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
#### Tecnici
|
||||||
|
- Ollama deve essere in esecuzione (hard requirement)
|
||||||
|
- Python 3.10+ necessario
|
||||||
|
- Docker richiesto per containerizzazione
|
||||||
|
- Browser moderno necessario (Web Workers)
|
||||||
|
|
||||||
|
#### Organizzativi
|
||||||
|
- Team: 1-2 developer
|
||||||
|
- Budget: Open source (free)
|
||||||
|
- Timeline: Sprint 2 settimane
|
||||||
|
|
||||||
|
#### User
|
||||||
|
- Conoscenza base di Docker
|
||||||
|
- Accesso locale a Ollama
|
||||||
|
- Browser moderno
|
||||||
|
|
||||||
|
### Assumptions
|
||||||
|
|
||||||
|
#### Prodotto
|
||||||
|
- Ollama API rimane stabile
|
||||||
|
- Modelli LLM sono relativamente statici (cambiano meno di 24h)
|
||||||
|
- Refresh ogni 30s è adeguato
|
||||||
|
|
||||||
|
#### Tecnico
|
||||||
|
- Web Workers supportati dai browser target
|
||||||
|
- localStorage disponibile (non private mode)
|
||||||
|
- CORS enabled tra client e server
|
||||||
|
|
||||||
|
#### Market
|
||||||
|
- Ollama diventerà standard per LLM locali
|
||||||
|
- Interesse crescente in monitoring tools
|
||||||
|
- Community contribuirà improvement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Note Implementative
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
```
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
requests==2.31.0
|
||||||
|
jinja2==3.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dev Dependencies
|
||||||
|
```
|
||||||
|
pytest==7.4.3
|
||||||
|
black==23.12.0
|
||||||
|
flake8==6.1.0
|
||||||
|
mypy==1.7.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
llm-monitor/
|
||||||
|
├── main.py # FastAPI entry point
|
||||||
|
├── app/
|
||||||
|
│ ├── config.py # Configuration
|
||||||
|
│ ├── api/ # Endpoints
|
||||||
|
│ ├── services/ # Business logic
|
||||||
|
│ └── web/ # Frontend (HTML, JS, CSS)
|
||||||
|
├── tests/ # Test suite
|
||||||
|
├── Dockerfile # Container
|
||||||
|
└── docker-compose.yml # Orchestration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Riferimenti
|
||||||
|
|
||||||
|
### Documentazione Esterna
|
||||||
|
- [FastAPI Docs](https://fastapi.tiangolo.com/)
|
||||||
|
- [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md)
|
||||||
|
- [Web Workers MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
||||||
|
- [localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
- [GitHub: llm-monitor](https://github.com/lucasacchiNet/llm-monitor)
|
||||||
|
- [Docker Hub: llm-monitor](https://hub.docker.com/r/lucasacchi/llm-monitor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✍️ Changelog PRD
|
||||||
|
|
||||||
|
| Data | Versione | Autore | Cambiamenti |
|
||||||
|
|------|----------|--------|------------|
|
||||||
|
| 2024-04-24 | 1.0 | Luca Sacchi | Documento iniziale |
|
||||||
|
| 2024-04-25 | 1.1 | - | TBD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documento approvato:** ✅
|
||||||
|
**Revisore:** Product Team
|
||||||
|
**Ultimo aggiornamento:** Aprile 2024
|
||||||
|
**Prossima review:** Giugno 2024
|
||||||
|
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* LLM Monitor - Main App
|
||||||
|
* Gestisce il Web Worker e aggiorna il DOM in modo efficiente
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LLMMonitorApp {
|
||||||
|
constructor() {
|
||||||
|
this.worker = null;
|
||||||
|
this.lastData = {
|
||||||
|
health: null,
|
||||||
|
models: null
|
||||||
|
};
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Inizializzare il Web Worker
|
||||||
|
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);
|
||||||
|
// Fallback: sincronizzazione nel main thread
|
||||||
|
this.syncDataInMainThread();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn("Web Workers not supported, using main thread");
|
||||||
|
this.syncDataInMainThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caricare dati da localStorage
|
||||||
|
this.loadFromLocalStorage();
|
||||||
|
|
||||||
|
// Listener al pulsante manuale
|
||||||
|
document.getElementById("refresh-btn")?.addEventListener("click", () => {
|
||||||
|
this.requestSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caricare dati da localStorage
|
||||||
|
loadFromLocalStorage() {
|
||||||
|
const healthStr = localStorage.getItem("llm_monitor_health");
|
||||||
|
const modelsStr = localStorage.getItem("llm_monitor_models");
|
||||||
|
|
||||||
|
if (healthStr) {
|
||||||
|
try {
|
||||||
|
this.lastData.health = JSON.parse(healthStr);
|
||||||
|
this.renderHealth(this.lastData.health);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing health data:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsStr) {
|
||||||
|
try {
|
||||||
|
this.lastData.models = JSON.parse(modelsStr);
|
||||||
|
this.renderModels(this.lastData.models);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing models data:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestire messaggi dal Worker
|
||||||
|
handleWorkerMessage(event) {
|
||||||
|
const { type, health, modelsData } = event.data;
|
||||||
|
|
||||||
|
if (type === "DATA_UPDATED") {
|
||||||
|
if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) {
|
||||||
|
this.lastData.health = health;
|
||||||
|
this.renderHealth(health);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsData && JSON.stringify(this.lastData.models) !== JSON.stringify(modelsData)) {
|
||||||
|
this.lastData.models = modelsData;
|
||||||
|
this.renderModels(modelsData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizzare Health (aggiornamento granulare)
|
||||||
|
renderHealth(health) {
|
||||||
|
if (!health) return;
|
||||||
|
|
||||||
|
const ollamaStatus = health.ollama_status;
|
||||||
|
const statusEl = document.getElementById("status-indicator");
|
||||||
|
const statusText = document.getElementById("status-text");
|
||||||
|
const ollamaStatusEl = document.getElementById("ollama-status");
|
||||||
|
|
||||||
|
if (ollamaStatus === "online") {
|
||||||
|
// Aggiornare solo se cambiato
|
||||||
|
if (!statusEl.classList.contains("bg-green-500")) {
|
||||||
|
statusEl.className = "w-3 h-3 bg-green-500 rounded-full";
|
||||||
|
statusText.className = "text-sm text-green-400";
|
||||||
|
statusText.textContent = "Ollama Online";
|
||||||
|
ollamaStatusEl.innerHTML = "🟢 Online";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!statusEl.classList.contains("bg-red-500")) {
|
||||||
|
statusEl.className = "w-3 h-3 bg-red-500 rounded-full";
|
||||||
|
statusText.className = "text-sm text-red-400";
|
||||||
|
statusText.textContent = "Ollama Offline";
|
||||||
|
ollamaStatusEl.innerHTML = "🔴 Offline";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizzare Modelli (aggiornamento granulare)
|
||||||
|
renderModels(modelsData) {
|
||||||
|
if (!modelsData) return;
|
||||||
|
|
||||||
|
// Aggiornare conteggio
|
||||||
|
document.getElementById("models-count").textContent = modelsData.total;
|
||||||
|
|
||||||
|
// Aggiornare spazio totale
|
||||||
|
document.getElementById("total-size").textContent = modelsData.totalSize;
|
||||||
|
|
||||||
|
// Aggiornare lista modelli
|
||||||
|
const container = document.getElementById("models-container");
|
||||||
|
const { models } = modelsData;
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-center py-8 text-gray-400">
|
||||||
|
<p>Nessun modello caricato</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparare con il rendering precedente (evitare re-render se identico)
|
||||||
|
const newHTML = models.map(model => this.renderModelCard(model)).join("");
|
||||||
|
|
||||||
|
// Aggiornare solo se veramente diverso
|
||||||
|
if (container.innerHTML !== newHTML) {
|
||||||
|
container.innerHTML = newHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizzare singola card modello
|
||||||
|
renderModelCard(model) {
|
||||||
|
const formattedDate = this.formatDate(model.modified_at);
|
||||||
|
return `
|
||||||
|
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 transition">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<h3 class="text-lg font-semibold">${this.escapeHtml(model.name)}</h3>
|
||||||
|
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Caricato</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400">Dimensione</p>
|
||||||
|
<p class="font-semibold">${this.formatBytes(model.size)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400">Ultimo aggiornamento</p>
|
||||||
|
<p class="font-semibold">${formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formattare bytes
|
||||||
|
formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formattare data
|
||||||
|
formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("it-IT", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escapare HTML (prevenire XSS)
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiedere sincronizzazione manuale al Worker
|
||||||
|
requestSync() {
|
||||||
|
if (this.worker) {
|
||||||
|
this.worker.postMessage({ type: "SYNC_NOW" });
|
||||||
|
} else {
|
||||||
|
this.syncDataInMainThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: sincronizzazione nel main thread
|
||||||
|
async syncDataInMainThread() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/v1/health");
|
||||||
|
if (response.ok) {
|
||||||
|
const health = await response.json();
|
||||||
|
this.lastData.health = health;
|
||||||
|
localStorage.setItem("llm_monitor_health", JSON.stringify(health));
|
||||||
|
this.renderHealth(health);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Health check error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/v1/models");
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const models = data.models || [];
|
||||||
|
const modelsData = {
|
||||||
|
models,
|
||||||
|
total: models.length,
|
||||||
|
totalSize: this.formatBytes(models.reduce((sum, m) => sum + m.size, 0)),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
this.lastData.models = modelsData;
|
||||||
|
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
|
||||||
|
this.renderModels(modelsData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Models loading error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inizializzare l'app quando il DOM è pronto
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.app = new LLMMonitorApp();
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* LLM Monitor - Data Sync Worker
|
||||||
|
* Aggiorna i dati in background e notifica il main thread
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE = "/api/v1";
|
||||||
|
const REFRESH_INTERVAL = 30000; // 30 secondi
|
||||||
|
|
||||||
|
// Formattare bytes
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recuperare health
|
||||||
|
async function fetchHealth() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/health`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
status: data.status,
|
||||||
|
ollama_status: data.ollama_status,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Health check error:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recuperare modelli
|
||||||
|
async function fetchModels() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/models`);
|
||||||
|
if (!response.ok) throw new Error("Errore nel caricamento");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const models = data.models || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
total: models.length,
|
||||||
|
totalSize: formatBytes(models.reduce((sum, m) => sum + m.size, 0)),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading models:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizzare i dati
|
||||||
|
async function syncData() {
|
||||||
|
const health = await fetchHealth();
|
||||||
|
const modelsData = await fetchModels();
|
||||||
|
|
||||||
|
// Salvare in localStorage
|
||||||
|
if (health) {
|
||||||
|
localStorage.setItem("llm_monitor_health", JSON.stringify(health));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsData) {
|
||||||
|
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notificare il main thread
|
||||||
|
self.postMessage({
|
||||||
|
type: "DATA_UPDATED",
|
||||||
|
health,
|
||||||
|
modelsData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eseguire la sincronizzazione iniziale
|
||||||
|
syncData();
|
||||||
|
|
||||||
|
// Pianificare aggiornamenti periodici
|
||||||
|
setInterval(syncData, REFRESH_INTERVAL);
|
||||||
|
|
||||||
|
// Gestire messaggi dal main thread
|
||||||
|
self.onmessage = (event) => {
|
||||||
|
if (event.data.type === "SYNC_NOW") {
|
||||||
|
syncData();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<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">
|
||||||
<h2 class="text-xl font-bold">Modelli Disponibili</h2>
|
<h2 class="text-xl font-bold">Modelli Disponibili</h2>
|
||||||
<button onclick="loadModels()" 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
|
🔄 Aggiorna
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,128 +97,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="/static/js/app.js"></script>
|
||||||
const API_BASE = "/api/v1";
|
|
||||||
|
|
||||||
// Formattare bytes in formato leggibile
|
|
||||||
function formatBytes(bytes) {
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formattare data
|
|
||||||
function formatDate(dateString) {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString("it-IT", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificare health
|
|
||||||
async function checkHealth() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/health`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
const statusEl = document.getElementById("status-indicator");
|
|
||||||
const statusText = document.getElementById("status-text");
|
|
||||||
const ollamaStatus = data.ollama_status;
|
|
||||||
|
|
||||||
if (ollamaStatus === "online") {
|
|
||||||
statusEl.className = "w-3 h-3 bg-green-500 rounded-full";
|
|
||||||
statusText.className = "text-sm text-green-400";
|
|
||||||
statusText.textContent = "Ollama Online";
|
|
||||||
document.getElementById("ollama-status").innerHTML = "🟢 Online";
|
|
||||||
} else {
|
|
||||||
statusEl.className = "w-3 h-3 bg-red-500 rounded-full";
|
|
||||||
statusText.className = "text-sm text-red-400";
|
|
||||||
statusText.textContent = "Ollama Offline";
|
|
||||||
document.getElementById("ollama-status").innerHTML = "🔴 Offline";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Health check error:", error);
|
|
||||||
document.getElementById("status-indicator").className = "w-3 h-3 bg-red-500 rounded-full";
|
|
||||||
document.getElementById("status-text").textContent = "Errore connessione";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caricare modelli
|
|
||||||
async function loadModels() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/models`);
|
|
||||||
if (!response.ok) throw new Error("Errore nel caricamento");
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const models = data.models || [];
|
|
||||||
|
|
||||||
// Aggiornare conteggio
|
|
||||||
document.getElementById("models-count").textContent = models.length;
|
|
||||||
|
|
||||||
// Calcolare spazio totale
|
|
||||||
const totalSize = models.reduce((sum, m) => sum + m.size, 0);
|
|
||||||
document.getElementById("total-size").textContent = formatBytes(totalSize);
|
|
||||||
|
|
||||||
// Renderizzare modelli
|
|
||||||
if (models.length === 0) {
|
|
||||||
document.getElementById("models-container").innerHTML = `
|
|
||||||
<div class="text-center py-8 text-gray-400">
|
|
||||||
<p>Nessun modello caricato</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
document.getElementById("models-container").innerHTML = models.map(model => `
|
|
||||||
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 transition">
|
|
||||||
<div class="flex items-start justify-between mb-3">
|
|
||||||
<h3 class="text-lg font-semibold">${model.name}</h3>
|
|
||||||
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Caricato</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Dimensione</p>
|
|
||||||
<p class="font-semibold">${formatBytes(model.size)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Ultimo aggiornamento</p>
|
|
||||||
<p class="font-semibold">${formatDate(model.modified_at)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<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">${model.digest.substring(0, 64)}...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join("");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading models:", error);
|
|
||||||
document.getElementById("models-container").innerHTML = `
|
|
||||||
<div class="text-center py-8 text-red-400">
|
|
||||||
<p>❌ Errore nel caricamento dei modelli</p>
|
|
||||||
<p class="text-sm mt-2">${error.message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inizializzazione
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
checkHealth();
|
|
||||||
loadModels();
|
|
||||||
|
|
||||||
// Refresh ogni 30 secondi
|
|
||||||
setInterval(() => {
|
|
||||||
checkHealth();
|
|
||||||
loadModels();
|
|
||||||
}, 30000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-43
@@ -1,23 +1,6 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Ollama Service
|
|
||||||
ollama:
|
|
||||||
image: ollama/ollama:latest
|
|
||||||
container_name: ollama-server
|
|
||||||
ports:
|
|
||||||
- "11434:11434"
|
|
||||||
environment:
|
|
||||||
OLLAMA_HOST: 0.0.0.0:11434
|
|
||||||
volumes:
|
|
||||||
- ollama_data:/root/.ollama
|
|
||||||
restart: unless-stopped
|
|
||||||
# Keep container running until stopped
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- llm-monitor-network
|
|
||||||
|
|
||||||
# LLM Monitor Dashboard
|
# LLM Monitor Dashboard
|
||||||
llm-monitor:
|
llm-monitor:
|
||||||
build:
|
build:
|
||||||
@@ -25,45 +8,22 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: llm-monitor-app
|
container_name: llm-monitor-app
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "${API_PORT:-8000}:${API_PORT:-8000}"
|
||||||
environment:
|
|
||||||
# Carica variabili da .env
|
|
||||||
OLLAMA_HOST: http://ollama:11434
|
|
||||||
OLLAMA_TIMEOUT: 30
|
|
||||||
API_HOST: 0.0.0.0
|
|
||||||
API_PORT: 8000
|
|
||||||
API_WORKERS: 4
|
|
||||||
CORS_ORIGINS: http://localhost:3000,http://localhost:5173,http://localhost:8000
|
|
||||||
LOG_LEVEL: INFO
|
|
||||||
ENVIRONMENT: production
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
depends_on:
|
|
||||||
- ollama
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
networks:
|
|
||||||
- llm-monitor-network
|
|
||||||
# Health check
|
# Health check
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:${API_PORT:-8000}/api/v1/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
volumes:
|
|
||||||
ollama_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
llm-monitor-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
# Istruzioni di avvio:
|
# Istruzioni di avvio:
|
||||||
# docker compose up -d # Avvia i servizi
|
# docker compose up -d # Avvia i servizi
|
||||||
# docker compose logs -f # Visualizza i log
|
# docker compose logs -f # Visualizza i log
|
||||||
# docker compose down # Ferma i servizi
|
# docker compose down # Ferma i servizi
|
||||||
# docker compose stop ollama # Ferma solo Ollama
|
# docker compose restart # Riavvia i servizi
|
||||||
# docker compose start ollama # Riavvia Ollama
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# LLM Monitor - Architettura Web Worker
|
||||||
|
|
||||||
|
## 📊 Architettura Moderna con Web Workers
|
||||||
|
|
||||||
|
Questa versione della dashboard utilizza **Web Workers** per un'esperienza utente ottimale senza blocchi dell'UI.
|
||||||
|
|
||||||
|
## 🏗️ Componenti
|
||||||
|
|
||||||
|
### 1. **data-sync.worker.js** (Web Worker)
|
||||||
|
Thread separato che:
|
||||||
|
- Effettua le richieste HTTP all'API (`/api/v1/health`, `/api/v1/models`)
|
||||||
|
- Aggiorna **localStorage** periodicamente (ogni 30 secondi)
|
||||||
|
- Invia messaggi al main thread con i dati aggiornati
|
||||||
|
- **NON blocca mai l'interfaccia utente**
|
||||||
|
|
||||||
|
### 2. **app.js** (Main Thread)
|
||||||
|
File principale che:
|
||||||
|
- Inizializza il Web Worker
|
||||||
|
- Carica dati da **localStorage** al boot
|
||||||
|
- Riceve messaggi dal Worker e aggiorna il DOM
|
||||||
|
- Aggiorna solo gli elementi DOM che sono effettivamente cambiati
|
||||||
|
- Fornisce fallback se Web Workers non sono supportati
|
||||||
|
|
||||||
|
### 3. **index.html**
|
||||||
|
Template HTML con struttura base e caricamento di app.js
|
||||||
|
|
||||||
|
## 🔄 Flusso Dati
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ MAIN THREAD (UI Thread) │
|
||||||
|
│ - Renderizza il DOM │
|
||||||
|
│ - Interagisce con l'utente │
|
||||||
|
│ - Legge da localStorage │
|
||||||
|
└─────────────────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
postMessage() / onmessage
|
||||||
|
│
|
||||||
|
┌─────────────────▼───────────────────────────────────┐
|
||||||
|
│ WEB WORKER (Separate Thread) │
|
||||||
|
│ - Fetch /api/v1/health │
|
||||||
|
│ - Fetch /api/v1/models │
|
||||||
|
│ - localStorage.setItem() │
|
||||||
|
│ - Eseguito ogni 30 secondi │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
postMessage({ DATA_UPDATED })
|
||||||
|
│
|
||||||
|
┌───────▼────────┐
|
||||||
|
│ localStorage │
|
||||||
|
│ persistente │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💾 LocalStorage
|
||||||
|
|
||||||
|
### Chiavi memorizzate:
|
||||||
|
- `llm_monitor_health` - Dati health check (status, ollama_status, timestamp)
|
||||||
|
- `llm_monitor_models` - Dati modelli (lista, total, totalSize, timestamp)
|
||||||
|
|
||||||
|
### Struttura dati:
|
||||||
|
|
||||||
|
**Health:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"ollama_status": "online",
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Models:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "llama2",
|
||||||
|
"digest": "abc123...",
|
||||||
|
"size": 3825922048,
|
||||||
|
"modified_at": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"totalSize": "3.56 GB",
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Vantaggi
|
||||||
|
|
||||||
|
### ✅ Performance
|
||||||
|
- **Main thread mai bloccato** - Le richieste HTTP avvengono nel Worker
|
||||||
|
- **DOM updates ottimizzate** - Aggiorna solo elementi cambiati
|
||||||
|
- **60 FPS garantito** - L'UI resta responsiva
|
||||||
|
|
||||||
|
### ✅ Offline Support
|
||||||
|
- I dati rimangono in **localStorage** anche se il server è offline
|
||||||
|
- La dashboard mostra l'ultimo stato noto
|
||||||
|
|
||||||
|
### ✅ Efficienza di Rete
|
||||||
|
- Una sola fetch ogni 30 secondi (dal Worker)
|
||||||
|
- Compressione gzip della risposta
|
||||||
|
- Ridotto uso di bandwidth
|
||||||
|
|
||||||
|
### ✅ Scalabilità
|
||||||
|
- Più tab della dashboard non sovraccaricare il server
|
||||||
|
- LocalStorage condiviso tra tab (gli aggiornamenti si sincronizzano)
|
||||||
|
|
||||||
|
## 🔧 Configurazione
|
||||||
|
|
||||||
|
### Intervallo di aggiornamento
|
||||||
|
Modifica in `data-sync.worker.js`:
|
||||||
|
```javascript
|
||||||
|
const REFRESH_INTERVAL = 30000; // 30 secondi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disabilitare Web Worker (debug)
|
||||||
|
Nel browser console:
|
||||||
|
```javascript
|
||||||
|
window.app.worker = null;
|
||||||
|
window.app.syncDataInMainThread();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Sviluppo
|
||||||
|
|
||||||
|
### Debug del Worker
|
||||||
|
```javascript
|
||||||
|
// In data-sync.worker.js
|
||||||
|
console.log("Worker sync triggered", new Date());
|
||||||
|
```
|
||||||
|
|
||||||
|
Console del browser (DevTools > Dedicated Worker)
|
||||||
|
|
||||||
|
### Ispezionare localStorage
|
||||||
|
```javascript
|
||||||
|
// In console del browser
|
||||||
|
JSON.parse(localStorage.getItem('llm_monitor_health'))
|
||||||
|
JSON.parse(localStorage.getItem('llm_monitor_models'))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Browser Support
|
||||||
|
|
||||||
|
- ✅ Chrome/Edge 4+
|
||||||
|
- ✅ Firefox 3.6+
|
||||||
|
- ✅ Safari 4+
|
||||||
|
- ✅ Opera 10.6+
|
||||||
|
- ⚠️ Fallback disponibile se non supportati
|
||||||
|
|
||||||
|
## 🚀 Ottimizzazioni Future
|
||||||
|
|
||||||
|
- [ ] IndexedDB per dati maggiori
|
||||||
|
- [ ] Service Worker per offline mode completo
|
||||||
|
- [ ] Sincronizzazione tra tab (BroadcastChannel API)
|
||||||
|
- [ ] Caching intelligente con TTL
|
||||||
|
- [ ] Compressione dati (Zstandard/Brotli)
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Worker non carica
|
||||||
|
- Verificare CORS
|
||||||
|
- Controllare DevTools > Application > Service Workers
|
||||||
|
- Verificare console per errori
|
||||||
|
|
||||||
|
### localStorage non persiste
|
||||||
|
- Modalità incognito/privato disabilita localStorage
|
||||||
|
- Spazio esaurito: svuotare localStorage
|
||||||
|
- Cookie di terze parti potrebbe essere disabilitato
|
||||||
|
|
||||||
|
### Aggiornamenti non visibili
|
||||||
|
- Controllare DevTools > Application > LocalStorage
|
||||||
|
- Verificare che il Worker sia attivo (DevTools > Dedicated Workers)
|
||||||
|
- Forzare refresh manuale con pulsante 🔄
|
||||||
|
|
||||||
|
## 📚 Riferimenti
|
||||||
|
|
||||||
|
- [MDN Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
||||||
|
- [localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
|
||||||
|
- [Performance Best Practices](https://web.dev/performance/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Sviluppato per LLM Monitor v1.0.0** 🦙
|
||||||
+7
-3
@@ -1,10 +1,14 @@
|
|||||||
# LLM Monitor - Environment Configuration Example
|
# LLM Monitor - Environment Configuration Example
|
||||||
# Copy this file to .env and adjust values for your environment
|
# Copia questo file in .env e personalizza per il tuo ambiente
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Ollama Configuration
|
# Ollama Configuration (Remote Server)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# URL base dell'API Ollama
|
# URL base dell'API Ollama (server remoto)
|
||||||
|
# Esempi:
|
||||||
|
# - http://localhost:11434 (sviluppo locale)
|
||||||
|
# - http://ollama.example.com:11434 (server remoto)
|
||||||
|
# - https://ollama.example.com (con SSL)
|
||||||
OLLAMA_HOST=http://localhost:11434
|
OLLAMA_HOST=http://localhost:11434
|
||||||
|
|
||||||
# Timeout per le richieste a Ollama (secondi)
|
# Timeout per le richieste a Ollama (secondi)
|
||||||
|
|||||||
Reference in New Issue
Block a user