Spec: F05 — Tasks
Status: shipped · Audited: 2026-06-10
1. Problem & CEO Job
See clearly + trust the signal. Tasks are Lumi’s atoms of work — and unlike the shared Excel sheet and the việc-giao-qua-Zalo our target contractor uses today, every task can carry business meaning: which customer (khách), how much money (công nợ / đặt cọc), what kind of impact (thi công delay, nghiệm thu blocker). That metadata is what lets the Brief say “this overdue task is 300M VND of delivery risk” instead of leaving it as one line in a spreadsheet no one re-reads. A task list the team won’t use produces no signal, so capture must be near-zero friction — faster than typing the same thing into a Zalo group.
2. User Stories & Acceptance Criteria
- As a member, I want to add a task in under 10 seconds, so that I actually record work.
- Given only a title, when I create, then the task exists (
todo,medium,business_impact=none). Everything else is progressive disclosure.
- Given only a title, when I create, then the task exists (
- As a CEO, I want to filter to what threatens money or customers, so that I skip the noise.
- Given filters (status, priority, project, owner, team, overdue), when combined, then results match all of them; “overdue” =
due_date < todayAND status in todo/in_progress/waiting.
- Given filters (status, priority, project, owner, team, overdue), when combined, then results match all of them; “overdue” =
- As a manager, I want to mark a task
waiting, so that blocked work is distinguishable from ignored work —waitingis a signal the chat agent uses for “ai đang bị kẹt?”. - As a member, I want money/customer fields on a task when relevant.
- Given
business_impact=cashandmoney_amount, then the Brief’s cash section can cite the amount (F12).
- Given
- Given status set to
done, thencompleted_atis stamped; reopening clears it. (A-r22:completeaction stamps,reopenclears; PATCH status→done also stamps now — verified 2026-06-12.) - As a client loading a large task history, I want an opt-in cursor path while existing page-number clients keep working unchanged.
-
GET /api/tasks/?pagination=cursorreturns 50 tasks ordered by descending UUIDv7id, with opaquenext/previouscursor links and no duplicate or skipped rows during ordinary forward traversal. - Omitting
paginationkeeps the existing page-number behavior and exact response envelope. - Both modes return
{count,next,previous,results}because shipped mobile clients require all four keys; cursor mode removes deep-pageOFFSET, but the exactcountquery remains until a versioned mobile contract can make it optional.
-
3. Scope Boundaries
- In: CRUD, status/priority/business-impact, owner/team/project links, subtasks (
parent_task), due dates, filters,source_typeprovenance (manual/pulse/ai/recurring/import). - Out: time tracking, estimates, comments/attachments, kanban boards, sprints.
Competitive note — the real alternative is Zalo + a shared Excel sheet. Today our target contractor records work two ways: a message in a Zalo group (“anh nhớ đặt cọc nhà cung cấp nhé”) that scrolls away and is owned by no one, or a row in a shared Excel file that captures a title and maybe a date but carries no business meaning and no follow-up. A task in Lumi needs only a title — as fast as typing that Zalo message — but business_impact, money_amount, customer_name, and owner are progressive disclosure: always optional, always additive. That is why time-to-create stays ≤ 10s (it has to beat a chat message) and why the Brief can cite VND amounts and overdue delivery risk, while a Zalo thread forgets and an Excel “overdue” view is blank on customer and money context. The win is not a richer issue tracker — our owner has never used one — it is turning chat-and-spreadsheet chaos into structured, owned, money-aware work.
4. Data Model
work.Task — status: todo → in_progress ⇄ waiting → done | cancelled. priority: low/medium/high/critical. business_impact: none/customer/revenue/cash/delivery/hiring/legal/operations. customer_name, money_amount + currency (default VND). source_type records provenance — AI- and recurring-born tasks must be distinguishable forever.
5. API Contract
| Method | Path | Min role | Notes |
|---|---|---|---|
| CRUD | /api/tasks/ | member (write) / viewer (read) | Router ViewSet; query params: status, priority, project, owner, team, overdue, pagination=page|cursor; page is the backward-compatible default |
| POST | /api/tasks/<id>/complete/ | member | Stamps completed_at, sets status=done |
| POST | /api/tasks/<id>/reopen/ | member | Clears completed_at, sets status=todo |
Tenant-scoped. List response is the standard {count,next,previous,results} envelope (page size 50).
Pagination behavior:
- No
paginationparameter, orpagination=page: existing page-number pagination (?page=N). pagination=cursor: UUIDv7 keyset pagination ordered by-id; cursor links preserve every active task filter andpagination=cursor.- A
cursorparameter with no explicit mode is treated as cursor mode so extracted cursor links remain usable; explicitpagination=pagewins if both are present. - Unknown
paginationvalues return 400. A malformed/expired cursor returns DRF’s standard 404Invalid cursorresponse. - Cursor mode rejects any explicit
orderingother than-idwith 400; it never silently ignores a requested sort. - Cursor tokens are opaque implementation details; clients follow
next/previousand never construct them.
6. UI / UX
/work/tasks(TasksPage.tsx) — filterable list + inline add form (title-first); create, detail, reorder, and confirmation-dialog code loads on intent. Delete intent warms its confirmation surface, and a dirty form warms discard confirmation before Escape/cancel can request it.- States: empty (“Thêm việc đầu tiên”), filtered-empty (“Không có việc nào khớp bộ lọc” + clear-filters action), loading skeletons.
- Priority/status as
Badgecolor language consistent with Risks (F10) — one severity palette across the app. - Overdue rows visually distinct without reading dates.
- Edit + Delete (added 2026-06-19):
TaskDetailPanel.tsxgains an edit mode for the task’s own title/description/priority/due_date (PATCH /tasks/:id/) plus a confirmation-gated Delete (DELETE /tasks/:id/, closes the panel).TasksPage.tsxrows gain a confirmation-gated Delete. Wired throughuseUpdateTask/useDeleteTask(the latter was previously defined-but-unused). Deletes are audit-logged (F16). Test idstask-edit-save/task-delete-confirm. - i18n:
tasks.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | CRUD + defaults (title-only create) | backend | tests/test_api.py::TestTask, tests/test_lumi.py::TestTaskCRUD |
| 2 | Filter combinations incl. overdue logic | backend | tests/test_api.py::TestTask |
| 3 | Cross-tenant blocked | backend | tests/test_lumi.py::TestTenantIsolation |
| 4 | List, filter, add-form flows | e2e | describe("Tasks") |
| 5 | Legacy page default + explicit page mode remain compatible | backend | tests/work/test_task_cursor_pagination.py |
| 6 | Cursor traversal is stable, filter-preserving, and tenant-scoped | backend | tests/work/test_task_cursor_pagination.py |
| 7 | Invalid mode/cursor failures are explicit | backend | tests/work/test_task_cursor_pagination.py |
8. Current Behavior
Shipped. Tenant-scoped task CRUD, priority/status management, project linkage, and common filters are live. completed_at is server-side stamped by the complete action and cleared by reopen; it is also stamped/cleared by PATCH status→done/todo (A-r22, 2026-06-12). Cross-team task visibility fixed (A-r07, 2026-06-12) — task list now respects project visibility; tasks in private/team projects are filtered by the same get_queryset visibility rules as ProjectViewSet. Task lists support both the backward-compatible page-number default and opt-in UUIDv7 cursor mode on the same endpoint.
9. Error States
| # | Error | Response | Notes |
|---|---|---|---|
| ES1 | Empty title | 400 {"title": ["This field may not be blank."]} | test_create_task_empty_title |
| ES2 | Title > 500 chars | 400 {"title": ["Ensure this field has no more than 500 characters."]} | DRF auto-validator |
| ES3 | Invalid status/priority/source_type/business_impact | 400 {"status": ["\"bogus\" is not a valid choice."]} | Model choices |
| ES4 | Invalid UUID for project/owner/team FK | 400 {"project": ["Invalid pk \"bad-id\" — object does not exist."]} | DRF FK validation |
| ES5 | Missing/malformed X-Organization-ID | 403 | IsOrgMember rejects invalid/non-UUID org IDs |
| ES6 | Non-member X-Organization-ID | 403 | User has no active membership in org |
| ES7 | Cross-tenant detail access | 404 | Queryset scoped to organization_id=request.organization_id |
| ES8 | Delete (204) | 204 — hard delete + AuditLog entry (action="task.deleted") | AuditDestroyMixin |
| ES9 | Complete on already-done task | 200 — re-stamps completed_at; status stays done | Idempotent by design |
| ES10 | Reopen on already-open task | 200 — completed_at stays null; status stays todo | Idempotent |
| ES11 | Complete from in_progress/waiting | 200 — completes from any status; no guard | Intentional for quick-close |
| ES12 | PATCH status→done without complete action | 200 but completed_at stamped (A-r22 fix) | Previously did not stamp; now fixed |
| ES13 | PATCH status→todo without reopen action | 200 — clears completed_at (A-r22 fix) | Previously did not clear |
| ES14 | Contradictory filter (overdue + future due_after) | 200 empty result set — no warning | due_date__lt=today AND due_date__gt=2050-01-01 is an empty set |
| ES15 | Invalid currency value | 200 — any string ≤ 10 chars accepted; no enum | Defaults to “VND”; no validation |
| ES16 | Unknown pagination mode | 400 {"pagination": ["Must be 'page' or 'cursor'."]} | Never silently guesses a new mode |
| ES17 | Malformed/expired cursor | 404 {"detail": "Invalid cursor"} | Standard DRF cursor behavior; no fallback to page 1 |
10. Diagrams & Docs Touched
backend/DIAGRAMS.md Task state machine · GLOSSARY status table.
11. Implementation Audit (2026-06-12)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Title-only create: defaults status=todo, priority=medium, business_impact=none, source_type=manual | ✅ match | test_solid::TestTaskAPI::test_create_task; model defaults |
| 2 | completed_at stamped by complete action + by PATCH status→done (A-r22); cleared by reopen + PATCH status→todo | ✅ match | A-r22 2026-06-12; TestCompletedAt tests in test_red_r22_e.py |
| 3 | Overdue filter: due_date__lt=today AND status__in=[todo,in_progress,waiting]; NULL excluded | ✅ match | TaskViewSet.get_queryset; tests/api/test_operations.py::test_overdue_filter_excludes_null_due_date; tests/api/test_chat_service.py::test_overdue_keyword_returns_overdue_only |
| 4 | Cross-team visibility: task list respects project visibility (A-r07) | ✅ match | A-r07 2026-06-12; TestTaskVisibility — 5 tests |
| 5 | source_type is client-settable — no server enforcement that only AI creates source_type=ai | ✅ fixed | source_type added to read_only_fields in TaskSerializer; client-supplied value silently ignored; default manual always stored. TestTaskCreateFields::test_task_source_type_client_cannot_set_ai |
| 6 | created_by not auto-populated — perform_create only sets organization_id | ✅ fixed | created_by=self.request.membership added to save_kwargs in TaskViewSet.perform_create; also added to read_only_fields to prevent forgery. TestTaskCreateFields::test_task_create_auto_sets_created_by, ::test_task_created_by_cannot_be_forged |
| 7 | money_amount decoupled from business_impact — no cross-field validation | ⚠️ gap | Task can have money_amount with business_impact=none; Brief silently ignores the amount |
| 8 | parent_task self-referential FK — no cycle prevention | ⚠️ gap | A task can reference its own ancestor; no depth limit; no cycle check |
| 9 | Overdue filter timezone boundary: server TZ vs org TZ — may differ by up to 24h | ⚠️ gap | timezone.now().date() is server UTC date; per-org TZ not applied |
| 10 | Hard-delete via AuditDestroyMixin — audit log entry written on every delete | ✅ match | apps/audit/mixins.py; action="task.deleted", target_type="task" |
| 11 | Visual QC: Vietnamese task list shows localized priority badges and task copy | ✅ match | 2026-06-18 screenshot pass; TasksPage.tsx passes translated priority labels to StatusBadge; TasksPage.test.tsx |
| 12 | Visual QC: task create state and row hierarchy help members scan urgency before detail | ✅ match | 2026-06-18 visual QC: /work/tasks now opens with a one-sentence lede + summary chips, the quick-add form explains the title-first workflow with visible labels, and each task row separates priority/status from due date and owner with clearer chip hierarchy. |
Summary: 9/12 match; 3 gaps (money/impact decoupling, no cycle prevention on parent_task, timezone boundary). source_type and created_by gaps closed 2026-06-28.
Edge Cases Catalog (formerly §10, 2026-06-12 + additions 2026-06-12; all items): Key items integrated into audit rows 5–9 and §9 error states. Full catalog: title validation (ES1-ES2), title-only defaults (row 1), source_type/created_by writability (rows 5–6), money/impact decoupling (rows 7 + ES15), parent_task cycles (row 8), nullable due_date (ES7 implicitly), overdue exclusion (row 3), complete/reopen idempotency (ES9-ES10), completed_at read-only (row 2), hard-delete cascade (ES8), CRM FK links (company/deal — no cross-tenant validation), evidence_sources M2M, subtask boundary, overdue TZ boundary (row 9), due_after/due_before with overdue (ES14), past dates, default ordering -created_at. Full catalog in git history.
12. Open Questions
- Should title-only quick-add learn defaults (last-used project)? Only if observed friction; not before.
13. Bug / Change Log
2026-07-16 — Task confirmations leave the cold route graph
Type: Frontend performance behavior change
Summary:
- Moved row-delete and dirty-form discard confirmations into an intent-warmed lazy module while preserving their existing localized shadcn dialogs, focus behavior, and confirmation gates.
- The confirmation module preloads on delete pointer/focus intent and as soon as the create form becomes dirty, so touch, keyboard, and Escape paths retain the same workflow.
- The focused production build lowers the Tasks static graph from 286.3 to 273.1 kB gzip and 51 to 43 files. A three-run seeded floor subset measures 299.8 kB browser JS, 3888 ms LCP, 1484 ms FCP, and 0.000 CLS; only a subsequent full PM audit may change W-SYS scores.
Tests:
frontend/src/test/_shared/bundle-budget.test.tsfrontend/src/test/work/TasksPage.test.tsx
2026-07-15 — Task interactions load on intent without losing workflow
Type: Frontend performance behavior change
Summary:
- Kept create, inspect, complete, delete, deep-link, dirty-discard, and focus behavior intact while moving the create form and detail panel out of the initial route graph.
- Replaced always-mounted drag-and-drop with an explicit reorder mode. The normal list remains actionable, and activating reorder loads the same keyboard-accessible DnD implementation.
- On the 320 px floor, task cards use a three-column mobile grid so title/details keep readable width while completion, status, and delete controls remain reachable.
- Added focused behavior and dependency-custody coverage; no API, tenancy, or task-state contract changed.
Tests:
frontend/src/test/work/TasksPage.test.tsxfrontend/src/test/_shared/bundle-budget.test.ts
Visual evidence:
docs/tests/evidence/web/W-WRK-01/2026-07-15-vi-320.jpg- Chromium + Firefox audit tour, en/vi, 1440/320
2026-07-14 — Subtask validation errors stay inside the task detail flow
Type: Frontend bug fix
Summary:
- A rejected subtask creation request is now caught by the task detail panel and shown through the existing localized task-creation error toast.
- The failed title remains available for correction instead of escaping as an unhandled Axios rejection to Sentry.
- Added an accessible label for the icon-only add-subtask control in English and Vietnamese.
Tests:
frontend/src/test/work/TaskDetailPanel.test.tsx
2026-07-13 — Contract Graph migration slice and viewer-write fix
Type: Contract and authorization traceability
Summary:
- Added the canonical F05 CUE contract for the six base task CRUD surfaces plus
completeandreopen, with executable proof IDs covering defaults, invalid status, tenant isolation, filters, lifecycle, and role boundaries. - Contract proof collection found that
TaskViewSetapplied onlyIsOrgMemberto mutation actions, allowing viewers to create, update, delete, complete, reopen, and reorder tasks despite the spec’s member-write/viewer-read boundary. AddedIsTaskWriterand a role-deny regression test. - Nested assignees, dependencies, files, execution events, worklogs, and field-day routes remain explicitly outside F05 for later F49/F90 slices.
Impact: Viewer task access is now read-only; active non-viewer members retain task mutation access.
Tests: The Contract Graph verifier collects and passes all 10 F05 pytest proof records. F05 remains
partial because task-boundary, timezone, browser, and named-platform runtime gaps are explicit in
contracts/features/F05.cue.
2026-07-13 — Additive task cursor-pagination contract
Type: Performance + API Compatibility
Shipped change:
- Preserve page-number pagination as the default for all shipped clients.
- Add
pagination=cursoron the same task-list endpoint, ordered by UUIDv7-idand backed by an(organization, -id)index. - Preserve the app-wide
{count,next,previous,results}envelope required by shipped mobile clients. - Add traversal, filtering, tenant-isolation, invalid-mode, and invalid-cursor regression tests before implementation.
Reason:
- Deep page-number pagination pays increasing
OFFSETcost as task history grows. Cursor mode makes the future scale upgrade mechanical without forcing an immediate client migration or changing existing responses.
Verification:
backend/tests/work/test_task_cursor_pagination.py— 9 tests, including SQL proof that cursor page two contains noOFFSET.backend/tests/core/test_pagination_envelope_contract.py— app-wide four-key envelope guard.backend/tests/api/test_projects_tasks.py,backend/tests/work/test_work_graph_routes.py,backend/tests/work/test_work_graph_depth.py, andbackend/tests/platform/test_performance.py— 121 regression tests.
2026-07-02 — Documentation truth: correct §11 row 3 overdue test citations
Type: Documentation Change
Summary:
- §11 row 3 cited
test_task_overdue_filterwhich does not exist in any test file. Corrected to the two actual overdue tests:tests/api/test_operations.py::test_overdue_filter_excludes_null_due_dateandtests/api/test_chat_service.py::test_overdue_keyword_returns_overdue_only. The overdue filter implementation itself is correct and unchanged.
Reason:
grep -rn "test_task_overdue_filter"acrossbackend/tests/produced no results. The replacement test names were confirmed withgrep -rn "test_overdue".
Impact:
- Documentation only. No API, DB, or UI behavior changed.
Tests:
- None (documentation change).
2026-06-28 — Close §11 rows 5 & 6: auto-populate created_by, enforce source_type read-only
Type: Security + Data Integrity Fix
Summary: TaskViewSet.perform_create was not setting created_by, leaving it NULL for all API-created tasks — breaking Brief attribution and audit trail. Any client could also pass source_type=ai to forge AI authorship. Fix: (1) added created_by=self.request.membership to save_kwargs; (2) added "created_by" and "source_type" to TaskSerializer.Meta.read_only_fields. Three new tests in TestTaskCreateFields cover both paths. Spec §11 rows 5 and 6 flipped to ✅.
2026-06-19 — Add task Edit + Delete affordances
Type: Feature (CRUD completion)
Summary: TaskViewSet (ModelViewSet) supported PATCH/DELETE but the UI only exposed create + status change + complete + reorder — title/description/priority/due were uneditable after creation and tasks could not be deleted (useDeleteTask existed but was never called). Added an edit mode + delete to TaskDetailPanel.tsx and a delete to TasksPage.tsx rows, both confirmation-gated. Deletes are audit-logged (F16). i18n keys added to en/vi. See §6.
2026-06-18 — Visual QC: Localize task priority badges and CTA copy
Type: UX Fix
Summary:
- The Vietnamese Tasks screenshot showed raw backend priority text (
high,medium,low) on task rows. - Task rows now pass translated priority labels into
StatusBadge, preserving the shared priority/severity color mapping. - Vietnamese task creation copy now uses
công việcinstead of mixed-languagetaskin the first viewport.
Verification:
frontend/screenshots/07-tasks/01-tasks-list.pngpnpm test -- src/test/TasksPage.test.tsxpnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "07-tasks"
2026-06-18 — Visual QC: task page lede, quick-add hierarchy, and calmer row metadata
Type: UX Fix
Summary:
- The screenshot audit showed
/work/tasksfunctioning correctly but reading a little too much like a raw list: the creation state had no visible framing, and row metadata felt like one unstructured strip. - Added a short page lede plus summary chips for open, waiting, and overdue work so the first viewport communicates operating load immediately.
- The quick-add card now names the title-first workflow and exposes visible labels instead of relying only on placeholders.
- Task rows now show status as a proper outline badge and reveal descriptions when present, which makes each row read more like a compact work summary than a bare title line.
Verification:
frontend/screenshots/07-tasks/01-tasks-list.pngfrontend/screenshots/07-tasks/02-add-task-form.png
2026-06-15 — Frontend route namespaced
Frontend route namespaced: /tasks -> /work/tasks (app-wide IA restructure; API paths unchanged).
2026-06-14 — Strategy v2 realignment (positioning)
Type: Documentation Change
Summary:
- Reframed two positioning passages. §1 intro: “unlike Jira” → “unlike the shared Excel sheet and việc-giao-qua-Zalo our target contractor uses today,” with trade vocabulary (khách / công nợ / đặt cọc / thi công / nghiệm thu). §3 “Competitive note — Jira” → “Competitive note — the real alternative is Zalo + a shared Excel sheet”: the bar is beating a chat message and a spreadsheet row on capture friction and business meaning, not out-tracking an issue tracker the owner has never used.
Reason:
- Realign to competitive-strategy-v2.md (Inversion 7 — real competitor is Zalo/WhatsApp + Excel; §6.4 / §7.4 vertical vocabulary) and the registry “Positioning doctrine (v2)” rules 1 and 4. The product fact (title-only create; business_impact/money_amount/customer_name/owner as progressive disclosure feeding the Brief) is unchanged — only the competitive frame moved.
Impact: Positioning only. No behavior change — CRUD, defaults, filters, completion stamping, and tenant scoping are all unchanged.
Tests: None (documentation change).
Notes: No edits to §4 Data Model, §5 API Contract, §7 Test Plan, §9 Error States, §11 Implementation Audit, or the Status line.
2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)
Type: Documentation Change
Summary:
- Numbered “Current Behavior” as §8.
- §8 Diagrams → §10; §9 Implementation Audit (narrative) → §11 (10-row table); §10 Edge Cases absorbed into §11 note; §11 Error States → §9; §12 Bug/Change Log and §13 Open Questions merged/reordered → §12 Open Questions, §13 Bug Log.
- §5 API contract: added
completeandreopenaction rows (were implicit). - §11 audit: 10 rows, 5 ✅, 5 gaps (source_type, created_by, money/impact, parent_task cycles, TZ boundary).
2026-06-12 — A-r22: completed_at stamped/cleared on PATCH status transitions
Type: Bug Fix
Summary:
perform_updatenow stampscompleted_at=timezone.now()when PATCH setsstatus=doneand clears it (completed_at=None) when PATCH setsstatus=todoorreopenaction fires.completed_atis read-only in the serializer.- Tests:
TestCompletedAtintest_red_r22_e.py— PATCH status=done stamps, PATCH/reopen clears, cross-tenant isolation, same-status no-op.
2026-06-12 — A-r07: Cross-team task visibility fix
Type: Bug Fix
Summary:
TaskViewSet.get_queryset()now respects project visibility (org-wide / team / private).- Tasks with
project=Noneremain org-wide. Explicit?project_id=bypasses visibility filter. - Tests:
TestTaskVisibility— 5 tests.
2026-06-12 — Spec hardening: Edge Cases & Error States added and verified (D-r22)
Type: Documentation Change
2026-06-12 — Success metrics + competitive framing (G-r17)
Type: Documentation Change
14. Success Metrics (Appendix)
- Time-to-create a task ≤ 10s (P95): measured client-side from “Add task” tap to 201 response. Tied to the title-only creation flow in §2 — any flow that requires more than a title is progressive disclosure, never blocking.
- Business-impact adoption > 60%: share of tasks created in the last 30 days with
business_impact != none. Measures whether the team enriches task data enough for the Brief to surface money- and customer-linked signals (F12). - Overdue signal density: share of overdue tasks carrying
business_impact != none. If overdue tasks lack business meaning, the Brief’s overdue section is noise — not signal (F12 §1).