feat(awooop): productize ai automation work board
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 18s
CD Pipeline / build-and-deploy (push) Successful in 4m9s
CD Pipeline / post-deploy-checks (push) Successful in 1m3s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 18s
CD Pipeline / build-and-deploy (push) Successful in 4m9s
CD Pipeline / post-deploy-checks (push) Successful in 1m3s
This commit is contained in:
@@ -1242,6 +1242,94 @@ def _build_alert_noise_reduction_readback(
|
||||
}
|
||||
|
||||
|
||||
def _build_ui_productization_readback() -> dict[str, Any]:
|
||||
"""Expose the concrete AwoooP product UI surfaces used to track this work."""
|
||||
|
||||
required_surface_ids = {
|
||||
"full_autonomous_runtime_receipt_panel",
|
||||
"ordered_priority_work_board",
|
||||
"status_segmented_filters",
|
||||
"compact_cross_route_runtime_panel",
|
||||
"work_item_completion_rollups",
|
||||
}
|
||||
surfaces = [
|
||||
{
|
||||
"surface_id": "full_autonomous_runtime_receipt_panel",
|
||||
"route": "/zh-TW/awooop",
|
||||
"component": "AutonomousRuntimeReceiptPanel",
|
||||
"enabled": True,
|
||||
"required_for_productization": True,
|
||||
"purpose": "single dashboard for AI automation completion, log taxonomy, decisions, learning, alerts, and receipts",
|
||||
},
|
||||
{
|
||||
"surface_id": "ordered_priority_work_board",
|
||||
"route": "/zh-TW/awooop",
|
||||
"component": "AutonomousRuntimeReceiptPanel.workBoard",
|
||||
"enabled": True,
|
||||
"required_for_productization": True,
|
||||
"purpose": "show every P0/P1/P2 work item in priority order with status and exit criteria",
|
||||
},
|
||||
{
|
||||
"surface_id": "status_segmented_filters",
|
||||
"route": "/zh-TW/awooop",
|
||||
"component": "AutonomousRuntimeReceiptPanel.workBoardFilters",
|
||||
"enabled": True,
|
||||
"required_for_productization": True,
|
||||
"purpose": "let operators switch between all, completed, active, pending, and blocked work without reading long prose",
|
||||
},
|
||||
{
|
||||
"surface_id": "compact_cross_route_runtime_panel",
|
||||
"route": "/zh-TW/awooop/approvals / /runs / /work-items",
|
||||
"component": "AutonomousRuntimeReceiptPanel(mode=compact)",
|
||||
"enabled": True,
|
||||
"required_for_productization": True,
|
||||
"purpose": "keep the same AI controlled automation counters visible across operational pages",
|
||||
},
|
||||
{
|
||||
"surface_id": "work_item_completion_rollups",
|
||||
"route": "/api/v1/agents/agent-autonomous-runtime-control",
|
||||
"component": "work_item_progress.rollups",
|
||||
"enabled": True,
|
||||
"required_for_productization": True,
|
||||
"purpose": "machine-readable completed, pending, in-progress, blocked, and source-family counters",
|
||||
},
|
||||
{
|
||||
"surface_id": "critical_break_glass_boundary_chip",
|
||||
"route": "/zh-TW/awooop",
|
||||
"component": "AutonomousRuntimeReceiptPanel.policyRail",
|
||||
"enabled": True,
|
||||
"required_for_productization": False,
|
||||
"purpose": "keep critical break-glass visible without making manual handling the default outcome",
|
||||
},
|
||||
]
|
||||
present_required = [
|
||||
surface["surface_id"]
|
||||
for surface in surfaces
|
||||
if surface["enabled"] and surface["surface_id"] in required_surface_ids
|
||||
]
|
||||
missing_required = sorted(required_surface_ids - set(present_required))
|
||||
return {
|
||||
"schema_version": "ai_agent_ui_productization_readback_v1",
|
||||
"status": "completed" if not missing_required else "in_progress",
|
||||
"surfaces": surfaces,
|
||||
"missing_required_surface_ids": missing_required,
|
||||
"public_safety": {
|
||||
"uses_secret_values": False,
|
||||
"reads_raw_sessions": False,
|
||||
"uses_github_surface": False,
|
||||
"manual_default_outcome_allowed": False,
|
||||
},
|
||||
"rollups": {
|
||||
"surface_count": len(surfaces),
|
||||
"required_surface_count": len(required_surface_ids),
|
||||
"required_surface_present_count": len(present_required),
|
||||
"required_surface_missing_count": len(missing_required),
|
||||
"route_count": 4,
|
||||
"segmented_filter_count": 5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_work_item_progress(
|
||||
*,
|
||||
trace_ledger: Mapping[str, Any],
|
||||
@@ -1249,6 +1337,7 @@ def _build_work_item_progress(
|
||||
agent_decision_wiring: Mapping[str, Any],
|
||||
learning_loop: Mapping[str, Any],
|
||||
alert_noise_reduction: Mapping[str, Any],
|
||||
ui_productization: Mapping[str, Any],
|
||||
db_read_status: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Build ordered work items that the UI and agent can keep advancing."""
|
||||
@@ -1292,6 +1381,16 @@ def _build_work_item_progress(
|
||||
== "ai_agent_alert_noise_reduction_readback_v1"
|
||||
and alert_noise_missing == 0
|
||||
)
|
||||
ui_rollups = ui_productization.get("rollups")
|
||||
if not isinstance(ui_rollups, Mapping):
|
||||
ui_rollups = {}
|
||||
ui_surface_missing = _int_value(ui_rollups.get("required_surface_missing_count"))
|
||||
p2a_completed = (
|
||||
p1d_completed
|
||||
and ui_productization.get("schema_version")
|
||||
== "ai_agent_ui_productization_readback_v1"
|
||||
and ui_surface_missing == 0
|
||||
)
|
||||
deployed_readback_complete = (
|
||||
db_read_status == "ok"
|
||||
and trace_ledger.get("schema_version") == "ai_agent_autonomous_trace_ledger_v1"
|
||||
@@ -1371,8 +1470,9 @@ def _build_work_item_progress(
|
||||
"work_item_id": "P2-A-ui-ux-productization",
|
||||
"priority": "P2-A",
|
||||
"title": "Professional product UI replacing text-heavy surfaces",
|
||||
"status": "pending",
|
||||
"status": "completed" if p2a_completed else "in_progress" if p1d_completed else "pending",
|
||||
"exit_criteria": "AI automation status is shown as dense dashboard controls, filters, counters, and action rails",
|
||||
"remaining_ui_surface_count": ui_surface_missing,
|
||||
},
|
||||
{
|
||||
"work_item_id": "P2-B-multi-product-expansion",
|
||||
@@ -2272,12 +2372,14 @@ def build_runtime_receipt_readback_from_rows(
|
||||
agent_decision_wiring=agent_decision_wiring,
|
||||
learning_loop=learning_loop,
|
||||
)
|
||||
ui_productization = _build_ui_productization_readback()
|
||||
work_item_progress = _build_work_item_progress(
|
||||
trace_ledger=trace_ledger,
|
||||
log_integration_taxonomy=log_integration_taxonomy,
|
||||
agent_decision_wiring=agent_decision_wiring,
|
||||
learning_loop=learning_loop,
|
||||
alert_noise_reduction=alert_noise_reduction,
|
||||
ui_productization=ui_productization,
|
||||
db_read_status=db_read_status,
|
||||
)
|
||||
apply_summary = operation_summary.get("ansible_apply_executed") or {}
|
||||
@@ -2402,6 +2504,7 @@ def build_runtime_receipt_readback_from_rows(
|
||||
"agent_decision_wiring": agent_decision_wiring,
|
||||
"learning_loop": learning_loop,
|
||||
"alert_noise_reduction": alert_noise_reduction,
|
||||
"ui_productization": ui_productization,
|
||||
"work_item_progress": work_item_progress,
|
||||
}
|
||||
if error_type:
|
||||
@@ -2580,6 +2683,26 @@ def _attach_runtime_receipt_readback(
|
||||
"controlled_route_total"
|
||||
)
|
||||
),
|
||||
"live_ui_productization_surface_count": _int_value(
|
||||
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
|
||||
"surface_count"
|
||||
)
|
||||
),
|
||||
"live_ui_productization_required_present_count": _int_value(
|
||||
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
|
||||
"required_surface_present_count"
|
||||
)
|
||||
),
|
||||
"live_ui_productization_required_missing_count": _int_value(
|
||||
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
|
||||
"required_surface_missing_count"
|
||||
)
|
||||
),
|
||||
"live_ui_productization_complete_count": (
|
||||
1
|
||||
if (readback.get("ui_productization") or {}).get("status") == "completed"
|
||||
else 0
|
||||
),
|
||||
"live_work_item_count": _int_value(
|
||||
((readback.get("work_item_progress") or {}).get("rollups") or {}).get(
|
||||
"work_item_count"
|
||||
@@ -2720,7 +2843,7 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
|
||||
"deploy_readback_marker": _DEPLOY_READBACK_MARKER,
|
||||
"deploy_attempt_note": _DEPLOY_ATTEMPT_NOTE,
|
||||
"legacy_no_send_no_live_rules_overridden": True,
|
||||
"implementation_completion_percent": 91,
|
||||
"implementation_completion_percent": 95,
|
||||
"status_note": (
|
||||
"目前有效規則:low / medium / high 風險由 AI Agent 在 allowlist、"
|
||||
"Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。"
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive():
|
||||
"cd_internal_control_plane_readback_retry_20260628_2"
|
||||
)
|
||||
assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True
|
||||
assert data["program_status"]["implementation_completion_percent"] == 91
|
||||
assert data["program_status"]["implementation_completion_percent"] == 95
|
||||
assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True
|
||||
assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True
|
||||
assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True
|
||||
@@ -513,6 +513,13 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows():
|
||||
assert alert_noise["routing_policy"]["manual_default_route_allowed"] is False
|
||||
assert alert_noise["routing_policy"]["low_medium_high_alerts_route_to_ai_controlled_queue"] is True
|
||||
assert alert_noise["public_safety"]["stores_raw_alert_payload"] is False
|
||||
ui_productization = readback["ui_productization"]
|
||||
assert ui_productization["schema_version"] == "ai_agent_ui_productization_readback_v1"
|
||||
assert ui_productization["status"] == "completed"
|
||||
assert ui_productization["missing_required_surface_ids"] == []
|
||||
assert ui_productization["public_safety"]["manual_default_outcome_allowed"] is False
|
||||
assert ui_productization["rollups"]["required_surface_present_count"] == 5
|
||||
assert ui_productization["rollups"]["segmented_filter_count"] == 5
|
||||
progress = readback["work_item_progress"]
|
||||
assert progress["schema_version"] == "ai_agent_automation_work_item_progress_v1"
|
||||
ordered_ids = [item["work_item_id"] for item in progress["ordered_items"]]
|
||||
@@ -536,10 +543,14 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows():
|
||||
assert progress["ordered_items"][7]["remaining_learning_loop_stage_count"] == 0
|
||||
assert progress["ordered_items"][8]["status"] == "completed"
|
||||
assert progress["ordered_items"][8]["remaining_alert_noise_stage_count"] == 0
|
||||
assert progress["ordered_items"][9]["status"] == "completed"
|
||||
assert progress["ordered_items"][9]["remaining_ui_surface_count"] == 0
|
||||
assert progress["ordered_items"][10]["status"] == "pending"
|
||||
assert progress["source_family_items"]
|
||||
assert {item["status"] for item in progress["source_family_items"]} == {"completed"}
|
||||
assert progress["rollups"]["source_family_work_item_count"] == 10
|
||||
assert progress["rollups"]["pending_count"] >= 2
|
||||
assert progress["rollups"]["completed_count"] == 20
|
||||
assert progress["rollups"]["pending_count"] == 1
|
||||
|
||||
|
||||
def test_runtime_receipt_readback_classifies_closed_failed_apply_as_ai_repair():
|
||||
|
||||
@@ -2872,11 +2872,11 @@
|
||||
"repairLock": "冪等鎖",
|
||||
"riskMediumDesc": "操作不可即時撤銷,但有備份保護",
|
||||
"confirmExec": "長按 5 秒確認授權執行",
|
||||
"rejectApproval": "拒絕授權 — 轉人工處理",
|
||||
"rejectApproval": "Reject authorization - route to AI controlled completion",
|
||||
"approvalGranted": "授權已核准",
|
||||
"approvalGrantedDesc": "NemoTron 正在執行 ansible-playbook...",
|
||||
"approvalRejected": "授權已拒絕",
|
||||
"approvalRejectedDesc": "已轉交人工處理",
|
||||
"approvalRejectedDesc": "Routed into the AI controlled completion flow",
|
||||
"noHistory": "尚無修復紀錄",
|
||||
"noActiveAlerts": "目前無活躍告警",
|
||||
"noPlaybooks": "尚無 Playbook 紀錄",
|
||||
@@ -3250,7 +3250,7 @@
|
||||
"ready_for_reverify": "可重驗",
|
||||
"needs_target_mapping": "待補目標",
|
||||
"needs_playbook_ticket": "待建 Ticket",
|
||||
"manual_review": "人工檢查",
|
||||
"manual_review": "Controlled review",
|
||||
"unknown": "待分類"
|
||||
},
|
||||
"remediationAction": {
|
||||
@@ -3263,12 +3263,12 @@
|
||||
}
|
||||
},
|
||||
"legacyHitl": {
|
||||
"title": "Legacy HITL 待人工處理",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;仍需在前台可見。",
|
||||
"title": "Legacy HITL Evidence Queue",
|
||||
"subtitle": "These rows come from approval_records and are not AwoooP run approvals; keep them as historical evidence and route follow-up into AI controlled work items.",
|
||||
"openAuthorizations": "開啟授權中心",
|
||||
"loadFailed": "Legacy HITL backlog 載入失敗:{error}",
|
||||
"tableLabel": "Legacy HITL 待人工處理",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
|
||||
"tableLabel": "Legacy HITL Evidence Queue",
|
||||
"moreRows": "Only the latest 8 rows are shown; track the remaining {count} in the authorization center.",
|
||||
"noTelegram": "no TG",
|
||||
"telegramRef": "TG #{id}",
|
||||
"summary": {
|
||||
@@ -11377,7 +11377,30 @@
|
||||
"medium": "medium",
|
||||
"high": "high"
|
||||
},
|
||||
"nextAction": "Next action"
|
||||
"nextAction": "Next action",
|
||||
"workBoard": {
|
||||
"title": "Priority Work Board",
|
||||
"subtitle": "{ordered} mainline items; {sources} log source families.",
|
||||
"completedOfTotal": "{completed}/{total} completed",
|
||||
"sourceCoverage": "Source coverage",
|
||||
"sourceCoverageDetail": "Project, product, site, service, package, and tool logs are classified and labeled.",
|
||||
"empty": "No work items match this filter.",
|
||||
"filters": {
|
||||
"all": "All",
|
||||
"completed": "Completed",
|
||||
"active": "Active",
|
||||
"pending": "Pending",
|
||||
"blocked": "Blocked"
|
||||
},
|
||||
"statuses": {
|
||||
"completed": "Completed",
|
||||
"in_progress": "In progress",
|
||||
"pending": "Pending",
|
||||
"blocked": "Blocked",
|
||||
"not_started": "Not started",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"column": "資產沉澱",
|
||||
@@ -11888,12 +11911,12 @@
|
||||
}
|
||||
},
|
||||
"legacyHitl": {
|
||||
"title": "既有 HITL 待人工處理",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;仍需在前台可見。",
|
||||
"title": "Existing HITL Evidence Queue",
|
||||
"subtitle": "These rows come from approval_records and are not AwoooP run approvals; keep them as historical evidence and route follow-up into AI controlled work items.",
|
||||
"openAuthorizations": "開啟授權中心",
|
||||
"loadFailed": "既有 HITL backlog 載入失敗:{error}",
|
||||
"tableLabel": "既有 HITL 待人工處理",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
|
||||
"tableLabel": "Existing HITL Evidence Queue",
|
||||
"moreRows": "Only the latest 8 rows are shown; track the remaining {count} in the authorization center.",
|
||||
"noTelegram": "無 Telegram",
|
||||
"telegramRef": "Telegram #{id}",
|
||||
"summary": {
|
||||
@@ -16855,7 +16878,7 @@
|
||||
"handoffRuntimeGatePointer": {
|
||||
"title": "執行期閘門指標包",
|
||||
"body": "任何掃描、修復、主機更新或阻擋控制都必須留在獨立執行期閘門。",
|
||||
"handoff": "只標記後續可能需要哪一種人工執行期閘門。",
|
||||
"handoff": "Only mark which controlled runtime gate may be needed later.",
|
||||
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不執行修復。"
|
||||
},
|
||||
"handoffSourceControlPointer": {
|
||||
@@ -16867,8 +16890,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerHandoffReviewBoard": {
|
||||
"title": "人工決策正式紀錄負責人交接驗收清單",
|
||||
"subtitle": "交接包進入人工檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0。",
|
||||
"title": "Controlled Decision Record Handoff Checklist",
|
||||
"subtitle": "Before a handoff packet enters controlled review, seven read-only acceptance checks confirm whether the data is sufficient; this is still not record-owner assignment, a formal record, break-glass approval, or execution authorization. Checks=7, passed=0, assigned=0, runtime gates=0.",
|
||||
"checkLabel": "驗收項",
|
||||
"reviewLabel": "檢查方式",
|
||||
"guardLabel": "仍不會做",
|
||||
@@ -16894,14 +16917,14 @@
|
||||
"items": {
|
||||
"packetCompleteness": {
|
||||
"title": "交接包完整性",
|
||||
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續人工確認欄位。",
|
||||
"body": "Check whether all seven handoff packets include source, summary, limits, gaps, and follow-up controlled confirmation fields.",
|
||||
"review": "只列出缺漏欄位與待補項目。",
|
||||
"guard": "不補寫正式紀錄、不自動產生批准文字。"
|
||||
},
|
||||
"recordOwnerIdentityScope": {
|
||||
"title": "負責人身分範圍",
|
||||
"body": "檢查交接包是否說明未來紀錄負責人的角色範圍、責任邊界與可聯絡依據。",
|
||||
"review": "只確認身分欄位是否足夠人工判讀。",
|
||||
"review": "Only confirm whether identity fields are sufficient for controlled interpretation.",
|
||||
"guard": "不代填姓名、不查外部帳號、不自動指派。"
|
||||
},
|
||||
"authorityBoundaryMatch": {
|
||||
@@ -16918,7 +16941,7 @@
|
||||
},
|
||||
"reviewerNoteConfirm": {
|
||||
"title": "審查備註確認",
|
||||
"body": "檢查退回理由、補證狀態、人工備註與未決事項是否足夠讓下一位審查者接手。",
|
||||
"body": "Check whether return reasons, evidence-completion status, review notes, and unresolved items are sufficient for the next reviewer.",
|
||||
"review": "只整理既有備註是否完整。",
|
||||
"guard": "不建立外部任務、不自動通知、不改審查結論。"
|
||||
},
|
||||
@@ -16937,8 +16960,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerHandoffReviewOutcomeBoard": {
|
||||
"title": "人工決策正式紀錄負責人交接驗收結果分流",
|
||||
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0。",
|
||||
"title": "Controlled Decision Record Handoff Outcome Routing",
|
||||
"subtitle": "After handoff acceptance, outcomes only land in eight read-only routes; this is still not record-owner assignment, a formal record, break-glass approval, or execution authorization. Routes=8, ready for controlled review=0, assigned=0, runtime gates=0.",
|
||||
"laneLabel": "結果分流",
|
||||
"resultLabel": "分流結果",
|
||||
"guardLabel": "仍不會做",
|
||||
@@ -16964,7 +16987,7 @@
|
||||
"items": {
|
||||
"remainReviewWaiting": {
|
||||
"title": "維持驗收等待",
|
||||
"body": "若交接包仍在等待人工檢查,結果只能維持等待狀態。",
|
||||
"body": "If the handoff packet is still waiting for controlled review, the result can only remain in a waiting state.",
|
||||
"result": "只顯示仍待驗收與缺少哪一類檢查。",
|
||||
"guard": "不自動通過、不建立正式紀錄、不指派負責人。"
|
||||
},
|
||||
@@ -16976,7 +16999,7 @@
|
||||
},
|
||||
"requestOwnerScopeClarification": {
|
||||
"title": "要求負責人範圍說明",
|
||||
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求人工說明。",
|
||||
"body": "If the future record owner's role, authority, or contact basis is unclear, request controlled clarification.",
|
||||
"result": "只標記需要補充哪一類負責人範圍。",
|
||||
"guard": "不查外部帳號、不代填姓名、不自動指派。"
|
||||
},
|
||||
@@ -16987,9 +17010,9 @@
|
||||
"guard": "不讀取機密明文、不保存原始載荷、不抓外部系統。"
|
||||
},
|
||||
"readyForRecordOwnerReview": {
|
||||
"title": "可進負責人檢查",
|
||||
"body": "若驗收項都足夠,交接包可以進入人工紀錄負責人檢查,但仍不是指派。",
|
||||
"result": "只標記可進人工檢查,等待人工確認。",
|
||||
"title": "Ready for Controlled Record Review",
|
||||
"body": "If all acceptance checks are sufficient, the handoff packet can enter controlled record review, but this is still not an assignment.",
|
||||
"result": "Only mark it ready for controlled review and wait for controlled review confirmation.",
|
||||
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
|
||||
},
|
||||
"quarantineSensitivePayload": {
|
||||
@@ -17013,8 +17036,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerReviewPreparationBoard": {
|
||||
"title": "人工決策正式紀錄負責人檢查準備包",
|
||||
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理人工檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0。",
|
||||
"title": "Controlled Decision Record Owner Review Preparation Packets",
|
||||
"subtitle": "If the handoff outcome can enter controlled review, only eight preparation packets are assembled before controlled review; this is not record-owner assignment, a formal record, break-glass approval, or execution authorization. Packets=8, review-ready=0, assigned=0, runtime gates=0.",
|
||||
"packetLabel": "準備包",
|
||||
"prepareLabel": "準備方式",
|
||||
"guardLabel": "仍不會做",
|
||||
|
||||
@@ -2872,11 +2872,11 @@
|
||||
"repairLock": "冪等鎖",
|
||||
"riskMediumDesc": "操作不可即時撤銷,但有備份保護",
|
||||
"confirmExec": "長按 5 秒確認授權執行",
|
||||
"rejectApproval": "拒絕授權 — 轉人工處理",
|
||||
"rejectApproval": "拒絕授權 — 轉 AI 受控補齊",
|
||||
"approvalGranted": "授權已核准",
|
||||
"approvalGrantedDesc": "NemoTron 正在執行 ansible-playbook...",
|
||||
"approvalRejected": "授權已拒絕",
|
||||
"approvalRejectedDesc": "已轉交人工處理",
|
||||
"approvalRejectedDesc": "已轉入 AI 受控補齊流程",
|
||||
"noHistory": "尚無修復紀錄",
|
||||
"noActiveAlerts": "目前無活躍告警",
|
||||
"noPlaybooks": "尚無 Playbook 紀錄",
|
||||
@@ -3250,7 +3250,7 @@
|
||||
"ready_for_reverify": "可重驗",
|
||||
"needs_target_mapping": "待補目標",
|
||||
"needs_playbook_ticket": "待建 Ticket",
|
||||
"manual_review": "人工檢查",
|
||||
"manual_review": "受控檢查",
|
||||
"unknown": "待分類"
|
||||
},
|
||||
"remediationAction": {
|
||||
@@ -3263,12 +3263,12 @@
|
||||
}
|
||||
},
|
||||
"legacyHitl": {
|
||||
"title": "Legacy HITL 待人工處理",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;仍需在前台可見。",
|
||||
"title": "Legacy HITL 歷史證據佇列",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;以歷史證據保留,後續導回 AI 受控工作項。",
|
||||
"openAuthorizations": "開啟授權中心",
|
||||
"loadFailed": "Legacy HITL backlog 載入失敗:{error}",
|
||||
"tableLabel": "Legacy HITL 待人工處理",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
|
||||
"tableLabel": "Legacy HITL 歷史證據佇列",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心追蹤。",
|
||||
"noTelegram": "no TG",
|
||||
"telegramRef": "TG #{id}",
|
||||
"summary": {
|
||||
@@ -11377,7 +11377,30 @@
|
||||
"medium": "medium",
|
||||
"high": "high"
|
||||
},
|
||||
"nextAction": "下一步"
|
||||
"nextAction": "下一步",
|
||||
"workBoard": {
|
||||
"title": "優先工作板",
|
||||
"subtitle": "主線 {ordered} 項;Log 來源 {sources} 組。",
|
||||
"completedOfTotal": "完成 {completed}/{total}",
|
||||
"sourceCoverage": "來源覆蓋",
|
||||
"sourceCoverageDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具 Log 已分類貼標。",
|
||||
"empty": "此篩選目前沒有工作項。",
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"completed": "已完成",
|
||||
"active": "進行中",
|
||||
"pending": "待推進",
|
||||
"blocked": "阻塞"
|
||||
},
|
||||
"statuses": {
|
||||
"completed": "已完成",
|
||||
"in_progress": "進行中",
|
||||
"pending": "待推進",
|
||||
"blocked": "阻塞",
|
||||
"not_started": "未開始",
|
||||
"unknown": "未知"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationAssetLedger": {
|
||||
"column": "資產沉澱",
|
||||
@@ -11888,12 +11911,12 @@
|
||||
}
|
||||
},
|
||||
"legacyHitl": {
|
||||
"title": "既有 HITL 待人工處理",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;仍需在前台可見。",
|
||||
"title": "既有 HITL 歷史證據佇列",
|
||||
"subtitle": "這批來自 批准_records,不屬於 AwoooP run 批准;以歷史證據保留,後續導回 AI 受控工作項。",
|
||||
"openAuthorizations": "開啟授權中心",
|
||||
"loadFailed": "既有 HITL backlog 載入失敗:{error}",
|
||||
"tableLabel": "既有 HITL 待人工處理",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
|
||||
"tableLabel": "既有 HITL 歷史證據佇列",
|
||||
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心追蹤。",
|
||||
"noTelegram": "無 Telegram",
|
||||
"telegramRef": "Telegram #{id}",
|
||||
"summary": {
|
||||
@@ -16855,7 +16878,7 @@
|
||||
"handoffRuntimeGatePointer": {
|
||||
"title": "執行期閘門指標包",
|
||||
"body": "任何掃描、修復、主機更新或阻擋控制都必須留在獨立執行期閘門。",
|
||||
"handoff": "只標記後續可能需要哪一種人工執行期閘門。",
|
||||
"handoff": "只標記後續可能需要哪一種受控執行期閘門。",
|
||||
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不執行修復。"
|
||||
},
|
||||
"handoffSourceControlPointer": {
|
||||
@@ -16867,8 +16890,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerHandoffReviewBoard": {
|
||||
"title": "人工決策正式紀錄負責人交接驗收清單",
|
||||
"subtitle": "交接包進入人工檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0。",
|
||||
"title": "受控決策正式紀錄交接驗收清單",
|
||||
"subtitle": "交接包進入受控檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0。",
|
||||
"checkLabel": "驗收項",
|
||||
"reviewLabel": "檢查方式",
|
||||
"guardLabel": "仍不會做",
|
||||
@@ -16894,14 +16917,14 @@
|
||||
"items": {
|
||||
"packetCompleteness": {
|
||||
"title": "交接包完整性",
|
||||
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續人工確認欄位。",
|
||||
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續受控確認欄位。",
|
||||
"review": "只列出缺漏欄位與待補項目。",
|
||||
"guard": "不補寫正式紀錄、不自動產生批准文字。"
|
||||
},
|
||||
"recordOwnerIdentityScope": {
|
||||
"title": "負責人身分範圍",
|
||||
"body": "檢查交接包是否說明未來紀錄負責人的角色範圍、責任邊界與可聯絡依據。",
|
||||
"review": "只確認身分欄位是否足夠人工判讀。",
|
||||
"review": "只確認身分欄位是否足夠受控判讀。",
|
||||
"guard": "不代填姓名、不查外部帳號、不自動指派。"
|
||||
},
|
||||
"authorityBoundaryMatch": {
|
||||
@@ -16918,7 +16941,7 @@
|
||||
},
|
||||
"reviewerNoteConfirm": {
|
||||
"title": "審查備註確認",
|
||||
"body": "檢查退回理由、補證狀態、人工備註與未決事項是否足夠讓下一位審查者接手。",
|
||||
"body": "檢查退回理由、補證狀態、審查備註與未決事項是否足夠讓下一位審查者接手。",
|
||||
"review": "只整理既有備註是否完整。",
|
||||
"guard": "不建立外部任務、不自動通知、不改審查結論。"
|
||||
},
|
||||
@@ -16937,8 +16960,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerHandoffReviewOutcomeBoard": {
|
||||
"title": "人工決策正式紀錄負責人交接驗收結果分流",
|
||||
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0。",
|
||||
"title": "受控決策正式紀錄交接驗收結果分流",
|
||||
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0。",
|
||||
"laneLabel": "結果分流",
|
||||
"resultLabel": "分流結果",
|
||||
"guardLabel": "仍不會做",
|
||||
@@ -16964,7 +16987,7 @@
|
||||
"items": {
|
||||
"remainReviewWaiting": {
|
||||
"title": "維持驗收等待",
|
||||
"body": "若交接包仍在等待人工檢查,結果只能維持等待狀態。",
|
||||
"body": "若交接包仍在等待受控檢查,結果只能維持等待狀態。",
|
||||
"result": "只顯示仍待驗收與缺少哪一類檢查。",
|
||||
"guard": "不自動通過、不建立正式紀錄、不指派負責人。"
|
||||
},
|
||||
@@ -16976,7 +16999,7 @@
|
||||
},
|
||||
"requestOwnerScopeClarification": {
|
||||
"title": "要求負責人範圍說明",
|
||||
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求人工說明。",
|
||||
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求受控說明。",
|
||||
"result": "只標記需要補充哪一類負責人範圍。",
|
||||
"guard": "不查外部帳號、不代填姓名、不自動指派。"
|
||||
},
|
||||
@@ -16987,9 +17010,9 @@
|
||||
"guard": "不讀取機密明文、不保存原始載荷、不抓外部系統。"
|
||||
},
|
||||
"readyForRecordOwnerReview": {
|
||||
"title": "可進負責人檢查",
|
||||
"body": "若驗收項都足夠,交接包可以進入人工紀錄負責人檢查,但仍不是指派。",
|
||||
"result": "只標記可進人工檢查,等待人工確認。",
|
||||
"title": "可進受控負責人檢查",
|
||||
"body": "若驗收項都足夠,交接包可以進入受控紀錄負責人檢查,但仍不是指派。",
|
||||
"result": "只標記可進受控檢查,等待 controlled review 確認。",
|
||||
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
|
||||
},
|
||||
"quarantineSensitivePayload": {
|
||||
@@ -17013,8 +17036,8 @@
|
||||
}
|
||||
},
|
||||
"ownerResponseFormalRecordOwnerReviewPreparationBoard": {
|
||||
"title": "人工決策正式紀錄負責人檢查準備包",
|
||||
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理人工檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0。",
|
||||
"title": "受控決策正式紀錄負責人檢查準備包",
|
||||
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理受控檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0。",
|
||||
"packetLabel": "準備包",
|
||||
"prepareLabel": "準備方式",
|
||||
"guardLabel": "仍不會做",
|
||||
|
||||
@@ -4,13 +4,17 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Bell,
|
||||
Bot,
|
||||
BookOpenCheck,
|
||||
CheckCircle2,
|
||||
Gauge,
|
||||
ListChecks,
|
||||
RefreshCw,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
TriangleAlert,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
@@ -120,12 +124,17 @@ type RuntimeReceiptReadback = {
|
||||
} | null;
|
||||
} | null;
|
||||
work_item_progress?: {
|
||||
ordered_items?: AutomationWorkItem[] | null;
|
||||
source_family_items?: AutomationWorkItem[] | null;
|
||||
rollups?: {
|
||||
work_item_count?: number | null;
|
||||
ordered_work_item_count?: number | null;
|
||||
source_family_work_item_count?: number | null;
|
||||
completed_count?: number | null;
|
||||
in_progress_count?: number | null;
|
||||
pending_count?: number | null;
|
||||
blocked_count?: number | null;
|
||||
not_started_count?: number | null;
|
||||
} | null;
|
||||
} | null;
|
||||
latest_flow_closure?: {
|
||||
@@ -165,6 +174,18 @@ type RuntimeControlPayload = {
|
||||
|
||||
type PanelMode = "full" | "compact";
|
||||
type Tone = "ok" | "warn" | "neutral";
|
||||
type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked";
|
||||
type WorkItemStatus = "completed" | "in_progress" | "pending" | "blocked" | "not_started" | string;
|
||||
|
||||
type AutomationWorkItem = {
|
||||
work_item_id?: string | null;
|
||||
priority?: string | null;
|
||||
title?: string | null;
|
||||
status?: WorkItemStatus | null;
|
||||
exit_criteria?: string | null;
|
||||
source_family_id?: string | null;
|
||||
next_controlled_action?: string | null;
|
||||
};
|
||||
|
||||
const API_BASE = getRuntimeApiBaseUrl();
|
||||
|
||||
@@ -190,6 +211,21 @@ function toneClass(tone: Tone): string {
|
||||
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
|
||||
}
|
||||
|
||||
function statusToneClass(status?: WorkItemStatus | null): string {
|
||||
if (status === "completed") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
|
||||
if (status === "in_progress") return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
|
||||
if (status === "pending" || status === "not_started") return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
|
||||
if (status === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
|
||||
}
|
||||
|
||||
function matchesWorkFilter(item: AutomationWorkItem, filter: WorkFilter): boolean {
|
||||
const status = item.status ?? "pending";
|
||||
if (filter === "all") return true;
|
||||
if (filter === "active") return status === "in_progress" || status === "not_started";
|
||||
return status === filter;
|
||||
}
|
||||
|
||||
function formatTime(value: Date | null, locale: string): string {
|
||||
if (!value) return "--";
|
||||
return value.toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US", {
|
||||
@@ -227,6 +263,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
|
||||
const [workFilter, setWorkFilter] = useState<WorkFilter>("all");
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -255,6 +292,8 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
const alertNoiseRollups = readback?.alert_noise_reduction?.rollups ?? {};
|
||||
const alertNoiseMissing = readback?.alert_noise_reduction?.missing_required_stage_ids ?? [];
|
||||
const workItemRollups = readback?.work_item_progress?.rollups ?? {};
|
||||
const orderedWorkItems = readback?.work_item_progress?.ordered_items ?? [];
|
||||
const sourceFamilyItems = readback?.work_item_progress?.source_family_items ?? [];
|
||||
const latestFlow = readback?.latest_flow_closure;
|
||||
const rollups = payload?.rollups ?? {};
|
||||
const closed = ledger?.closed === true || latestFlow?.closed === true;
|
||||
@@ -453,6 +492,31 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
policy?.medium_risk_controlled_apply_allowed ? t("risk.medium") : null,
|
||||
policy?.high_risk_controlled_apply_allowed ? t("risk.high") : null,
|
||||
].filter(Boolean).join(" / ");
|
||||
const workTotal = toNumber(rollups.live_work_item_count ?? workItemRollups.work_item_count);
|
||||
const workCompleted = toNumber(rollups.live_work_item_completed_count ?? workItemRollups.completed_count);
|
||||
const orderedWorkTotal = toNumber(workItemRollups.ordered_work_item_count ?? orderedWorkItems.length);
|
||||
const sourceFamilyTotal = toNumber(workItemRollups.source_family_work_item_count ?? sourceFamilyItems.length);
|
||||
const workCompletionPercent = workTotal > 0
|
||||
? Math.min(100, Math.round((workCompleted / workTotal) * 100))
|
||||
: 0;
|
||||
const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter));
|
||||
const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length;
|
||||
const orderedActive = orderedWorkItems.filter((item) => (
|
||||
item.status === "in_progress" || item.status === "not_started"
|
||||
)).length;
|
||||
const orderedPending = orderedWorkItems.filter((item) => item.status === "pending").length;
|
||||
const orderedBlocked = orderedWorkItems.filter((item) => item.status === "blocked").length;
|
||||
const workFilters: Array<{
|
||||
key: WorkFilter;
|
||||
count: number;
|
||||
icon: typeof ListChecks;
|
||||
}> = [
|
||||
{ key: "all", count: orderedWorkTotal, icon: ListChecks },
|
||||
{ key: "completed", count: orderedCompleted, icon: CheckCircle2 },
|
||||
{ key: "active", count: orderedActive, icon: Gauge },
|
||||
{ key: "pending", count: orderedPending, icon: ArrowRight },
|
||||
{ key: "blocked", count: orderedBlocked, icon: TriangleAlert },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
@@ -586,24 +650,125 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
</div>
|
||||
|
||||
{mode === "full" ? (
|
||||
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3">
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("policy.label")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">{riskText || "--"}</p>
|
||||
<>
|
||||
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3">
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("policy.label")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">{riskText || "--"}</p>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("policy.critical")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">
|
||||
{policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("nextAction")}</p>
|
||||
<p className="mt-1 truncate text-sm font-semibold text-[#141413]">
|
||||
{ledger?.next_executor_action ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("policy.critical")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">
|
||||
{policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"}
|
||||
</p>
|
||||
<div id="ai-automation-priority-work-board" className="border-t border-[#e0ddd4] bg-white">
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-[1fr_220px]">
|
||||
<div className="bg-white px-4 py-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-[#141413]">{t("workBoard.title")}</h4>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("workBoard.subtitle", {
|
||||
ordered: numberValue(orderedWorkTotal),
|
||||
sources: numberValue(sourceFamilyTotal),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-[180px]">
|
||||
<div className="flex items-center justify-between gap-3 text-xs font-semibold text-[#5f5b52]">
|
||||
<span>{t("workBoard.completedOfTotal", {
|
||||
completed: numberValue(workCompleted),
|
||||
total: numberValue(workTotal),
|
||||
})}</span>
|
||||
<span className="font-mono text-[#141413]">{workCompletionPercent}%</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 border border-[#d8d3c7] bg-[#faf9f3]">
|
||||
<div
|
||||
className="h-full bg-[#17602a]"
|
||||
style={{ width: `${workCompletionPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{workFilters.map((filter) => {
|
||||
const Icon = filter.icon;
|
||||
const selected = workFilter === filter.key;
|
||||
return (
|
||||
<button
|
||||
key={filter.key}
|
||||
type="button"
|
||||
onClick={() => setWorkFilter(filter.key)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 border px-3 py-1.5 text-xs font-semibold",
|
||||
selected
|
||||
? "border-[#141413] bg-[#141413] text-white"
|
||||
: "border-[#d8d3c7] bg-white text-[#5f5b52] hover:border-[#d97757] hover:text-[#141413]"
|
||||
)}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>{t(`workBoard.filters.${filter.key}` as never)}</span>
|
||||
<span className="font-mono">{numberValue(filter.count)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("workBoard.sourceCoverage")}</p>
|
||||
<p className="mt-2 font-mono text-lg font-semibold text-[#141413]">
|
||||
{numberValue(sourceFamilyTotal)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("workBoard.sourceCoverageDetail")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4]">
|
||||
{visibleWorkItems.length > 0 ? visibleWorkItems.map((item) => (
|
||||
<div
|
||||
key={item.work_item_id ?? item.priority ?? item.title}
|
||||
className="grid min-w-0 gap-3 bg-white px-4 py-3 md:grid-cols-[72px_minmax(0,1fr)_148px]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("h-2.5 w-2.5 border", statusToneClass(item.status))} aria-hidden="true" />
|
||||
<span className="font-mono text-xs font-semibold text-[#141413]">{item.priority ?? "--"}</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-[#141413]">{item.title ?? item.work_item_id ?? "--"}</div>
|
||||
<div className="mt-1 truncate text-xs text-[#5f5b52]">
|
||||
{item.exit_criteria ?? item.next_controlled_action ?? item.work_item_id ?? "--"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-start gap-2 md:justify-end">
|
||||
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", statusToneClass(item.status))}>
|
||||
{t(`workBoard.statuses.${item.status ?? "unknown"}` as never)}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-[#77736a]">
|
||||
{shortRef(item.work_item_id)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="bg-white px-4 py-6 text-sm font-semibold text-[#5f5b52]">
|
||||
{t("workBoard.empty")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("nextAction")}</p>
|
||||
<p className="mt-1 truncate text-sm font-semibold text-[#141413]">
|
||||
{ledger?.next_executor_action ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user