feat(awooop): surface cicd rollout evidence
All checks were successful
CD Pipeline / tests (push) Successful in 4m1s
Code Review / ai-code-review (push) Successful in 17s
CD Pipeline / build-and-deploy (push) Successful in 3m27s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s

This commit is contained in:
Your Name
2026-05-21 20:06:26 +08:00
parent 0c59a1aafd
commit 4bdb012caa
7 changed files with 468 additions and 2 deletions

View File

@@ -11,6 +11,7 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { AlertTriangle, CheckCircle2, Clock3, GitCommit, RefreshCw } from 'lucide-react'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -30,6 +31,29 @@ interface Host {
last_check: string
}
interface CicdEvent {
id: string
alertname: string
stage: string | null
status: string | null
severity: string | null
commit_sha: string | null
triggered_by: string | null
duration_seconds: number
summary: string | null
description: string | null
workflow_url: string | null
action_detail: string | null
needs_attention: boolean
created_at: string
}
interface CicdEventsResponse {
items?: CicdEvent[]
total: number
limit: number
}
const STATUS_COLOR: Record<string, string> = {
up: '#22C55E',
healthy: '#22C55E',
@@ -38,9 +62,37 @@ const STATUS_COLOR: Record<string, string> = {
unreachable: '#87867f',
}
const CICD_STATUS_STYLE: Record<string, { border: string; background: string; color: string }> = {
success: { border: '#9bc7a4', background: '#f0faf2', color: '#17602a' },
running: { border: '#9bb6d9', background: '#eef5ff', color: '#1f5b9b' },
pending: { border: '#d9b36f', background: '#fff7e8', color: '#8a5a08' },
failed: { border: '#e2a29b', background: '#fff0ef', color: '#9f2f25' },
}
const CICD_STATUS_LABEL_KEYS: Record<string, string> = {
failed: 'cicd.status.failed',
pending: 'cicd.status.pending',
running: 'cicd.status.running',
success: 'cicd.status.success',
}
const CICD_STAGE_LABEL_KEYS: Record<string, string> = {
'code-review': 'cicd.stage.codeReview',
'post-deploy': 'cicd.stage.postDeploy',
'rollout-risk': 'cicd.stage.rolloutRisk',
tests: 'cicd.stage.tests',
}
function shortCommit(value: string | null | undefined) {
return value ? value.slice(0, 8) : null
}
export function DeploymentsPanel() {
const t = useTranslations('deployments')
const [hosts, setHosts] = useState<Host[]>([])
const [cicdEvents, setCicdEvents] = useState<CicdEvent[]>([])
const [cicdLoading, setCicdLoading] = useState(true)
const [cicdError, setCicdError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -51,6 +103,24 @@ export function DeploymentsPanel() {
.catch(err => { setError(String(err)); setLoading(false) })
}, [])
useEffect(() => {
fetch(`${API_BASE}/api/v1/platform/cicd/events?project_id=awoooi&limit=12`)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then((data: CicdEventsResponse) => {
setCicdEvents(Array.isArray(data.items) ? data.items : [])
setCicdError(null)
setCicdLoading(false)
})
.catch(err => {
setCicdError(String(err))
setCicdEvents([])
setCicdLoading(false)
})
}, [])
const k3sHosts = hosts.filter(h => h.role === 'k3s' || h.ip.includes('120'))
const displayHosts = k3sHosts.length > 0 ? k3sHosts : hosts
@@ -60,6 +130,82 @@ export function DeploymentsPanel() {
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
</div>
<section style={{ background: '#fff', border: '0.5px solid #e0ddd4', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '12px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<RefreshCw size={16} color="#d97757" aria-hidden="true" />
<div>
<h2 style={{ fontSize: 14, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('cicd.title')}</h2>
<p style={{ fontSize: 11, color: '#87867f', margin: '2px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('cicd.subtitle')}</p>
</div>
</div>
<span style={{ border: '0.5px solid #d8d3c7', background: '#fff', color: '#5f5b52', padding: '3px 8px', fontSize: 11, fontWeight: 600, fontFamily: 'var(--font-body), monospace' }}>
{t('cicd.visibleCount', { count: cicdEvents.length })}
</span>
</div>
{cicdLoading ? (
<div style={{ padding: '22px 14px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 12 }}>{t('cicd.loading')}</div>
) : cicdError ? (
<div style={{ padding: '22px 14px', color: '#9f2f25', fontFamily: 'var(--font-body), monospace', fontSize: 12 }}>{t('cicd.error')}</div>
) : cicdEvents.length === 0 ? (
<div style={{ padding: '22px 14px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 12 }}>{t('cicd.empty')}</div>
) : (
<div style={{ display: 'grid', gap: 0 }}>
{cicdEvents.map(event => {
const status = event.status ?? 'unknown'
const stage = event.stage ?? 'unknown'
const statusStyle = CICD_STATUS_STYLE[status] ?? { border: '#d8d3c7', background: '#fff', color: '#5f5b52' }
const statusKey = CICD_STATUS_LABEL_KEYS[status]
const stageKey = CICD_STAGE_LABEL_KEYS[stage]
const StatusIcon = event.needs_attention ? AlertTriangle : CheckCircle2
return (
<article key={event.id} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12, padding: '11px 14px', borderTop: '0.5px solid #f0ede4', alignItems: 'start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 26, height: 26, border: `0.5px solid ${statusStyle.border}`, background: statusStyle.background, color: statusStyle.color, flex: '0 0 auto' }}>
<StatusIcon size={14} aria-hidden="true" />
</span>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>
{statusKey ? t(statusKey as never) : status}
</div>
<div style={{ marginTop: 2, fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
{stageKey ? t(stageKey as never) : stage}
</div>
</div>
</div>
<div style={{ minWidth: 0, fontFamily: 'var(--font-body), monospace' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#5f5b52' }}>
<GitCommit size={13} aria-hidden="true" />
<span>{shortCommit(event.commit_sha) ?? t('cicd.emptyValue')}</span>
<span style={{ color: '#b8b2a6' }}>/</span>
<span>{event.triggered_by ?? t('cicd.emptyValue')}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 5, fontSize: 11, color: '#87867f' }}>
<Clock3 size={13} aria-hidden="true" />
<span>{event.created_at ? new Date(event.created_at).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' }) : t('cicd.emptyValue')}</span>
<span style={{ color: '#b8b2a6' }}>/</span>
<span>{event.duration_seconds > 0 ? t('cicd.durationSeconds', { seconds: event.duration_seconds }) : t('cicd.durationNotRecorded')}</span>
</div>
</div>
<div style={{ minWidth: 0, fontFamily: 'var(--font-body), monospace' }}>
<div style={{ fontSize: 12, fontWeight: 700, color: '#141413', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{event.summary || event.action_detail || event.alertname}
</div>
<div style={{ marginTop: 4, fontSize: 11, lineHeight: 1.55, color: '#5f5b52', overflowWrap: 'anywhere' }}>
{event.description || event.alertname}
</div>
{event.workflow_url && (
<a href={event.workflow_url} target="_blank" rel="noreferrer" style={{ display: 'inline-flex', marginTop: 6, fontSize: 11, fontWeight: 600, color: '#1f5b9b', textDecoration: 'none' }}>
{t('cicd.openWorkflow')}
</a>
)}
</div>
</article>
)
})}
</div>
)}
</section>
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
) : error ? (