Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
13 KiB
13 KiB
SCRATCHPAD — Outbound Webhooks for Projects
Decisions (with rationale)
- 2026-05-15 — Single
projectallowlist entity (user-confirmed via AskUserQuestion). Both project- and task-level events route toproject;webhookEntityForEventType()unchanged. Accepted cost: field picker forproject.createdlists task-only fields. Mitigation: task events passextraAlwaysIncluded:['task_id']to the projection helper sotask_idsurvives even when not selected. - 2026-05-15 — Rename
projectWebhookPayload→applyPayloadAllowlist. The verb "project" (to project/filter) collides with the newprojectentity noun;projectWebhookPayload('project', …)is unreadable. Cheap now — Phase 0 already moved the function topayloadFields.ts. - 2026-05-15 — Keep
project.completed/project.task.completedas deprecated accepted aliases, not removed. The public enum gates subscription create/update validation; removal would break storedwebhook_subscriptionsrows and the OpenAPI contract even though project webhooks never delivered before. - 2026-05-15 —
PROJECT_TASK_UPDATEDemitted fromupdateTaskWithChecklistAND the interactive tag path (F008). The other five task write paths (status/phase-move/reorder/reorder-in-status/ dependency) still have dedicated events or no webhook-relevant delta. - 2026-05-15 — Tag changes trigger webhooks for project_task AND ticket
(F008), parity (user-confirmed; largest of the three options). Emit
PROJECT_TASK_UPDATED/TICKET_UPDATEDwithchanges.tagsfrom the interactive tag path. No new public events — reuses*.updated. Rationale: the no-trigger gap is identical for tickets today; fixing only projects would leave the product inconsistent.
Discoveries (grounded against codebase)
- Phase 0 ALREADY IMPLEMENTED as unstaged changes; tests pass
(
payloadFields.test.ts2/2;webhookDelivery.*3/3). It consolidated MORE than the original draft:payloadFields.tsalso ownspayloadFieldsByEntitySchema,webhookEntityForEventType, and the projection helper (moved out ofwebhookTicketPayload.ts; deprecatedprojectTicketWebhookPayloadshim deleted). Treat as precondition only. ALWAYS_INCLUDED_KEYS_BY_ENTITYshipped as... as const satisfies Record<WebhookPayloadEntity, readonly string[]>(strict). Keep it — compiler forces theprojectkey when registry grows.- Tag system (
shared/models/tagModel.ts):tagged_typeenum =['client','contact','project_task','document','knowledge_base_article']. → NOprojecttype. Project payload ships withouttags. Task payload tags via'project_task'. (Original draft had this inverted.) projectActions.tsemits: PROJECT_CREATED :986, PROJECT_ASSIGNED :1052, PROJECT_STATUS_CHANGED :1089, PROJECT_UPDATED :1101, PROJECT_CLOSED :1532.- Task mutation entry points in
packages/projects/src/actions/projectTaskActions.ts:updateTaskWithChecklist:505 (← emit PROJECT_TASK_UPDATED here only),updateTaskStatus:688,moveTaskToPhase:1559,reorderTask:2102,reorderTasksInStatus:2178,updateTaskDependency:2478. - Current public enum (
webhookSchemas.ts:40-45): project.created, project.updated, project.completed, project.task.created, project.task.updated, project.task.completed. Missing: project.status_changed, project.assigned, project.closed, project.task.status_changed, project.task.assigned. - OpenAPI route now derives the per-entity payload schema from
WEBHOOK_PAYLOAD_FIELDS_BY_ENTITY→ addingprojectauto-extends docs. - F008 grounding:
packages/tags/src/actions/tagActions.tsemits onlyTAG_DEFINITION_UPDATED(lines 283, 831 — rename/recolor, not per-entity). Entry points:createTag:101,deleteTag:335 (interactive single),createTagsForEntity:534 +createTagsForEntityWithTransaction:571 (bulk, creation-time),deleteAllTagsByText:923 (bulk by text). TICKET_UPDATEDinternal event exists (eventBusSchema.ts:160, schema :785); ticket webhook builder already attacheschangeswheneventType === 'TICKET_UPDATED'(webhookTicketPayload.ts:143-144) vianormalizeChanges(:369). → ticket side of F008 needs ZERO builder change, only event emission. F005 must mirror thischangesattach forPROJECT_TASK_UPDATED.- ⚠️ DOUBLE-FIRE GOTCHA (F008):
TaskForm.tsx:910callscreateTagsForEntity(taskId,'project_task',pendingTags)right after the task is created (PROJECT_TASK_CREATED already fired). Emitting from the bulk create path would double-fire CREATED + spurious UPDATED on every create-with-tags. → emit only from interactive single-tag mutations, never the bulk creation-time path. Same caution for ticket create-with-tags.
Open questions / to verify during impl
- Task webhook
urlroute shape: confirm/msp/projects/:id?taskId=:taskIdagainst the Next.js app router before shipping F005. - Confirm an existing
normalizeChanges-style helper in the project actions to reuse forchanges(do not roll a new diff util).
Loop runbook
- Feature order (array order in features.json) = F001(verify/commit precondition) → F002 → F003 → F008 → F004 → F005 → F006 → F007. Each feature = one commit. F008 runs 4th (needs F003's PROJECT_TASK_UPDATED; ticket side uses existing TICKET_UPDATED).
- Per-feature gate before flipping
implemented:true:cd server && npx vitest run <relevant specs> --coverage=false- workspace type-check.
- Useful commands:
cd /Users/natalliabukhtsik/Desktop/Desktop/projects/alga-psa/server && npx vitest run src/lib/webhooks/__tests__/payloadFields.test.ts --coverage=false... npx vitest run src/test/integration/webhookDelivery.entityIdFilter.test.ts src/test/integration/webhookDelivery.tenantIsolation.test.ts --coverage=false
- ⚠️ Repo policy: do NOT stage/commit/push without explicit user request, even inside the loop. The loop implements + verifies; the user commits.
Status log
- 2026-05-15: Plan created (PRD + 7 features + 25 tests). Phase 0 done (F001 implemented:true, uncommitted). Phase 1 not started.
- 2026-05-15: Added F008 (tag-change webhooks, projects + tickets parity) per user decision → 8 features + 30 tests. Runs 4th in execution order.
- 2026-05-15: Completed F002. Renamed
projectWebhookPayloadtoapplyPayloadAllowlistinpayloadFields.ts, updated the ticket webhook subscriber and payload-field tests, and added the optionalextraAlwaysIncludedparameter for future task payload projection. Verification:cd server && npx vitest run src/lib/webhooks/__tests__/payloadFields.test.ts --coverage=false(4/4) andcd server && npx vitest run src/test/integration/webhookDelivery.entityIdFilter.test.ts src/test/integration/webhookDelivery.tenantIsolation.test.ts --coverage=false(3/3). Source search with coverage excluded has no remainingprojectWebhookPayloadreferences.npm run typecheckOOMed under the default Node heap;NODE_OPTIONS='--max-old-space-size=8192' npm run typecheckpassed and also satisfies T003. - 2026-05-15: Completed F003. Added canonical
PROJECT_TASK_UPDATEDschema withprojectTaskId+phaseId, exportedProjectTaskUpdatedEvent, and emit it fromupdateTaskWithChecklistonly whenbuildProjectTaskWebhookChanges(...)returns a non-empty diff. Extracted the diff builder topackages/projects/src/lib/projectTaskWebhookChanges.tsso date normalization and no-op behavior are unit tested without a DB/auth harness. Added a contract test proving the five out-of-scope task mutation entry points do not emitPROJECT_TASK_UPDATED. Verification:cd server && npx vitest run ../packages/event-schemas/src/schemas/eventBusSchema.projectTaskUpdated.test.ts ../packages/projects/src/lib/projectTaskWebhookChanges.test.ts ../packages/projects/src/actions/projectTaskWebhookUpdated.contract.test.ts --coverage=false(9/9) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck. - 2026-05-15: Completed F008. Interactive
createTag/deleteTagnow snapshots unique tag text sets before and after mutation and publishes entity update events only when the set changes.project_taskresolves{ projectId, phaseId }and emitsPROJECT_TASK_UPDATED;ticketemitsTICKET_UPDATED; both carrychanges.tags. AddedsuppressEntityUpdateEventforcreateTagand use it fromcreateTagsForEntityso initial tag application does not double-fire;createTagsForEntityWithTransactionremains bulk-only and never calls the entity update publisher. Added a ticket payload regression provingTICKET_UPDATEDtag diffs reachpayload.changes. Verification:cd server && npx vitest run ../packages/tags/src/actions/tagActions.webhookEmission.contract.test.ts src/lib/eventBus/subscribers/webhook/__tests__/webhookTicketPayload.test.ts --coverage=false(8/8) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck. - 2026-05-15: Completed F004. Added project and project-task public events to
SUPPORTED_WEBHOOK_EVENTS,webhookEventTypeSchema, and the OpenAPI route event enum, preservingproject.completedas the deprecatedproject.closedcompatibility alias. AddedWEBHOOK_PROJECT_PAYLOAD_FIELDSand the singleprojectpayload entity;project_idis excluded from the selectable list and retained viaALWAYS_INCLUDED_KEYS_BY_ENTITY.project. Task-onlytagsis present in the combined project field list, with no project-level tag field in the future project payload. Verification:cd server && npx vitest run src/lib/webhooks/__tests__/payloadFields.test.ts src/lib/api/schemas/__tests__/webhookSchemas.test.ts src/lib/actions/__tests__/webhookActions.supportedEvents.test.ts --coverage=false(7/7) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck. - 2026-05-15: Completed F005. Added
webhookProjectEventMap.tswith internal to public project mappings, includingPROJECT_CLOSED-> bothproject.closedand deprecatedproject.completed. AddedwebhookProjectPayload.tsfor project and task payload builders, 60s/256 LRU caches,project_tasktag resolution for task payloads, status-change previous status enrichment, updatechanges, and uncachedphases/task_countshelpers. Task URL is implemented as/msp/projects/:projectId?taskId=:taskId; no existing route reference was found in source search, matching the PRD's accepted shape. Verification:cd server && npx vitest run src/lib/eventBus/subscribers/webhook/__tests__/webhookProjectEventMap.test.ts src/lib/eventBus/subscribers/webhook/__tests__/webhookProjectPayload.test.ts --coverage=false(7/7) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck. - 2026-05-15: Completed F006. Added live
projectWebhookSubscriber.ts, registered it from subscriber index, and widenedWebhookDeliveryQueuetyping from ticket-only to generic webhook event/payload because the queue was already runtime entity-agnostic. Project events filter onprojectId; task events filter onprojectTaskId/taskIdand project allowlist projection passesextraAlwaysIncluded:['task_id']. Projectphasesandtask_countsopt-ins are fetched lazily once per event and reused for all matching subscribers. Verification:cd server && npx vitest run src/lib/eventBus/subscribers/__tests__/projectWebhookSubscriber.test.ts src/lib/eventBus/subscribers/__tests__/subscriberIndex.projectWebhook.test.ts src/test/integration/webhookDelivery.entityIdFilter.test.ts src/test/integration/webhookDelivery.tenantIsolation.test.ts --coverage=false(6/6) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck. - 2026-05-15: Completed F007. Added
webhookDelivery.projectWebhooks.test.tscovering project.created projection withphases, project.task.updated tag-only delivery withtask_idretained, and ticket.updated tag-only parity. The "integration" style matches the existing webhook delivery integration tests in this repo: subscriber registration, event dispatch, mocked model/payload builders, and queue job assertions rather than a live HTTP server/DB row. Verification:cd server && npx vitest run src/test/integration/webhookDelivery.projectWebhooks.test.ts src/test/integration/webhookDelivery.entityIdFilter.test.ts src/test/integration/webhookDelivery.tenantIsolation.test.ts src/lib/eventBus/subscribers/webhook/__tests__/webhookProjectEventMap.test.ts src/lib/eventBus/subscribers/webhook/__tests__/webhookProjectPayload.test.ts src/lib/eventBus/subscribers/__tests__/projectWebhookSubscriber.test.ts src/lib/eventBus/subscribers/__tests__/subscriberIndex.projectWebhook.test.ts src/lib/webhooks/__tests__/payloadFields.test.ts --coverage=false(20/20) andcd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck.