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.
- Given
- 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:
| Endpoint | New params | Field |
|---|---|---|
GET /api/tasks/ | due_after, due_before | due_date |
GET /api/commitments/ | due_after, due_before | due_date |
GET /api/decisions/ | needed_after, needed_before | decision_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
EmptyStateprimitive with aTriangleAlertglyph and a retryButtonthat refetches) — never the misleading positive empty-state. A partial failure degrades gracefully (renders the lists that loaded). - i18n: new
horizonnamespace, en+vi together. - Data fetching: composed via the
useHorizonDataTanStack Query hook (seven org-scoped list reads), per the codebase server-state convention — notuseEffect+ manual fetch.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Date-range filters return correct items | backend | backend/tests/api/test_pulses_horizon.py::TestHorizonFilters |
| 2 | Cross-tenant isolation on date-range params | backend | backend/tests/api/test_pulses_horizon.py::TestHorizonFilters |
| 3 | Page renders day groups + overdue pin; item tap navigates | e2e | frontend/tests/e2e/f23-horizon.spec.ts |
| 4 | Empty days collapse and the route remains useful | e2e | frontend/tests/e2e/f23-horizon.spec.ts |
| 5 | Overdue pin: active overdue only, excludes done/closed, caps at 20 | unit | HorizonPage.test.tsx (buildOverdueItems) |
| 6 | Day groups: group by day, sum money, skip empty days | unit | HorizonPage.test.tsx (buildDayGroups) |
| 7 | Renders overdue + day groups + money; week-2 collapsed until expanded | unit | HorizonPage.test.tsx |
| 8 | Item tap navigates to the correct list page | unit | HorizonPage.test.tsx |
| 9 | Positive empty state when nothing is due | unit | HorizonPage.test.tsx |
| 10 | Total load failure → inline error + retry, never the empty state | unit | HorizonPage.test.tsx |
| 11 | Partial failure degrades gracefully (renders what loaded) | unit | HorizonPage.test.tsx |
| 12 | Populated shared mobile refresh failure keeps grouped tasks visible through repeated failure; explicit Retry recovers | shared JVM + Android runtime policy | HorizonScreenModelTest, test_f23_horizon_mobile_recovery_policy.py, flow 159 |
| 13 | Shared mobile task card forwards its exact task ID into typed Android/iOS Task Detail routing | shared JVM source contract | HorizonTaskNavigationSourceTest |
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
- 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. - 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 byHorizonPage.test.tsx(partial-failure degradation + total-error). - Network timeout during page load. Resolved (2026-06-21). A total load failure now
renders an inline error state (
EmptyState+TriangleAlert+ retryButtonthat refetches via TanStack Query), replacing the previous toast-only path that misleadingly fell through to the positive empty-state. Covered byHorizonPage.test.tsx. - Cross-tenant data leak attempt. Tenant isolation is enforced by existing
organization_idscoping in each ViewSet (§5). New date-range params must be regression-tested to confirm they do not bypass scoping. - 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.
- 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)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | GET /api/tasks/ accepts due_after/due_before | ✅ match | P03 PR-1; TestHorizonFilters in backend/tests/api/test_pulses_horizon.py |
| 2 | GET /api/commitments/ accepts due_after/due_before | ✅ match | same PR and test class |
| 3 | GET /api/decisions/ accepts needed_after/needed_before | ✅ match | same PR and test class |
| 4 | TestHorizonFilters covers 4 test cases | ✅ match | P03 PR-1 commit note |
| 5 | HorizonPage.tsx exists with day-grouped list + overdue pin + week-2 expander | ✅ match | P03 PR-2; frontend/src/pages/work/HorizonPage.tsx |
| 6 | Route /work/horizon registered | ✅ match | P03 PR-2; App.tsx + frontend/DIAGRAMS.md |
| 7 | horizon i18n namespace with en+vi (12 keys each) | ✅ match | P03 PR-2; i18n/en/horizon.json, i18n/vi/horizon.json |
| 8 | frontend/DIAGRAMS.md route tree updated | ✅ match | P03 PR-2 explicit note |
| 9 | Three registered Chromium Horizon journeys execute | ✅ match | frontend/tests/e2e/f23-horizon.spec.ts; 3/3 passed on commit 972d3643d (2026-07-20) |
| 10 | No 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_datefields 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_beforeis 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:
HorizonTaskNavigationSourceTestguards 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 underdocs/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.tsjourneys in canonical Chromium against the real local backend/frontend servers on merge commit972d3643d. - 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.cuefor 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 inLumiDates.kt. - Updated
humanizeHorizonHeader(): days7..13now returnNextWeekday(previously fell through toDated, showing raw ISO strings like “2026-07-17” as section headers). - Updated
DateSectionHeaderinHorizonScreen.ktto mapNextWeekdayto “Next Monday” … “Next Sunday” / “Thứ Hai tuần sau” … “Chủ Nhật tuần sau” via 7 new KMP string resources in bothvalues/andvalues-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 assertNextWeekday.
Impact:
android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/LumiDates.ktandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/horizon/HorizonScreen.ktandroid/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.tsxto reflect the IA restructure. Verified byfind 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 correctfrontend/src/pages/work/HorizonPage.tsx.
2026-06-21 — TanStack Query migration + inline error state + QA suite
Type: Polish (QA/QC + UX)
Summary:
- Migrated
HorizonPagedata fetching fromuseEffect+Promise.allto the codebase-standarduseHorizonDataTanStack Query hook (seven org-scoped reads), completing the previously abandoned, incompleteuseHorizon.tshook 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.tsxfrom 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 --noEmitclean, 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_beforequery params to Task and Commitment list endpoints. - Added
needed_after/needed_beforequery 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) inbackend/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
/horizoninApp.tsx;CalendarRangeicon inSidebar.tsx. horizoni18n namespace:en/horizon.json+vi/horizon.json, 12 keys each.- E2E:
describe("Horizon")— 3 tests inlumi.spec.ts. frontend/DIAGRAMS.mdroute 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/horizonso 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.tsxfrontend/tests/e2e/screenshot-flow.spec.tsfrontend/src/i18n/locales/en/horizon.jsonfrontend/src/i18n/locales/vi/horizon.json
Tests:
frontend/src/test/HorizonPage.test.tsx- Playwright screenshot flow filtered to
26-work-horizon