Files
PROJECT-CONTORL/openspec/changes/archive/2025-12-30-fix-realtime-notifications/design.md
beabigegg 64874d5425 feat: enhance weekly report and realtime notifications
Weekly Report (fix-weekly-report):
- Remove 5-task limit, show all tasks per category
- Add blocked tasks with blocker_reason and blocked_since
- Add next week tasks (due in coming week)
- Add assignee_name, completed_at, days_overdue to task details
- Frontend collapsible sections for each task category
- 8 new tests for enhanced report content

Realtime Notifications (fix-realtime-notifications):
- SQLAlchemy event-based notification publishing
- Redis Pub/Sub for multi-process broadcast
- Fix soft rollback handler stacking issue
- Fix ping scheduling drift (send immediately when interval expires)
- Frontend NotificationContext with WebSocket reconnection

Spec Fixes:
- Add missing ## Purpose sections to 5 specs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:52:08 +08:00

135 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Context
collaboration spec 要求即時通知透過 WebSocket 推播,但現行 NotificationService 僅寫入資料庫,未實作即時推送。
## Goals / Non-Goals
**Goals:**
- 建立 WebSocket 連線管理基礎設施
- 通知建立時透過 Redis Pub/Sub 廣播
- 使用者連線時補送未讀通知
- 前端即時接收並更新通知
**Non-Goals:**
- 不實作通知偏好設定(靜音/訂閱)
- 不實作 Push Notification (PWA/Mobile)
- 不實作通知分組或摺疊
## Decisions
### 1. WebSocket 連線管理
**Decision:** 使用 in-memory dict + Redis Pub/Sub
**Rationale:**
- 單 process 內使用 dict 維護 WebSocket 連線
- 跨 process 透過 Redis Pub/Sub 廣播
- 簡單且符合現有架構
```python
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {}
async def connect(self, user_id: str, websocket: WebSocket):
await websocket.accept()
if user_id not in self.active_connections:
self.active_connections[user_id] = []
self.active_connections[user_id].append(websocket)
async def disconnect(self, user_id: str, websocket: WebSocket):
self.active_connections[user_id].remove(websocket)
if not self.active_connections[user_id]:
del self.active_connections[user_id]
async def send_to_user(self, user_id: str, message: dict):
if user_id in self.active_connections:
for connection in self.active_connections[user_id]:
await connection.send_json(message)
```
### 2. Redis Pub/Sub 架構
**Decision:** 使用 user-specific channel
**Rationale:**
- Channel 命名: `notifications:{user_id}`
- 避免 broadcast 給不相關的 worker
- 減少訊息處理量
```python
async def publish_notification(user_id: str, notification: dict):
channel = f"notifications:{user_id}"
await redis.publish(channel, json.dumps(notification))
async def subscribe_notifications(user_id: str):
pubsub = redis.pubsub()
await pubsub.subscribe(f"notifications:{user_id}")
return pubsub
```
### 3. 連線時補送未讀
**Decision:** 連線建立後立即查詢並推送
**Rationale:**
- 確保使用者不漏接通知
- 簡化前端狀態同步邏輯
```python
@router.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)):
user = await verify_ws_token(token)
await manager.connect(user.id, websocket)
# 連線時補送未讀
unread = get_unread_notifications(db, user.id)
await websocket.send_json({
"type": "unread_sync",
"notifications": [n.dict() for n in unread]
})
# 開始監聽
await listen_for_notifications(user.id, websocket)
```
### 4. 訊息格式
**Decision:** 統一 JSON 格式
```json
{
"type": "notification",
"data": {
"id": "uuid",
"type": "mention|assignment|blocker|...",
"title": "...",
"message": "...",
"reference_type": "task|comment",
"reference_id": "uuid",
"created_at": "ISO8601"
}
}
```
## API Changes
```
# 新增 WebSocket endpoint
WS /ws/notifications?token={jwt_token}
# 訊息類型
-> {"type": "unread_sync", "notifications": [...]} # 連線時
-> {"type": "notification", "data": {...}} # 新通知
-> {"type": "mark_read", "notification_id": "..."} # 已讀確認
<- {"type": "ping"} # 心跳
```
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| WebSocket 連線數量大 | 使用心跳偵測清理斷線 |
| Redis Pub/Sub 可靠性 | Pub/Sub 為 fire-and-forget已有 DB 紀錄作為 fallback |
| Token 驗證 in query | WebSocket 標準限制token 有過期機制 |