{
  "slug": "tasks-board-engine",
  "spec_file": "docs/specs/2026-05-30-tasks-board-engine.md",
  "findings": [
    {
      "id": "tasks-board-engine-001",
      "severity": "P0",
      "type": "security",
      "summary": "createTask/updateTask/bulkUpdateStatus accept status_id without tenant ownership check",
      "spec_ref": "Permissions + tenant isolation — tasks reference tenant-scoped statuses",
      "code_ref": "packages/db/src/queries/tasks.ts:310",
      "evidence": "updateTask sets statusId from patch with only tasks.tenant_id filter; no join/lookup on task_statuses.tenant_id. FK is tasks.status_id → task_statuses.id (not compound tenant). Attacker with tasks:write could attach another tenant's status UUID.",
      "repro": "PATCH /api/tasks/:id { status_id: <foreign-tenant-status-uuid> } while authenticated in tenant A."
    },
    {
      "id": "tasks-board-engine-002",
      "severity": "P0",
      "type": "broken",
      "summary": "Cron sync and inbound auto-create use tenantId as reporter_id (invalid users FK)",
      "spec_ref": "tasks.reporter_id UUID NOT NULL REFERENCES users",
      "code_ref": "apps/zync-api/src/routes/cron/tasks-sync.ts:158",
      "evidence": "createTask called with reporter_id: tenantId placeholder; tasks-inbound.ts:94 same. reporter_id FK references users(id); tenant UUID is not a user row — insert fails or corrupts if FK bypassed.",
      "repro": "POST /api/cron/tasks-sync with valid secret when adapter returns tasks → createTask throws FK violation."
    },
    {
      "id": "tasks-board-engine-003",
      "severity": "P0",
      "type": "missing",
      "summary": "Inbound auto-create handler never wired to comms.inbound queue consumer",
      "spec_ref": "Auto-create from inbound messages — routeInboundMessage / comms.inbound queue",
      "code_ref": "apps/zync-api/src/queues/comms-inbound.ts:84",
      "evidence": "handleInboundForTasks exists in queue/tasks-inbound.ts but grep shows zero call sites; comms-inbound.ts routes only to createSupportTicketStub and dispatchToAiAssistant.",
      "repro": "Enqueue comms.inbound message with auto_create_tickets_from.email=true → no task row created."
    },
    {
      "id": "tasks-board-engine-004",
      "severity": "P0",
      "type": "broken",
      "summary": "Board settings Sync now sends placeholder CRON_SECRET and always 401s",
      "spec_ref": "Manual sync now from board settings panel",
      "code_ref": "apps/zync-app/src/features/tasks/components/BoardSettingsPanel.tsx:40",
      "evidence": "triggerSyncNow POSTs X-Cron-Secret: '__manual__'; cron route timingSafeEqual rejects non-matching secret (tasks-sync.ts:41).",
      "repro": "Open board settings → Sync now → 401 Forbidden."
    },
    {
      "id": "tasks-board-engine-005",
      "severity": "P1",
      "type": "missing",
      "summary": "user_preferences.board_column_order column absent from schema and migrations",
      "spec_ref": "Kanban column reorder — persisted to user_preferences.board_column_order",
      "code_ref": "packages/db/src/schema/user-preferences.ts:12",
      "evidence": "userPreferences pgTable lists notificationChannels, sidebarCollapsed, etc.; no board_column_order JSONB. grep board_column_order only in spec/plan and client store comment.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-006",
      "severity": "P1",
      "type": "missing",
      "summary": "PATCH /api/user/preferences does not accept or persist board_column_order",
      "spec_ref": "BoardPreferences — columnOrder server-side via user preferences",
      "code_ref": "apps/zync-api/src/routes/user-preferences.ts:28",
      "evidence": "updateUserPreferencesSchema allows only ui_theme and timezone; board-preferences.ts:35 PATCHes { board_column_order } which is rejected or ignored.",
      "repro": "Drag-reorder Kanban column → network PATCH /api/user/preferences returns 422 No updatable field."
    },
    {
      "id": "tasks-board-engine-007",
      "severity": "P1",
      "type": "missing",
      "summary": "GET/PATCH /api/settings/tasks/sync route not implemented",
      "spec_ref": "Board settings panel — task_sync_settings sync interval and auto_create toggles",
      "code_ref": "apps/zync-app/src/features/tasks/components/BoardSettingsPanel.tsx:31",
      "evidence": "BoardSettingsPanel fetches/patches /api/settings/tasks/sync; grep apps/zync-api/src/routes finds no matching mount. upsertTaskSyncSettings exists in @zync/db only.",
      "repro": "Open board settings → auto-create toggles PATCH /api/settings/tasks/sync → 404."
    },
    {
      "id": "tasks-board-engine-008",
      "severity": "P1",
      "type": "missing",
      "summary": "dhtmlx Gantt library not integrated; Timeline is custom HTML table",
      "spec_ref": "Timeline View — Library: dhtmlx Gantt",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:154",
      "evidence": "TimelineView.tsx header claims dhtmlx but implements custom table/bars; apps/zync-app/package.json has @dnd-kit deps only, no dhtmlx-gantt; grep dhtmlx import = 0 in app src.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-009",
      "severity": "P1",
      "type": "missing",
      "summary": "Filter toolbar UI missing project, status, assignee, source, and labels controls",
      "spec_ref": "Filters table — project, status, priority, assignee, source, due_from, due_to, labels, q",
      "code_ref": "apps/zync-app/src/features/tasks/components/FilterToolbar.tsx:128",
      "evidence": "FilterToolbar renders search, priority buttons, due date inputs, and chips; no project/status/assignee/source/labels pickers despite useTaskFilters supporting those URL params.",
      "repro": "Cannot set ?project= or ?status= via UI; only manual URL edit."
    },
    {
      "id": "tasks-board-engine-010",
      "severity": "P1",
      "type": "missing",
      "summary": "dhtmlx GPLv2 license note in repo docs not present",
      "spec_ref": "Timeline View — note license requirement in project docs",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:17",
      "evidence": "TimelineView references docs/third-party-licenses.md; glob **/third-party* returns 0 files under repo.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-011",
      "severity": "P2",
      "type": "divergent",
      "summary": "Task sync cron logs to integration_sync_logs with wrong columns and CHECK values",
      "spec_ref": "Sync result logged to integration_sync_logs (status, items_count, error_msg)",
      "code_ref": "apps/zync-api/src/routes/cron/tasks-sync.ts:187",
      "evidence": "INSERT uses adapter_id, entity_type 'task_sync', status runStatus 'success'|'error', error_message; schema invoice-adapters.ts:56 requires provider IN (morning,...), entity_type IN (invoice,customer), status IN (success,failure,skipped), no items_count column. Insert caught and swallowed.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-012",
      "severity": "P2",
      "type": "divergent",
      "summary": "Cron trigger runs hourly not every 2 hours per spec default",
      "spec_ref": "Sync schedule default every 2h — wrangler crons 0 */2 * * *",
      "code_ref": "apps/zync-api/src/cron/runner.ts:17",
      "evidence": "CRON_ROUTE_MAP maps '0 * * * *' to /api/cron/tasks-sync; plan wrangler.toml comment shows 0 */2 * * *.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-013",
      "severity": "P2",
      "type": "missing",
      "summary": "Adapter pushUpdate (Trello/Jira 2-way sync) never invoked on task PATCH",
      "spec_ref": "External Adapter Imports — pushUpdate optional 2-way sync",
      "code_ref": "packages/integrations/src/tasks/trello.ts:89",
      "evidence": "trello.ts and jira.ts implement pushUpdate; grep pushUpdate under apps/zync-api returns no matches.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-014",
      "severity": "P2",
      "type": "divergent",
      "summary": "Timeline groupBy Project/Status/Assignee not implemented",
      "spec_ref": "Timeline View Config — Group by: Project (default), Status, Assignee",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:154",
      "evidence": "TimelineView accepts groupBy prop but binds _groupBy default with no UI or grouping logic; flat task list only.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-015",
      "severity": "P2",
      "type": "missing",
      "summary": "Timeline lacks pointer drag-to-reschedule; only keyboard due_date nudge",
      "spec_ref": "Timeline — Drag to reschedule: updates due_date via PATCH",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:66",
      "evidence": "GanttBar uses dnd-kit sortable for row drag only; no onPointer drag on bar to change dates; handleExtend only via ArrowLeft/Right keys.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-016",
      "severity": "P2",
      "type": "divergent",
      "summary": "Gantt bars missing role=button and tabIndex required for keyboard focus",
      "spec_ref": "Timeline Keyboard — Bars focusable via Tab; role=button",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:91",
      "evidence": "GanttBar div has aria-label and onKeyDown but no role='button' or tabIndex={0}; file comment claims role=button.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-017",
      "severity": "P2",
      "type": "divergent",
      "summary": "Task card missing role=button, attachment count, and comment count",
      "spec_ref": "Task card — title, priority, assignee, due, labels, attachment count, comment count",
      "code_ref": "apps/zync-app/src/features/tasks/components/TaskCard.tsx:50",
      "evidence": "Sortable card div has aria-label only; no role='button'; no attachment/comment fields rendered (grep attachment|comment in features/tasks = comment in file header only).",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-018",
      "severity": "P2",
      "type": "divergent",
      "summary": "List view missing Project column; uses raw table not DataTable/Popover+Select",
      "spec_ref": "List View — columns Title, Status, Priority, Assignee, Project, Due date, Created",
      "code_ref": "apps/zync-app/src/features/tasks/views/TaskListView.tsx:129",
      "evidence": "thead has Title, Status, Priority, Assignee, Due date, Created — no Project; inline edits use native <select> not @zync/ui Popover+Select; comment claims DataTable.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-019",
      "severity": "P2",
      "type": "missing",
      "summary": "Board settings panel missing status reorder, sync interval edit, and sync log display",
      "spec_ref": "Task 14 — status CRUD+reorder, sync interval, integration_sync_logs result",
      "code_ref": "apps/zync-app/src/features/tasks/components/BoardSettingsPanel.tsx:143",
      "evidence": "Panel lists statuses with create/delete only; sync interval read-only label; sync success message generic, no integration_sync_logs fetch.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-020",
      "severity": "P2",
      "type": "divergent",
      "summary": "Collapsed column state not in per-user localStorage key board_columns_{userId}",
      "spec_ref": "Kanban — Collapse state per column per user in localStorage('board_columns_{userId}')",
      "code_ref": "apps/zync-app/src/features/tasks/store/board-preferences.ts:69",
      "evidence": "Single persist name 'board_preferences' stores view, columnOrder, collapsedColumns together; no board_columns_{userId} key or userId parameterization.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-021",
      "severity": "P2",
      "type": "divergent",
      "summary": "Board client loads up to 200 tasks in one request without cursor pagination",
      "spec_ref": "GET /api/tasks — filterable, paginated",
      "code_ref": "apps/zync-app/src/features/tasks/TasksBoardPage.tsx:45",
      "evidence": "fetchTasksAll sets limit=200 and returns data.rows only; never follows nextCursor from listTasks encodeCursor pagination.",
      "repro": "Tenant with >200 tasks → board silently truncates."
    },
    {
      "id": "tasks-board-engine-022",
      "severity": "P2",
      "type": "divergent",
      "summary": "tasks.source DB CHECK includes 'api' but API Zod sourceSchema omits it",
      "spec_ref": "tasks.source CHECK manual|email|telegram|...|clickup",
      "code_ref": "packages/db/src/schema/tasks.ts:115",
      "evidence": "sourceCheck IN (...,'api'); apps/zync-api/src/schemas/tasks.ts:12 enum ends at clickup without api.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-023",
      "severity": "P2",
      "type": "divergent",
      "summary": "External task import uses createTask only; duplicate skip is catch-not-upsert",
      "spec_ref": "External task source+external_id prevent duplicate imports (upsert/no-op)",
      "code_ref": "apps/zync-api/src/routes/cron/tasks-sync.ts:154",
      "evidence": "Cron calls createTask per external task; on unique violation catches and continues — no update of changed title/status on re-sync.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-024",
      "severity": "P2",
      "type": "divergent",
      "summary": "Timeline row reorder is local state only, not persisted to API",
      "spec_ref": "Timeline row reordering — keyboard/drag reorder rows",
      "code_ref": "apps/zync-app/src/features/tasks/views/TimelineView.tsx:245",
      "evidence": "handleRowDragEnd updates rowOrder React state only; no PATCH position or order endpoint call.",
      "repro": "Reorder Gantt rows → reload page → original order restored."
    },
    {
      "id": "tasks-board-engine-025",
      "severity": "P2",
      "type": "broken",
      "summary": "Kanban card hover quick actions not wired (onEdit never passed)",
      "spec_ref": "Task card hover quick actions assign, set priority, set due date",
      "code_ref": "apps/zync-app/src/features/tasks/views/KanbanBoard.tsx:220",
      "evidence": "Column/TaskCard support onEdit callback; KanbanBoard Column render omits onEditTask; buttons call onEdit?.(task) no-op.",
      "repro": "Hover task card → click Assign → nothing happens."
    },
    {
      "id": "tasks-board-engine-026",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "TasksBoardPage uses inline empty/error markup not @zync/ui EmptyState/ErrorState",
      "spec_ref": "Task 14 — Use @zync/ui EmptyState for no-tasks and ErrorState for load failures",
      "code_ref": "apps/zync-app/src/features/tasks/TasksBoardPage.tsx:170",
      "evidence": "Custom div 'No tasks found' and error div; grep EmptyState in features/tasks = comment in TasksBoardPage header only.",
      "repro": ""
    },
    {
      "id": "tasks-board-engine-027",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Task card assignee uses initials div not @zync/ui Avatar",
      "spec_ref": "Task card — assignee avatar",
      "code_ref": "apps/zync-app/src/features/tasks/components/TaskCard.tsx:105",
      "evidence": "Renders div with assignee_id slice(0,2); no Avatar import from @zync/ui.",
      "repro": ""
    }
  ],
  "summary": { "P0": 4, "P1": 6, "P2": 15, "P3": 2, "total": 27 }
}
