Add production diagnostics and planning features
This commit is contained in:
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.actuators.cache_db import DashboardCache
|
||||
from app.actuators.lifecycle import ActuatorReconciliationService
|
||||
from app.actuators.models import ActuatorRecord, AnomalyEvent, ReconciliationState, SensorWeightGroup
|
||||
from app.actuators.models import ActuatorRecord, AnomalyEvent, FeedbackKind, ReconciliationState, SensorWeightGroup
|
||||
from app.actuators.models import JobQueueItem, JobQueueState, JobStatus, SafetyProfile
|
||||
from app.actuators.store import ActuatorStore
|
||||
from app.behavior.engine import BehaviorEngine
|
||||
@@ -55,6 +55,29 @@ class WeightOverrideRequest(BaseModel):
|
||||
class FeedbackRequest(BaseModel):
|
||||
correct: bool
|
||||
expected_state: str | None = Field(default=None, max_length=100)
|
||||
kind: FeedbackKind | None = None
|
||||
|
||||
|
||||
class DryRunRequest(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class BackupPayload(BaseModel):
|
||||
exported_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
records: list[ActuatorRecord] = Field(default_factory=list)
|
||||
reconciliation: ReconciliationState = Field(default_factory=ReconciliationState)
|
||||
jobs: JobQueueState = Field(default_factory=JobQueueState)
|
||||
|
||||
|
||||
class RestoreRequest(BaseModel):
|
||||
backup: BackupPayload
|
||||
replace_existing: bool = False
|
||||
|
||||
|
||||
class RestoreResult(BaseModel):
|
||||
restored_records: int = 0
|
||||
skipped_existing: int = 0
|
||||
restored_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class SafetyProfileRequest(BaseModel):
|
||||
@@ -406,6 +429,48 @@ def list_anomalies(request: Request) -> list[AnomalyOverview]:
|
||||
return overview
|
||||
|
||||
|
||||
@router.get("/backup/export", response_model=BackupPayload)
|
||||
def export_backup(request: Request) -> BackupPayload:
|
||||
store = getattr(request.app.state, "actuator_store", None)
|
||||
if not isinstance(store, ActuatorStore):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Actuator Store nicht initialisiert.",
|
||||
)
|
||||
return BackupPayload(
|
||||
records=store.list(),
|
||||
reconciliation=store.load_reconciliation_state(),
|
||||
jobs=store.load_job_queue(),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/backup/restore", response_model=RestoreResult)
|
||||
def restore_backup(payload: RestoreRequest, request: Request) -> RestoreResult:
|
||||
store = getattr(request.app.state, "actuator_store", None)
|
||||
if not isinstance(store, ActuatorStore):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Actuator Store nicht initialisiert.",
|
||||
)
|
||||
existing_ids = {record.actuator_entity_id for record in store.list()}
|
||||
restored = 0
|
||||
skipped = 0
|
||||
for record in payload.backup.records:
|
||||
if record.actuator_entity_id in existing_ids and not payload.replace_existing:
|
||||
skipped += 1
|
||||
continue
|
||||
store.upsert(record)
|
||||
restored += 1
|
||||
store.save_reconciliation_state(payload.backup.reconciliation)
|
||||
store.save_job_queue(payload.backup.jobs)
|
||||
return RestoreResult(restored_records=restored, skipped_existing=skipped)
|
||||
|
||||
|
||||
@router.post("/planning/refresh", response_model=list[ActuatorRecord])
|
||||
def refresh_planning_insights(request: Request) -> list[ActuatorRecord]:
|
||||
return _behavior(request).refresh_planning_insights()
|
||||
|
||||
|
||||
@router.get("", response_model=list[ActuatorRecord])
|
||||
def list_configured(request: Request) -> list[ActuatorRecord]:
|
||||
return _service(request).list_configured()
|
||||
@@ -513,11 +578,24 @@ def record_feedback(
|
||||
actuator_entity_id,
|
||||
correct=payload.correct,
|
||||
expected_state=payload.expected_state,
|
||||
kind=payload.kind,
|
||||
)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{actuator_entity_id}/dry-run", response_model=ActuatorRecord)
|
||||
def set_dry_run(
|
||||
actuator_entity_id: str,
|
||||
payload: DryRunRequest,
|
||||
request: Request,
|
||||
) -> ActuatorRecord:
|
||||
try:
|
||||
return _behavior(request).set_dry_run(actuator_entity_id, enabled=payload.enabled)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{actuator_entity_id}/safety", response_model=ActuatorRecord)
|
||||
def set_safety_profile(
|
||||
actuator_entity_id: str,
|
||||
|
||||
Reference in New Issue
Block a user