Files
MobilityOps/backend/app/services/operations.py
T
NuklearRabbit c5b7e21f81 M6: implement ITWorx MCP Hub publication
Four read-only, service-token-protected MCP provider endpoints (operations summary, attention vehicles, vehicle details, knowledge search facade). Shared-secret auth reusing the M4 callback pattern. Service-request audit trail for every call. Extracted shared operations-summary logic out of the dashboard router to avoid duplicating retrieval logic. 66 backend tests passing, ruff clean. Verified all four endpoints and audit trail directly via curl against the live stack (no live MCP Hub instance available in this environment).
2026-08-01 23:05:04 +02:00

78 lines
2.8 KiB
Python

from __future__ import annotations
from datetime import date
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.models.data_quality import DataQualityIssue
from app.models.outbox import OutboxEvent
from app.models.vehicle import Vehicle
from app.schemas import DashboardMetrics
SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2}
def compute_metrics(db: Session) -> DashboardMetrics:
"""Shared operations-summary computation used by both the dashboard and the MCP
provider API, so the two never drift out of sync with two copies of the same query."""
status_counts = dict(
db.execute(
select(Vehicle.operational_status, func.count()).group_by(Vehicle.operational_status)
).all()
)
open_issues = db.scalar(
select(func.count()).select_from(DataQualityIssue).where(DataQualityIssue.status == "open")
)
pending_or_failed = db.scalar(
select(func.count())
.select_from(OutboxEvent)
.where(OutboxEvent.delivery_status.in_(["pending", "failed"]))
)
return DashboardMetrics(
available=status_counts.get("available", 0),
rented=status_counts.get("rented", 0),
cleaning=status_counts.get("cleaning", 0),
maintenance=status_counts.get("maintenance", 0),
blocked=status_counts.get("blocked", 0),
open_quality_issues=open_issues or 0,
pending_or_failed_workflows=pending_or_failed or 0,
)
def list_attention_vehicles(
db: Session, minimum_severity: str = "medium", on_or_before: date | None = None, limit: int = 20
) -> list[dict]:
max_rank = SEVERITY_ORDER.get(minimum_severity, 1)
stmt = (
select(DataQualityIssue)
.where(DataQualityIssue.entity_type == "vehicle", DataQualityIssue.status == "open")
.order_by(DataQualityIssue.detected_at.asc())
)
if on_or_before is not None:
stmt = stmt.where(func.date(DataQualityIssue.detected_at) <= on_or_before)
issues = db.scalars(stmt).all()
filtered = [i for i in issues if SEVERITY_ORDER.get(i.severity, 3) <= max_rank]
filtered.sort(key=lambda i: SEVERITY_ORDER.get(i.severity, 3))
vehicle_ids = {i.entity_id for i in filtered}
vehicles_by_id = {
v.id: v for v in db.scalars(select(Vehicle).where(Vehicle.id.in_(vehicle_ids))).all()
}
results = []
for issue in filtered[:limit]:
vehicle = vehicles_by_id.get(issue.entity_id)
if vehicle is None:
continue
results.append(
{
"vehicle_ref": vehicle.public_ref,
"severity": issue.severity,
"rule_type": issue.rule_type,
"summary": issue.evidence_json.get("summary", ""),
"detected_at": issue.detected_at,
}
)
return results