From 17eba515d2aea32dd40939cd15aeab7a347d63ad Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 13:19:11 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(S2):=20dock=20the=20Tasks=20detail=20s?= =?UTF-8?q?urface=20=E2=80=94=20/tasks=20stops=20opening=20records=20as=20?= =?UTF-8?q?a=20pop-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner ruling: Projects is canonical, Tasks conforms. DESIGN_SYSTEM §6 has specified the house layout as an optional w-[380px] desktop side panel (bottom sheet on mobile) all along; /tasks was the only app opening a record as an overlay. Desktop list/board views now dock the detail as a third column — page.tsx renders beside ItemList, driven by the store's selectedItemId, mirroring how projects/page.tsx docks TaskPanel. Present only while something is selected, closes from its own ✕. TaskFocusModal keeps two lives: the phone branch (store-driven, full-screen, the same move Projects makes with fixed inset-0), and an explicit maximise from the docked pane (controlled by itemId/onClose), which is the mitigation for the max-w-3xl reading width the 380px column cannot give. Inbox, Engage, Calendar and the Assistant carry no pane and keep raising the store-driven overlay. Two dead exports came back rather than being deleted: ItemDetail() as the pane's entry (restoring the INBOX → ClarifyPanel branch, now with the pane's ✕ so an inbox selection on a task view is dismissable) and the "Open full page" button, which now has an unfocused mount to live on. openFocus stays the app-wide "open this task" verb every list component calls; on the docked surface the page drops its focus half so a row selects into the pane — dropped, not ignored, or a stale id would pop the overlay open on the next view switch. Also: sm:rounded-2xl → sm:rounded-lg on the modal sheet (§4 radius; a fixed 16px corner ignores Graphite's 0.125rem and Material's 1rem). No other rounded-xl site under src/app/tasks/ was swept. R4: task_manager_app.md status header records the change; workbench/AGENTS.md gains the tasks entry with the openFocus rule. R7, honestly: nothing in this tree tests layout, panel counts or mobile branches, so the composition is ADVISORY — held by review and the theme-switch pass, which could not be run here (no browser; Playwright cannot install). Verified: npx tsc --noEmit → 0 · npx vitest run → 73 files, 1586 tests, 0 · npx vitest run src/lib/theme/ → 353 tests, 0 · eslint on the three files → 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W --- project-docs/specs/task_manager_app.md | 14 +-- workbench/AGENTS.md | 1 + .../src/app/tasks/components/ItemDetail.tsx | 83 ++++++++++++++++- .../app/tasks/components/TaskFocusModal.tsx | 61 +++++++++--- .../control_plane/src/app/tasks/page.tsx | 93 ++++++++++++++++--- 5 files changed, 216 insertions(+), 36 deletions(-) diff --git a/project-docs/specs/task_manager_app.md b/project-docs/specs/task_manager_app.md index 2d04db024..855d02d34 100644 --- a/project-docs/specs/task_manager_app.md +++ b/project-docs/specs/task_manager_app.md @@ -8,6 +8,7 @@ > - **`expected_by IS NOT NULL` ⇒ a person actually promised this date.** It stands on its own and is deliberately independent of `due_at`, in both directions: a promise for next week keeps an item past my deadline off the overdue list, and a promise for yesterday flags an item whose deadline is still ahead. > > Implementation: all four INSERT sites (`items.py` delegate + clarify-delegate, `capture_email.py` ×2, `sync.py` provider pull) write **no** `expected_by` — at each of them the value on offer was literally the item's own due date under another name. `isWaitingOverdue(item)` judges `expectedBy ?? dueAt` (with `waitingLine()` naming *which* fact, so the row reads "promised by …" or "due …" rather than a bare date). A promise is stated explicitly through **`PATCH /tasks/items/{id}` `{expected_by}`** — the same endpoint, auth and ISO/`""`-clears convention as the due-date edit, applied to the item's OPEN `gtd_waiting` row (it is the only field on that patch that lands on another table); the control is the "Promised by" editor in the Waiting-on section of `ItemDetail.tsx`. **No migration** — mig-48 columns, unchanged. **Data note:** rows delegated between the previous deploy and this change carry a snapshot `expected_by`. They keep it and stay judged on it, exactly as if that date had been promised. There is no backfill and no data migration; clearing one is a normal edit ("Promised by" → ✕). +> **Update 2026-08-10 (S2, detail surface docked — owner ruling: "Projects is canonical, Tasks conforms").** `/tasks` was the only app in the product that opened a record as a pop-up; `DESIGN_SYSTEM.md` §6 has specified the house layout as an optional `w-[380px]` desktop side panel (bottom sheet on mobile) all along, and `/projects` implements it. **Desktop list/board views now dock the detail** as a third column — `page.tsx` renders `` beside `ItemList`, driven by the store's `selectedItemId`, which is the composition `projects/page.tsx` uses for `TaskPanel`. The pane is present only while something is selected (Projects' behaviour) and closes from its own ✕. **`TaskFocusModal` keeps two lives:** the **phone** branch, full-screen, exactly as Projects goes `fixed inset-0` on a phone; and an explicit **maximise** from the docked pane, because the pane is narrower than the modal's `max-w-3xl` and this detail is far denser than Projects' — that is the mitigation for the reading width the dock costs. Two exports that were imported nowhere came back rather than being deleted: `ItemDetail()` (the pane's entry, which restores the INBOX → `ClarifyPanel` branch) and the "Open full page" button, which now has an unfocused mount to live on. `openFocus` stays the app-wide "open this task" verb — on the docked surface the page drops its focus half so the row selects into the pane, and Inbox/Engage/Calendar/Assistant, which have no pane, keep raising the overlay. **Frontend only; no migration, no API change.** Fences, stated honestly: `npx tsc --noEmit`, `npx vitest run` and `npx vitest run src/lib/theme/` all pass, and `sm:rounded-2xl` on the modal sheet became `sm:rounded-lg` (§4 radius) — but **nothing in this tree tests layout, panel counts or mobile branches**, so "the detail is docked" is advisory, held by review and by the Fluent/Material/Graphite pass, which could not be run here (Playwright cannot install in the build environment). > **v0.2 review pass:** reconciled the GTD "lightweight project" vs "first-class project" framing (§5.1); clarified the delegation-write vs Action-Broker sequencing (§6, Phase 3); pinned the migration (`48_*`, idempotent, FK-dependency apply order — §4); placed the new GTD tools in `skill-task-gtd` over the canonical store and demoted `skill-clickup-sync` to the reference connector (§3.1); matched the gateway route to the `routes//` package precedent (§8); de-duplicated horizon levels vs projects/items (§4); aligned F1 capture channels with the phasing (Q3); added a build-order summary (§9). > **Sibling spec:** [`archive/email_ai_assistant.md`](archive/email_ai_assistant.md) — the Task Manager app deliberately mirrors its architecture (multi-panel client + AI assistant + provider abstraction + Postgres sync + automation engine + follow-up tracking). Read it first; this doc reuses its patterns by reference. (Email's living plan is now [`email_app_master_plan.md`](email_app_master_plan.md).) @@ -625,10 +626,10 @@ src/app/tasks/ │ ├── ListsSidebar.tsx — Inbox · Next (by @context) · Waiting · Projects · Calendar · Someday · Horizons │ ├── CaptureBar.tsx — universal quick-add (global hotkey) │ ├── ItemList.tsx — processed-task views with a List ⇄ Board toggle -│ ├── TaskCard.tsx — rich PM-tool task card (board card + dense row); opens the focus modal +│ ├── TaskCard.tsx — rich PM-tool task card (board card + dense row); opens the task (docked pane on desktop, focus modal on a phone) │ ├── TaskBoard.tsx — Kanban board (columns by @context / stage / disposition), native HTML5 drag-to-refile → updateItem (back-syncs to ClickUp) -│ ├── ItemDetail.tsx — editable task detail (+ ClickUp back-sync, comments/attachments/subtasks) -│ ├── TaskFocusModal.tsx — full-page focused task view (the "task card pops up" surface) +│ ├── ItemDetail.tsx — editable task detail (+ ClickUp back-sync, comments/attachments/subtasks); `ItemDetail()` is the docked pane's entry, `TaskDetail` the editor both surfaces share +│ ├── TaskFocusModal.tsx — full-page focused task view: the phone's detail surface, and desktop's **maximise** out of the docked pane │ ├── ClarifyPanel.tsx — GTD decision-tree UI (agent proposal + approve/edit) │ ├── ProjectPlanner.tsx — natural-planning flow │ ├── EngageView.tsx — "Now": filter by context/time/energy/priority @@ -717,9 +718,10 @@ extended (`_build_item_update`). (2) **ClickUp back-sync** — `provider.update_ (add/rem assignee delta) + `_push_patch_upstream` best-effort back-sync of a SYNCED task's edits. (3) **Rich ClickUp detail** — `provider.get_task_detail` + `GET /items/{id}/detail` → comments/attachments/subtasks rendered in the panel. -(4) **Full-page view** — `TaskFocusModal` (store `focusedItemId`/`openFocus`). -(5) **List ⇄ Board** — `TaskCard` (rich card, board + dense-row variants; click → -focus modal) + `TaskBoard` (Kanban; columns by workflow stage [Next] / provider +(4) **Full-page view** — `TaskFocusModal` (store `focusedItemId`/`openFocus`; +since 2026-08-10 the phone's detail surface and desktop's *maximise*, not the +desktop default — see the status header). (5) **List ⇄ Board** — `TaskCard` +(rich card, board + dense-row variants; click → the detail surface) + `TaskBoard` (Kanban; columns by workflow stage [Next] / provider stage [Waiting/Someday] / disposition; **native HTML5 drag** — no DnD lib); `ItemList` gained a sticky List/Board toggle (Calendar/Archive stay list-only). diff --git a/workbench/AGENTS.md b/workbench/AGENTS.md index c624f55e5..6058504ae 100644 --- a/workbench/AGENTS.md +++ b/workbench/AGENTS.md @@ -13,6 +13,7 @@ Control Plane (Next.js browser UI) and local development tools. - control_plane/src/app/page.tsx -- Home grid. **Known issue (2026-08-03, deliberately unfixed):** it renders `NAV_SECTIONS` directly (`:11-12`) with **no** access filter, so every pane — Centers included — is advertised to every viewer, including signed-out ones, while the sidebar correctly hides them via `visibleSections()`. Whoever fixes it must decide what the grid shows while access is still resolving (`useAccess()` returns null on first paint, and nav's convention is "show everything" so the UI does not visibly shrink — the wrong default for a landing page) - control_plane/src/lib/centers.ts -- THE Departmental Center registry: `nav.ts` builds the Centers nav section from it and `access.ts` builds the `/centers/` → `center.` route map from it. Editing this file alone does NOT add a Center — a slug here whose feature is absent from `acb_auth.permissions.FEATURES` is dropped from the nav and 403s at AccessGate for every principal, owner included (that was the live defect of 2026-08-03). `tests/unit/test_org_access_control.py::test_centers_registry_matches_the_feature_vocabulary` parses this file and pins it both ways to `FEATURES`; the full five-place registration checklist is `project-docs/specs/department_centers.md` §2. ⚠️ **That invariant reads each Center's `feature:` field and NOTHING else, so it fences nothing inside `apps[]`** — which is what the Center page actually renders. `CenterApp` is therefore a union discriminated on `status`: a `live` entry without an `href` is a **compile** error (the page links to `app.href ?? "#"`, so it would ship as a clickable card that goes nowhere), and a `planned` entry WITH one is too (it is live and mis-labelled, and the page files it under "coming soon"). Runtime twin: `src/lib/centers.test.ts` - control_plane/src/app/crm/ -- The native CRM (spec: `project-docs/specs/crm_app.md` §5; WS-26c, extended by WS-26f). Deals kanban landing tab + a list per entity on one shared list contract, a record sheet that opens OVER the list from `?deal=`/`?lead=`/`?contact=`/`?organization=` (no `/[id]` routes — Back closes the sheet, and v1 has no saved-views table so view state IS the URL), timeline with a note/task/call composer, inline-edit fields panel, convert modal, quick-create. ⚠️ **Everything server-shaped is pure and lives in `lib/`, each file unit-tested**: `urlState.ts` (the URL grammar), `board.ts` (lane order/tone, the move plan, the optimistic re-tally, `needsLostReason`), `filters.ts` (the list contract — including *never* sending `?status_id` to contacts/organizations, which the gateway 422s by design), `convert.ts` (§3.7's match rules mirrored so the modal pre-selects what the server would do), `format.ts` (₹ in `en-IN` lakh/crore grouping), `api.ts` (the BFF client; refusals keep their status so a 409 is explained, not reported as a failure). `components/` is composition only. Proxy: `src/app/api/crm/[...path]/route.ts` (the tasks-proxy shape). Three rules here were learned by breaking them: **(1) every list filter — `sort`/`dir` included — lives in `CrmView` and therefore in the URL**, because the load effect keys on view fields and a filter held in component state is a control that changes its own appearance and issues no request (`sort` is also cleared by `selectTab`: the keys are a per-entity server allowlist and a carried-over one is a 422, not an odd order). **(2) A write re-reads what is on screen, whatever the response was** — `refreshCollection()` off the store's `lastView`. The refusal half is obvious (a stale row after a 409 reads as success); the success half is the same lie told backwards, and it shipped first as a created record invisible until manual refresh and a rename that left the row behind the sheet unchanged. **(3) A post-write re-read carries the filters the view was loaded with** — `moveDeal`'s re-read used to fetch the pipeline unscoped, silently widening an owner-filtered board the moment somebody dragged a card on it. **WS-26f adds a fourth tab, `?tab=settings`** (three grids over the EXISTING admin API — deal stages, lead statuses, lost reasons; `lib/settings.ts` owns what a drag-reorder renumbers, which rows that actually PATCHes, and the client half of D-CRM-10's won=100/lost=0 rule). ⚠️ **`settings` is a tab that is NOT a collection, and every unchecked `as EntitySlug` cast on this page was a place that assumed otherwise** — `isEntity()` from `urlState.ts` is the guard, and the load effect, `store.refreshCollection()`, `setCreating` and `PARAM_FOR` all go through it; `parseView`'s old `isEntity(tab) ? tab : "board"` *swallowed* `?tab=settings` and landed the deep link on the kanban. Weighted ₹ (`lane.weighted`) comes from the gateway per lane and is **never** recomputed from `lane.rows`, which is one page of it; `board.ts` keeps the same formula (`weightedDeal`/`weightedRows`, `WEIGHTED_TYPES` = open+ongoing, a NULL probability inheriting the stage default) for the header rollup and the record sheet. **WS-26g adds a fifth tab, `?tab=reports`** (`components/Reports.tsx` + pure `lib/reports.ts`, over `GET /crm/reports/*`): forecast by stage, funnel, win/loss, owner leaderboard. It is another entry in the SAME `NON_ENTITY_TABS` grammar rather than a route, so the record sheet stays open across it — `?tab=reports&deal=` is a link somebody sends. ⚠️ **The reports surface computes nothing**: every figure is server-side and rendered verbatim, and `lib/reports.ts` holds bar widths and wording only. The one formula that legitimately lives on both sides is `board.ts`'s weighted ₹, and it is held to the gateway's SQL by a fixture BOTH runners read — `tests/fixtures/crm_weighted_parity.json`, from pytest through the emitted statement and from `board.test.ts` through `weightedDeal`/`weightedRows`. Adding a non-collection tab is never one edit: `NON_ENTITY_TABS`, the `page.tsx` branch chain AND the load effect all have to learn it, or the tab parses, the pill highlights, and the screen keeps showing whatever was there. ⚠️ **A new tab also has to be reachable from `refreshCollection()`, and every write has to GO through it** — `moveDeal` used to hard-code a board fetch, so the reports branch was unreachable from the one write its own comment named and a deal moved from `?tab=reports&deal=` left all four blocks stale while the fetched pipeline landed in state that tab does not render. `store.test.ts` pins WHICH request each write issues, not the state it settles into: the store's job is deciding what to re-read, and a final-state assertion cannot see a decision that was never made +- control_plane/src/app/tasks/ -- Task Manager (GTD; spec: `project-docs/specs/task_manager_app.md`). **The detail surface is DOCKED, not a pop-up** (owner ruling 2026-08-10, "Projects is canonical, Tasks conforms"; DESIGN_SYSTEM §6's `w-[380px]` side panel). On desktop list/board views `page.tsx` renders `