Handoff: Cockpit app + Tasks cross-workspace controls, BUILT (awaiting review/merge)¶
URL: https://mkdocs.justinsforge.com/memory/handoffs/creatortrack-cockpit-and-tasks-controls-built-2026-07-06/
Date: 2026-07-06
Branch: feat/cockpit-and-tasks-controls (4 commits off main @ 00c3fab). NOT merged, NOT deployed.
Dev slot: worktree /home/justinwieb/forge-suite-wt/feat/cockpit-and-tasks-controls, DB ct_feat_cockpit_and_tasks_controls, live preview http://100.97.43.104:3063 (port 3061 was held by an orphaned server from the removed multiuser-apps-michael slot; reaper will collect it).
Spec: creatortrack-cockpit-and-tasks-controls-2026-07-06
Key design departure from the spec¶
Tasks are collection rows (nodes + collectionProps/Values), not a SQL table, so "assigned_to column" became an Assignee person prop (stored user id[], single id by convention), seeded on new tasks dbs and lazily backfilled by ensureTasksApp exactly like Tags/Reminder. No schema change to tasks.
The one real migration (20260706T180445): SECURITY DEFINER core.list_workspace_members(workspace, caller), gated on the caller's own membership. Needed because core.users/core.memberships RLS is self-only; the pre-existing lib/data.ts listWorkspaceMembers silently returned only the caller, so person-prop pickers were single-user. Fixing it there lit up the picker for every person prop app-wide.
Deliverable 1, Tasks¶
CleanTask.assignedTo/assignedToName;TaskPatch.assignedTo(number|null);createTask/createTaskRowacceptassignedTo. API passthrough on POST /api/tasks and PATCH /api/tasks/[id].- Assignee picker: free via the generic row page (
PageBody->RowPropertiesHeader-> PersonCell); Peek and the full page both show it. Members list = task's own workspace roster via newGET /api/workspace/[id]/members. - Visibility toggles (prefs
tasks_app.assignee_show_{mine,unassigned,others}, defaults ON/ON/OFF): Settings -> Workspaces page, "Assignment" section. Filter applies insidelistTasksONLY when noworkspaceopt is set; a workspace-scoped view always shows its full backlog. Composes with excluded-workspaces. - TaskRow shows a small "user firstname" badge when a task is assigned.
Deliverable 2, Cockpit¶
- Catalog: id
cockpit, essentials,preview: true(Early access),ownerOnly: true, route/cockpit, icon grid. lib/cockpit.ts getCockpitData: reuseslistTasks(D1 toggles apply verbatim),getSalesDataper workspace withsalesinstalled (open pipeline by stage, open/won cents, overdue follow-ups via the same cadence math as FollowUpsStrip),listBrandRollupsfor campaigns. Read-only; stores nothing.- UI:
app/(suite)/cockpit/page.tsx+components/apps/cockpit/CockpitClient.tsx+ cockpit.css. Task links ->/n/{id}(no workspace switch needed); "Open pipeline" POSTs /api/workspace/switch then routes to /sales.
Verified (dev slot, end-to-end)¶
- API: create-with-assignee, clear, reassign, name resolution, aggregate hides when showMine=false, workspace view unaffected, settings GET/PATCH round-trip. tsc clean.
- Browser: /cockpit renders all panels with real clone data; Assignee row + person picker (search + member list) works on a task page.
Live-review fix (11ffc17)¶
Justin: cockpit task links opened the bare /n/{id} node page (full screen, no Tasks-app sidebar). Now: switch active workspace -> /tasks?peek={id} redirect -> Tasks app with the task side-peeked. FOLLOW-UP (not this branch): every /n/{id} link in CreatorTrack (e.g. Peek's expand) has the same no-app-chrome problem; general fix = node page or link builders detect the owning app and route through it with ?peek=.
Review pointers¶
- Migration grants execute to lifeos_app; fn is STABLE, caller-gated. Worth a second read.
assembleTasksruns one roster query per workspace batch, skipped when no task carries an assignee.- Merge is ONCE via integrator; teardown slot after (forge_creatortrack_agent_teardown.sh / dev-slot reaper).
[Claude Code]