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>
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 fromold_values.UPDATE→ writeold_valuesback.- 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) andtask_audits(an unrelated document-matching audit feature) already exist — this new system is distinct from both; avoid name collisions (task_auditsis 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 admingenerate-*-tasksroutes,/api/tasks/generate-and-assign.
Open / not yet designed (resume point)
- 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'sUNTRACKEDfallback for lower-value paths). - 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). - 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). - UI for the Admin run-list/restore page — not discussed at all yet.
- Whether restoring a task whose client or policy itself was deleted since the run needs special handling (FK now dangling).
- 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.