Workspaces: a workspace deleted from admin still appears in the user's New Board picker, and selecting it fails with "FOREIGN KEY constraint failed" #191
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#191
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
Deleting a workspace from the admin interface does not remove it from
the user-facing workspace list. The deleted workspace remains selectable
in the
New Boarddialog'sWorkspacedropdown until reload, and attempting tocreate a board with that workspace selected fails with the raw database
error
FOREIGN KEY constraint failedsurfaced directly in the UI.Steps to reproduce
(or that exists in their cached workspace list).
+ New Board.New Boarddialog, select the deleted workspace from theWorkspacedropdown.Create.Expected
Workspacedropdown — it should be removed (or marked unavailable) as soon as
the user's view is refreshed, ideally pushed live.
server should respond with a clear, user-friendly error such as
"This workspace no longer exists. Please pick another." — never a
raw
FOREIGN KEY constraint failedmessage.Actual
New Boardwith that workspace selected returnsFOREIGN KEY constraint failed, rendered in red inside the dialog.workspace or reloading the page.
Implementation Spec for Issue #191
Objective
Stop the end-user whiteboard home page from showing a deleted workspace in its "New Board" dropdown, and prevent the raw
FOREIGN KEY constraint failedSQLite error from ever surfacing. When a workspace was deleted out from under the open page, show a friendly notice and re-sync the dropdown.Root cause
loadWorkspaces()inhome.htmlpopulatesworkspacesCacheonce at page load and isn't re-invoked when the New Board modal opens. The end-user home page has no WebSocket connection —sync.jsonly opens one for a specific board (/ws/{board_id}), so admin-sideworkspace.deletedoesn't propagate here.handlers::board::create(handlers/board.rs:27-62) inserts directly; if the workspace was hard-deleted the rusqlite FK error becomese.to_string()→ JSON-RPCmessage: "FOREIGN KEY constraint failed".submitNewBoard(home.html:534-536) renders that raw string into#new-board-error.Files to Modify
crates/hero_whiteboard_server/src/handlers/board.rs— workspace-existence precheck increatewith a stable friendly error message.crates/hero_whiteboard_server/src/handlers/object.rs— regression test in the existing#[cfg(test)] mod tests.crates/hero_whiteboard_admin/templates/web/home.html— re-fetch workspaces every time the New Board modal opens; recover gracefully whenboard.createreports "workspace has been deleted".Implementation Plan
Step 1: Server-side precheck + friendly error
File:
crates/hero_whiteboard_server/src/handlers/board.rsIn
create, afterlet db = state.db.lock().unwrap();and before the existingboard_name_takencheck, insert:Mirrors the existing
"board has been deleted"convention inboard.rs:72-78.Dependencies: none.
Step 2: Regression test
File:
crates/hero_whiteboard_server/src/handlers/object.rs— inside the existing#[cfg(test)] mod tests, afterworkspace_delete_returns_cascaded_board_ids:Dependencies: Step 1.
Step 3: Re-fetch workspaces on modal open
File:
crates/hero_whiteboard_admin/templates/web/home.htmlChange
function openNewBoardModal() {(~line 440) toasync function openNewBoardModal() {.Right after the early field-init block (just before reading
workspacesCache), add:The
onclick="openNewBoardModal()"callers don't need a change — calling anasyncfunction fromonclickreturns a discarded promise, same pattern assubmitNewBoardalready in the file.Side benefit:
loadWorkspaces()also refreshes the top#workspace-selectfilter.Dependencies: none.
Step 4: Recover when
board.createreports the friendly errorFile:
crates/hero_whiteboard_admin/templates/web/home.htmlReplace the existing
submitNewBoardcatchblock (lines 534-536) with:Modal stays open so the user can retry without losing the typed board name.
Dependencies: Step 1 (message string is the contract).
Acceptance Criteria
cargo test -p hero_whiteboard_serverpasses, including the new test.board.createagainst a deleted workspace returns JSON-RPC error withmessage: "workspace has been deleted"and noFOREIGN KEYsubstring.Notes
ErrorCode::ConstraintViolationbut matching FK vs UNIQUE vs CHECK requires the extended code (SQLITE_CONSTRAINT_FOREIGNKEY = 787) and a message string that's locale-flaky. A precheck inside the same lock costs one indexed lookup, is race-free against the connection's own writes, and yields one friendly message — symmetric withboard.gettranslatingQueryReturnedNoRowsto"board has been deleted".workspace.deletedover WebSockets. The end-user WS is per-board (/ws/{board_id}); the home page has no WS connection. Adding a workspace channel would need new server plumbing for one rare race. Re-fetchingworkspace.liston modal open eliminates the stale-dropdown case for ~all real usage; Step 4 handles the residual race recoverably.board.deletedbroadcast unchanged. The RPC proxy'sworkspace.deletesniffer already broadcastsboard.deletedto every open editor for boards inside the cascaded workspace. Users editing a doomed board still see the existingshowBoardDeletedNoticeoverlay. This fix only targets the home page's New Board dialog.Test Results
The new regression test (
board_create_after_workspace_delete_returns_friendly_error) asserts both that the friendly"workspace has been deleted"message is returned AND that no"FOREIGN KEY"substring leaks.Other gates:
cargo fmt --check— passcargo clippy --workspace --all-targets -- -D warnings— passImplementation Summary
Changes
crates/hero_whiteboard_server/src/handlers/board.rs—createnow prechecks workspace existence (mirroringboard.get's"board has been deleted"translation). FK violations no longer reach the wire as raw SQLite strings; instead the handler returns a stable"workspace has been deleted"message.crates/hero_whiteboard_server/src/handlers/object.rs— added regression testboard_create_after_workspace_delete_returns_friendly_errorthat asserts both the friendly message and the absence of any"FOREIGN KEY"substring.crates/hero_whiteboard_admin/templates/web/home.html:openNewBoardModalis nowasyncandawaitsloadWorkspaces()before populating the dropdown — admin-deleted workspaces disappear on the next modal open.submitNewBoard's catch path recognises the friendly server error, re-fetches workspaces, rebuilds the dropdown, and surfaces "That workspace no longer exists. Pick another or create a new one." in the existing inline error slot. The modal stays open so the user can retry without losing the typed board name.Test Results
cargo test -p hero_whiteboard_server— 7 passed / 0 failed, including the new regression test.cargo fmt --check— pass.cargo clippy --workspace --all-targets -- -D warnings— pass.Behaviour after fix
FOREIGN KEY constraint failedtext appears anywhere.Notes
sync.jsis per-board), so the fix relies on re-fetchingworkspace.liston dialog open plus recoverable error handling — no new server channel needed.board.deletedbroadcasts fromworkspace.deletecontinue to fire for users actively editing a board in the deleted workspace.