Board theme change does not propagate to other open viewers of the same board until they reload #90
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#90
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
When the same board is open in two windows, changing the board theme in one window does not propagate to the other in real time. The other window continues rendering the previous theme until it is manually reloaded — even though other realtime activity is clearly working between the two clients.
Steps to reproduce
cursor moving on the canvas and new text added reflects in real time.
(e.g. Dark → Light).
Expected
Tab A flips to the new theme immediately (this already works), and tab B
also receives the theme change in real time and updates to match —
consistent with how other live updates (cursors, edits) propagate.
Actual
new theme after a manual page reload, despite the realtime channel
clearly being active (cursor presence from tab A is visible in tab B).
Spec — Issue #90: Board theme change must propagate to other open viewers in real time
Objective
When a user changes the board theme in one window/tab, every other client connected to the same board over the existing collaboration WebSocket must apply the same theme immediately, without reloading. The theme must also remain persisted on the server so any future opener of the board gets it on initial load (current behavior — unchanged).
Requirements
crates/hero_whiteboard_ui/src/ws.rs. Do not introduce a new transport, channel, or socket.{ type: '...', sender: ..., data: ... }) used forobject.created/object.updated. Add a newtypevalueboard.theme.updated.board.updateRPC intoboard.data.theme/board.data.customCSS(already implemented instatic/web/js/whiteboard/sync.js'ssaveBoardTheme). No server-side schema change.board.theme.updatedarrives, the client must apply the theme by callingWhiteboardThemes.loadTheme(name, customCSS)so the canvas background, grid, calendars, and theme-dependent renderers refresh — exactly as on first load.board.updateRPC or another WebSocket broadcast (no echo loop).board_view.html) if it sharessync.js— verifyWhiteboardThemesis loaded there; otherwise leave its behavior unchanged (out of scope if themes is editor-only).Files to Modify
1.
crates/hero_whiteboard_ui/static/web/js/whiteboard/themes.jsapplyTheme(name, opts)—opts._fromSync(default false). When true:WhiteboardSync.saveBoardTheme(...)at the bottom ofapplyThemeso we don't re-persist or re-broadcast a theme update we just received._fromSyncis false and the theme was successfully applied), broadcast a WebSocket message to all other clients of this board. Use the existingWhiteboardSync._wsSend(already exposed) with payload: Send it aftersaveBoardThemeis queued (order does not matter — WS and RPC are independent).applyThemeinto an internalapplyThemeLocal(name)helper that both the user-driven and sync-driven paths share, to keep the diff minimal and readable. Keep the public surface (applyTheme,loadTheme) backward compatible.2.
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jshandleWsMessage, add a new branch (placed alongside the existingobject.*/connector.*/comment.*handlers, before thecursor/join/leavecases for clarity): Note:loadThemealready callsapplyThemeinternally — to suppress re-broadcast/re-persist when applied from sync, either:loadThemeaccept anoptsarg and pass{ _fromSync: true }through toapplyTheme, orWhiteboardThemes.applyRemoteTheme(name, customCSS)that wraps the local apply path with_fromSyncsemantics. Pick whichever produces the smaller diff againstthemes.js.sync.js— the existing echo-skip (msg.sender === localUserId) already prevents the originator from re-applying its own broadcast, becausewsSendstampsmsg.sender = localUserIdbefore sending.3. (No server changes required)
crates/hero_whiteboard_server/src/handlers/board.rsalready mergesparams.dataintoboard.dataonboard.update, sothemeandcustomCSScontinue to round-trip through the DB. No edit needed.crates/hero_whiteboard_ui/src/routes.rsandsrc/ws.rsalready provide the per-board broadcast — no edit needed. The relay simply forwards arbitrary text frames between clients of the same board, so adding a newtypevalue is purely a client-side contract.Step-by-Step Implementation Plan
Step 1 — Add
_fromSyncopt tothemes.jscrates/hero_whiteboard_ui/static/web/js/whiteboard/themes.jsapplyTheme(name)signature toapplyTheme(name, opts);opts = opts || {}.WhiteboardSync.saveBoardTheme(...)call inif (!opts._fromSync) { ... }.loadTheme(name, themeData)to forward_fromSync— change toloadTheme(name, themeData, opts)and passopts(or{ _fromSync: true }) toapplyTheme.Step 2 — Broadcast on local theme change
crates/hero_whiteboard_ui/static/web/js/whiteboard/themes.js(same file as Step 1)WhiteboardSync.saveBoardTheme(...)block inapplyTheme, when!opts._fromSyncandWhiteboardSync && WhiteboardSync._wsSend, send:currentThemeascustomCSS: matches the existingsaveBoardTheme(name, JSON.stringify(currentTheme))shape and theloadTheme(name, themeData)consumer signature, so receivers can fall back to deserializing the same blob even if a custom theme name is unknown locally._fromSyncguard exists; otherwise re-applying remotely would re-broadcast).Step 3 — Handle remote theme updates in
sync.jscrates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jshandleWsMessage, add theboard.theme.updatedbranch as shown above. CallWhiteboardThemes.loadTheme(msg.data.name, msg.data.customCSS, { _fromSync: true })(orapplyRemoteThemeif that variant was chosen).if (msg.sender === localUserId) return;already runs before this branch — confirm placement is after that guard so the originator does not re-apply.Step 4 — Manual smoke test
Acceptance Criteria
board.updateRPC from the receiver (verify in network tab — only the originator hitsboard.update).handleWsMessage.Notes / Caveats / Out of Scope
sync.js's existingif (msg.sender === localUserId) return;becausewsSendstampsmsg.sender = localUserId. We rely on this; do not introduce a separate dedup. The_fromSyncflag is a defense-in-depth guard that also prevents re-broadcast/re-persist if anything else (e.g.syncFromServer) ever callsloadThemeprogrammatically.board_view.html): the read-only template should still receive and apply the theme so its rendering stays consistent with the editor. IfWhiteboardThemesandWhiteboardSyncare both loaded there, no extra work is needed. IfWhiteboardThemesis only loaded in the editor template, do not add it now — that's a separate scope.BUILTIN_THEMES,loadTheme(name, customCSS)already populatescustomThemes[name]fromcustomCSSand applies it. This path is exercised by the broadcast.routes.rs. The relay is intentionally generic — adding a new client-sidetypeis sufficient.board.update(similar to howboard.deletedoes it). Theme changes are routed directly client→client over the same WS the originator already has open; piggybacking on the RPC proxy is unnecessary complexity.Test Results
Implementation Summary
Two files changed, JS-only — no server / SDK / OpenRPC / DB changes. Reuses the existing per-board WebSocket relay; no new transport.
crates/hero_whiteboard_ui/static/web/js/whiteboard/themes.jsapplyTheme(name)is nowapplyTheme(name, opts)withopts = opts || {}. The persistence call (WhiteboardSync.saveBoardTheme) and a new live-broadcast call are both gated on!opts._fromSync, so a sync-driven apply does not re-persist or re-echo.saveBoardTheme, when not from sync, the function broadcasts:loadTheme(name, themeData)is nowloadTheme(name, themeData, opts)and forwardsoptsintoapplyTheme. Existing callers pass noopts, so the new arg is fully backward-compatible.crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsboard.theme.updatedbranch inhandleWsMessage, alongside the existingobject.*/connector.*/comment.*branches and after themsg.sender === localUserIdecho-skip:Verification
cargo fmt --all -- --check: cleancargo check --workspace: cleancargo clippy --workspace -- -D warnings: cleancargo test --workspace --lib: pass (no lib tests in scope for this change)node --checkon both modified JS modules: cleanManual smoke test (recommended on the deployed instance)
Notes
board.updatealready mergesdata.theme/data.customCSS, so persistence is unchanged.sync.jsis gated onWhiteboardThemesbeing defined, so templates that do not load the themes module will simply ignore the message._fromSyncguard is defense-in-depth on top of the existingsender === localUserIdecho-skip — the originator does not re-apply, and even if some other code path calledloadThemewith the sync flag in the future, it still would not re-persist or re-broadcast.