from typing import Optional from fastapi import APIRouter, Body, Header from app.schemas.common import ApiResponse from app.services.build_info_service import get_runtime_build_info from app.services.cluster_runtime_service import get_cluster_snapshot from app.services.debug_event_service import get_debug_diagnosis, get_debug_event_overview, get_debug_handoff_report, ingest_debug_event, list_debug_events from app.services.detect_job_service import get_detect_queue_health from app.services.runtime_control_service import runtime_action from app.services.runtime_status_service import get_runtime_preflight, get_runtime_status from app.services.sync_push_service import ( ack_detect_task_projection, export_detect_task_projection, ingest_runtime_projection, ) from app.services.sync_record_service import get_sync_summary, list_sync_records router = APIRouter(tags=["runtime"]) def _filter_debug_handover_by_node_code(payload: dict, node_code: str) -> dict: normalized_node_code = str(node_code or "").strip() if not normalized_node_code: return dict(payload or {}) normalized_payload = dict(payload or {}) def _matches_node(item: object) -> bool: if not isinstance(item, dict): return False return str(item.get("node_code") or "").strip() == normalized_node_code overview = dict(normalized_payload.get("overview") or {}) if overview: overview["recent_issues"] = [ item for item in list(overview.get("recent_issues") or []) if _matches_node(item) ] normalized_payload["overview"] = overview normalized_payload["recent_issues"] = [ item for item in list(normalized_payload.get("recent_issues") or []) if _matches_node(item) ] normalized_payload["issue_groups"] = [ item for item in list(normalized_payload.get("issue_groups") or []) if _matches_node(item) ] failure_handoff = dict(normalized_payload.get("failure_handoff") or {}) if failure_handoff: failure_handoff["recent_issues"] = [ item for item in list(failure_handoff.get("recent_issues") or []) if _matches_node(item) ] failure_handoff["issue_groups"] = [ item for item in list(failure_handoff.get("issue_groups") or []) if _matches_node(item) ] normalized_payload["failure_handoff"] = failure_handoff return normalized_payload @router.get("/runtime/status", response_model=ApiResponse) def runtime_status() -> ApiResponse: return ApiResponse(data=get_runtime_status()) @router.get("/runtime/readiness", response_model=ApiResponse) def runtime_readiness() -> ApiResponse: return ApiResponse(data=(get_runtime_status().get("readiness") or {})) @router.get("/runtime/preflight", response_model=ApiResponse) def runtime_preflight() -> ApiResponse: return ApiResponse(data=get_runtime_preflight()) @router.get("/runtime/cluster", response_model=ApiResponse) def runtime_cluster() -> ApiResponse: return ApiResponse(data=get_cluster_snapshot()) @router.get("/runtime/build-info", response_model=ApiResponse) def runtime_build_info() -> ApiResponse: return ApiResponse(data=get_runtime_build_info()) @router.get("/runtime/sync-summary", response_model=ApiResponse) def runtime_sync_summary() -> ApiResponse: return ApiResponse(data=get_sync_summary()) @router.get("/runtime/sync-records", response_model=ApiResponse) def runtime_sync_records(limit: int = 20) -> ApiResponse: return ApiResponse(data={"records": list_sync_records(limit=limit)}) @router.get("/runtime/debug-events", response_model=ApiResponse) def runtime_debug_events( limit: int = 50, service: Optional[str] = None, event_type: Optional[str] = None, source_region: Optional[str] = None, node_code: Optional[str] = None, level: Optional[str] = None, before_id: Optional[int] = None, after_id: Optional[int] = None, created_after: Optional[str] = None, ) -> ApiResponse: return ApiResponse( data=list_debug_events( limit=limit, service=service, event_type=event_type, source_region=source_region, node_code=node_code, level=level, before_id=before_id, after_id=after_id, created_after=created_after, ) ) @router.get("/runtime/debug-overview", response_model=ApiResponse) def runtime_debug_overview(window_minutes: int = 10, source_region: Optional[str] = None) -> ApiResponse: return ApiResponse(data=get_debug_event_overview(window_minutes=window_minutes, source_region=source_region)) @router.get("/runtime/debug-diagnosis", response_model=ApiResponse) def runtime_debug_diagnosis(window_minutes: int = 10, source_region: Optional[str] = None) -> ApiResponse: sync_summary = get_sync_summary() return ApiResponse(data=get_debug_diagnosis(window_minutes=window_minutes, source_region=source_region, sync_summary=sync_summary)) @router.get("/runtime/debug-handoff", response_model=ApiResponse) def runtime_debug_handoff(window_minutes: int = 10, source_region: Optional[str] = None) -> ApiResponse: sync_summary = get_sync_summary() runtime_status_payload = get_runtime_status() return ApiResponse( data=get_debug_handoff_report( window_minutes=window_minutes, source_region=source_region, sync_summary=sync_summary, readiness=runtime_status_payload.get("readiness") or {}, ) ) @router.get("/runtime/queue-health", response_model=ApiResponse) def runtime_queue_health(window_minutes: int = 15) -> ApiResponse: return ApiResponse(data=get_detect_queue_health(window_minutes=window_minutes)) @router.get("/runtime/health-handover", response_model=ApiResponse) def runtime_health_handover( node_code: Optional[str] = None, window_minutes: int = 10, source_region: Optional[str] = None, ) -> ApiResponse: sync_summary = get_sync_summary() runtime_status_payload = get_runtime_status() data = get_debug_handoff_report( window_minutes=window_minutes, source_region=source_region, sync_summary=sync_summary, readiness=runtime_status_payload.get("readiness") or {}, ) normalized_node_code = str(node_code or "").strip() if normalized_node_code: data = _filter_debug_handover_by_node_code(data, normalized_node_code) return ApiResponse(data=data) @router.post("/runtime/sync-ingest", response_model=ApiResponse) def runtime_sync_ingest(payload: dict, x_domaincheck_sync_token: Optional[str] = Header(default=None)) -> ApiResponse: ok, message, data = ingest_runtime_projection(payload, shared_token=x_domaincheck_sync_token) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.get("/runtime/task-export", response_model=ApiResponse) def runtime_task_export(limit: int = 1000, x_domaincheck_sync_token: Optional[str] = Header(default=None)) -> ApiResponse: ok, message, data = export_detect_task_projection(limit=limit, shared_token=x_domaincheck_sync_token) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/runtime/task-ack", response_model=ApiResponse) def runtime_task_ack(payload: dict, x_domaincheck_sync_token: Optional[str] = Header(default=None)) -> ApiResponse: ok, message, data = ack_detect_task_projection(payload, shared_token=x_domaincheck_sync_token) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/runtime/debug-ingest", response_model=ApiResponse) def runtime_debug_ingest(payload: dict, x_domaincheck_sync_token: Optional[str] = Header(default=None)) -> ApiResponse: ok, message, data = ingest_debug_event(payload, shared_token=x_domaincheck_sync_token) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/runtime/actions/{action}", response_model=ApiResponse) def runtime_action_trigger(action: str, payload: Optional[dict] = Body(default=None)) -> ApiResponse: ok, message, data = runtime_action(action, payload=payload) return ApiResponse(code=0 if ok else 1, message=message, data=data)