Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

13 KiB

SCRATCHPAD — Outbound Webhooks for Projects

Decisions (with rationale)

  • 2026-05-15 — Single project allowlist entity (user-confirmed via AskUserQuestion). Both project- and task-level events route to project; webhookEntityForEventType() unchanged. Accepted cost: field picker for project.created lists task-only fields. Mitigation: task events pass extraAlwaysIncluded:['task_id'] to the projection helper so task_id survives even when not selected.
  • 2026-05-15 — Rename projectWebhookPayloadapplyPayloadAllowlist. The verb "project" (to project/filter) collides with the new project entity noun; projectWebhookPayload('project', …) is unreadable. Cheap now — Phase 0 already moved the function to payloadFields.ts.
  • 2026-05-15 — Keep project.completed / project.task.completed as deprecated accepted aliases, not removed. The public enum gates subscription create/update validation; removal would break stored webhook_subscriptions rows and the OpenAPI contract even though project webhooks never delivered before.
  • 2026-05-15 — PROJECT_TASK_UPDATED emitted from updateTaskWithChecklist AND 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_UPDATED with changes.tags from 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.ts 2/2; webhookDelivery.* 3/3). It consolidated MORE than the original draft: payloadFields.ts also owns payloadFieldsByEntitySchema, webhookEntityForEventType, and the projection helper (moved out of webhookTicketPayload.ts; deprecated projectTicketWebhookPayload shim deleted). Treat as precondition only.
  • ALWAYS_INCLUDED_KEYS_BY_ENTITY shipped as ... as const satisfies Record<WebhookPayloadEntity, readonly string[]> (strict). Keep it — compiler forces the project key when registry grows.
  • Tag system (shared/models/tagModel.ts): tagged_type enum = ['client','contact','project_task','document','knowledge_base_article']. → NO project type. Project payload ships without tags. Task payload tags via 'project_task'. (Original draft had this inverted.)
  • projectActions.ts emits: 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 → adding project auto-extends docs.
  • F008 grounding: packages/tags/src/actions/tagActions.ts emits only TAG_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_UPDATED internal event exists (eventBusSchema.ts:160, schema :785); ticket webhook builder already attaches changes when eventType === 'TICKET_UPDATED' (webhookTicketPayload.ts:143-144) via normalizeChanges (:369). → ticket side of F008 needs ZERO builder change, only event emission. F005 must mirror this changes attach for PROJECT_TASK_UPDATED.
  • ⚠️ DOUBLE-FIRE GOTCHA (F008): TaskForm.tsx:910 calls createTagsForEntity(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 url route shape: confirm /msp/projects/:id?taskId=:taskId against the Next.js app router before shipping F005.
  • Confirm an existing normalizeChanges-style helper in the project actions to reuse for changes (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 projectWebhookPayload to applyPayloadAllowlist in payloadFields.ts, updated the ticket webhook subscriber and payload-field tests, and added the optional extraAlwaysIncluded parameter for future task payload projection. Verification: cd server && npx vitest run src/lib/webhooks/__tests__/payloadFields.test.ts --coverage=false (4/4) and cd 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 remaining projectWebhookPayload references. npm run typecheck OOMed under the default Node heap; NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck passed and also satisfies T003.
  • 2026-05-15: Completed F003. Added canonical PROJECT_TASK_UPDATED schema with projectTaskId + phaseId, exported ProjectTaskUpdatedEvent, and emit it from updateTaskWithChecklist only when buildProjectTaskWebhookChanges(...) returns a non-empty diff. Extracted the diff builder to packages/projects/src/lib/projectTaskWebhookChanges.ts so 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 emit PROJECT_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) and cd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck.
  • 2026-05-15: Completed F008. Interactive createTag / deleteTag now snapshots unique tag text sets before and after mutation and publishes entity update events only when the set changes. project_task resolves { projectId, phaseId } and emits PROJECT_TASK_UPDATED; ticket emits TICKET_UPDATED; both carry changes.tags. Added suppressEntityUpdateEvent for createTag and use it from createTagsForEntity so initial tag application does not double-fire; createTagsForEntityWithTransaction remains bulk-only and never calls the entity update publisher. Added a ticket payload regression proving TICKET_UPDATED tag diffs reach payload.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) and cd 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, preserving project.completed as the deprecated project.closed compatibility alias. Added WEBHOOK_PROJECT_PAYLOAD_FIELDS and the single project payload entity; project_id is excluded from the selectable list and retained via ALWAYS_INCLUDED_KEYS_BY_ENTITY.project. Task-only tags is 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) and cd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck.
  • 2026-05-15: Completed F005. Added webhookProjectEventMap.ts with internal to public project mappings, including PROJECT_CLOSED -> both project.closed and deprecated project.completed. Added webhookProjectPayload.ts for project and task payload builders, 60s/256 LRU caches, project_task tag resolution for task payloads, status-change previous status enrichment, update changes, and uncached phases / task_counts helpers. 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) and cd 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 widened WebhookDeliveryQueue typing from ticket-only to generic webhook event/payload because the queue was already runtime entity-agnostic. Project events filter on projectId; task events filter on projectTaskId/taskId and project allowlist projection passes extraAlwaysIncluded:['task_id']. Project phases and task_counts opt-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) and cd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck.
  • 2026-05-15: Completed F007. Added webhookDelivery.projectWebhooks.test.ts covering project.created projection with phases, project.task.updated tag-only delivery with task_id retained, 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) and cd server && NODE_OPTIONS='--max-old-space-size=8192' npm run typecheck.