You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Real multi-level undo for planning edits, built on SQLite's canonical trigger-based undo-log pattern (https://www.sqlite.org/undoredo.html). This supersedes the undo-toast half of #83 and complements snapshots (#85): undo unwinds your last few edits, snapshots restore how a block looked at a chosen point.
Design
Trigger-based inverse log.AFTER INSERT/UPDATE/DELETE triggers on the planning tables write the inverse statement into an undo_log table — a DELETE logs the restoring INSERT with the old values, an UPDATE logs an UPDATE with the old values. Pure SQL, works through better-sqlite3/Drizzle, lives in a normal migration.
Table-scoped, structurally. Triggers go only on user-planning tables (blocks, recipe rows, goals, splits/modules, tasks, notes, folders). Imported Factorio-dump tables, live-state/bridge tables, app config, snapshots, and the undo log itself never get triggers — the high-volume system writes are excluded by construction, not by per-call discipline. This also keeps data sync and imports at full speed.
Action grouping. An undo step is a user action, not a row change. Every mutating server function runs through a shared mutation wrapper that opens an action (one action id), so "apply this plan" (dozens of rows) pops as a single undo. better-sqlite3 is synchronous and each request runs to completion, so actions can't interleave.
Tracking is opt-out, not opt-in. The wrapper records an undo action by default; the rare system-driven writes to planning tables (snapshot restore internals, migration backfills, dump-import scaffolding) explicitly pass { undo: false }. Rationale: almost every write to a planning table is a deliberate user edit, and the failure modes are asymmetric — a forgotten opt-out just puts one odd entry on the stack, while under opt-in a forgotten opt-in silently makes an action un-undoable. The triggers' WHEN clause checks the current-action marker, so any write that somehow bypasses the wrapper is simply untracked (fail-soft) rather than logged as an orphan.
Linear undo only. Undo pops strictly from the top of the stack — no undoing action N−3 while later actions touched the same rows. Executing an undo must not re-log itself as a new action (either log it as a redo entry or mark it undo: false); redo support is optional and can come later.
Retention. Keep the last N actions (e.g. 50) per project, trimmed on write. The log is scoped per project db like everything else.
UI. Ctrl+Z / Cmd+Z via the global hotkey layer from Command palette: Ctrl+K / / search-everything #78, a toolbar/undo-menu affordance showing what the next undo will revert ("Undo: remove 3 recipes from Iron Pulp"), and a brief toast after each undo. Assistant-applied changes (draft/revise apply) are single actions and therefore undoable — a nice safety valve for propose-then-apply.
Undo covers changes to what the plan is — anything the solver or the stored plan consumes (goals, rates, recipe sets, splits, modules, enabled flags, keep-in-stock/refill windows, notes). It deliberately excludes view state and preferences — transport-visibility toggles, display units (/s vs /min), compact-number formatting, collapsed groups, tabs, sidebar layout. Undoing a hidden view toggle is disorienting and buries real edits.
Mechanically this falls out of the table scoping: preferences live in localStorage or a prefs/settings table (no triggers), plan content lives on planning rows (triggers). Display-ish columns that happen to live on planning rows (e.g. a per-goal rate-window unit) ride along in undo rather than being excluded with column-comparing WHEN clauses — such edits are rare and undoing one is harmless. Corollary for future features: state that shouldn't be undoable belongs in prefs storage, not on plan rows.
Real multi-level undo for planning edits, built on SQLite's canonical trigger-based undo-log pattern (https://www.sqlite.org/undoredo.html). This supersedes the undo-toast half of #83 and complements snapshots (#85): undo unwinds your last few edits, snapshots restore how a block looked at a chosen point.
Design
AFTER INSERT/UPDATE/DELETEtriggers on the planning tables write the inverse statement into anundo_logtable — a DELETE logs the restoring INSERT with the old values, an UPDATE logs an UPDATE with the old values. Pure SQL, works through better-sqlite3/Drizzle, lives in a normal migration.{ undo: false }. Rationale: almost every write to a planning table is a deliberate user edit, and the failure modes are asymmetric — a forgotten opt-out just puts one odd entry on the stack, while under opt-in a forgotten opt-in silently makes an action un-undoable. The triggers'WHENclause checks the current-action marker, so any write that somehow bypasses the wrapper is simply untracked (fail-soft) rather than logged as an orphan.undo: false); redo support is optional and can come later./search-everything #78, a toolbar/undo-menu affordance showing what the next undo will revert ("Undo: remove 3 recipes from Iron Pulp"), and a brief toast after each undo. Assistant-applied changes (draft/revise apply) are single actions and therefore undoable — a nice safety valve for propose-then-apply.Interplay
Part of the polish epic #35.
Scope: plan content vs view state
Undo covers changes to what the plan is — anything the solver or the stored plan consumes (goals, rates, recipe sets, splits, modules, enabled flags, keep-in-stock/refill windows, notes). It deliberately excludes view state and preferences — transport-visibility toggles, display units (/s vs /min), compact-number formatting, collapsed groups, tabs, sidebar layout. Undoing a hidden view toggle is disorienting and buries real edits.
Mechanically this falls out of the table scoping: preferences live in localStorage or a prefs/settings table (no triggers), plan content lives on planning rows (triggers). Display-ish columns that happen to live on planning rows (e.g. a per-goal rate-window unit) ride along in undo rather than being excluded with column-comparing
WHENclauses — such edits are rare and undoing one is harmless. Corollary for future features: state that shouldn't be undoable belongs in prefs storage, not on plan rows.