shipped (2026-06-10)

Spec: F23 — Deadline Horizon (was “Calendar view”)

Status: shipped (2026-06-10)

1. Problem & CEO Job

See clearly — forward. Brief and Today show what’s burning now; the CEO also needs where promises and dues stack up this week, and which days are overloaded. That is a deadline exposure view, not a calendar.

⚠️ Scope conflict, resolved: docs/AGENTS.md → What Not To Build bans “Calendar/Gantt views” — correctly. This spec therefore defines not a calendar: no month grid, no drag-to-reschedule, no Gantt. It is a forward-looking ranked list grouped by day. If during build it starts resembling Google Calendar, stop — the spec has been violated. Update the What-Not-To-Build entry to reference this distinction in the implementing PR.

2. User Stories & Acceptance Criteria

  • As a CEO, I want the next 14 days as day-grouped deadlines, so that I see crunch before it arrives.
    • Given /work/horizon, then days with due items render in order; each day lists its tasks + commitments + decision deadlines (decision_needed_by), each with owner and impact badge.
    • Given a day with nothing due, it collapses to nothing — no empty grid cells (this is a list, not a calendar).
    • Overdue items appear pinned above day 1 (“Quá hạn — N items”) — the past-due backlog is part of the horizon.
  • As a CEO, I want money exposure visible per day where data exists (“thứ 5: 2 cam kết, ~450tr”).
  • Read-only in v1: tapping a web item navigates to its owning list; tapping a task on the shared Android/iOS Horizon opens the canonical Task Detail for that exact task. No editing or rescheduling happens inside Horizon.

3. Scope Boundaries

  • In: 14-day day-grouped deadline list over existing Task/Commitment/DecisionRequest data; overdue pin; impact/money annotations.
  • Out (hard): month grid, week grid, Gantt, drag-reschedule, event creation, external calendar sync, recurring-task future projection (only materialized tasks appear).

4. Data Model

None new. Reads existing models by due_date / decision_needed_by.

5. API Contract (as built)

No new endpoints — composing 3 existing list endpoints with new date-range query params:

EndpointNew paramsField
GET /api/tasks/due_after, due_beforedue_date
GET /api/commitments/due_after, due_beforedue_date
GET /api/decisions/needed_after, needed_beforedecision_needed_by

All filters are inclusive (gte/lte). Null due dates are excluded from range results (standard Django behavior). Tenant isolation enforced by existing organization_id scoping in each ViewSet.

6. UI / UX

  • New route /work/horizon + page; nav label “Sắp tới” (not “Lịch” — naming shapes expectations).
  • Visual: vertical day-sectioned list, same item cards as Today (F13) — zero new component vocabulary to learn.
  • States: empty horizon is positive (“14 ngày tới chưa có deadline dồn”); loading skeletons; inline error + retry when the load fails completely (reuses the EmptyState primitive with a TriangleAlert glyph and a retry Button that refetches) — never the misleading positive empty-state. A partial failure degrades gracefully (renders the lists that loaded).
  • i18n: new horizon namespace, en+vi together.
  • Data fetching: composed via the useHorizonData TanStack Query hook (seven org-scoped list reads), per the codebase server-state convention — not useEffect + manual fetch.

7. Test Plan

#CaseTypeWhere
1Date-range filters return correct itemsbackendbackend/tests/api/test_pulses_horizon.py::TestHorizonFilters
2Cross-tenant isolation on date-range paramsbackendbackend/tests/api/test_pulses_horizon.py::TestHorizonFilters
3Page renders day groups + overdue pin; item tap navigatese2efrontend/tests/e2e/f23-horizon.spec.ts
4Empty days collapse and the route remains usefule2efrontend/tests/e2e/f23-horizon.spec.ts
5Overdue pin: active overdue only, excludes done/closed, caps at 20unitHorizonPage.test.tsx (buildOverdueItems)
6Day groups: group by day, sum money, skip empty daysunitHorizonPage.test.tsx (buildDayGroups)
7Renders overdue + day groups + money; week-2 collapsed until expandedunitHorizonPage.test.tsx
8Item tap navigates to the correct list pageunitHorizonPage.test.tsx
9Positive empty state when nothing is dueunitHorizonPage.test.tsx
10Total load failure → inline error + retry, never the empty stateunitHorizonPage.test.tsx
11Partial failure degrades gracefully (renders what loaded)unitHorizonPage.test.tsx
12Populated shared mobile refresh failure keeps grouped tasks visible through repeated failure; explicit Retry recoversshared JVM + Android runtime policyHorizonScreenModelTest, test_f23_horizon_mobile_recovery_policy.py, flow 159
13Shared mobile task card forwards its exact task ID into typed Android/iOS Task Detail routingshared JVM source contractHorizonTaskNavigationSourceTest

