feat(governance): 新增 TG canary delivery rehearsal
This commit is contained in:
@@ -3721,9 +3721,14 @@ export function AutomationInventoryTab() {
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_bot_api_call_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_secret_read_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_paid_api_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_rehearsal_live_send_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_rehearsal_gateway_queue_write_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_rehearsal_bot_api_call_enabled_count
|
||||
+ professionalTaskExpansion.rollups.canary_delivery_rehearsal_receipt_write_enabled_count
|
||||
)
|
||||
const professionalTaskCanarySendPacket = professionalTaskExpansion.telegram_runtime_bridge.canary_send_approval_packet
|
||||
const professionalTaskCanaryDeliveryGate = professionalTaskExpansion.telegram_runtime_bridge.canary_delivery_gate
|
||||
const professionalTaskCanaryDeliveryRehearsal = professionalTaskExpansion.telegram_runtime_bridge.canary_delivery_rehearsal
|
||||
const professionalTaskCanaryApprovalGaps = (
|
||||
professionalTaskExpansion.rollups.canary_approval_granted_count
|
||||
+ professionalTaskExpansion.rollups.canary_selected_message_type_count
|
||||
@@ -4178,6 +4183,129 @@ export function AutomationInventoryTab() {
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 0 }}>
|
||||
<div style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid #19875440',
|
||||
background: 'rgba(25,135,84,0.08)',
|
||||
color: '#198754',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<BellRing size={18} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 18, fontWeight: 700, color: '#141413', lineHeight: 1.15, overflowWrap: 'anywhere' }}>
|
||||
{t('reportDeliveryHero.title')}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#5c5a55', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{t('reportDeliveryHero.subtitle', {
|
||||
current: reportLiveDeliveryApprovalPackage.program_status.current_task_id,
|
||||
next: reportLiveDeliveryApprovalPackage.program_status.next_task_id,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={t('reportDeliveryHero.route', { room: 'AwoooI SRE 戰情室' })} />
|
||||
<Chip value={t('reportDeliveryHero.source', {
|
||||
generated: formatDateTime(reportLiveDeliveryApprovalPackage.generated_at),
|
||||
current: reportLiveDeliveryApprovalPackage.program_status.current_task_id,
|
||||
})} muted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 11, border: '0.5px solid #b7d6c4', borderRadius: 7, background: '#f8fffb', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#375446', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{t('reportDeliveryHero.statusLine')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t('reportDeliveryHero.flags.scheduler', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.scheduler_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.gateway', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.gateway_queue_write_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.telegram', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.telegram_send_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.botApi', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.bot_api_call_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.receipt', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.report_receipt_write_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.analysis', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.ai_analysis_run_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.flags.ownerReview', { value: String(reportLiveDeliveryApprovalPackage.delivery_approval_truth.owner_review_required_before_delivery) })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(148px, 1fr))', gap: 10 }} className="automation-inventory-live-read-kpi-grid">
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.packets')} value={reportDeliveryPackets} tone="warn" icon={<FileText size={16} />} />
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.routeGates')} value={reportDeliveryRouteGates} tone="warn" icon={<Route size={16} />} />
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.receipts')} value={reportDeliveryReceipts} tone="warn" icon={<Archive size={16} />} />
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.approvalRequired')} value={reportDeliveryApprovalRequired} tone="warn" icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.telegramSends')} value={reportDeliveryTelegramSends} tone={reportDeliveryTelegramSends === 0 ? 'ok' : 'danger'} icon={<BellRing size={16} />} />
|
||||
<MetricCard label={t('reportDeliveryHero.metrics.liveWrites')} value={reportDeliveryLiveWrites} tone={reportDeliveryLiveWrites === 0 ? 'ok' : 'danger'} icon={<Database size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
|
||||
{visibleReportDeliveryPackets.map(packet => (
|
||||
<div key={`delivery-hero-${packet.packet_id}`} style={{ padding: 12, border: '0.5px solid #b7d6c4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 9, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 14, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
|
||||
{packet.display_name}
|
||||
</span>
|
||||
<Chip value={t(`reportLiveDeliveryApprovalPackage.cadences.${packet.cadence}` as never)} muted />
|
||||
</div>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#5c5a55', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{packet.operator_guidance}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={redisDryRunValueLabel('agents', packet.owner_agent)} muted />
|
||||
<Chip value={t(`reportLiveDeliveryApprovalPackage.packetStatuses.${packet.status}` as never)} muted={packet.status === 'ready_for_owner_review'} />
|
||||
<Chip value={t(`reportLiveDeliveryApprovalPackage.riskTiers.${packet.risk_tier}` as never)} muted={packet.risk_tier !== 'critical'} />
|
||||
<Chip value={t('reportDeliveryHero.labels.noSendMode', { value: String(packet.no_send_mode) })} muted />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 10 }} className="automation-inventory-live-read-grid">
|
||||
<div style={{ padding: 12, border: '0.5px solid #b7d6c4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 9, minWidth: 0 }}>
|
||||
<SmallLabel>{t('reportDeliveryHero.routeGateTitle')}</SmallLabel>
|
||||
{visibleReportRouteGates.slice(0, 3).map(gate => (
|
||||
<div key={`delivery-hero-gate-${gate.gate_id}`} style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
|
||||
{gate.display_name}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t(`reportLiveDeliveryApprovalPackage.gateStatuses.${gate.status}` as never)} muted={gate.status === 'ready_for_owner_review'} />
|
||||
<Chip value={t('reportDeliveryHero.labels.requiredEvidence', { count: gate.required_evidence.length })} muted />
|
||||
<Chip value={t('reportDeliveryHero.labels.gateway', { value: String(gate.gateway_queue_write_enabled) })} muted />
|
||||
<Chip value={t('reportDeliveryHero.labels.telegram', { value: String(gate.telegram_send_enabled) })} muted />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #b7d6c4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 9, minWidth: 0 }}>
|
||||
<SmallLabel>{t('reportDeliveryHero.receiptTitle')}</SmallLabel>
|
||||
{visibleReportDeliveryReceipts.map(receipt => (
|
||||
<div key={`delivery-hero-receipt-${receipt.receipt_id}`} style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
|
||||
{receipt.display_name}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t(`reportLiveDeliveryApprovalPackage.receiptStatuses.${receipt.status}` as never)} muted={receipt.status === 'ready_for_owner_review'} />
|
||||
<Chip value={t('reportDeliveryHero.labels.requiredFields', { count: receipt.required_fields.length })} muted />
|
||||
<Chip value={t('reportDeliveryHero.labels.liveSendCount', { count: receipt.live_send_count })} muted />
|
||||
<Chip value={t('reportDeliveryHero.labels.receiptWrite', { value: String(receipt.receipt_write_allowed) })} muted />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<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' }}>
|
||||
@@ -4342,6 +4470,9 @@ export function AutomationInventoryTab() {
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.deliveryFields')} value={professionalTaskExpansion.rollups.canary_delivery_required_field_count} tone="warn" icon={<ClipboardCheck size={16} />} />
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.deliveryPreflight')} value={professionalTaskExpansion.rollups.canary_delivery_preflight_check_count} tone="warn" icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.deliveryHoldReasons')} value={professionalTaskExpansion.rollups.canary_delivery_hold_reason_count} tone="danger" icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.deliveryRehearsal')} value={professionalTaskExpansion.rollups.canary_delivery_rehearsal_count} tone="ok" icon={<Route size={16} />} />
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.rehearsalChecks')} value={`${professionalTaskExpansion.rollups.canary_delivery_rehearsal_completed_check_count}/${professionalTaskExpansion.rollups.canary_delivery_rehearsal_readback_check_count}`} tone="ok" icon={<ClipboardCheck size={16} />} />
|
||||
<MetricCard label={t('professionalTaskExpansion.metrics.rehearsalStops')} value={professionalTaskExpansion.rollups.canary_delivery_rehearsal_stop_condition_count} tone="danger" icon={<ShieldAlert size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 10 }}>
|
||||
@@ -4512,6 +4643,56 @@ export function AutomationInventoryTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 10, border: '0.5px solid #a7d4bd', borderRadius: 7, background: '#f8fffb', minWidth: 0 }}>
|
||||
<SmallLabel>{t('professionalTaskExpansion.canaryDeliveryRehearsalTitle')}</SmallLabel>
|
||||
<p style={{ margin: '6px 0 8px', fontFamily: "'DM Mono', monospace", fontSize: 10, lineHeight: 1.5, color: '#375446', overflowWrap: 'anywhere' }}>
|
||||
{professionalTaskCanaryDeliveryRehearsal.rehearsal_id} · {professionalTaskCanaryDeliveryRehearsal.status}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 }}>
|
||||
<Chip value={t('professionalTaskExpansion.labels.rehearsalReady', { value: String(professionalTaskCanaryDeliveryRehearsal.rehearsal_ready) })} />
|
||||
<Chip value={t('professionalTaskExpansion.labels.selectedMessage', { value: professionalTaskCanaryDeliveryRehearsal.selected_message_type })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.targetRoom', { value: professionalTaskCanaryDeliveryRehearsal.target_room_env })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.valueVisible', { value: String(professionalTaskCanaryDeliveryRehearsal.target_room_value_visible) })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.dedup', { value: professionalTaskCanaryDeliveryRehearsal.dedup_key })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.previewHash', { value: professionalTaskCanaryDeliveryRehearsal.sanitized_preview_hash })} muted />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 8 }} className="automation-inventory-live-read-grid">
|
||||
<div style={{ padding: 8, borderRadius: 6, background: '#fff', border: '0.5px solid #c7e7d4', display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<SmallLabel>{t('professionalTaskExpansion.rehearsalEnvelopeTitle')}</SmallLabel>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t('professionalTaskExpansion.labels.queueWrites', { value: String(professionalTaskCanaryDeliveryRehearsal.gateway_envelope_preview.queue_write_enabled) })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.botCalls', { value: String(professionalTaskCanaryDeliveryRehearsal.gateway_envelope_preview.bot_api_call_enabled) })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.send', { value: String(professionalTaskCanaryDeliveryRehearsal.gateway_envelope_preview.telegram_send_enabled) })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.receiptWrite', { value: String(professionalTaskCanaryDeliveryRehearsal.gateway_envelope_preview.delivery_receipt_write_enabled) })} muted />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 8, borderRadius: 6, background: '#fff', border: '0.5px solid #c7e7d4', display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<SmallLabel>{t('professionalTaskExpansion.rehearsalReadbackTitle')}</SmallLabel>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t('professionalTaskExpansion.labels.owner', { value: professionalTaskCanaryDeliveryRehearsal.readback_drill.owner_agent })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.completedChecks', { done: professionalTaskCanaryDeliveryRehearsal.readback_drill.completed_check_count, total: professionalTaskCanaryDeliveryRehearsal.readback_drill.required_checks.length })} />
|
||||
<Chip value={t('professionalTaskExpansion.labels.failedChecks', { count: professionalTaskCanaryDeliveryRehearsal.readback_drill.failed_check_count })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.receiptWrite', { value: String(professionalTaskCanaryDeliveryRehearsal.readback_drill.production_receipt_write_enabled) })} muted />
|
||||
<Chip value={t('professionalTaskExpansion.labels.liveReadback', { value: String(professionalTaskCanaryDeliveryRehearsal.readback_drill.live_receipt_readback_enabled) })} muted />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 8, marginTop: 8 }}>
|
||||
{professionalTaskCanaryDeliveryRehearsal.dry_run_steps.map(step => (
|
||||
<div key={step} style={{ padding: 8, borderRadius: 6, background: '#eef8f4', fontFamily: "'DM Mono', monospace", fontSize: 10, lineHeight: 1.45, color: '#176d8a', overflowWrap: 'anywhere' }}>
|
||||
{step}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 8, marginTop: 8 }}>
|
||||
{professionalTaskCanaryDeliveryRehearsal.stop_conditions.slice(0, 7).map(condition => (
|
||||
<div key={condition} style={{ padding: 8, borderRadius: 6, background: '#fff7ed', fontFamily: "'DM Mono', monospace", fontSize: 10, lineHeight: 1.45, color: '#7a4a20', overflowWrap: 'anywhere' }}>
|
||||
{t('professionalTaskExpansion.labels.stopCondition', { value: condition })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmallLabel>{t('professionalTaskExpansion.tasksTitle')}</SmallLabel>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
|
||||
{visibleProfessionalTasks.map(task => {
|
||||
|
||||
Reference in New Issue
Block a user