Backend (FastAPI): - Database migration for spaces, projects, task_statuses, tasks tables - SQLAlchemy models with relationships - Pydantic schemas for CRUD operations - Spaces API: CRUD with soft delete - Projects API: CRUD with auto-created default statuses - Tasks API: CRUD, status change, assign, subtask support - Permission middleware with Security Level filtering - Subtask depth limit (max 2 levels) Frontend (React + Vite): - Layout component with navigation - Spaces list page - Projects list page - Tasks list page with status management Fixes: - auth_client.py: use 'username' field for external API - config.py: extend JWT expiry to 7 days - auth/router.py: sync Redis session with JWT expiry Tests: 36 passed (unit + integration) E2E: All APIs verified with real authentication OpenSpec: add-task-management archived 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
10 KiB
Design: add-task-management
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Spaces │ │ Projects │ │ Tasks │ │ Task Detail │ │
│ │ List │ │ List │ │ ListView │ │ Form │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
└───────┼─────────────┼─────────────┼────────────────┼────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌──────────────┐ ┌────────────────┐ ┌─────────────────┐ │
│ │ /api/spaces │ │ /api/projects │ │ /api/tasks │ │
│ │ router │ │ router │ │ router │ │
│ └──────┬───────┘ └───────┬────────┘ └────────┬────────┘ │
└─────────┼──────────────────┼────────────────────┼───────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Middleware Layer │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ Auth Middleware│ │ Permission Middleware │ │
│ │ (JWT驗證) │ │ (RBAC + Security Level) │ │
│ └────────┬────────┘ └────────────────┬────────────────┘ │
└───────────┼────────────────────────────┼────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ pjctrl_ │ │ pjctrl_ │ │ pjctrl_ │ │
│ │ spaces │ │ projects │ │ tasks │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
Data Model Details
pjctrl_spaces
空間是最上層的組織單位,用於將相關專案分組。
CREATE TABLE pjctrl_spaces (
id CHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
owner_id CHAR(36) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES pjctrl_users(id)
);
CREATE INDEX idx_spaces_owner ON pjctrl_spaces(owner_id);
CREATE INDEX idx_spaces_active ON pjctrl_spaces(is_active);
pjctrl_projects
專案屬於某個空間,具有預算、時程和安全等級設定。
CREATE TABLE pjctrl_projects (
id CHAR(36) PRIMARY KEY,
space_id CHAR(36) NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT,
owner_id CHAR(36) NOT NULL,
budget DECIMAL(15, 2),
start_date DATE,
end_date DATE,
security_level ENUM('public', 'department', 'confidential') DEFAULT 'department',
status VARCHAR(50) DEFAULT 'active',
department_id CHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (space_id) REFERENCES pjctrl_spaces(id) ON DELETE CASCADE,
FOREIGN KEY (owner_id) REFERENCES pjctrl_users(id),
FOREIGN KEY (department_id) REFERENCES pjctrl_departments(id)
);
CREATE INDEX idx_projects_space ON pjctrl_projects(space_id);
CREATE INDEX idx_projects_owner ON pjctrl_projects(owner_id);
CREATE INDEX idx_projects_department ON pjctrl_projects(department_id);
CREATE INDEX idx_projects_security ON pjctrl_projects(security_level);
pjctrl_task_statuses
定義專案內可用的任務狀態。
CREATE TABLE pjctrl_task_statuses (
id CHAR(36) PRIMARY KEY,
project_id CHAR(36) NOT NULL,
name VARCHAR(50) NOT NULL,
color VARCHAR(7) DEFAULT '#808080',
position INT DEFAULT 0,
is_done BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES pjctrl_projects(id) ON DELETE CASCADE,
UNIQUE KEY uk_status_name (project_id, name)
);
CREATE INDEX idx_statuses_project ON pjctrl_task_statuses(project_id);
pjctrl_tasks
任務是核心實體,支援父子任務關係。
CREATE TABLE pjctrl_tasks (
id CHAR(36) PRIMARY KEY,
project_id CHAR(36) NOT NULL,
parent_task_id CHAR(36),
title VARCHAR(500) NOT NULL,
description TEXT,
assignee_id CHAR(36),
status_id CHAR(36),
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
original_estimate DECIMAL(8, 2),
time_spent DECIMAL(8, 2) DEFAULT 0,
blocker_flag BOOLEAN DEFAULT false,
due_date DATETIME,
position INT DEFAULT 0,
created_by CHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES pjctrl_projects(id) ON DELETE CASCADE,
FOREIGN KEY (parent_task_id) REFERENCES pjctrl_tasks(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES pjctrl_users(id),
FOREIGN KEY (status_id) REFERENCES pjctrl_task_statuses(id),
FOREIGN KEY (created_by) REFERENCES pjctrl_users(id)
);
CREATE INDEX idx_tasks_project ON pjctrl_tasks(project_id);
CREATE INDEX idx_tasks_parent ON pjctrl_tasks(parent_task_id);
CREATE INDEX idx_tasks_assignee ON pjctrl_tasks(assignee_id);
CREATE INDEX idx_tasks_status ON pjctrl_tasks(status_id);
CREATE INDEX idx_tasks_due ON pjctrl_tasks(due_date);
CREATE INDEX idx_tasks_blocker ON pjctrl_tasks(blocker_flag);
Permission Model
Security Level 行為
| Security Level | 可見範圍 |
|---|---|
| public | 所有登入使用者 |
| department | 同部門使用者 + 專案成員 |
| confidential | 僅專案成員 |
權限檢查流程
def check_project_access(user, project):
# 系統管理員可存取所有
if user.is_system_admin:
return True
# 專案擁有者
if project.owner_id == user.id:
return True
# 依 security_level 判斷
if project.security_level == 'public':
return True
elif project.security_level == 'department':
return user.department_id == project.department_id
else: # confidential
return is_project_member(user, project)
API Design
Spaces API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/spaces | 列出使用者可見的空間 |
| POST | /api/spaces | 建立新空間 |
| GET | /api/spaces/{id} | 取得空間詳情 |
| PATCH | /api/spaces/{id} | 更新空間 |
| DELETE | /api/spaces/{id} | 刪除空間 (軟刪除) |
Projects API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/spaces/{space_id}/projects | 列出空間內的專案 |
| POST | /api/spaces/{space_id}/projects | 建立新專案 |
| GET | /api/projects/{id} | 取得專案詳情 |
| PATCH | /api/projects/{id} | 更新專案 |
| DELETE | /api/projects/{id} | 刪除專案 |
Tasks API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/projects/{project_id}/tasks | 列出專案內的任務 |
| POST | /api/projects/{project_id}/tasks | 建立新任務 |
| GET | /api/tasks/{id} | 取得任務詳情 |
| PATCH | /api/tasks/{id} | 更新任務 |
| DELETE | /api/tasks/{id} | 刪除任務 |
| PATCH | /api/tasks/{id}/status | 變更任務狀態 |
| PATCH | /api/tasks/{id}/assign | 指派任務 |
Default Task Statuses
每個新專案自動建立以下預設狀態:
| Name | Color | is_done |
|---|---|---|
| To Do | #808080 | false |
| In Progress | #0066cc | false |
| Blocked | #cc0000 | false |
| Done | #00cc66 | true |
Trade-offs & Decisions
- 子任務深度限制: 限制為 2 層 (Task → Sub-task),避免過度巢狀造成的複雜度
- 軟刪除 vs 硬刪除: Space 使用軟刪除 (is_active),Tasks 使用級聯硬刪除
- 狀態定義層級: 狀態定義在 Project 層級,而非全域,提供專案彈性