Two new endpoints. GET /api/v1/search returns bounded typed results (vehicle, booking, data-quality-issue, application section) instead of the frontend guessing routes from regex patterns against public-ref prefixes; data-quality and manager-only sections are filtered server-side by role, and customers are deliberately never returned since no customer detail route exists in this PoC. GET /api/v1/integrations/status aggregates outbox delivery counts (pending/delivering/succeeded/failed) into a single truthful n8n state (disabled/unavailable/degraded/operational/no_evidence) instead of the UI showing whichever status the single most recent event happened to be in -- a vehicle_status_conflict-style bug where one stale failure or one lucky success could misreport the dispatcher's actual health. Also fixes a real config gap this surfaced: MCP_HUB_REGISTRATION_ENABLED was documented in .env.example but had no corresponding Settings field, so it was silently ignored by pydantic-settings' extra="ignore" and never actually read anywhere in the codebase.
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_db, require_operations_manager
|
|
from app.core.config import get_settings
|
|
from app.models.outbox import OutboxEvent
|
|
from app.schemas import (
|
|
CurrentUser,
|
|
IntegrationStatusOut,
|
|
McpHubIntegrationStatus,
|
|
N8nIntegrationStatus,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v1/integrations", tags=["integrations"])
|
|
settings = get_settings()
|
|
|
|
|
|
def _n8n_status(db: Session) -> N8nIntegrationStatus:
|
|
counts: dict[str, int] = dict(
|
|
db.execute(
|
|
select(OutboxEvent.delivery_status, func.count()).group_by(OutboxEvent.delivery_status)
|
|
).all() # type: ignore[arg-type]
|
|
)
|
|
pending = counts.get("pending", 0)
|
|
delivering = counts.get("delivering", 0)
|
|
failed = counts.get("failed", 0)
|
|
succeeded = counts.get("succeeded", 0)
|
|
|
|
latest_success_at = db.scalar(
|
|
select(func.max(OutboxEvent.updated_at)).where(OutboxEvent.delivery_status == "succeeded")
|
|
)
|
|
latest_failure_at = db.scalar(
|
|
select(func.max(OutboxEvent.updated_at)).where(OutboxEvent.delivery_status == "failed")
|
|
)
|
|
|
|
state: Literal["disabled", "unavailable", "degraded", "operational", "no_evidence"]
|
|
if not settings.n8n_dispatch_enabled:
|
|
state = "disabled"
|
|
elif failed > 0 and succeeded == 0:
|
|
state = "unavailable"
|
|
elif failed > 0:
|
|
state = "degraded"
|
|
elif succeeded > 0 or pending > 0 or delivering > 0:
|
|
state = "operational"
|
|
else:
|
|
state = "no_evidence"
|
|
|
|
return N8nIntegrationStatus(
|
|
configured=bool(settings.n8n_webhook_url),
|
|
dispatch_enabled=settings.n8n_dispatch_enabled,
|
|
state=state,
|
|
pending=pending,
|
|
delivering=delivering,
|
|
failed=failed,
|
|
succeeded=succeeded,
|
|
latest_success_at=latest_success_at,
|
|
latest_failure_at=latest_failure_at,
|
|
)
|
|
|
|
|
|
@router.get("/status", response_model=IntegrationStatusOut)
|
|
def integration_status(
|
|
db: Session = Depends(get_db),
|
|
_user: CurrentUser = Depends(require_operations_manager),
|
|
) -> IntegrationStatusOut:
|
|
return IntegrationStatusOut(
|
|
n8n=_n8n_status(db),
|
|
mcp_hub=McpHubIntegrationStatus(
|
|
registration_enabled=settings.mcp_hub_registration_enabled,
|
|
state="configured" if settings.mcp_hub_registration_enabled else "not_configured",
|
|
),
|
|
)
|