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:
Luca Sacchi Ricciardi
2026-04-24 19:35:24 +02:00
parent 893376dc14
commit 32b1130632
9 changed files with 69 additions and 9 deletions
+8 -3
View File
@@ -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
+20 -3
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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

+1 -1
View File
@@ -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) -->
+4
View File
@@ -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
# =========================================== # ===========================================
+6
View File
@@ -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():
+10
View File
@@ -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