feat(rules): show what an apply run changed, and which rule ran
ci / lint-test (push) Successful in 38s

Apply History listed only counts - '13 matches · 13 transactions' - which reads
identically whether the run renamed a merchant or split every transaction with
another participant. Revert is destructive, so that is not enough to decide on.

Two additions. Migration 0017 records rule_id, rule_name and source on each run:
rule_name is denormalised so history stays readable after a rule is edited or
deleted, and there is no FK so deleting a rule cannot cascade away the audit
trail. Both write paths now populate it - the condition-matched run and the
selection-based quick action.

And rows expand to show the run's snapshot set against current values: which
transactions were touched and what changed on each. Rows changed by something
else since the run are called out, because reverting restores the pre-run value
and would discard that later edit.

Runs recorded before this show 'Unknown rule' - the rule they came from is not
recoverable.
This commit is contained in:
2026-07-26 15:20:56 +10:00
parent cc852e7c6f
commit 3778bfe836
7 changed files with 342 additions and 30 deletions
+5 -3
View File
@@ -110,9 +110,11 @@ export async function POST(req: NextRequest) {
}
const run = await queryRaw<{ id: number }>(
`INSERT INTO rule_apply_runs (owner_id, split_from, matched, transactions_affected, snapshot)
VALUES ($1, NULL, $2, $3, $4) RETURNING id`,
[user.id, ids.length, ids.length, JSON.stringify(snapshot)]
`INSERT INTO rule_apply_runs (owner_id, split_from, matched, transactions_affected, snapshot,
rule_id, rule_name, source)
VALUES ($1, NULL, $2, $3, $4, $5, $6, 'selection') RETURNING id`,
[user.id, ids.length, ids.length, JSON.stringify(snapshot),
rules[0].id, rules[0].name]
);
return NextResponse.json({ updated: ids.length, run_id: run[0].id, rule: rules[0].name });