Backend coverage ≥99%; 3-test E2E describe("Horizon") block shipped; 8-test HorizonPage.test.tsx unit suite covers the full state matrix (loading→data, overdue, money, week-2 expand, navigation, empty, total-error+retry, partial-failure degradation).

8. Current Behavior

Shipped as Deadline Horizon. The current built surface reflects the renamed, narrower executive-focus contract rather than a generic calendar product. Route /work/horizon is live; nav label is “Sắp tới”; week-2 items collapse by default and expand on tap. Overdue items pin above the day list.

The shared Android/iOS task-only Horizon keeps the last committed grouped timeline visible when a populated refresh fails. It places one compact localized warning and explicit Retry above the readable tasks; repeated failures retain the same snapshot, successful retry clears the warning, and only an initial failure without data uses a blocking recovery state.

Each populated task card is one accessible activation target. Android maps the exact task ID to TaskDetailNavKey; iOS maps it to SharedLaneRoute.taskDetail. Shared hosts without a task-detail callback keep the card read-only rather than exposing a dead control.

Architecture decision (2026-06-10): build the Deadline Horizon as specced; never build a calendar. If stakeholders specifically want a calendar grid, that is a new scope conversation, not this feature.

9. Error States

  1. Invalid date-format in query params. due_after=not-a-date — the spec does not define whether the backend returns 400 or silently ignores the param and returns 14 days. Gap: error format for invalid filter params is unspecified.
  2. One of the endpoints returns 5xx. Resolved (2026-06-21). The page composes seven org-scoped reads via useHorizonData. A partial failure degrades gracefully — the lists that loaded still render; only a total failure (every query failed) shows the inline error state. Covered by HorizonPage.test.tsx (partial-failure degradation + total-error).
  3. Network timeout during page load. Resolved (2026-06-21). A total load failure now renders an inline error state (EmptyState + TriangleAlert + retry Button that refetches via TanStack Query), replacing the previous toast-only path that misleadingly fell through to the positive empty-state. Covered by HorizonPage.test.tsx.
  4. Cross-tenant data leak attempt. Tenant isolation is enforced by existing organization_id scoping in each ViewSet (§5). New date-range params must be regression-tested to confirm they do not bypass scoping.
  5. Date-range params exceeding 14 days. The API does not appear to clamp the window. A client could request a 1000-day range. Gap: server-side range limit is not defined.
  6. Native populated refresh failure. Resolved structurally and on a managed Android emulator (2026-08-10). The shared model carries current-or-stale grouped tasks across repeated failures, and the shared UI keeps them readable behind one concise Retry row. iOS, named-device, accessibility-service, store, deployed, and production behavior remain explicit evidence gaps.

10. Diagrams & Docs Touched

frontend/DIAGRAMS.md route tree · docs/AGENTS.md What-Not-To-Build clarification · registry F23 row.

11. Implementation Audit (2026-06-14)

