shipped

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.
  • 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 < today AND status in todo/in_progress/waiting.
  • As a manager, I want to mark a task waiting, so that blocked work is distinguishable from ignored work — waiting is 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=cash and money_amount, then the Brief’s cash section can cite the amount (F12).
  • Given status set to done, then completed_at is stamped; reopening clears it. (A-r22: complete action stamps, reopen clears; 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=cursor returns 50 tasks ordered by descending UUIDv7 id, with opaque next/previous cursor links and no duplicate or skipped rows during ordinary forward traversal.
    • Omitting pagination keeps 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-page OFFSET, but the exact count query 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_type provenance (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

MethodPathMin roleNotes
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/memberStamps completed_at, sets status=done
POST/api/tasks/<id>/reopen/memberClears completed_at, sets status=todo

Tenant-scoped. List response is the standard {count,next,previous,results} envelope (page size 50).

Pagination behavior:

  • No pagination parameter, or pagination=page: existing page-number pagination (?page=N).
  • pagination=cursor: UUIDv7 keyset pagination ordered by -id; cursor links preserve every active task filter and pagination=cursor.
  • A cursor parameter with no explicit mode is treated as cursor mode so extracted cursor links remain usable; explicit pagination=page wins if both are present.
  • Unknown pagination values return 400. A malformed/expired cursor returns DRF’s standard 404 Invalid cursor response.
  • Cursor mode rejects any explicit ordering other than -id with 400; it never silently ignores a requested sort.
  • Cursor tokens are opaque implementation details; clients follow next/previous and 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 Badge color 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.tsx gains 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.tsx rows gain a confirmation-gated Delete. Wired through useUpdateTask/useDeleteTask (the latter was previously defined-but-unused). Deletes are audit-logged (F16). Test ids task-edit-save/task-delete-confirm.
  • i18n: tasks.

7. Test Plan

#CaseTypeWhere
1CRUD + defaults (title-only create)backendtests/test_api.py::TestTask, tests/test_lumi.py::TestTaskCRUD
2Filter combinations incl. overdue logicbackendtests/test_api.py::TestTask
3Cross-tenant blockedbackendtests/test_lumi.py::TestTenantIsolation
4List, filter, add-form flowse2edescribe("Tasks")
5Legacy page default + explicit page mode remain compatiblebackendtests/work/test_task_cursor_pagination.py
6Cursor traversal is stable, filter-preserving, and tenant-scopedbackendtests/work/test_task_cursor_pagination.py
7Invalid mode/cursor failures are explicitbackendtests/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

#ErrorResponseNotes
ES1Empty title400 {"title": ["This field may not be blank."]}test_create_task_empty_title
ES2Title > 500 chars400 {"title": ["Ensure this field has no more than 500 characters."]}DRF auto-validator
ES3Invalid status/priority/source_type/business_impact400 {"status": ["\"bogus\" is not a valid choice."]}Model choices
ES4Invalid UUID for project/owner/team FK400 {"project": ["Invalid pk \"bad-id\" — object does not exist."]}DRF FK validation
ES5Missing/malformed X-Organization-ID403IsOrgMember rejects invalid/non-UUID org IDs
ES6Non-member X-Organization-ID403User has no active membership in org
ES7Cross-tenant detail access404Queryset scoped to organization_id=request.organization_id
ES8Delete (204)204 — hard delete + AuditLog entry (action="task.deleted")AuditDestroyMixin
ES9Complete on already-done task200 — re-stamps completed_at; status stays doneIdempotent by design
ES10Reopen on already-open task200 — completed_at stays null; status stays todoIdempotent
ES11Complete from in_progress/waiting200 — completes from any status; no guardIntentional for quick-close
ES12PATCH status→done without complete action200 but completed_at stamped (A-r22 fix)Previously did not stamp; now fixed
ES13PATCH status→todo without reopen action200 — clears completed_at (A-r22 fix)Previously did not clear
ES14Contradictory filter (overdue + future due_after)200 empty result set — no warningdue_date__lt=today AND due_date__gt=2050-01-01 is an empty set
ES15Invalid currency value200 — any string ≤ 10 chars accepted; no enumDefaults to “VND”; no validation
ES16Unknown pagination mode400 {"pagination": ["Must be 'page' or 'cursor'."]}Never silently guesses a new mode
ES17Malformed/expired cursor404 {"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)

#ClaimStatusEvidence
1Title-only create: defaults status=todo, priority=medium, business_impact=none, source_type=manual✅ matchtest_solid::TestTaskAPI::test_create_task; model defaults
2completed_at stamped by complete action + by PATCH status→done (A-r22); cleared by reopen + PATCH status→todo✅ matchA-r22 2026-06-12; TestCompletedAt tests in test_red_r22_e.py
3Overdue filter: due_date__lt=today AND status__in=[todo,in_progress,waiting]; NULL excluded✅ matchTaskViewSet.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
4Cross-team visibility: task list respects project visibility (A-r07)✅ matchA-r07 2026-06-12; TestTaskVisibility — 5 tests
5source_type is client-settable — no server enforcement that only AI creates source_type=ai✅ fixedsource_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
6created_by not auto-populated — perform_create only sets organization_id✅ fixedcreated_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
7money_amount decoupled from business_impact — no cross-field validation⚠️ gapTask can have money_amount with business_impact=none; Brief silently ignores the amount
8parent_task self-referential FK — no cycle prevention⚠️ gapA task can reference its own ancestor; no depth limit; no cycle check
9Overdue filter timezone boundary: server TZ vs org TZ — may differ by up to 24h⚠️ gaptimezone.now().date() is server UTC date; per-org TZ not applied
10Hard-delete via AuditDestroyMixin — audit log entry written on every delete✅ matchapps/audit/mixins.py; action="task.deleted", target_type="task"
11Visual QC: Vietnamese task list shows localized priority badges and task copy✅ match2026-06-18 screenshot pass; TasksPage.tsx passes translated priority labels to StatusBadge; TasksPage.test.tsx
12Visual QC: task create state and row hierarchy help members scan urgency before detail✅ match2026-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.ts
  • frontend/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.tsx
  • frontend/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 complete and reopen, with executable proof IDs covering defaults, invalid status, tenant isolation, filters, lifecycle, and role boundaries.
  • Contract proof collection found that TaskViewSet applied only IsOrgMember to mutation actions, allowing viewers to create, update, delete, complete, reopen, and reorder tasks despite the spec’s member-write/viewer-read boundary. Added IsTaskWriter and 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=cursor on the same task-list endpoint, ordered by UUIDv7 -id and 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 OFFSET cost 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 no OFFSET.
  • 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, and backend/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_filter which 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_date and tests/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" across backend/tests/ produced no results. The replacement test names were confirmed with grep -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ệc instead of mixed-language task in the first viewport.

Verification:

  • frontend/screenshots/07-tasks/01-tasks-list.png
  • pnpm test -- src/test/TasksPage.test.tsx
  • pnpm 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/tasks functioning 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.png
  • frontend/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 complete and reopen action 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_update now stamps completed_at=timezone.now() when PATCH sets status=done and clears it (completed_at=None) when PATCH sets status=todo or reopen action fires.
  • completed_at is read-only in the serializer.
  • Tests: TestCompletedAt in test_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=None remain 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).