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).
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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
|
||||
Reference in New Issue
Block a user