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 有過期機制 |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Change: Fix Real-time Notifications Alignment
|
||||
|
||||
## Why
|
||||
現行實作與 collaboration spec 的 Real-time Notifications requirement 有以下差距:
|
||||
1. 通知僅寫入資料庫,未透過 WebSocket 即時推播
|
||||
2. 未使用 Redis Pub/Sub 處理多 process 推播
|
||||
3. 使用者連線時未補送未讀通知
|
||||
|
||||
## What Changes
|
||||
- **WebSocket Manager** - 建立 WebSocket 連線管理模組
|
||||
- **Redis Pub/Sub** - 整合 Redis 處理跨 process 通知推播
|
||||
- **NotificationService** - 新增即時推播呼叫
|
||||
- **API** - 新增 `/ws/notifications` WebSocket endpoint
|
||||
- **Frontend** - 整合 WebSocket 接收即時通知
|
||||
|
||||
## Impact
|
||||
- Affected specs: `collaboration`
|
||||
- Affected code:
|
||||
- `backend/app/core/websocket.py` - 新增 WebSocket 管理
|
||||
- `backend/app/core/redis_pubsub.py` - 新增 Redis Pub/Sub 服務
|
||||
- `backend/app/services/notification_service.py` - 加入即時推播
|
||||
- `backend/app/api/notifications/router.py` - 新增 WebSocket endpoint
|
||||
- `frontend/src/services/websocket.ts` - 新增 WebSocket client
|
||||
- `frontend/src/contexts/NotificationContext.tsx` - 整合即時通知
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: WebSocket Infrastructure
|
||||
- WebSocket 連線管理器
|
||||
- 使用者連線/斷線處理
|
||||
- 連線時補送未讀通知
|
||||
|
||||
### Phase 2: Redis Pub/Sub Integration
|
||||
- Redis Pub/Sub 服務封裝
|
||||
- 多 process 通知廣播
|
||||
- 訊息序列化/反序列化
|
||||
|
||||
### Phase 3: Service Integration
|
||||
- NotificationService 加入推播
|
||||
- 前端 WebSocket client
|
||||
- 未讀數量即時更新
|
||||
|
||||
## Dependencies
|
||||
- collaboration (已完成)
|
||||
- Redis 已在 user-auth 中使用
|
||||
|
||||
## Technical Decisions
|
||||
- 使用 FastAPI WebSocket 原生支援
|
||||
- Redis Pub/Sub 處理多 worker 同步
|
||||
- 使用者以 user_id 為 channel key
|
||||
@@ -0,0 +1,36 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Real-time Notifications
|
||||
系統 SHALL 透過 WebSocket 與 Redis Pub/Sub 推播即時通知。
|
||||
|
||||
#### Scenario: 即時通知推播
|
||||
- **GIVEN** 發生需要通知的事件(如:被指派任務、被 @提及、阻礙標記)
|
||||
- **WHEN** NotificationService.create_notification() 執行
|
||||
- **THEN** 系統透過 Redis Pub/Sub 發布通知至 `notifications:{user_id}` channel
|
||||
- **AND** 訂閱該 channel 的 WebSocket 連線接收訊息
|
||||
- **AND** ConnectionManager 推送通知給使用者的 WebSocket
|
||||
|
||||
#### Scenario: 連線時補送未讀
|
||||
- **GIVEN** 使用者建立 WebSocket 連線
|
||||
- **WHEN** 連線驗證成功
|
||||
- **THEN** 系統查詢該使用者的未讀通知 (is_read = false)
|
||||
- **AND** 透過 unread_sync 訊息一次推送所有未讀通知
|
||||
- **AND** 開始訂閱 Redis channel 接收新通知
|
||||
|
||||
#### Scenario: 心跳偵測
|
||||
- **GIVEN** 使用者已建立 WebSocket 連線
|
||||
- **WHEN** 連線超過心跳間隔無回應
|
||||
- **THEN** 系統將連線標記為斷線並從 ConnectionManager 移除
|
||||
|
||||
## MODIFIED Technical Notes
|
||||
|
||||
- 使用 Redis Pub/Sub 處理即時通知推播
|
||||
- WebSocket 連線管理:
|
||||
- ConnectionManager 維護 user_id → WebSocket[] 映射
|
||||
- 心跳偵測清理斷線連線
|
||||
- Token 驗證透過 query parameter
|
||||
- 通知推播流程:
|
||||
1. NotificationService.create_notification() 建立通知
|
||||
2. 呼叫 redis_pubsub.publish_notification() 發布
|
||||
3. 訂閱該 user channel 的 worker 收到訊息
|
||||
4. ConnectionManager.send_to_user() 推送給連線的 WebSocket
|
||||
@@ -0,0 +1,56 @@
|
||||
## Phase 1: WebSocket Infrastructure
|
||||
|
||||
### 1.1 Connection Manager
|
||||
- [ ] 1.1.1 建立 backend/app/core/websocket.py
|
||||
- [ ] 1.1.2 實作 ConnectionManager class
|
||||
- [ ] 1.1.3 實作 connect/disconnect/send_to_user 方法
|
||||
- [ ] 1.1.4 加入心跳偵測機制
|
||||
|
||||
### 1.2 WebSocket Endpoint
|
||||
- [ ] 1.2.1 新增 WS /ws/notifications endpoint
|
||||
- [ ] 1.2.2 實作 WebSocket token 驗證
|
||||
- [ ] 1.2.3 連線時查詢並推送未讀通知
|
||||
- [ ] 1.2.4 處理 WebSocket 異常與斷線
|
||||
|
||||
### 1.3 Testing - Phase 1
|
||||
- [ ] 1.3.1 WebSocket 連線測試
|
||||
- [ ] 1.3.2 未讀通知補送測試
|
||||
- [ ] 1.3.3 斷線處理測試
|
||||
|
||||
## Phase 2: Redis Pub/Sub Integration
|
||||
|
||||
### 2.1 Redis Pub/Sub Service
|
||||
- [ ] 2.1.1 建立 backend/app/core/redis_pubsub.py
|
||||
- [ ] 2.1.2 實作 publish_notification 函數
|
||||
- [ ] 2.1.3 實作 subscribe_user_channel 函數
|
||||
- [ ] 2.1.4 訊息 JSON 序列化處理
|
||||
|
||||
### 2.2 Cross-Process Broadcasting
|
||||
- [ ] 2.2.1 WebSocket endpoint 訂閱 user channel
|
||||
- [ ] 2.2.2 收到 Redis 訊息時推送給連線
|
||||
- [ ] 2.2.3 處理訂閱錯誤與重連
|
||||
|
||||
### 2.3 Testing - Phase 2
|
||||
- [ ] 2.3.1 Redis Pub/Sub 單元測試
|
||||
- [ ] 2.3.2 跨 process 通知測試(手動驗證)
|
||||
|
||||
## Phase 3: Service Integration
|
||||
|
||||
### 3.1 NotificationService 整合
|
||||
- [ ] 3.1.1 create_notification 後呼叫 publish_notification
|
||||
- [ ] 3.1.2 確保所有通知類型都即時推播
|
||||
- [ ] 3.1.3 處理 Redis 連線失敗 gracefully
|
||||
|
||||
### 3.2 Frontend WebSocket Client
|
||||
- [ ] 3.2.1 建立 frontend/src/services/websocket.ts
|
||||
- [ ] 3.2.2 實作 WebSocket 連線與重連邏輯
|
||||
- [ ] 3.2.3 訊息處理與分發
|
||||
|
||||
### 3.3 NotificationContext 整合
|
||||
- [ ] 3.3.1 修改 NotificationContext 使用 WebSocket
|
||||
- [ ] 3.3.2 收到通知時更新未讀數量
|
||||
- [ ] 3.3.3 收到 unread_sync 時同步狀態
|
||||
|
||||
### 3.4 Testing - Phase 3
|
||||
- [ ] 3.4.1 完整即時通知流程測試
|
||||
- [ ] 3.4.2 前端 WebSocket 整合測試
|
||||
119
openspec/changes/archive/2025-12-30-fix-weekly-report/design.md
Normal file
119
openspec/changes/archive/2025-12-30-fix-weekly-report/design.md
Normal file
@@ -0,0 +1,119 @@
|
||||
## Context
|
||||
|
||||
automation spec 定義週報須包含:已完成、進行中、逾期、阻礙中、下週預計任務清單。現行實作僅提供數量統計與部分摘要。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 週報內容包含完整任務清單
|
||||
- 新增阻礙中任務清單
|
||||
- 新增下週預計完成任務清單
|
||||
|
||||
**Non-Goals:**
|
||||
- 不實作週報自訂欄位篩選
|
||||
- 不實作週報匯出 PDF/Excel
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 週報內容結構
|
||||
|
||||
**Decision:** 擴充 content JSON 結構
|
||||
|
||||
**Current:**
|
||||
```json
|
||||
{
|
||||
"summary": { "completed_count": 5, "in_progress_count": 3, ... },
|
||||
"projects": [
|
||||
{
|
||||
"completed_tasks": [{"id": "...", "title": "..."}], // 限制 5 筆
|
||||
"overdue_tasks": [...] // 限制 5 筆
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"completed_count": 5,
|
||||
"in_progress_count": 3,
|
||||
"overdue_count": 2,
|
||||
"blocked_count": 1,
|
||||
"next_week_count": 4,
|
||||
"total_tasks": 15
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"project_id": "uuid",
|
||||
"project_title": "Project Name",
|
||||
"completed_tasks": [
|
||||
{"id": "...", "title": "...", "completed_at": "ISO8601", "assignee_name": "..."}
|
||||
],
|
||||
"in_progress_tasks": [
|
||||
{"id": "...", "title": "...", "assignee_name": "...", "due_date": "ISO8601"}
|
||||
],
|
||||
"overdue_tasks": [
|
||||
{"id": "...", "title": "...", "due_date": "ISO8601", "days_overdue": 3}
|
||||
],
|
||||
"blocked_tasks": [
|
||||
{"id": "...", "title": "...", "blocker_reason": "...", "blocked_since": "ISO8601"}
|
||||
],
|
||||
"next_week_tasks": [
|
||||
{"id": "...", "title": "...", "due_date": "ISO8601", "assignee_name": "..."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 阻礙任務識別
|
||||
|
||||
**Decision:** 查詢 Blocker 表關聯
|
||||
|
||||
```python
|
||||
# 取得有未解除阻礙的任務
|
||||
blocked_task_ids = db.query(Blocker.task_id).filter(
|
||||
Blocker.resolved_at.is_(None)
|
||||
).subquery()
|
||||
|
||||
blocked_tasks = db.query(Task).filter(
|
||||
Task.project_id.in_(project_ids),
|
||||
Task.id.in_(blocked_task_ids)
|
||||
).all()
|
||||
```
|
||||
|
||||
### 3. 下週預計任務
|
||||
|
||||
**Decision:** due_date 在下週一至週日
|
||||
|
||||
```python
|
||||
next_week_start = week_end # 本週末 = 下週初
|
||||
next_week_end = next_week_start + timedelta(days=7)
|
||||
|
||||
next_week_tasks = db.query(Task).filter(
|
||||
Task.project_id.in_(project_ids),
|
||||
Task.due_date >= next_week_start,
|
||||
Task.due_date < next_week_end,
|
||||
Task.status.not_in(["done", "completed"])
|
||||
).all()
|
||||
```
|
||||
|
||||
### 4. 任務明細欄位
|
||||
|
||||
**Decision:** 包含以下欄位供顯示
|
||||
|
||||
| 任務類型 | 額外欄位 |
|
||||
|---------|---------|
|
||||
| completed | completed_at (updated_at), assignee_name |
|
||||
| in_progress | assignee_name, due_date |
|
||||
| overdue | due_date, days_overdue |
|
||||
| blocked | blocker_reason, blocked_since |
|
||||
| next_week | due_date, assignee_name |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| 大量任務導致 content JSON 過大 | 評估後續加入分頁或限制 |
|
||||
| 阻礙查詢需 JOIN | 使用 subquery 減少 N+1 |
|
||||
@@ -0,0 +1,37 @@
|
||||
# Change: Fix Weekly Report Content Alignment
|
||||
|
||||
## Why
|
||||
現行 ReportService.get_weekly_stats 與 automation spec 的週報內容要求有差距:
|
||||
1. 任務清單僅顯示 5 筆摘要,spec 要求完整清單
|
||||
2. 未包含阻礙中任務清單 (blocker_flag = true)
|
||||
3. 未包含下週預計完成任務 (due_date 在下週)
|
||||
|
||||
## What Changes
|
||||
- **ReportService** - 擴充 get_weekly_stats 回傳完整任務明細
|
||||
- **Report Content** - 新增 blocked_tasks 與 next_week_tasks 欄位
|
||||
- **ReportHistory** - content JSON 結構擴充
|
||||
|
||||
## Impact
|
||||
- Affected specs: `automation`
|
||||
- Affected code:
|
||||
- `backend/app/services/report_service.py` - 擴充週報內容
|
||||
- `frontend/src/components/WeeklyReportPreview.tsx` - 顯示完整清單
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Backend Report Enhancement
|
||||
- 擴充 get_weekly_stats 回傳完整任務清單
|
||||
- 新增 blocked_tasks 欄位
|
||||
- 新增 next_week_tasks 欄位
|
||||
|
||||
### Phase 2: Frontend Display
|
||||
- 更新 WeeklyReportPreview 顯示完整清單
|
||||
- 可摺疊/展開的任務分類區塊
|
||||
|
||||
## Dependencies
|
||||
- automation (已完成)
|
||||
- collaboration (blocker 功能)
|
||||
|
||||
## Technical Decisions
|
||||
- 任務清單不設上限,由前端分頁或摺疊處理
|
||||
- 下週預計任務以 due_date 在下週一至週日為準
|
||||
@@ -0,0 +1,38 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Automated Weekly Report
|
||||
系統 SHALL 每週五下午 4:00 自動彙整完整任務清單發送給主管。
|
||||
|
||||
#### Scenario: 週報內容完整清單
|
||||
- **GIVEN** 週報生成中
|
||||
- **WHEN** 系統彙整資料
|
||||
- **THEN** 週報包含各專案的:
|
||||
- 本週已完成任務清單(含 completed_at, assignee_name)
|
||||
- 進行中任務清單(含 assignee_name, due_date)
|
||||
- 逾期任務警示(含 due_date, days_overdue)
|
||||
- 阻礙中任務清單(含 blocker_reason, blocked_since)
|
||||
- 下週預計完成任務(含 due_date, assignee_name)
|
||||
- **AND** 不設任務數量上限
|
||||
|
||||
#### Scenario: 阻礙任務識別
|
||||
- **GIVEN** 任務有未解除的 Blocker 記錄
|
||||
- **WHEN** 週報查詢阻礙任務
|
||||
- **THEN** 系統查詢 Blocker 表 resolved_at IS NULL 的任務
|
||||
- **AND** 顯示阻礙原因與開始時間
|
||||
|
||||
#### Scenario: 下週預計任務
|
||||
- **GIVEN** 任務的 due_date 在下週範圍內
|
||||
- **WHEN** 週報查詢下週預計任務
|
||||
- **THEN** 系統篩選 due_date >= 下週一 且 < 下週日
|
||||
- **AND** 排除已完成狀態的任務
|
||||
|
||||
## MODIFIED Technical Notes
|
||||
|
||||
- 週報 content JSON 結構擴充:
|
||||
- summary: 包含 blocked_count, next_week_count
|
||||
- projects[].completed_tasks: 無數量限制,含 completed_at, assignee_name
|
||||
- projects[].in_progress_tasks: 新增欄位
|
||||
- projects[].blocked_tasks: 新增欄位,含 blocker_reason, blocked_since
|
||||
- projects[].next_week_tasks: 新增欄位,含 due_date, assignee_name
|
||||
- 阻礙任務透過 Blocker 表 subquery 查詢
|
||||
- 下週計算以本週結束後 7 天為範圍
|
||||
@@ -0,0 +1,38 @@
|
||||
## Phase 1: Backend Report Enhancement
|
||||
|
||||
### 1.1 ReportService 擴充
|
||||
- [x] 1.1.1 移除 completed_tasks/overdue_tasks 的 5 筆限制
|
||||
- [x] 1.1.2 新增 in_progress_tasks 完整清單
|
||||
- [x] 1.1.3 新增 blocked_tasks 查詢與清單
|
||||
- [x] 1.1.4 新增 next_week_tasks 查詢與清單
|
||||
- [x] 1.1.5 擴充 summary 包含 blocked_count 與 next_week_count
|
||||
|
||||
### 1.2 任務明細欄位
|
||||
- [x] 1.2.1 completed_tasks 加入 completed_at, assignee_name
|
||||
- [x] 1.2.2 in_progress_tasks 加入 assignee_name, due_date
|
||||
- [x] 1.2.3 overdue_tasks 加入 days_overdue 計算
|
||||
- [x] 1.2.4 blocked_tasks 加入 blocker_reason, blocked_since
|
||||
- [x] 1.2.5 next_week_tasks 加入 due_date, assignee_name
|
||||
|
||||
### 1.3 Testing - Phase 1
|
||||
- [x] 1.3.1 週報內容結構測試
|
||||
- [x] 1.3.2 阻礙任務查詢測試
|
||||
- [x] 1.3.3 下週預計任務測試
|
||||
|
||||
## Phase 2: Frontend Display
|
||||
|
||||
### 2.1 WeeklyReportPreview 更新
|
||||
- [x] 2.1.1 新增 BlockedTasksSection 元件
|
||||
- [x] 2.1.2 新增 NextWeekTasksSection 元件
|
||||
- [x] 2.1.3 更新 CompletedTasksSection 顯示完整清單
|
||||
- [x] 2.1.4 更新 InProgressTasksSection 顯示完整清單
|
||||
- [x] 2.1.5 更新 OverdueTasksSection 顯示 days_overdue
|
||||
|
||||
### 2.2 UI 改善
|
||||
- [x] 2.2.1 可摺疊區塊設計
|
||||
- [x] 2.2.2 任務項目樣式統一
|
||||
- [x] 2.2.3 逾期/阻礙 highlight 樣式
|
||||
|
||||
### 2.3 Testing - Phase 2
|
||||
- [x] 2.3.1 前端週報顯示測試
|
||||
- [x] 2.3.2 空清單狀態測試
|
||||
Reference in New Issue
Block a user