From 6add97b9d7442af11948098f677aae71b355bf30 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 31 May 2026 21:03:10 +0800 Subject: [PATCH] fix(web): connect authorizations to incident truth chain --- apps/web/messages/en.json | 69 +++++ apps/web/messages/zh-TW.json | 69 +++++ .../src/app/[locale]/authorizations/page.tsx | 99 ++++++ .../app/[locale]/awooop/approvals/page.tsx | 9 + .../approval/incident-authorization-focus.tsx | 293 ++++++++++++++++++ .../approval/live-approval-panel.tsx | 59 ++-- apps/web/src/stores/approval.store.ts | 8 +- 7 files changed, 585 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/components/approval/incident-authorization-focus.tsx diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index c40a7875..0b225786 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -865,6 +865,23 @@ "signComment": "簽核備註 (選填)", "enterComment": "輸入備註...", "noApprovals": "目前沒有待簽核項目", + "csrfLoadFailed": "CSRF Token 載入失敗,簽核功能暫時無法使用", + "csrfLoading": "正在載入安全憑證...", + "loginIdentity": "登入身份", + "resolvedApproved": "已批准", + "resolvedRejected": "已拒絕", + "requiredRoleInline": "需要 {roles}", + "openIncidentTruthChain": "查看 Incident 授權真相鏈", + "securityValidationFailed": "安全驗證失敗,請重新整理頁面後再試", + "signFailed": "簽核失敗,請檢查網路連線或重新整理頁面", + "accessDenied": { + "title": "權限不足", + "riskBadge": "{risk} 風險", + "message": "此操作需要更高權限簽核", + "yourRole": "您的角色", + "requiredRoles": "需要以下角色之一", + "return": "了解,返回" + }, "fetchError": "無法取得授權清單", "noPendingApprovals": "目前無待授權項目", "selectApproval": "請選擇一個待授權項目", @@ -889,6 +906,57 @@ "securityNote": "CRITICAL 風險與 DESTRUCTIVE 資料影響的項目需單獨審核,無法批次核准。" } }, + "authorizations": { + "incidentFocus": { + "title": "焦點 Incident 授權真相鏈", + "loading": "讀取中", + "statusChainLoadFailed": "status-chain 載入失敗", + "timelineLoadFailed": "timeline 載入失敗", + "pendingLoadFailed": "pending approvals 載入失敗", + "openApprovals": "AwoooP 審批", + "openWorkItems": "Work Items", + "openRuns": "Runs", + "openTickets": "Tickets", + "authorizationTitle": "授權關聯狀態", + "flowTitle": "AI 處理流程", + "timelineEmpty": "尚未取得 Incident timeline。", + "emptyApprovalIds": "目前沒有關聯 approval id", + "notPendingExplanation": "指定 approval id 不在目前待簽清單;它可能已完成、過期、拒絕,或已轉成驗證後人工接手。", + "noPendingExplanation": "此 Incident 目前沒有 pending HITL 列;請依 status-chain 的下一步與 Work Items 追後續處置。", + "openFocusedAuthorization": "開啟此授權焦點", + "signatures": "簽核 {current}/{required}", + "boundary": "此區塊只做 read-only truth-chain 對齊:不新增批准、不拒絕、不觸發執行、不覆寫 pending approval。實際處置仍以 HITL 卡片、AwoooP Work Items 與 verified evidence 為準。", + "states": { + "pending": "仍在待簽", + "timelineLinked": "已在 timeline 關聯,非待簽", + "notPending": "不在待簽清單", + "notPendingLinked": "有歷史關聯,非待簽", + "noApproval": "無授權關聯" + }, + "needsHuman": { + "yes": "需要人工", + "no": "不需人工" + }, + "metrics": { + "authorization": "授權狀態", + "pendingRows": "待簽列", + "stage": "目前階段", + "verification": "驗證", + "handoff": "人工接手" + }, + "evidence": { + "nextAction": "下一步", + "reason": "原因", + "execution": "執行判定", + "ansible": "Ansible", + "mcp": "MCP", + "mcpValue": "{success}/{total} success;top {tool}", + "km": "KM", + "notification": "通知通道", + "events": "Timeline events" + } + } + }, "risk": { "low": "低風險", "medium": "中風險", @@ -4165,6 +4233,7 @@ "title": "焦點 Incident 審批真相鏈", "loading": "讀取中", "loadFailed": "焦點 Incident 真相鏈載入失敗;請改從 Work Items 或 Runs 檢查同一筆事件。", + "openAuthorizations": "授權中心", "openWorkItems": "Work Items", "openRuns": "Runs", "openTickets": "Tickets", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c40a7875..0b225786 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -865,6 +865,23 @@ "signComment": "簽核備註 (選填)", "enterComment": "輸入備註...", "noApprovals": "目前沒有待簽核項目", + "csrfLoadFailed": "CSRF Token 載入失敗,簽核功能暫時無法使用", + "csrfLoading": "正在載入安全憑證...", + "loginIdentity": "登入身份", + "resolvedApproved": "已批准", + "resolvedRejected": "已拒絕", + "requiredRoleInline": "需要 {roles}", + "openIncidentTruthChain": "查看 Incident 授權真相鏈", + "securityValidationFailed": "安全驗證失敗,請重新整理頁面後再試", + "signFailed": "簽核失敗,請檢查網路連線或重新整理頁面", + "accessDenied": { + "title": "權限不足", + "riskBadge": "{risk} 風險", + "message": "此操作需要更高權限簽核", + "yourRole": "您的角色", + "requiredRoles": "需要以下角色之一", + "return": "了解,返回" + }, "fetchError": "無法取得授權清單", "noPendingApprovals": "目前無待授權項目", "selectApproval": "請選擇一個待授權項目", @@ -889,6 +906,57 @@ "securityNote": "CRITICAL 風險與 DESTRUCTIVE 資料影響的項目需單獨審核,無法批次核准。" } }, + "authorizations": { + "incidentFocus": { + "title": "焦點 Incident 授權真相鏈", + "loading": "讀取中", + "statusChainLoadFailed": "status-chain 載入失敗", + "timelineLoadFailed": "timeline 載入失敗", + "pendingLoadFailed": "pending approvals 載入失敗", + "openApprovals": "AwoooP 審批", + "openWorkItems": "Work Items", + "openRuns": "Runs", + "openTickets": "Tickets", + "authorizationTitle": "授權關聯狀態", + "flowTitle": "AI 處理流程", + "timelineEmpty": "尚未取得 Incident timeline。", + "emptyApprovalIds": "目前沒有關聯 approval id", + "notPendingExplanation": "指定 approval id 不在目前待簽清單;它可能已完成、過期、拒絕,或已轉成驗證後人工接手。", + "noPendingExplanation": "此 Incident 目前沒有 pending HITL 列;請依 status-chain 的下一步與 Work Items 追後續處置。", + "openFocusedAuthorization": "開啟此授權焦點", + "signatures": "簽核 {current}/{required}", + "boundary": "此區塊只做 read-only truth-chain 對齊:不新增批准、不拒絕、不觸發執行、不覆寫 pending approval。實際處置仍以 HITL 卡片、AwoooP Work Items 與 verified evidence 為準。", + "states": { + "pending": "仍在待簽", + "timelineLinked": "已在 timeline 關聯,非待簽", + "notPending": "不在待簽清單", + "notPendingLinked": "有歷史關聯,非待簽", + "noApproval": "無授權關聯" + }, + "needsHuman": { + "yes": "需要人工", + "no": "不需人工" + }, + "metrics": { + "authorization": "授權狀態", + "pendingRows": "待簽列", + "stage": "目前階段", + "verification": "驗證", + "handoff": "人工接手" + }, + "evidence": { + "nextAction": "下一步", + "reason": "原因", + "execution": "執行判定", + "ansible": "Ansible", + "mcp": "MCP", + "mcpValue": "{success}/{total} success;top {tool}", + "km": "KM", + "notification": "通知通道", + "events": "Timeline events" + } + } + }, "risk": { "low": "低風險", "medium": "中風險", @@ -4165,6 +4233,7 @@ "title": "焦點 Incident 審批真相鏈", "loading": "讀取中", "loadFailed": "焦點 Incident 真相鏈載入失敗;請改從 Work Items 或 Runs 檢查同一筆事件。", + "openAuthorizations": "授權中心", "openWorkItems": "Work Items", "openRuns": "Runs", "openTickets": "Tickets", diff --git a/apps/web/src/app/[locale]/authorizations/page.tsx b/apps/web/src/app/[locale]/authorizations/page.tsx index 11e285aa..b613e288 100644 --- a/apps/web/src/app/[locale]/authorizations/page.tsx +++ b/apps/web/src/app/[locale]/authorizations/page.tsx @@ -7,20 +7,119 @@ * 含 Multi-Sig、權限擋板、SSE 即時更新 * * @updated 2026-04-01 ogt - 從佔位符升級為完整頁面 + * @updated 2026-05-31 Codex - 焦點 Incident 接上 status-chain / timeline / pending approval 真相鏈 */ +import { useEffect, useState } from 'react' +import { useSearchParams } from 'next/navigation' +import { useTranslations } from 'next-intl' + import { AppLayout } from '@/components/layout' +import { IncidentAuthorizationFocus } from '@/components/approval/incident-authorization-focus' import { LiveApprovalPanel } from '@/components/approval/live-approval-panel' +import type { AwoooPStatusChain } from '@/components/awooop/status-chain' import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge' +import type { IncidentTimelineResponse } from '@/lib/api-client' +import type { ApprovalRequest } from '@/stores/approval.store' + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +async function fetchJson(url: string): Promise { + const response = await fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + return response.json() +} + +function normalizePendingApprovals(payload: unknown): ApprovalRequest[] { + if (Array.isArray(payload)) return payload as ApprovalRequest[] + if (payload && typeof payload === 'object' && Array.isArray((payload as { approvals?: unknown }).approvals)) { + return (payload as { approvals: ApprovalRequest[] }).approvals + } + return [] +} export default function AuthorizationsPage({ params, }: { params: { locale: string } }) { + const t = useTranslations('authorizations.incidentFocus') + const searchParams = useSearchParams() + const projectId = searchParams.get('project_id') ?? 'awoooi' + const incidentId = searchParams.get('incident_id') + const approvalId = searchParams.get('approval_id') + const [chain, setChain] = useState(null) + const [timeline, setTimeline] = useState(null) + const [pendingApprovals, setPendingApprovals] = useState([]) + const [focusLoading, setFocusLoading] = useState(false) + const [focusError, setFocusError] = useState(null) + + useEffect(() => { + let cancelled = false + if (!incidentId) { + setChain(null) + setTimeline(null) + setPendingApprovals([]) + setFocusError(null) + setFocusLoading(false) + return () => { + cancelled = true + } + } + const focusedIncidentId = incidentId + + async function loadFocus() { + setFocusLoading(true) + setFocusError(null) + const encodedProjectId = encodeURIComponent(projectId) + const encodedIncidentId = encodeURIComponent(focusedIncidentId) + const [statusChainResult, timelineResult, pendingResult] = await Promise.allSettled([ + fetchJson( + `${API_BASE}/api/v1/platform/status-chain?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` + ), + fetchJson(`${API_BASE}/api/v1/incidents/${encodedIncidentId}/timeline`), + fetchJson(`${API_BASE}/api/v1/approvals/pending`), + ]) + + if (cancelled) return + + const nextChain = statusChainResult.status === 'fulfilled' ? statusChainResult.value : null + const nextTimeline = timelineResult.status === 'fulfilled' ? timelineResult.value : null + const nextPending = pendingResult.status === 'fulfilled' ? normalizePendingApprovals(pendingResult.value) : [] + setChain(nextChain) + setTimeline(nextTimeline) + setPendingApprovals(nextPending) + + const failures = [ + statusChainResult.status === 'rejected' ? t('statusChainLoadFailed') : null, + timelineResult.status === 'rejected' ? t('timelineLoadFailed') : null, + pendingResult.status === 'rejected' ? t('pendingLoadFailed') : null, + ].filter(Boolean) + setFocusError(failures.length > 0 ? failures.join(' / ') : null) + setFocusLoading(false) + } + + loadFocus() + return () => { + cancelled = true + } + }, [incidentId, projectId, t]) + return (
+ {incidentId ? ( + + ) : null}
diff --git a/apps/web/src/app/[locale]/awooop/approvals/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/page.tsx index fbfcee45..d55b7b99 100644 --- a/apps/web/src/app/[locale]/awooop/approvals/page.tsx +++ b/apps/web/src/app/[locale]/awooop/approvals/page.tsx @@ -987,6 +987,15 @@ function FocusedIncidentApprovalPanel({ {t("loading")} ) : null} + + {t("openAuthorizations")} +