feat: Add new API endpoints and HTML pages for ML model management

- Implemented HTML pages for datasets, models, training, testing, and results.
- Created API endpoints for managing repositories, results, tests, and training sessions.
- Added functionality for streaming training progress via Server-Sent Events (SSE).
- Introduced a Dockerfile for the ML runner with necessary dependencies.
- Developed an SDK for user code execution within the runner container.
- Enhanced CSS styles for improved UI layout and navigation.
- Established a layout template for consistent HTML structure across pages.
- Added JavaScript for dynamic interactions on the models page.
- Implemented WebSocket handling for real-time communication with kiosk devices and controllers.
- Implemented model registration and management API at /api/models
- Added Gitea proxy API for repository interactions at /api/repos
- Created results API for listing and comparing training results at /api/results
- Developed training management API for enqueueing and retrieving training jobs at /api/trainings
- Introduced SSE endpoint for live training progress updates
- Added HTML pages for models, datasets, and training management
- Created a Dockerfile for the ML runner with necessary dependencies
- Developed SDK for user code execution within the runner container
- Enhanced CSS styles for improved UI/UX
- Implemented WebSocket communication for real-time device and controller interactions in the kiosk system
This commit is contained in:
Giuseppe Raffa
2026-04-28 09:24:38 +02:00
parent ee478e52ef
commit 0ce879aa44
81 changed files with 7491 additions and 746 deletions

131
ml/routers/models.py Normal file
View File

@@ -0,0 +1,131 @@
"""API /api/models — registro modelli (repo Gitea + metadata)."""
from __future__ import annotations
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from core import db
from core.auth import require_auth
from core.model_spec import fetch_and_parse_spec
router = APIRouter(prefix="/api/models", tags=["models"])
def _row(r) -> Optional[dict]:
if r is None:
return None
d = dict(r)
for k in ("created_at", "updated_at"):
if d.get(k) is not None and hasattr(d[k], "isoformat"):
d[k] = d[k].isoformat()
return d
@router.get("")
async def list_models(user=Depends(require_auth)):
rows = await db.fetch("SELECT * FROM models ORDER BY created_at DESC LIMIT 500")
return {"count": len(rows), "models": [_row(r) for r in rows]}
@router.post("", status_code=201)
async def create_model(body: dict, user=Depends(require_auth)):
required = ("name", "type", "gitea_repo")
for k in required:
if not body.get(k):
raise HTTPException(400, f"missing field: {k}")
# prova a pre-caricare model.yml dal default branch (non fatale)
spec = None
try:
spec = await fetch_and_parse_spec(body["gitea_repo"], body.get("default_branch") or "main")
except Exception:
spec = None
row = await db.fetchrow(
"""
INSERT INTO models (name, type, gitea_repo, default_branch, spec, created_by)
VALUES ($1,$2,$3,$4,$5,$6)
RETURNING *
""",
body["name"],
body["type"],
body["gitea_repo"],
body.get("default_branch") or "main",
spec,
user.get("username") or "unknown",
)
return _row(row)
@router.get("/{model_id}")
async def get_model(model_id: str, user=Depends(require_auth)):
row = await db.fetchrow("SELECT * FROM models WHERE id = $1", uuid.UUID(model_id))
if not row:
raise HTTPException(404, "not found")
return _row(row)
@router.patch("/{model_id}")
async def patch_model(model_id: str, body: dict, user=Depends(require_auth)):
allowed = {"name", "type", "default_branch"}
sets = []
args: list = []
for k, v in body.items():
if k in allowed:
args.append(v)
sets.append(f"{k} = ${len(args)}")
if not sets:
raise HTTPException(400, "no fields to update")
args.append(uuid.UUID(model_id))
row = await db.fetchrow(
f"UPDATE models SET {', '.join(sets)} WHERE id = ${len(args)} RETURNING *",
*args,
)
if not row:
raise HTTPException(404, "not found")
return _row(row)
@router.delete("/{model_id}", status_code=204)
async def delete_model(model_id: str, user=Depends(require_auth)):
await db.execute("DELETE FROM models WHERE id = $1", uuid.UUID(model_id))
return None
# ── Notes ──────────────────────────────────────────────────────────────────
@router.get("/{model_id}/notes")
async def list_notes(model_id: str, user=Depends(require_auth)):
rows = await db.fetch(
"SELECT id, author, text, created_at FROM model_notes WHERE model_id = $1 ORDER BY created_at DESC",
uuid.UUID(model_id),
)
return [
{
"id": str(r["id"]),
"author": r["author"],
"text": r["text"],
"created_at": r["created_at"].isoformat(),
}
for r in rows
]
@router.post("/{model_id}/notes", status_code=201)
async def add_note(model_id: str, body: dict, user=Depends(require_auth)):
text = (body.get("text") or "").strip()
if not text:
raise HTTPException(400, "text required")
row = await db.fetchrow(
"INSERT INTO model_notes (model_id, author, text) VALUES ($1, $2, $3) RETURNING *",
uuid.UUID(model_id),
user.get("username") or "unknown",
text,
)
return {
"id": str(row["id"]),
"author": row["author"],
"text": row["text"],
"created_at": row["created_at"].isoformat(),
}