Ran a dedicated post-M7 release-readiness audit. Found and fixed the one real gap: mypy was a declared dev dependency but had never been run in any milestone's validation loop. Fixed all 43 pre-existing type errors it surfaced, including two genuine defensive- programming gaps (unguarded Optional vehicle/customer lookups that could have crashed with unhandled 500s instead of clean 404/401 responses) rather than suppressing them. make lint now runs ruff + mypy; mypy reports zero errors across 44 source files. Re-verified end to end against a genuinely wiped-volumes clean checkout: automatic migrations, deterministic seed, 66/66 backend tests, and the full user-journey matrix (login, dashboard, vehicle/booking detail, return workflow, invalid-mileage rejection, data-quality review, duplicate-customer merge, audit trail, Knowledge Assistant, n8n, MCP Hub) via curl and Playwright. Live-verified both external-dependency degraded modes, not just unit tests: stopped n8n mid-flow and confirmed a return still commits with the outbox event staying pending and retrying with backoff, then self-healing to succeeded with zero manual intervention once n8n came back; verified RAGcore's unavailable-degradation path against an unreachable host. Added frontend/e2e/interactive-elements.spec.ts (11 tests covering every nav item, filter, tab, and role boundary) alongside the existing demo script test — 12/12 e2e tests passing. Verified no secrets are committed (.env never tracked, clean git history scan) and .env.example covers every operator-configurable setting. Confirmed no placeholders, TODOs, fake responses, hardcoded metrics, or dead routes anywhere in the codebase. Updated README.md with an honest integration-status section and PROJECT_STATE.md with the full audit findings. Added artifacts/final-acceptance/summary.md as the authoritative final evidence document (commands, results, URLs, demo access, integration status per external dependency, known limitations, deployment instructions, five-minute demo flow).
78 lines
2.8 KiB
Python
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[str, int] = dict(
|
|
db.execute(
|
|
select(Vehicle.operational_status, func.count()).group_by(Vehicle.operational_status)
|
|
).all() # type: ignore[arg-type]
|
|
)
|
|
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
|