"""Project persisted runtime state into the read-only supply workbench feed.""" from collections.abc import Callable from datetime import datetime, timezone from math import isfinite from typing import Any from app.domain.schemas.workbench_runtime import WorkbenchPersistedAlert _ANALYSIS_TYPE_LABELS = { "burst_analysis": "爆管分析", "burst_detection": "爆管检测", "burst_location": "爆管定位", "contaminant_analysis": "污染物分析", "contaminant_simulation": "污染物模拟", "dma_leak_identification": "DMA 漏损识别", "flushing_analysis": "冲洗分析", "pressure_regulation": "压力调控", "sensor_placement": "传感器布点", "valve_close_analysis": "关阀分析", } _ANALYSIS_STATUS_LABELS = { "queued": "排队中", "pending": "等待中", "running": "运行中", "completed": "已完成", "succeeded": "已成功", "failed": "已失败", "error": "异常", "cancelled": "已取消", } async def build_workbench_runtime_snapshot( *, analysis_runs: list[dict[str, Any]] | None = None, latest_analysis_results: list[dict[str, Any]] | None = None, latest_analysis_map_payload: dict[str, Any] | None = None, alert_storage_configured: bool = False, persisted_alerts: list[dict[str, Any]] | None = None, scenario_availability: str | None = None, artifact_availability: str | None = None, schedule_availability: str | None = None, alert_availability: str | None = None, projection_degraded: bool = False, clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc), ) -> dict[str, Any]: persisted_analysis_runs = (analysis_runs or [])[:100] scenarios = [project_analysis_scenario(run) for run in persisted_analysis_runs[:20]] latest_analysis = ( project_latest_analysis( persisted_analysis_runs[0], latest_analysis_results or [], referenced_map_payload=latest_analysis_map_payload, ) if persisted_analysis_runs else None ) stored_alerts = [ WorkbenchPersistedAlert.model_validate(item) for item in ( (persisted_alerts or [])[:100] if alert_storage_configured else [] ) ] active_stored_alerts = [ { "id": f"backend-alert-{alert.alert_id}", "condition_id": alert.condition_id, "source": alert.source.model_dump(), "title": alert.title, "description": alert.description, "occurred_at": alert.occurred_at, "severity": alert.severity, } for alert in stored_alerts if alert.status == "active" ] projected_alerts = active_stored_alerts projected_alerts.sort( key=lambda alert: _timestamp(alert["occurred_at"]), reverse=True, ) source = "none" resolved_scenario_availability = scenario_availability or ( "ready" if scenarios else "not_configured" ) resolved_artifact_availability = artifact_availability or ( "ready" if latest_analysis else "not_configured" ) resolved_schedule_availability = schedule_availability or "not_configured" resolved_work_order_availability = "not_configured" resolved_alert_availability = alert_availability or ( "ready" if alert_storage_configured else "not_configured" ) section_availabilities = ( resolved_scenario_availability, resolved_artifact_availability, resolved_schedule_availability, resolved_work_order_availability, resolved_alert_availability, ) return { "availability": ( "degraded" if projection_degraded or "degraded" in section_availabilities else "ready" if ( source != "none" or alert_storage_configured ) else "not_configured" ), "source": source, "scenario_availability": resolved_scenario_availability, "artifact_availability": resolved_artifact_availability, "schedule_availability": resolved_schedule_availability, "work_order_availability": resolved_work_order_availability, "alert_availability": resolved_alert_availability, "generated_at": clock(), "conditions": [], "alerts": projected_alerts, "scenarios": scenarios, "latest_analysis": latest_analysis, "work_orders": [], } def project_analysis_scenario(run: dict[str, Any]) -> dict[str, Any]: run_id = str(run["run_id"]) run_type = str(run.get("run_type") or "analysis") run_status = str(run.get("status") or "unknown").lower() type_label = _analysis_type_label(run_type) return { "id": f"analysis-run-{run_id}", "run_id": run_id, "name": str(run.get("name") or type_label), "description": ( f"{type_label} · {_analysis_status_label(run_status)} · " f"创建人 {run.get('created_by') or 'unknown'}" ), "status": _analysis_scenario_state(run_status), "run_type": run_type, "run_type_label": type_label, "run_status": run_status, "created_at": run["created_at"], } def project_analysis_history_item(run: dict[str, Any]) -> dict[str, Any]: run_type = str(run.get("run_type") or "analysis") run_status = str(run.get("status") or "unknown").lower() created_at = run["created_at"] return { "run_id": str(run["run_id"]), "name": str(run.get("name") or _analysis_type_label(run_type)), "run_type": run_type, "run_type_label": _analysis_type_label(run_type), "status": run_status, "status_label": _analysis_status_label(run_status), "created_by": str(run.get("created_by") or "unknown"), "created_at": created_at, "started_at": run.get("started_at") or created_at, "has_results": bool(run.get("has_results")), } def project_latest_analysis( run: dict[str, Any], results: list[dict[str, Any]], *, referenced_map_payload: dict[str, Any] | None = None, ) -> dict[str, Any]: run_type = str(run.get("run_type") or "analysis") run_status = str(run.get("status") or "unknown").lower() created_at = run["created_at"] return { "run_id": str(run["run_id"]), "name": str(run.get("name") or _analysis_type_label(run_type)), "run_type": run_type, "run_type_label": _analysis_type_label(run_type), "status": run_status, "status_label": _analysis_status_label(run_status), "created_by": str(run.get("created_by") or "unknown"), "created_at": created_at, "started_at": run.get("started_at") or created_at, "results": [ { "result_type": str(result.get("result_type") or "analysis_result"), "created_at": result.get("created_at") or created_at, "facts": _analysis_result_facts(result), } for result in results ], "map_payload": _analysis_map_payload(results) or referenced_map_payload, } def sensor_location_ids_for_map_projection( results: list[dict[str, Any]], ) -> list[str]: if _analysis_map_payload(results) is not None: return [] for result in reversed(results): if result.get("result_type") != "sensor_placement": continue payload = result.get("payload") if not isinstance(payload, dict): continue locations = _non_empty_text_list(payload.get("sensor_locations")) if locations: return list(dict.fromkeys(locations))[:500] return [] def project_sensor_location_map_payload( node_ids: list[str], map_points: list[dict[str, Any]] ) -> dict[str, Any] | None: points_by_id = { str(point.get("node_id")): point for point in map_points if point.get("node_id") is not None } features: list[dict[str, Any]] = [] for position, node_id in enumerate(node_ids, start=1): point = points_by_id.get(node_id) if point is None: continue longitude = point.get("longitude") latitude = point.get("latitude") if ( isinstance(longitude, bool) or isinstance(latitude, bool) or not isinstance(longitude, (int, float)) or not isinstance(latitude, (int, float)) or not isfinite(float(longitude)) or not isfinite(float(latitude)) ): continue features.append({ "type": "Feature", "id": node_id, "properties": { "label": f"监测点 {position} · {node_id}", "node_id": node_id, "result_type": "sensor_placement", "position": position, }, "geometry": { "type": "Point", "coordinates": [float(longitude), float(latitude)], }, }) if not features: return None return {"type": "FeatureCollection", "features": features} def _analysis_result_facts(result: dict[str, Any]) -> list[dict[str, str]]: facts: list[dict[str, str]] = [] node_id = _non_empty_text(result.get("node_id")) link_id = _non_empty_text(result.get("link_id")) if node_id: facts.append({"label": "关联节点", "value": node_id}) if link_id: facts.append({"label": "关联管段", "value": link_id}) payload = result.get("payload") if not isinstance(payload, dict): return facts result_type = str(result.get("result_type") or "") if result_type == "sensor_placement": locations = _non_empty_text_list(payload.get("sensor_locations")) sensor_count = _finite_number_text(payload.get("sensor_number")) if sensor_count is None and locations: sensor_count = str(len(locations)) if sensor_count is not None: facts.append({"label": "测点数量", "value": sensor_count}) minimum_diameter = _finite_number_text(payload.get("minimum_diameter")) if minimum_diameter is not None: facts.append({"label": "最小管径参数", "value": minimum_diameter}) if locations: facts.append({ "label": "测点位置", "value": _summarize_identifiers(locations), }) elif result_type == "leakage_identification": run_status = _non_empty_text(payload.get("run_status")) if run_status: facts.append({"label": "结果状态", "value": run_status}) sensor_nodes = _non_empty_text_list(payload.get("sensor_nodes")) if sensor_nodes: facts.append({"label": "传感器节点数", "value": str(len(sensor_nodes))}) rows = payload.get("rows") if isinstance(rows, list): facts.append({"label": "结果行数", "value": str(len(rows))}) areas = payload.get("areas") if isinstance(areas, list): facts.append({"label": "分区数量", "value": str(len(areas))}) return facts def _non_empty_text(value: object) -> str | None: if not isinstance(value, str): return None normalized = value.strip() return normalized or None def _non_empty_text_list(value: object) -> list[str]: if not isinstance(value, list): return [] return [text for item in value if (text := _non_empty_text(item)) is not None] def _finite_number_text(value: object) -> str | None: if isinstance(value, bool) or not isinstance(value, (int, float)): return None if isinstance(value, float) and not isfinite(value): return None return str(value) def _summarize_identifiers(values: list[str], limit: int = 12) -> str: visible = values[:limit] suffix = f" 等 {len(values)} 个" if len(values) > limit else "" return "、".join(visible) + suffix def _analysis_map_payload( results: list[dict[str, Any]], ) -> dict[str, Any] | None: for result in reversed(results): payload = result.get("payload") if not isinstance(payload, dict): continue candidate = payload.get("drawing_payload") if not isinstance(candidate, dict): candidate = payload if ( candidate.get("type") == "FeatureCollection" and isinstance(candidate.get("features"), list) and candidate["features"] ): return candidate return None def _analysis_type_label(run_type: str) -> str: return _ANALYSIS_TYPE_LABELS.get(run_type, run_type) def _analysis_status_label(status: str) -> str: return _ANALYSIS_STATUS_LABELS.get(status, status) def _analysis_scenario_state(status: str) -> str: if status == "running": return "active" if status in {"completed", "succeeded", "failed", "error"}: return "review" return "draft" def _timestamp(value: datetime | str) -> float: parsed = ( value if isinstance(value, datetime) else datetime.fromisoformat(value.replace("Z", "+00:00")) ) return parsed.timestamp()