Skip to content

Fix undo to restore clipped polygons via event sourcing - #28

Merged
cafca merged 4 commits into
mainfrom
claude/mystifying-ardinghelli-ea0481
Apr 16, 2026
Merged

Fix undo to restore clipped polygons via event sourcing#28
cafca merged 4 commits into
mainfrom
claude/mystifying-ardinghelli-ea0481

Conversation

@cafca

@cafca cafca commented Apr 16, 2026

Copy link
Copy Markdown
Owner

What broke

Undo was silently losing data after any paint that triggered polygon clipping. The eraser removed the newly-painted polygon, but the original polygons it had clipped were gone forever — they were destroyed at paint time with no record kept.

What changed

Replaced the ephemeral browser-side undo stack with a server-backed event log.

Data model: New paint_events table is the source of truth (user_id, seq, geometry, value, status). rated_areas becomes a derived cache rebuilt by replaying active events. Undo marks the latest active event as undone and rebuilds the cache from scratch; redo reactivates the earliest undone event and applies it incrementally.

Backend (ratings.rs):

  • Extracted apply_paint() helper (the clipping logic) so rebuild_rated_areas_for_user() can call it per-event
  • paint endpoint: appends event, clears redo stack, applies to cache
  • New POST /api/ratings/undo and POST /api/ratings/redo endpoints
  • All responses include can_undo / can_redo; GET /api/ratings includes them too for page-load state

Frontend:

  • Drop ephemeral undoStack / redoStack arrays — history lives on the server
  • Button state driven from server responses
  • Initial state seeded from first overlay fetch

Why this approach

The alternative (capture originals in the paint response and send them back through a restore endpoint) requires threading fragile state through a UNION CTE across two RETURNING clauses. Event sourcing makes the invariant structural: originals are never lost because paint never destroys — it appends. Undo/redo also now persist across reloads and devices.

Testing

  • Simple paint + undo (regression): polygon disappears, redo brings it back
  • Undo full coverage: paint small green, large red covers it → undo restores green
  • Undo partial overlap: original polygon restored to full shape, no fragment left
  • Multi-step undo/redo chain
  • Redo invalidation after a new paint
  • All 49 unit tests + 29 integration tests pass

cafca added 4 commits April 16, 2026 23:13
Undo was broken after any paint that triggered clipping: the eraser
removed the new polygon but the originals were gone permanently since
they were destroyed at paint time and never recorded.

Approach: treat each paint as an immutable event in a new paint_events
table and make rated_areas a derived cache rebuilt by replaying active
events. Undo flips the latest event to 'undone' and rebuilds; redo
reactivates it incrementally. Undo/redo state now persists across
reloads and is authoritative on the server.

- Add migration 006: paint_events table (user_id, seq, geometry, value, status)
- Extract apply_paint() helper from the paint handler so rebuild can call it per-event
- Add rebuild_rated_areas_for_user() that wipes and replays all active events
- paint endpoint: appends event, clears redo stack, applies to cache
- New POST /api/ratings/undo and /api/ratings/redo endpoints
- All responses include can_undo/can_redo; GET /api/ratings includes them too
- Frontend: drop ephemeral undoStack/redoStack, drive button state from server
Backfill paint_events from existing rated_areas during migration so that
users with pre-migration data don't lose their areas on the first undo
after upgrading. Existing areas are non-overlapping by invariant, so
replaying them in any order produces the same derived state.

Integration tests added:
- undo/redo require auth
- undo is a no-op when history is empty
- simple paint → undo removes area; redo restores it
- new paint clears the redo stack
- undo restores a partially clipped polygon to its original full shape
- undo restores a fully covered (deleted) polygon
- multi-step undo chain empties the overlay
- GET /api/ratings includes can_undo/can_redo flags
Simulates the production migration scenario directly: paints two areas,
wipes paint_events to replicate pre-migration state, re-runs the backfill
SQL from migration 006, then verifies that a subsequent paint + undo leaves
the pre-migration areas intact rather than destroying them.
@cafca
cafca merged commit 946aa2c into main Apr 16, 2026
5 checks passed
@cafca
cafca deleted the claude/mystifying-ardinghelli-ea0481 branch April 16, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant