seubert-claims/ondeck/docs/superpowers/specs/2026-07-17-task-change-log-and-restore-design.md
lorentz e7da36e7e7 docs: capture in-progress design for task change log & restore feature
Paused mid-brainstorm at user request. Records the approved architecture
shape (DB-trigger-based change capture, run-scoped restore, hourly rollup
for oversight) and the open items to pick up on resume.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 18:46:02 +00:00

6.2 KiB

Task Change Log & Restore — Design (paused, in progress)

Status: Paused mid-brainstorm at user's request. Overall architecture shape is approved. Rollout specifics (which existing code paths need wrapping, and the hourly rollup query) are NOT yet designed — that's the resume point.

Problem

Horizon has no way to see the history of a task's state over time, or to undo the effect of a specific automation run (sync, auto-generate) or a bad manual edit. This came up directly after a manual backfill script created ~86 tasks for two clients (Sultan Trans, Dagostino Electronic Services) by writing raw SQL — a change with no audit trail beyond a generic audit_logs row.

Goal: an oversight system that (a) snapshots open-task state hourly for trend/impact visibility per advocate/client/etc, and (b) lets an Admin literally restore tasks to a prior state, scoped to a specific run or trigger — covering both automation-triggered changes and manual UI edits.

Decisions made so far

Question Decision
Primary use case Literal restore capability, not just diffing
Restore scope Scoped to a specific run/trigger, not whole-system point-in-time
Coverage Both automation (sync, auto-generate, admin backfills) AND manual UI edits
Retention 1 year, then prune
Restore access Admin-only, via a page listing runs (pick one, restore just those tasks)
Capture mechanism Postgres DB trigger (not app-level-only helper) — see rationale below

Why a DB trigger over an app-level helper: an app-level helper (a shared TS function all mutation code paths call) is simpler and more idiomatic for this codebase, but silently misses any write that doesn't go through it — like today's raw-SQL backfill script. A trigger on the tasks / task_assignments tables fires on every INSERT/UPDATE/DELETE regardless of what wrote the row, so nothing is invisible by construction. Run-tagging (which run caused a given change) is layered on top via a Postgres session variable (SET LOCAL app.run_id) that callers set before writing; if unset, the event is still captured, just tagged UNTRACKED instead of getting a rich label.

Architecture (approved shape)

Data model

changes_runs — groups events from one logical action:

  • id, trigger_type (MANUAL_EDIT / SYNC / AUTO_GENERATE / CRON / ADMIN_BACKFILL / RESTORE / UNTRACKED), triggered_by (nullable FK → users), started_at, completed_at, description

task_change_events — one row per actual field change on a tasks or task_assignments row (not per-request, per-column):

  • id, task_id, run_id (nullable FK → changes_runs), change_type (INSERT/UPDATE/DELETE), old_values (jsonb, null for INSERT), new_values (jsonb, null for DELETE), changed_at

task_rollup_snapshots — the hourly oversight/trend view from the original ask, kept separate from restore data since it's just for trend-watching, not restore:

  • one row per (hour, advocate | client | department | status) combo with an open-task count. Cheap aggregate, not full-row duplication.

Capture mechanism

AFTER INSERT OR UPDATE OR DELETE trigger on tasks and task_assignments writes to task_change_events. Skips no-op updates (OLD = NEW). run_id comes from the session variable set by the caller; unset → UNTRACKED.

Restore flow

Admin-only page lists recent runs (trigger type, who/what, timestamp, task count affected). Picking a run and confirming:

  • For each task touched in that run, take the earliest event in that run (state immediately before the run started).
  • INSERT → delete the task. DELETE → recreate from old_values. UPDATE → write old_values back.
  • The restore itself runs through the same capture path, becoming its own new run (trigger_type = RESTORE) — so a restore is itself visible in the run list and can be undone.

Retention

Nightly prune job (same systemd-timer + x-cron-secret pattern as the existing ondeck-sync.timer/api/cron/sync) deletes task_change_events and task_rollup_snapshots older than 1 year.

Existing patterns this builds on

  • Scheduling: ondeck-sync.timer (systemd) → curl -X POST http://localhost:3000/api/cron/sync -H 'x-cron-secret: ...'. New cron endpoints (hourly rollup, nightly prune) should follow this exact pattern.
  • audit_logs (generic action log) and task_audits (an unrelated document-matching audit feature) already exist — this new system is distinct from both; avoid name collisions (task_audits is taken).
  • Task mutation surface to eventually wrap/verify against the trigger: task edit modal, complete/NA/cancel dialogs, bulk assign/transfer routes, src/lib/sync/auto-generate.ts, src/lib/sync/sync-engine.ts (runPostSyncAutomation), the three admin generate-*-tasks routes, /api/tasks/generate-and-assign.

Open / not yet designed (resume point)

  1. Rollout specifics — exact list of every existing task-mutation code path, and whether/how each needs to set SET LOCAL app.run_id (vs relying on the trigger's UNTRACKED fallback for lower-value paths).
  2. Hourly rollup query — the exact aggregation query/schema for task_rollup_snapshots (dimensions: advocate, client, department, status; need to confirm which combos matter for the oversight dashboard).
  3. Prisma modeling of trigger-based tables (Prisma doesn't manage triggers natively — will need a raw SQL migration for the trigger + trigger function, with the tables themselves modeled normally in prisma/schema.prisma).
  4. UI for the Admin run-list/restore page — not discussed at all yet.
  5. Whether restoring a task whose client or policy itself was deleted since the run needs special handling (FK now dangling).
  6. Spec self-review and user sign-off on the written spec (paused before this step — do this when resuming, before moving to implementation planning).

Next step on resume

Continue the brainstorming flow from "rollout specifics" (item 1 above) — enumerate every task-mutation code path and decide the wrapping approach — then finish the spec self-review and get explicit sign-off before invoking the writing-plans skill.