#ClaimStatusEvidence
1GET /api/tasks/ accepts due_after/due_before✅ matchP03 PR-1; TestHorizonFilters in backend/tests/api/test_pulses_horizon.py
2GET /api/commitments/ accepts due_after/due_before✅ matchsame PR and test class
3GET /api/decisions/ accepts needed_after/needed_before✅ matchsame PR and test class
4TestHorizonFilters covers 4 test cases✅ matchP03 PR-1 commit note
5HorizonPage.tsx exists with day-grouped list + overdue pin + week-2 expander✅ matchP03 PR-2; frontend/src/pages/work/HorizonPage.tsx
6Route /work/horizon registered✅ matchP03 PR-2; App.tsx + frontend/DIAGRAMS.md
7horizon i18n namespace with en+vi (12 keys each)✅ matchP03 PR-2; i18n/en/horizon.json, i18n/vi/horizon.json
8frontend/DIAGRAMS.md route tree updated✅ matchP03 PR-2 explicit note
9Three registered Chromium Horizon journeys execute✅ matchfrontend/tests/e2e/f23-horizon.spec.ts; 3/3 passed on commit 972d3643d (2026-07-20)
10No new data model — pure composition✅ match§4 data model: “None new”

Gaps flagged (not blocking — unspecified behavior):

  • Timezone/date boundary (Edge case 4, 9): due_date fields and filter params are date-only but timezone handling is not specified — a task due “2026-06-12” could fall on the wrong boundary.
  • Week-2 collapse with empty week 1 (Edge case 6): expander count/visibility when week 1 is empty is unstated.
  • Overdue-only view (Edge case 2): layout when all items are overdue and the day list is empty is not specified.
  • Server-side range clamping (§9 error 5): due_before is unclamped — a 1000-day request is valid per current spec.
  • 14-day vs 7-day open question (§12): start with 14, collapse beyond day 7 by default. Validate with first real users.

12. Open Questions / Decisions

  • 14 days vs 7: start with 14, collapse beyond day 7 by default. Validate with first real users — if week 2 is never expanded, consider hiding it permanently.

13. Bug / Change Log

2026-08-12 - Native Horizon tasks open Task Detail

Type: Behavior Change

Summary:

  • Made each populated shared Horizon task card an accessible activation target with a stable task ID tag.
  • Routed the exact task ID through the existing Android and iOS typed Task Detail destinations.
  • Kept hosts without a Task Detail callback read-only, with no fake or dead action.

Reason:

  • Native Horizon showed actionable deadlines as static cards, forcing users to leave the timeline and find the same task again.

Impact:

  • Shared KMP Horizon UI plus Android and iOS shell routing only. API, grouping, recovery, and Mac behavior are unchanged.

Tests:

  • HorizonTaskNavigationSourceTest guards shared semantics and both typed shell routes.
  • Guarded Android flow 159 passed exact task activation and Back on the built/installed prodDebug APK SHA-256 015417dbcab768be98142d82476526c2bda9b30f747d6b1cb2b9b6cb4ad416a4, after recovering the populated timeline from two bounded task-list failures. Inspected task-detail, failure, and recovery frames plus the proxy trace are retained under docs/tests/evidence/WRK-09/. This does not claim iOS, named-device, accessibility-service, store, deployed, or production proof.

2026-08-10 — Shared mobile Horizon refresh recovery

Type: Production UX recovery / native runtime evidence

Summary:

  • Replaced the populated native blocking error page with a compact localized warning and explicit Retry while preserving the last committed grouped task timeline.
  • Changed reload and retry to read current-or-stale state so a second failure cannot discard the snapshot; added a focused repeated-failure → successful-retry model proof.
  • Added guarded Android flow 159 with an isolated tenant, two dated tasks, an allowlisted two-request GET /api/tasks/ outage, exact built/installed APK identity, server custody, cleanup, retained failure/recovery frames, and runtime shutdown.
  • Exact Android emulator APK SHA-256: 90a2d14596b07953c6036834b47ca29d8a9ffc4e68f1deac20abaee4ffc2dfba. This does not claim iOS, named-device, accessibility-service, store, deployed, or production proof.

2026-07-20 — Commit-bound Horizon browser execution

Type: Runtime evidence / Contract Graph

Summary:

  • Executed the three registered f23-horizon.spec.ts journeys in canonical Chromium against the real local backend/frontend servers on merge commit 972d3643d.
  • Heading/deadline rendering, omission of empty-day placeholders, and read-only item navigation all passed (3/3). This removes only the stale browser-runtime gap.
  • Invalid-date policy, range bounds, timezone semantics, and named Android/iOS/macOS runtime remain explicit gaps; no native, live-provider, Redis, Sentry, visual, or production evidence is inferred.

2026-07-14 — Contract Graph migration and explicit runtime/date boundaries

Type: Contract / evidence audit

Summary:

  • Added contracts/features/F23.cue for the three date-window APIs, Horizon composition/state matrix, and the real browser journey.
  • Registered proof IDs in pytest, Vitest, and Playwright test titles and added the implementation audit plus dedicated test plan.
  • Reclassified invalid-date behavior, wide-range policy, timezone semantics, browser runtime, and named native runtime as explicit gaps; the browser gap was later closed by the 2026-07-20 commit-bound Chromium run, while the other four remain.

2026-07-10 — KMP mobile: humanize “Next [weekday]” headers for days 7–13

Type: Polish (UX)

Summary:

  • Added HorizonGroupHeader.NextWeekday(val isoDayNumber: Int) to the sealed interface in LumiDates.kt.
  • Updated humanizeHorizonHeader(): days 7..13 now return NextWeekday (previously fell through to Dated, showing raw ISO strings like “2026-07-17” as section headers).
  • Updated DateSectionHeader in HorizonScreen.kt to map NextWeekday to “Next Monday” … “Next Sunday” / “Thứ Hai tuần sau” … “Chủ Nhật tuần sau” via 7 new KMP string resources in both values/ and values-vi/.
  • Updated HorizonGroupHeaderTest.kt: 4 new tests (7 days → NextWeekday Friday, 8 days → NextWeekday Saturday, 13 days → NextWeekday Wednesday, 14 days → Dated boundary); changed the existing “seven days ahead returns Dated” assertion to assert NextWeekday.

Impact:

  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/LumiDates.kt
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/horizon/HorizonScreen.kt
  • android/shared/src/commonMain/composeResources/values/strings.xml (+7 en keys)
  • android/shared/src/commonMain/composeResources/values-vi/strings.xml (+7 vi keys)
  • android/shared/src/jvmTest/kotlin/com/locuno/lumi/shared/ui/HorizonGroupHeaderTest.kt (+4 tests, 1 updated)
  • POLISH WRK-09: UX raw-ISO-header blocker resolved.

Tests:

  • HorizonGroupHeaderTest (17 tests), pure jvmTest (no Compose runtime required).

2026-07-02 — Correct stale page path in §11 row 5 (batch-1 spec audit)

Type: Documentation Fix

Summary:

  • §11 row 5: Updated frontend/src/pages/HorizonPage.tsx → frontend/src/pages/work/HorizonPage.tsx to reflect the IA restructure. Verified by find frontend/src -name HorizonPage.tsx = frontend/src/pages/work/HorizonPage.tsx.
  • Historical note: the 2026-06-10 change log entry (PR-2) records the original creation path frontend/src/pages/HorizonPage.tsx (new) — that entry is preserved unchanged (historical record). The 2026-06-15 entry records the route rename /horizon → /work/horizon; the 2026-06-19 entry already references the correct frontend/src/pages/work/HorizonPage.tsx.

2026-06-21 — TanStack Query migration + inline error state + QA suite

Type: Polish (QA/QC + UX)

Summary:

  • Migrated HorizonPage data fetching from useEffect + Promise.all to the codebase-standard useHorizonData TanStack Query hook (seven org-scoped reads), completing the previously abandoned, incomplete useHorizon.ts hook and removing the convention drift.
  • Extracted grouping into pure, unit-testable helpers (buildOverdueItems, buildDayGroups).
  • Replaced the toast-only error path — which let a failed load fall through to the positive empty-state (“no deadlines in 14 days”) — with an inline error + retry state on total failure; partial failures now degrade gracefully (render the lists that loaded). Resolves §9 errors 2–3 and the UI/UX audit Remaining Issue #2.
  • New i18n keys error_title, error_retry (en + vi, propagated to all locales).
  • Expanded HorizonPage.test.tsx from 1 happy-path test to an 8-test state matrix.

