fix(web): 穩定治理頁 deep link 與盤點容錯
All checks were successful
CD Pipeline / tests (push) Successful in 1m23s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 4m18s
CD Pipeline / post-deploy-checks (push) Successful in 1m29s

This commit is contained in:
Your Name
2026-06-05 10:33:11 +08:00
parent b09b5151c2
commit fd33591cd6
2 changed files with 46 additions and 22 deletions

View File

@@ -212,7 +212,7 @@ export function AutomationInventoryTab() {
const fetchSnapshot = () => {
setLoading(true)
Promise.all([
const requests = [
apiClient.getAiAgentAutomationInventorySnapshot(),
apiClient.getAiAgentAutomationBacklogSnapshot(),
apiClient.getBackupDrTargetInventory(),
@@ -220,16 +220,35 @@ export function AutomationInventoryTab() {
apiClient.getBackupNotificationPolicy(),
apiClient.getOffsiteEscrowReadinessStatus(),
apiClient.getRuntimeSurfaceInventory(),
])
.then(([inventoryData, backlogData, targetData, readinessData, policyData, offsiteEscrowData, runtimeSurfaceData]) => {
setSnapshot(inventoryData)
setBacklog(backlogData)
setBackupTargets(targetData)
setBackupReadiness(readinessData)
setBackupPolicy(policyData)
setOffsiteEscrow(offsiteEscrowData)
setRuntimeSurface(runtimeSurfaceData)
setError(false)
] as const
Promise.allSettled(requests)
.then((results) => {
const [
inventoryResult,
backlogResult,
targetResult,
readinessResult,
policyResult,
offsiteEscrowResult,
runtimeSurfaceResult,
] = results
setSnapshot(inventoryResult.status === 'fulfilled' ? inventoryResult.value : null)
setBacklog(backlogResult.status === 'fulfilled' ? backlogResult.value : null)
setBackupTargets(targetResult.status === 'fulfilled' ? targetResult.value : null)
setBackupReadiness(readinessResult.status === 'fulfilled' ? readinessResult.value : null)
setBackupPolicy(policyResult.status === 'fulfilled' ? policyResult.value : null)
setOffsiteEscrow(offsiteEscrowResult.status === 'fulfilled' ? offsiteEscrowResult.value : null)
setRuntimeSurface(runtimeSurfaceResult.status === 'fulfilled' ? runtimeSurfaceResult.value : null)
setError([
inventoryResult,
backlogResult,
targetResult,
readinessResult,
policyResult,
offsiteEscrowResult,
].some(result => result.status === 'rejected'))
})
.catch(() => setError(true))
.finally(() => setLoading(false))
@@ -327,7 +346,7 @@ export function AutomationInventoryTab() {
)
}
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !runtimeSurface) {
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow) {
return (
<div style={{ padding: 20 }}>
<GlassCard variant="subtle" padding="lg">
@@ -374,10 +393,10 @@ export function AutomationInventoryTab() {
const actionRequiredOffsiteCards = offsiteEscrow.rollups.by_readiness.action_required ?? 0
const blockedEscrowCards = offsiteEscrow.rollups.by_readiness.blocked ?? 0
const executionBlockedCards = offsiteEscrow.rollups.execution_blocked_card_ids.length
const runtimeActionRequired = runtimeSurface.rollups.action_required_surface_ids.length
const runtimeSecrets = runtimeSurface.rollups.secret_surface_ids.length
const runtimeLiveMissing = runtimeSurface.rollups.live_check_missing_surface_ids.length
const runtimeBoundComponents = runtimeSurface.rollups.source_components_with_runtime_binding
const runtimeActionRequired = runtimeSurface?.rollups.action_required_surface_ids.length ?? 0
const runtimeSecrets = runtimeSurface?.rollups.secret_surface_ids.length ?? 0
const runtimeLiveMissing = runtimeSurface?.rollups.live_check_missing_surface_ids.length ?? 0
const runtimeBoundComponents = runtimeSurface?.rollups.source_components_with_runtime_binding ?? 0
const backlogProgressPercent = backlog.progress_summary.overall_percent
const explicitApprovalItemCount = backlog.item_approval_boundary_rollup.items_requiring_explicit_approval.length
const taskBoundaryCount = snapshot.task_approval_boundary_rollup.total_tasks
@@ -873,6 +892,7 @@ export function AutomationInventoryTab() {
</div>
</GlassCard>
{runtimeSurface && (
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
@@ -976,6 +996,7 @@ export function AutomationInventoryTab() {
</div>
</div>
</GlassCard>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 0.8fr)', gap: 12 }} className="automation-inventory-bottom-grid">
<GlassCard variant="subtle" padding="md">

View File

@@ -101,12 +101,15 @@ export function PageTabs({ tabs, defaultTab, syncWithUrl = true }: PageTabsProps
const [activeTab, setActiveTab] = useState(initialTab)
const tabIds = tabs.map(t => t.id).join('|')
const firstTabId = tabs[0]?.id || ''
const knownTabIds = useMemo(() => tabIds.split('|'), [tabIds])
const urlActiveTab = syncWithUrl && urlTab && knownTabIds.includes(urlTab) ? urlTab : null
const resolvedActiveTab = urlActiveTab || activeTab || defaultTab || firstTabId
useEffect(() => {
const nextTab = (syncWithUrl ? urlTab : null) || defaultTab || firstTabId
if (!nextTab || !tabIds.split('|').includes(nextTab)) return
if (!nextTab || !knownTabIds.includes(nextTab)) return
setActiveTab(prev => prev === nextTab ? prev : nextTab)
}, [defaultTab, firstTabId, syncWithUrl, tabIds, urlTab])
}, [defaultTab, firstTabId, knownTabIds, syncWithUrl, urlTab])
// 切換 Tab
const switchTab = useCallback((tabId: string) => {
@@ -126,8 +129,8 @@ export function PageTabs({ tabs, defaultTab, syncWithUrl = true }: PageTabsProps
// 找到目前的 Tab 內容
const activeContent = useMemo(() => {
return tabs.find(t => t.id === activeTab)?.content ?? tabs[0]?.content
}, [tabs, activeTab])
return tabs.find(t => t.id === resolvedActiveTab)?.content ?? tabs[0]?.content
}, [tabs, resolvedActiveTab])
return (
<>
@@ -147,17 +150,17 @@ export function PageTabs({ tabs, defaultTab, syncWithUrl = true }: PageTabsProps
}}
>
{tabs.map(tab => {
const isActive = tab.id === activeTab
const isActive = tab.id === resolvedActiveTab
return (
<button
key={tab.id}
data-tab-id={tab.id}
onClick={() => switchTab(tab.id)}
style={{
padding: '0 14px',
fontSize: 12,
fontWeight: isActive ? 600 : 500,
color: isActive ? '#d97757' : '#87867f',
borderBottom: `2px solid ${isActive ? '#d97757' : 'transparent'}`,
background: 'transparent',
border: 'none',
borderBottomWidth: 2,