Connector delete after endpoint deletion shows misleading "board has been deleted" overlay #117
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#117
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Delete an object that has a connector attached → that connector is cascade-deleted on the server but stays visible in the client. Click the orphan connector and press Delete → the page shows the "This board has been deleted by its owner" overlay even though the board is alive.
Steps to reproduce
Expected
Connector C is removed silently. The board stays open.
Actual
The board-deleted overlay is shown, the editor is torn down, and the user has to navigate back. The board is not actually deleted.
Root cause
crates/hero_whiteboard_server/src/migrations/001_core_schema.sql:93declaresfrom_idandto_idon connectors withON DELETE CASCADE, so SQLite cascade-deletes the connector row the moment its endpoint object is deleted.crates/hero_whiteboard_server/src/handlers/connector.rs::deletecallsis_connector_board_live(db, id):That returns
falsein two unrelated cases:The handler then bails with the literal error string
"board has been deleted".crates/hero_whiteboard_ui/static/web/js/whiteboard/rpc.jsmatches that substring and callsWhiteboardSync.markBoardDeleted(), which surfaces the deletion overlay. So a missing-connector delete is wrongly reported as a board deletion.is_comment_board_livehas the same pattern — comment delete on a stale id would behave the same way, though the cascade trigger is different (a comment is only deleted by the user, not by FK cascade).Fix scope
connector.delete(andcomment.delete) idempotent. If the row is missing, return{deleted: 0}with no error. If the board is genuinely deleted (row exists,boards.deleted_at IS NOT NULL), keep returning the board-deleted error.WhiteboardObjects.deleteObject(id)runs, also remove any local connectors whosefromId === id || toId === idand broadcast / persist their deletion. This eliminates the orphan-connector window entirely. Once the server is idempotent, the client cleanup is purely UX polish but worth doing in the same pass since the symptoms are entangled.Implementation Spec for Issue #117
Objective
Stop showing the board-deleted overlay when the user deletes a connector whose endpoint object was already removed (and the connector therefore cascade-deleted on the server). The board is alive — the missing connector should be a silent no-op, not a misleading error.
Root cause recap
connectors.from_id/to_iduseON DELETE CASCADE, so SQLite cascade-deletes connectors when an endpoint object is deleted.is_connector_board_livereturns false both when the board is soft-deleted and when the connector row is gone. The handler bails with the literal string"board has been deleted", and the rpc client matches that substring to trigger the deletion overlay.Requirements
{ deleted: 0 }) instead of the board-deleted error.comment.delete.Files to Modify
crates/hero_whiteboard_server/src/db/queries.rs— replaceis_connector_board_liveandis_comment_board_livewith a tri-state status helper, or add a parallel one. Keep the existing helpers if other callers rely on them.crates/hero_whiteboard_server/src/handlers/connector.rs—deleteuses the new status helper; "missing" →{ deleted: 0 }, "board deleted" → bail, "live" → delete and return rows affected.crates/hero_whiteboard_server/src/handlers/comment.rs— same change todelete.crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js—deleteObject(id)walksWhiteboardConnectors.getConnectors()and callsWhiteboardConnectors.deleteConnector(connId)for each connector whosefromId === id || toId === id.Implementation Plan
Step 1: Server — tri-state status helpers
File:
crates/hero_whiteboard_server/src/db/queries.rsAdd an enum and two helpers:
Keep the existing
is_*_board_livefunctions in place (other callers —connector.create/update,comment.create/updateetc. — still want the boolean check; "row missing" for those write paths is genuinely an error, but they look up byboard_idnot by row id, so they aren't affected by the cascade bug).Dependencies: none.
Step 2: Server — connector.delete + comment.delete switch on status
Files:
crates/hero_whiteboard_server/src/handlers/connector.rs,crates/hero_whiteboard_server/src/handlers/comment.rsReplace the existing two-line check:
with:
Same change in
comment::deleteusingcomment_board_status.Dependencies: Step 1.
Step 3: Client — cleanup orphan connectors on object delete
File:
crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jsIn
deleteObject(id), before destroying the object, iterateWhiteboardConnectors.getConnectors()and for each connector withfromId === id || toId === id, callWhiteboardConnectors.deleteConnector(connId). The connector module already broadcastsconnector.deleted, persists, and removes from the canvas — so we just delegate.Defensive: wrap in
typeof WhiteboardConnectors !== 'undefined'and copy the keys before iterating (since we're mutating the map).Dependencies: none. Independent of server changes.
Acceptance Criteria
connector.deletedbroadcast path handles this).{ deleted: 0 }and does not trigger the board-deleted overlay.{ deleted: 0 }.cargo fmt,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace --libclean.Notes
objects.parent_idcascades to NULL, not to row delete, so an object delete won't disappear out from under the client. The rest of the write paths look up byboard_id, not by row id, so they don't hit this conflation.crates/hero_whiteboard_server/src/handlers/connector.rs::teststhat creates a connector, deletes one of its endpoint objects (cascade fires), then callsconnector::deleteon the now-missing row and asserts{ deleted: 0 }and no error. Mirror for comment if straightforward.Test Results
cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo check -p hero_whiteboard_server— cleancargo test --workspace --lib— 0 failednode --check objects.js— cleanImplementation Summary
Server:
connector.deleteandcomment.deleteare now idempotent for missing rows. Client:deleteObjectcleans up orphan connectors before destroying the object.crates/hero_whiteboard_server/src/db/queries.rsAdded a tri-state enum and two helpers:
Each runs
SELECT b.deleted_at FROM <table> JOIN boards ...and inspects the result: no row → Missing; row withdeleted_at IS NOT NULL→ BoardDeleted; otherwise Live. The originalis_*_board_livehelpers are kept for other callers.crates/hero_whiteboard_server/src/handlers/connector.rsandcomment.rsBoth
deletehandlers replaced their two-line is-live check with amatchon the new status:Missing→Ok({ "deleted": 0 })BoardDeleted→bail!("board has been deleted")Live→ run the actual deleteSo a connector that was cascade-deleted when its endpoint object was removed (FK
ON DELETE CASCADEfromconnectors.from_id/to_id) is now a successful no-op rather than an error that the rpc client maps to the deletion overlay.crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jsdeleteObject(id)now walksWhiteboardConnectors.getConnectors()first and callsWhiteboardConnectors.deleteConnector(connId)for each connector withfromId === id || toId === id. The connector module already broadcastsconnector.deletedand persists to server, so we just delegate. Defensive: copy the keys before iterating since deletion mutates the map.Files Changed
crates/hero_whiteboard_server/src/db/queries.rs—+44 / 0crates/hero_whiteboard_server/src/handlers/connector.rs—+12 / -3crates/hero_whiteboard_server/src/handlers/comment.rs—+11 / -3crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js—+15 / 0Test Results
cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo check -p hero_whiteboard_server— cleancargo test --workspace --lib— 0 failednode --check objects.js— cleanManual smoke
BoardDeletedarm is unchanged).{ deleted: 0 }cleanly.Notes
object.deleteis unaffected:objects.parent_idcascades to NULL rather than a row delete, so an object never disappears out from under a sibling write path.is_*_board_liveretained: write paths (*.create,*.update) still use the boolean check; for those, "row missing" is genuinely an error.