Skip to content

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/createTaskRow accept assignedTo. 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 new GET /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 inside listTasks ONLY when no workspace opt 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: reuses listTasks (D1 toggles apply verbatim), getSalesData per workspace with sales installed (open pipeline by stage, open/won cents, overdue follow-ups via the same cadence math as FollowUpsStrip), listBrandRollups for 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.
  • assembleTasks runs 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]