fix(web): connect alerts to incident evidence chain
All checks were successful
CD Pipeline / tests (push) Successful in 1m31s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 6m49s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s

This commit is contained in:
Your Name
2026-05-31 22:41:29 +08:00
parent 3c7a469ae4
commit 7d30b0342c
4 changed files with 388 additions and 15 deletions

View File

@@ -19,8 +19,10 @@ import type { IncidentResponse, DecisionInfo, IncidentTimelineResponse } from '@
import { apiClient } from '@/lib/api-client'
import { CURRENT_USER } from '@/lib/constants'
import { useCSRF } from '@/hooks/useCSRF'
import { Link } from '@/i18n/routing'
import { FlowPipeline, type FlowStage } from './flow-pipeline'
import type { AwoooPStatusChain } from '@/components/awooop/status-chain'
import { Activity, GitBranch, Lightbulb, ListChecks, Monitor, ShieldCheck } from 'lucide-react'
// =============================================================================
// Types
@@ -34,6 +36,7 @@ export interface IncidentCardProps {
incident: IncidentResponse
decision?: DecisionInfo | null
statusChain?: AwoooPStatusChain | null
projectId?: string
onApprovalChange?: (proposalId: string, newStatus: 'approved' | 'rejected') => void
}
@@ -286,7 +289,7 @@ function useApprovalAction(
// Component
// =============================================================================
export function IncidentCard({ incident, decision, statusChain, onApprovalChange }: IncidentCardProps) {
export function IncidentCard({ incident, decision, statusChain, projectId = 'awoooi', onApprovalChange }: IncidentCardProps) {
const t = useTranslations('incident.card')
const { csrfToken } = useCSRF()
@@ -428,6 +431,40 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
latest: latestSource,
})
: null
const encodedProjectId = encodeURIComponent(projectId)
const encodedIncidentId = encodeURIComponent(incident.incident_id)
const truthLinks = [
{
key: 'monitoring',
label: t('truthLinkMonitoring'),
href: `/monitoring?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
Icon: Monitor,
},
{
key: 'workItems',
label: t('truthLinkWorkItems'),
href: `/awooop/work-items?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
Icon: ListChecks,
},
{
key: 'runs',
label: t('truthLinkRuns'),
href: `/awooop/runs?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
Icon: Activity,
},
{
key: 'approvals',
label: t('truthLinkApprovals'),
href: `/awooop/approvals?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
Icon: ShieldCheck,
},
{
key: 'tickets',
label: t('truthLinkTickets'),
href: `/tickets?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
Icon: GitBranch,
},
]
const serviceName = incident.affected_services?.[0] ?? '--'
const duration = formatDuration(incident.created_at)
@@ -738,6 +775,44 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
</span>
</div>
<div style={{
margin: '0 14px 10px',
padding: '7px 9px',
border: '0.5px solid #e0ddd4',
borderRadius: 6,
background: '#fff',
display: 'flex',
alignItems: 'center',
gap: 7,
flexWrap: 'wrap',
}}>
<span style={{ fontSize: 11, color: '#87867f', fontWeight: 700 }}>
{t('truthLinksLabel')}
</span>
{truthLinks.map(({ key, label, href, Icon }) => (
<Link
key={key}
href={href}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
padding: '3px 7px',
border: '0.5px solid #d8d3c7',
borderRadius: 5,
background: '#faf9f3',
color: '#3f3a32',
fontSize: 11,
fontWeight: 600,
textDecoration: 'none',
}}
>
<Icon aria-hidden="true" size={12} strokeWidth={1.8} />
{label}
</Link>
))}
</div>
<button
onClick={handleTimelineToggle}
style={{
@@ -954,8 +1029,9 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
}}>
<div style={{ color: '#141413', marginBottom: 4 }}>{decisionAction}</div>
{decisionReasoning && (
<div style={{ color: '#87867f', fontStyle: 'italic' }}>
💡 {decisionReasoning.slice(0, 150)}{decisionReasoning.length > 150 ? '...' : ''}
<div style={{ color: '#87867f', fontStyle: 'italic', display: 'flex', alignItems: 'flex-start', gap: 5 }}>
<Lightbulb aria-hidden="true" size={14} strokeWidth={1.8} style={{ flexShrink: 0, marginTop: 1 }} />
<span>{decisionReasoning.slice(0, 150)}{decisionReasoning.length > 150 ? '...' : ''}</span>
</div>
)}
</div>