Impact:

  • frontend/src/pages/work/HorizonPage.tsx, frontend/src/hooks/queries/useHorizon.ts, frontend/src/test/HorizonPage.test.tsx, i18n/*/horizon.json.
  • No API/route/model change; tenant isolation unchanged (existing org-scoped list endpoints).
  • POLISH F23: QA/QC 2 → 9, UX 8 → 9.

Tests:

  • pnpm test HorizonPage (8 unit tests), tsc --noEmit clean, i18n parity green.

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /horizon -> /work/horizon (app-wide IA restructure; API paths unchanged).

2026-06-14 — Restructure to TEMPLATE.md section order (SP fleet)

Type: Documentation Change

Summary:

  • Renumbered all sections to match TEMPLATE.md (§8 Current Behavior, §9 Error States, §10 Diagrams, §11 Implementation Audit, §12 Open Questions, §13 Bug / Change Log).
  • Added §11 Implementation Audit table (10 claims, all ✅ match).
  • Folded §12 Edge Cases into §11 Audit gaps and §9 Error States.

Reason:

  • Section order was non-standard — §9 was Implementation Audit, §13 was Error States.

Impact:

  • No behavior change. Spec more useful as audit reference.

Tests:

  • No code changes.

2026-06-12 — Added Edge Cases and Error States sections (CMD fleet agent E, round 11)

Type: Documentation Change

Summary:

  • Added Edge Cases (10 items covering empty/overdue/boundary/collapse/partial-data/timezone concerns).
  • Added Error States (7 items covering invalid input, partial failure, timeout, concurrency, range limits).

Reason:

  • Round 11 spec-hardening: identify gaps without inventing new behavior.

Impact:

  • F23 spec is more useful as a review and implementation reference.

Tests:

  • No code changes.

Notes:

  • Several gaps flagged as explicitly unspecified rather than resolved.

2026-06-10 — Date-range filter params (P03 PR-1)

Type: Feature

Summary:

  • Added due_after/due_before query params to Task and Commitment list endpoints.
  • Added needed_after/needed_before query params to DecisionRequest list endpoint.

Reason:

  • Required for Horizon page to fetch 14-day window without a dedicated endpoint.

Impact:

  • apps/work/views.py, apps/operations/views.py.

Tests:

  • TestHorizonFilters (4 tests) in backend/tests/test_api.py.

2026-06-10 — Horizon page frontend (P03 PR-2)

Type: Feature

Summary:

  • Added HorizonPage.tsx: 14-day deadline list, overdue pin, week-2 expander.
  • Route /horizon in App.tsx; CalendarRange icon in Sidebar.tsx.
  • horizon i18n namespace: en/horizon.json + vi/horizon.json, 12 keys each.
  • E2E: describe("Horizon") — 3 tests in lumi.spec.ts.
  • frontend/DIAGRAMS.md route tree updated.

Impact:

  • frontend/src/pages/HorizonPage.tsx (new), App.tsx, Sidebar.tsx, i18n/index.ts.

Tests:

  • E2E: describe("Horizon") (3 tests).

2026-06-19 — Horizon visual QC polish

Type: Polish

Summary:

  • Added the screenshot-flow capture block for /work/horizon so the page is now part of the ongoing visual audit set (frontend/screenshots/26-work-horizon/ stays local and is not committed).
  • Fixed the 14-day split so week-two items no longer leak into week one before expansion; the expander now reveals only the second-week rows instead of duplicating them.
  • Made day-group labels honor the active UI locale and added a calmer header subtitle so the page reads correctly in Vietnamese during CEO-mode walkthroughs.

Impact:

  • frontend/src/pages/work/HorizonPage.tsx
  • frontend/tests/e2e/screenshot-flow.spec.ts
  • frontend/src/i18n/locales/en/horizon.json
  • frontend/src/i18n/locales/vi/horizon.json

Tests:

  • frontend/src/test/HorizonPage.test.tsx
  • Playwright screenshot flow filtered to 26-work-horizon