# Audit Logging Fix Plan _Scheduled: after hours_ ## Problem Two code paths can change a task's status without capturing the **previous** status in the audit log: 1. **`POST /api/tasks/[id]/notes`** — When a user adds a note and simultaneously changes task status, the audit entry is `TASK_NOTE_ADDED` with only `statusChange: `. The old status is never recorded. 2. **`src/lib/shape-import/run-import.ts`** — SHAPE import directly calls `prisma.task.update()` and `prisma.task.create()` with `COMPLETED`/`NA` status. Zero per-task audit entries. The notes-route gap is the most operationally urgent because users are actively using it daily. The import gap is lower risk (import is a rare admin operation). --- ## Fix 1 — Notes Route: Capture Old Status (Priority: High) **File:** `src/app/api/tasks/[id]/notes/route.ts` ### What to change The task is already fetched before the note is written (to check assignment). Add the old status to the audit log, and emit a **separate** `TASK_STATUS_CHANGED` entry when a status change accompanies the note. ```ts // After fetching task (line ~42), old status is already available as task.status if (status && ['COMPLETED', 'NOT_STARTED', 'IN_PROGRESS'].includes(status)) { await prisma.task.update({ ... }) // existing logic, unchanged // ADD: dedicated status-change audit entry await prisma.auditLog.create({ data: { userId, action: 'TASK_STATUS_CHANGED', entityType: 'Task', entityId: id, oldValues: { status: task.status }, newValues: { status, source: 'note' }, }, }) } // Existing TASK_NOTE_ADDED entry — update to include oldStatus too await prisma.auditLog.create({ data: { userId, action: 'TASK_NOTE_ADDED', entityType: 'Task', entityId: id, oldValues: { status: task.status }, // ADD this newValues: { noteId: note.id, statusChange: status ?? null }, }, }) ``` ### Verification after deploy ```sql -- Confirm old status is now captured SELECT old_values->>'status', new_values->>'status', new_values->>'source', created_at FROM audit_logs WHERE action = 'TASK_STATUS_CHANGED' ORDER BY created_at DESC LIMIT 10; ``` --- ## Fix 2 — PATCH Route: Confirm old_values.status is always set (Priority: Medium) **File:** `src/app/api/tasks/[id]/route.ts` The `TASK_UPDATED` entries already capture `oldValues: { status: task.status }`, but only when a status field is present in the request body. Verify the task is always fetched before the update (it is — line ~30 fetches `task`). No code change needed; just confirm via spot-check. ```sql -- Spot-check: any TASK_UPDATED entries missing old_values.status SELECT COUNT(*) FROM audit_logs WHERE action = 'TASK_UPDATED' AND old_values->>'status' IS NULL; ``` --- ## Fix 3 — SHAPE Import: Per-task Audit Logging (Priority: Low) **File:** `src/lib/shape-import/run-import.ts` This is a bulk operation run rarely by admins. The `ShapeImportRun` table already records run-level stats. Acceptable to leave per-task audit logging out for now given the volume (hundreds of tasks per run would bloat the audit table). **Recommendation:** Add a summary `SHAPE_IMPORT_TASK_SUMMARY` audit entry per import run that records counts by status (created COMPLETED, created NOT_STARTED, updated, skipped). Already partially done via `ImportStats`. --- ## Deployment Steps 1. Apply Fix 1 to `notes/route.ts` 2. TypeScript check: `npx tsc --noEmit` 3. Commit: `git commit -m "Fix: capture old status in task notes audit log"` 4. Push + deploy: `docker compose -f /opt/stacks/horizon/docker-compose.yml up -d --build` 5. Run verification queries above to confirm 6. Optionally apply Fix 3 summary entry to shape import --- ## Notes - No DB schema changes required — `old_values` and `new_values` are already `jsonb` columns. - No migration needed. - Zero downtime deploy (standard Next.js container rebuild). - Fix 1 is safe to apply during off-hours with no user impact.