docs: 完整治理架構 ADR-010/011/012 + CLAUDE.md 鐵律更新

2026-03-23 重大事故修復與治理:

1. ADR-010: Secrets 集中管理 (Bitwarden + Sealed Secrets)
2. ADR-011: NetworkPolicy 變更治理 (偵測 + 告警 + 人工決策)
3. ADR-012: 危險操作治理 (Tier 分級 + CI/CD 攔截 + 審計)
4. UX-001: 告警疲勞解決方案 (時間衰減 + 智慧分組)

CLAUDE.md 更新:
- 新增最高優先級鐵律 (禁止 ClawBot、OpenClaw 核心、禁止危險 API)
- 新增任務開始前必讀 Memory 對照表

事故教訓:
- Telegram Token 連續三次被 logOut 失效
- AWOOOI API 程式碼呼叫 logOut 導致災難
- 已停用 AWOOOI API Telegram,OpenClaw 為唯一 Gateway

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-23 19:44:56 +08:00
parent e23493741a
commit 2b1264df05
6 changed files with 1424 additions and 7 deletions

View File

@@ -0,0 +1,327 @@
# UX-001: 事件卡片告警疲勞解決方案
**狀態**: 提案
**日期**: 2026-03-23
**提案者**: 統帥
**問題**: 事件卡片長時間堆積導致用戶麻木,失去警示效果
---
## 問題分析
```
現況:
┌─────────────────────────────────────────────────────┐
│ 活躍事件 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ P0 紅色 │ │ P0 紅色 │ │ P0 紅色 │ │
│ │ 09:39 │ │ 09:44 │ │ 09:57 │ ... │
│ │ mitigating │ │ mitigating │ │ mitigating │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 問題:全部看起來一樣 → 視覺疲勞 → 無人處理 │
└─────────────────────────────────────────────────────┘
```
### 心理學原理
1. **Weber-Fechner Law**: 持續刺激會導致感知鈍化
2. **Banner Blindness**: 重複視覺元素被大腦自動忽略
3. **Decision Fatigue**: 太多選擇導致不做選擇
---
## 解決方案:四層優化
### Layer 1: 時間衰減視覺 (Time-based Visual Decay)
```
新舊卡片視覺差異:
🔴 剛發生 (< 5分鐘) → 紅色脈動動畫 + 聲音提示
🟠 較新 (5-30分鐘) → 橘色漸層 + 輕微動畫
🟡 等待中 (30分-2小時) → 黃色靜態
⚪ 陳舊 (> 2小時) → 灰色 + 淡化 50%
```
```tsx
// 時間衰減樣式
const getCardStyle = (createdAt: Date) => {
const ageMinutes = (Date.now() - createdAt.getTime()) / 60000;
if (ageMinutes < 5) return {
borderColor: 'red',
animation: 'pulse 1s infinite',
playSound: true,
};
if (ageMinutes < 30) return {
borderColor: 'orange',
animation: 'subtle-glow 2s infinite',
};
if (ageMinutes < 120) return {
borderColor: 'yellow',
opacity: 0.9,
};
return {
borderColor: 'gray',
opacity: 0.5,
filter: 'grayscale(50%)',
};
};
```
### Layer 2: 智慧分組 (Smart Grouping)
```
優化後佈局:
┌─────────────────────────────────────────────────────┐
│ 🚨 需要立即處理 (2) [全部處理] │
│ ┌────────────────────────────────────────────────┐ │
│ │ 🔴 postgres-primary-0 P0 剛剛 │ │
│ │ 🔴 awoooi-worker P0 3分鐘前 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ⏳ 等待處理 (3) [批量關閉 ▼] │
│ ┌────────────────────────────────────────────────┐ │
│ │ 🟡 harbor-core P0 45分鐘前 [Y] [n] │ │
│ │ 🟡 postgres-native P0 1小時前 [Y] [n] │ │
│ │ 🟡 health-check-test P2 2小時前 [Y] [n] │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ 📦 陳舊事件 (1) [自動關閉倒數] │
│ ┌────────────────────────────────────────────────┐ │
│ │ ⚪ postgres-primary-0 P0 3小時前 將在1h自動關 │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
### Layer 3: 升級機制 (Escalation)
```
時間軸自動升級:
0-15分鐘: Dashboard 顯示
15-30分鐘: Telegram 通知 (首次)
30-60分鐘: Telegram 再次提醒 + @mention 負責人
1-2小時: 電話通知 (P0/P1)
2小時+: 自動降級為 P3 或自動關閉 (視規則)
```
```yaml
# 升級規則配置
escalation_rules:
P0:
- after: 15m
action: telegram_notify
message: "🚨 P0 事件等待處理: {incident_id}"
- after: 30m
action: telegram_mention
mention: "@oncall"
- after: 1h
action: phone_call
to: oncall_phone
- after: 4h
action: auto_close
reason: "超過 4 小時無人處理,自動關閉"
P1:
- after: 30m
action: telegram_notify
- after: 2h
action: auto_downgrade
to: P3
P2:
- after: 2h
action: auto_close
reason: "低優先級超時自動關閉"
```
### Layer 4: 智慧合併 (Smart Merge)
```
同類事件合併:
Before:
├── postgres-primary-0 崩潰 (09:44)
├── postgres-primary-0 重啟 (09:57)
├── postgres-primary-0 OOM (10:15)
After:
├── 📦 postgres-primary-0 問題群組 (3 個事件)
│ └── 最近: OOM (10:15)
│ └── [展開詳情]
```
---
## 實施計畫
### Phase 1: 視覺優化 (Week 1)
```tsx
// apps/web/src/components/incident/IncidentCard.tsx
import { formatDistanceToNow } from 'date-fns';
import { zhTW } from 'date-fns/locale';
interface IncidentCardProps {
incident: Incident;
}
export const IncidentCard = ({ incident }: IncidentCardProps) => {
const ageMinutes = getAgeInMinutes(incident.created_at);
const urgencyLevel = getUrgencyLevel(ageMinutes, incident.priority);
return (
<motion.div
className={cn(
"incident-card",
urgencyLevel === 'critical' && "animate-pulse border-red-500",
urgencyLevel === 'urgent' && "border-orange-400",
urgencyLevel === 'waiting' && "border-yellow-300 opacity-90",
urgencyLevel === 'stale' && "border-gray-300 opacity-50 grayscale-50"
)}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
>
{/* 時間徽章 */}
<div className="absolute top-2 right-2">
<Badge variant={urgencyLevel}>
{formatDistanceToNow(incident.created_at, {
addSuffix: true,
locale: zhTW
})}
</Badge>
</div>
{/* 自動關閉倒數 (陳舊事件) */}
{urgencyLevel === 'stale' && (
<div className="text-xs text-gray-400 mt-2">
{getAutoCloseCountdown(incident)}
</div>
)}
{/* ... 其他內容 */}
</motion.div>
);
};
```
### Phase 2: 分組佈局 (Week 2)
```tsx
// apps/web/src/components/incident/IncidentDashboard.tsx
export const IncidentDashboard = () => {
const { incidents } = useIncidents();
const grouped = useMemo(() => ({
critical: incidents.filter(i => getAgeMinutes(i) < 15),
waiting: incidents.filter(i => getAgeMinutes(i) >= 15 && getAgeMinutes(i) < 120),
stale: incidents.filter(i => getAgeMinutes(i) >= 120),
}), [incidents]);
return (
<div className="space-y-6">
{/* 緊急區 - 紅色背景 */}
{grouped.critical.length > 0 && (
<Section
title="🚨 需要立即處理"
count={grouped.critical.length}
variant="critical"
action={<Button></Button>}
>
{grouped.critical.map(i => <IncidentCard key={i.id} incident={i} />)}
</Section>
)}
{/* 等待區 - 黃色背景 */}
{grouped.waiting.length > 0 && (
<Section
title="⏳ 等待處理"
count={grouped.waiting.length}
variant="waiting"
collapsible
>
{grouped.waiting.map(i => <IncidentCard key={i.id} incident={i} />)}
</Section>
)}
{/* 陳舊區 - 可折疊 */}
{grouped.stale.length > 0 && (
<Section
title="📦 陳舊事件"
count={grouped.stale.length}
variant="stale"
collapsible
defaultCollapsed
>
{grouped.stale.map(i => <IncidentCard key={i.id} incident={i} />)}
</Section>
)}
</div>
);
};
```
### Phase 3: 升級機制 (Week 3)
```python
# apps/api/src/services/escalation_service.py
class EscalationService:
"""事件升級服務"""
RULES = {
'P0': [
{'after_minutes': 15, 'action': 'telegram_notify'},
{'after_minutes': 30, 'action': 'telegram_mention', 'mention': '@oncall'},
{'after_minutes': 60, 'action': 'phone_call'},
{'after_minutes': 240, 'action': 'auto_close'},
],
'P1': [
{'after_minutes': 30, 'action': 'telegram_notify'},
{'after_minutes': 120, 'action': 'auto_downgrade', 'to': 'P3'},
],
'P2': [
{'after_minutes': 120, 'action': 'auto_close'},
],
}
async def check_escalations(self):
"""定期檢查需要升級的事件"""
incidents = await self.get_open_incidents()
for incident in incidents:
age_minutes = self.get_age_minutes(incident)
rules = self.RULES.get(incident.priority, [])
for rule in rules:
if age_minutes >= rule['after_minutes']:
if not await self.is_escalation_sent(incident.id, rule):
await self.execute_escalation(incident, rule)
```
---
## 預期效果
| 指標 | 現況 | 優化後 |
|------|------|--------|
| 平均處理時間 | > 2 小時 | < 30 分鐘 |
| 忽略率 | ~80% | < 20% |
| 用戶注意力 | 分散 | 集中在關鍵事件 |
| 告警疲勞 | 嚴重 | 可控 |
---
## 驗收標準
- [ ] 新事件有脈動動畫吸引注意
- [ ] 陳舊事件自動淡化
- [ ] 事件按時間分組顯示
- [ ] P0 事件 15 分鐘未處理自動 Telegram 通知
- [ ] 超過 4 小時自動關閉 (可配置)
- [ ] 同類事件智慧合併