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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
## 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 有過期機制 |
|
||||
Reference in New Issue
Block a user