docs: add comprehensive README and project scaffolding

- README completo con istruzioni di installazione, configurazione e utilizzo
- API Swagger/OpenAPI documentata
- File env.example con variabili di configurazione
- Dockerfile multi-stage ottimizzato
- Docker Compose con Ollama e LLM Monitor
- Struttura completa dell'app FastAPI (main.py, config, api routes)
- Servizio client Ollama reusabile
- Dashboard web HTML con TailwindCSS
- Test suite con pytest
- Makefile per comandi comuni
- CONTRIBUTING.md per i contributori
- LICENSE MIT
- .editorconfig e .dockerignore
- requirements.txt e requirements-dev.txt
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-24 19:11:58 +02:00
commit 4b782ffdc8
28 changed files with 2087 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
"""
Web templates and static files
"""
+224
View File
@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM Monitor - Dashboard Ollama</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</head>
<body class="bg-gray-900 text-white">
<div class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-gray-800 border-b border-gray-700 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center font-bold text-lg">
🦙
</div>
<h1 class="text-2xl font-bold">LLM Monitor</h1>
</div>
<div class="flex items-center gap-4">
<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>
<span id="status-text" class="text-sm text-gray-400">Controllo...</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-1">
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Stats Cards -->
<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="text-gray-400 text-sm font-medium">Modelli Caricati</div>
<div id="models-count" class="text-4xl font-bold mt-2">-</div>
</div>
<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 id="total-size" class="text-4xl font-bold mt-2">-</div>
</div>
<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 id="ollama-status" class="text-4xl font-bold mt-2">-</div>
</div>
</div>
<!-- Models Section -->
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
<div class="flex items-center justify-between mb-6">
<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">
🔄 Aggiorna
</button>
</div>
<!-- Models List -->
<div id="models-container" class="space-y-4">
<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>
<p class="text-gray-400 mt-4">Caricamento modelli...</p>
</div>
</div>
</div>
<!-- API Documentation Section -->
<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>
<p class="text-gray-300 mb-4">La API è documentata e testabile direttamente da:</p>
<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">
Swagger UI
</a>
<a href="/redoc" target="_blank" class="inline-block bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition">
ReDoc
</a>
</div>
</div>
</div>
</main>
<!-- Footer -->
<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">
<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>
</div>
</footer>
</div>
<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>
</html>