Files
awoooi/docs/design/UX-001-incident-card-fatigue.md
OG T 2b1264df05 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>
2026-03-23 19:44:56 +08:00

10 KiB

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%
// 時間衰減樣式
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 或自動關閉 (視規則)
# 升級規則配置
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)

// 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)

// 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)

# 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 小時自動關閉 (可配置)
  • 同類事件智慧合併