seubert-claims/dev/audit-logging-fix-plan.md
lorentz 20f7ec1188 Fix: 8 issues from Horizon Issues & Next Steps doc
1. Tasks disappearing (Mimi bug) - open tasks now always visible
   regardless of renewal group date; only terminal tasks filter by group age

2. Star date change recalculates task due dates - PATCH policy-group
   now updates dueDate on all open template tasks when renewalDate changes

3. Auto task generation on setup complete - wizard calls generate-tasks
   for each saved group immediately after Save & Complete

4. Manual sync date range - defaults are now dynamic (prev year to +2y)
   instead of hardcoded 2026-01-01 / 2026-12-31

5. Setup queue filters out clients with no active policies; shows
   skipped count so managers know what was excluded

6. Audit logging gap fixed - notes route now emits TASK_STATUS_CHANGED
   with old+new status when note submission also changes status

7. Renewal day-count warnings now shown on individual policy cards
   in client detail (<=30d destructive, <=90d secondary badge)

8. Client notes now show save timestamp after editing
2026-05-28 23:33:54 +00:00

3.9 KiB

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: <new>. 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.

// 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

-- 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.

-- 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.