feat: add favicon.ico and gate model write APIs by env flag
- Generate and serve real /favicon.ico from static assets - Update HTML to use /favicon.ico - Add ENABLE_MODEL_RW_API setting (default: false) - Disable POST/DELETE model endpoints by default - Hide write endpoints from OpenAPI when disabled - Return 404 for write endpoints when disabled - Update env.example with ENABLE_MODEL_RW_API documentation - Update README and PRD with R/W API policy and remote compose notes - Add tests to verify write endpoints are disabled by default
This commit is contained in:
@@ -188,10 +188,15 @@ Dettagli di un modello specifico
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `POST /api/v1/models/{model_name}/pull`
|
#### `POST /api/v1/models/{model_name}/pull`
|
||||||
Scarica/carica un modello
|
Scarica/carica un modello (**disabilitato di default**)
|
||||||
|
|
||||||
#### `DELETE /api/v1/models/{model_name}`
|
#### `DELETE /api/v1/models/{model_name}`
|
||||||
Elimina un modello
|
Elimina un modello (**disabilitato di default**)
|
||||||
|
|
||||||
|
#### Policy endpoint R/W
|
||||||
|
- Gli endpoint `POST/DELETE` sono **non disponibili** per default.
|
||||||
|
- Si abilitano solo con variabile ambiente `ENABLE_MODEL_RW_API=true`.
|
||||||
|
- Se non abilitati, gli endpoint non sono esposti in Swagger e rispondono con `404`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -244,7 +249,7 @@ Elimina un modello
|
|||||||
|
|
||||||
**Componenti:**
|
**Componenti:**
|
||||||
- Dockerfile multi-stage ottimizzato
|
- Dockerfile multi-stage ottimizzato
|
||||||
- docker-compose.yml con Ollama incluso
|
- docker-compose.yml per la sola dashboard (Ollama esterno/remoto)
|
||||||
- Health checks configurati
|
- Health checks configurati
|
||||||
- Sempre acceso fino all'arresto manuale
|
- Sempre acceso fino all'arresto manuale
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ OLLAMA_TIMEOUT=30
|
|||||||
API_HOST=0.0.0.0
|
API_HOST=0.0.0.0
|
||||||
API_PORT=8000
|
API_PORT=8000
|
||||||
API_WORKERS=4
|
API_WORKERS=4
|
||||||
|
ENABLE_MODEL_RW_API=false
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||||
@@ -105,6 +106,7 @@ ENVIRONMENT=development
|
|||||||
| `API_HOST` | `0.0.0.0` | Host su cui esporre l'API |
|
| `API_HOST` | `0.0.0.0` | Host su cui esporre l'API |
|
||||||
| `API_PORT` | `8000` | Porta dell'API |
|
| `API_PORT` | `8000` | Porta dell'API |
|
||||||
| `API_WORKERS` | `4` | Worker processes |
|
| `API_WORKERS` | `4` | Worker processes |
|
||||||
|
| `ENABLE_MODEL_RW_API` | `false` | Abilita endpoint `POST/DELETE` sui modelli |
|
||||||
| `CORS_ORIGINS` | `http://localhost:3000` | Origini CORS consentite |
|
| `CORS_ORIGINS` | `http://localhost:3000` | Origini CORS consentite |
|
||||||
| `LOG_LEVEL` | `INFO` | Livello di logging |
|
| `LOG_LEVEL` | `INFO` | Livello di logging |
|
||||||
| `ENVIRONMENT` | `development` | Ambiente (development/production) |
|
| `ENVIRONMENT` | `development` | Ambiente (development/production) |
|
||||||
@@ -151,6 +153,21 @@ GET /api/v1/models/{model_name}
|
|||||||
GET /api/v1/health
|
GET /api/v1/health
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Endpoint R/W modelli (opzionali)
|
||||||
|
|
||||||
|
Per impostazione predefinita gli endpoint di scrittura sono **disabilitati** e non disponibili.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/v1/models/{model_name}/pull
|
||||||
|
DELETE /api/v1/models/{model_name}
|
||||||
|
```
|
||||||
|
|
||||||
|
Per abilitarli, imposta nel file `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_MODEL_RW_API=true
|
||||||
|
```
|
||||||
|
|
||||||
**Risposta:**
|
**Risposta:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -215,16 +232,16 @@ docker compose restart llm-monitor
|
|||||||
|
|
||||||
### Container sempre acceso
|
### Container sempre acceso
|
||||||
|
|
||||||
Il container Ollama rimarrà in esecuzione fino al suo arresto manuale:
|
Il container `llm-monitor` rimarrà in esecuzione fino al suo arresto manuale:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fermare
|
# Fermare
|
||||||
docker compose stop ollama
|
docker compose stop llm-monitor
|
||||||
# oppure
|
# oppure
|
||||||
docker stop llm-monitor
|
docker stop llm-monitor
|
||||||
|
|
||||||
# Riavviare
|
# Riavviare
|
||||||
docker compose start ollama
|
docker compose start llm-monitor
|
||||||
# oppure
|
# oppure
|
||||||
docker start llm-monitor
|
docker start llm-monitor
|
||||||
```
|
```
|
||||||
|
|||||||
+19
-2
@@ -13,6 +13,15 @@ from app.config import settings
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_rw_api_enabled() -> None:
|
||||||
|
"""Blocca le API di scrittura se non abilitate esplicitamente."""
|
||||||
|
if not settings.ENABLE_MODEL_RW_API:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Endpoint non disponibile"
|
||||||
|
)
|
||||||
|
|
||||||
class ModelInfo(BaseModel):
|
class ModelInfo(BaseModel):
|
||||||
"""Informazioni su un modello"""
|
"""Informazioni su un modello"""
|
||||||
name: str
|
name: str
|
||||||
@@ -165,7 +174,10 @@ async def get_model(model_name: str):
|
|||||||
detail="Errore nel recupero del modello"
|
detail="Errore nel recupero del modello"
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/models/{model_name}/pull")
|
@router.post(
|
||||||
|
"/models/{model_name}/pull",
|
||||||
|
include_in_schema=settings.ENABLE_MODEL_RW_API
|
||||||
|
)
|
||||||
async def pull_model(model_name: str):
|
async def pull_model(model_name: str):
|
||||||
"""
|
"""
|
||||||
Scarica/carica un modello in Ollama
|
Scarica/carica un modello in Ollama
|
||||||
@@ -176,6 +188,7 @@ async def pull_model(model_name: str):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Status del download
|
dict: Status del download
|
||||||
"""
|
"""
|
||||||
|
ensure_rw_api_enabled()
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{settings.OLLAMA_HOST}/api/pull",
|
f"{settings.OLLAMA_HOST}/api/pull",
|
||||||
@@ -198,7 +211,10 @@ async def pull_model(model_name: str):
|
|||||||
detail="Errore nel pull del modello"
|
detail="Errore nel pull del modello"
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.delete("/models/{model_name}")
|
@router.delete(
|
||||||
|
"/models/{model_name}",
|
||||||
|
include_in_schema=settings.ENABLE_MODEL_RW_API
|
||||||
|
)
|
||||||
async def delete_model(model_name: str):
|
async def delete_model(model_name: str):
|
||||||
"""
|
"""
|
||||||
Elimina un modello da Ollama
|
Elimina un modello da Ollama
|
||||||
@@ -209,6 +225,7 @@ async def delete_model(model_name: str):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Confirmazione eliminazione
|
dict: Confirmazione eliminazione
|
||||||
"""
|
"""
|
||||||
|
ensure_rw_api_enabled()
|
||||||
try:
|
try:
|
||||||
response = requests.delete(
|
response = requests.delete(
|
||||||
f"{settings.OLLAMA_HOST}/api/delete",
|
f"{settings.OLLAMA_HOST}/api/delete",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
|
|||||||
API_HOST: str = "0.0.0.0"
|
API_HOST: str = "0.0.0.0"
|
||||||
API_PORT: int = 8000
|
API_PORT: int = 8000
|
||||||
API_WORKERS: int = 4
|
API_WORKERS: int = 4
|
||||||
|
ENABLE_MODEL_RW_API: bool = False
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
|
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -4,7 +4,7 @@
|
|||||||
<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 - Dashboard Ollama</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🦙</text></svg>">
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||||
<!-- 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">
|
||||||
<!-- Fallback CDN for development (if output.css not available) -->
|
<!-- Fallback CDN for development (if output.css not available) -->
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ API_PORT=8000
|
|||||||
# Numero di worker processes per uVicorn
|
# Numero di worker processes per uVicorn
|
||||||
API_WORKERS=4
|
API_WORKERS=4
|
||||||
|
|
||||||
|
# Abilita API R/W modelli (POST /pull, DELETE /models/{name})
|
||||||
|
# Default sicuro: false (endpoint non disponibili)
|
||||||
|
ENABLE_MODEL_RW_API=false
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ async def dashboard():
|
|||||||
"""Dashboard principale"""
|
"""Dashboard principale"""
|
||||||
return FileResponse(templates_path / "index.html")
|
return FileResponse(templates_path / "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
|
async def favicon():
|
||||||
|
"""Favicon dell'applicazione."""
|
||||||
|
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():
|
||||||
|
|||||||
@@ -92,3 +92,13 @@ def test_openapi_schema(client):
|
|||||||
assert "paths" in schema
|
assert "paths" in schema
|
||||||
assert "/api/v1/health" in schema["paths"]
|
assert "/api/v1/health" in schema["paths"]
|
||||||
assert "/api/v1/models" in schema["paths"]
|
assert "/api/v1/models" in schema["paths"]
|
||||||
|
assert "/api/v1/models/{model_name}/pull" not in schema["paths"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_endpoints_disabled_by_default(client):
|
||||||
|
"""POST/DELETE sui modelli devono essere non disponibili di default."""
|
||||||
|
response_pull = client.post("/api/v1/models/llama2/pull")
|
||||||
|
assert response_pull.status_code == 404
|
||||||
|
|
||||||
|
response_delete = client.delete("/api/v1/models/llama2")
|
||||||
|
assert response_delete.status_code == 404
|
||||||
|
|||||||
Reference in New Issue
Block a user