SQLite-Ereignisse und Personenprofile vorbereiten

This commit is contained in:
marcinator_2 committed 2026-10-01 03:50:26 +02:00
1 parent 643146599d
commit f15ab1d399
9 files changed
+637 -21

No files matched your search

+85 -5
View File
@@ -1,25 +1,38 @@
"""Lokale WebUI und MJPEG-Livebild; alle Aktoren bleiben simuliert."""
from contextlib import asynccontextmanager
from contextlib import asynccontextmanager, suppress
from pathlib import Path
import asyncio
import logging
import os
import sqlite3
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi import Depends, FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, Response, StreamingResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from .camera import CameraStream
from .controller import Controller, MODES
from .event_store import EventStore
PAGE = (Path(__file__).parent / "index.html").read_text(encoding="utf-8")
PROFILE_CLEANUP_INTERVAL_SECONDS = 3600
LOGGER = logging.getLogger(__name__)
class ModeInput(BaseModel):
mode: str
class ProfileInput(BaseModel):
label: str = Field(min_length=1, max_length=80)
class ProfileAssignment(BaseModel):
profile_id: int | None
async def require_ui(request: Request):
if request.headers.get("X-Halloween-UI") != "1":
raise HTTPException(status_code=403, detail="UI-Anfrage erforderlich")
@@ -29,16 +42,32 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
directory = state_dir or Path(os.environ.get("HALLOWEEN_STATE_DIR", "data"))
camera = CameraStream(demo=demo)
controller = Controller(directory)
events = EventStore(directory)
@asynccontextmanager
async def lifespan(app: FastAPI):
async def periodic_profile_cleanup():
while True:
await asyncio.sleep(PROFILE_CLEANUP_INTERVAL_SECONDS)
try:
events.cleanup_expired()
except sqlite3.Error:
LOGGER.exception("Abgelaufene Profile konnten nicht gelöscht werden")
camera.start()
yield
camera.stop()
cleanup_task = asyncio.create_task(periodic_profile_cleanup())
try:
yield
finally:
cleanup_task.cancel()
with suppress(asyncio.CancelledError):
await cleanup_task
camera.stop()
app = FastAPI(title="Halloween-Grab", lifespan=lifespan)
app.state.camera = camera
app.state.controller = controller
app.state.events = events
@app.middleware("http")
async def security_headers(request: Request, call_next):
@@ -57,6 +86,49 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
async def status():
return {"controller": controller.status(), "camera": camera.status()}
@app.get("/api/events")
async def recent_events(limit: int = Query(20, ge=1, le=100)):
try:
return {"events": events.recent(limit)}
except sqlite3.Error as exc:
raise HTTPException(status_code=503, detail=f"Datenbank nicht verfügbar: {exc}") from exc
@app.get("/api/profiles")
async def list_profiles():
try:
return {"profiles": events.profiles()}
except sqlite3.Error as exc:
raise HTTPException(status_code=503, detail=f"Datenbank nicht verfügbar: {exc}") from exc
@app.post("/api/profiles", dependencies=[Depends(require_ui)])
async def create_profile(payload: ProfileInput):
try:
return {"profile": events.create_profile(payload.label)}
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except sqlite3.Error as exc:
raise HTTPException(status_code=503, detail=f"Profil nicht gespeichert: {exc}") from exc
@app.patch("/api/events/{event_id}/profile", dependencies=[Depends(require_ui)])
async def assign_profile(event_id: int, payload: ProfileAssignment):
try:
events.assign_profile(event_id, payload.profile_id)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc.args[0])) from exc
except sqlite3.Error as exc:
raise HTTPException(status_code=503, detail=f"Zuordnung nicht gespeichert: {exc}") from exc
return {"updated": True}
@app.delete("/api/profiles/{profile_id}", dependencies=[Depends(require_ui)])
async def delete_profile(profile_id: int):
try:
deleted = events.delete_profile(profile_id)
except sqlite3.Error as exc:
raise HTTPException(status_code=503, detail=f"Profil nicht gelöscht: {exc}") from exc
if not deleted:
raise HTTPException(status_code=404, detail="Profil nicht gefunden")
return {"deleted": True}
@app.post("/api/mode", dependencies=[Depends(require_ui)])
async def set_mode(payload: ModeInput):
if payload.mode not in MODES:
@@ -75,6 +147,14 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
@app.post("/api/test-trigger", dependencies=[Depends(require_ui)])
async def test_trigger():
accepted, reason = controller.trigger()
if accepted:
try:
events.record_test_trigger(controller.mode)
except sqlite3.Error as exc:
raise HTTPException(
status_code=503,
detail=f"Test gestartet, aber Ereignis nicht gespeichert: {exc}",
) from exc
return {"accepted": accepted, "reason": reason, "controller": controller.status()}
@app.post("/api/stop", dependencies=[Depends(require_ui)])