Canvas read-only viewer: data-loss illusion (local edits vanish on refresh) #26
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_collab#26
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
Read-only canvas viewers (users with role
vieweron a canvas, or workspace members viewing a canvas they're not a collaborator on) experienced a data-loss illusion: clicking format buttons (color, bold, etc.) on the bubble menu visibly changed the canvas locally, then those changes vanished on refresh. To the user this looks like "the system saved my edit and then lost it" — a worse failure mode than disabled buttons.Server-side gating was always correct:
routes.rs::handle_canvas_wsdrops viewer YjsUpdate/SyncStep2frames at line 1072 before broadcast/persist.canvas.save_stateRPC rejects viacheck_canvas_access(write_required: true)at canvas.rs:740.The bug was purely client-side: Tiptap's
setEditable(false)only blocks contenteditable typing; it does not block programmatic dispatch (editor.chain().toggleBold().run()) from toolbar buttons or keyboard shortcuts. The local Yjs Doc accepted these mutations, the y-websocket provider sent them upstream, the server dropped them silently, and refresh reverted to the (unchanged) authoritative state.Reproduction (verified empirically)
viewer_test(read-only on canvas 1):canvas_state.updated_atandcanvases.updated_atnever advanced.Discussed in
Surfaced and analyzed in #10 — see comment #24493 for the empirical investigation that established this issue's framing.
Fix scope
Implemented in PR (linked below):
filterTransactionProseMirror plugin registered on viewers' editors rejects any doc-changing transaction that isn't tagged withySyncPluginKey(the meta @tiptap/y-tiptap sets on remote Yjs sync transactions). This is the structural choke point — catches programmatic dispatch, keyboard shortcuts, and any future code path.editable: falseset at editor construction (vs late toggle) for typing-input gating.body[data-canvas-mode="viewer"]hide the static top toolbar, share button, and the various popovers/pickers.ySyncPluginKey(from@tiptap/y-tiptap) andPlugin(from@tiptap/pm/state) so the filterTransaction wiring is reachable from canvas-app.js.Out of scope (deliberately)
window.editor.chain().toggleBold().run()from DevTools): can still mutate the local Doc, but the filterTransaction plugin still blocks it. Even if it didn't, the server is authoritative — the WS handler andcanvas.save_statereject anyway. Honest UI for legitimate users; not a security boundary.loadChannelsauto-create-#generalbug surfaced in the same investigation thread is a separate concern (chat sidebar, not canvas) and should get its own issue.