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>
This commit is contained in:
parent
38365e97b5
commit
e7da36e7e7
1 changed files with 118 additions and 0 deletions
|
|
@ -0,0 +1,118 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue