Web frame: position does not sync to other viewers when moved/resized #101

Open
opened 2026-04-29 10:04:59 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

When a webframe (iframe-backed object) is moved in tab A, tab B is left in a desynced state: the Konva placeholder card (header + URL label + dark background) moves to the new position, but the actual iframe DOM overlay (the live website content) stays anchored at the old screen coordinates. After tab B is reloaded the two pieces snap back together at the persisted position, so the data is correct on disk — only the live update is broken.

Steps to reproduce

  1. Open a board in tab A.
  2. Open the same board in tab B via the shared link.
  3. Confirm both tabs are connected (cursor presence visible).
  4. In tab A, place a webframe pointing at any URL (e.g. https://example.com). Wait for the iframe to load in both tabs.
  5. In tab A, drag the webframe to a new position on the canvas.
  6. Observe tab B.

Expected

In tab B the iframe DOM overlay (live website content) follows the Konva group as it moves, the same way other objects' visuals do.

Actual

In tab B the Konva placeholder card moves but the iframe overlay stays at the old screen position. The two visually drift apart on every drag. Reloading tab B brings them back together at the persisted location.

Root cause (likely)

webframe.js positions the iframe via an absolutely-positioned <div> overlay outside the Konva stage. updateOverlayPosition(id, group) is the only function that writes the overlay's screen left/top. It is currently only called on:

  • local dragstart / dragend of the group itself,
  • the stage's dragstart / dragend / wheel (pan and zoom).

The applySyncUpdate branch for type === 'webframe' in sync.js only patches existing.url — it does NOT call WhiteboardWebframe.updateOverlayPosition(id, node) after the position write at the top of applySyncUpdate. So a remote position update moves the Konva group but the iframe overlay is never repositioned.

Suggested fix

Two complementary changes:

  1. Expose WhiteboardWebframe.updateOverlayPosition(id, group) (already exported on the module) and have applySyncUpdate call it for any webframe whose position/dimensions changed in the incoming payload, after the Konva group's x/y/width/height have been written. Alternatively, have webframe.js install a node.on('xChange yChange transform') listener that re-runs updateOverlayPosition on every Konva position change — this is the most general fix and also covers programmatic transforms (resize from the Properties panel, undo/redo, etc.).

  2. Ensure the same hook fires when the stage's transform changes programmatically (not just via user drag/wheel). The current stage.on('dragstart/dragend/wheel') listeners miss WhiteboardCanvas.setZoom(...) and stage.position(...) calls — see the related presentation-mode bug for the same root cause manifestation.

Acceptance criteria

  • In tab B, the iframe DOM overlay tracks the Konva placeholder during a remote drag, ending at the same screen position as in tab A.
  • Same for resize: changing the webframe's width/height via the Properties panel in tab A updates the iframe size in tab B live.
  • No regression on local drag/resize/zoom in tab A.
  • No double-apply or flicker on the originator (existing echo skip stays correct).
## Summary When a webframe (iframe-backed object) is moved in tab A, tab B is left in a desynced state: the Konva placeholder card (header + URL label + dark background) moves to the new position, but the actual iframe DOM overlay (the live website content) stays anchored at the old screen coordinates. After tab B is reloaded the two pieces snap back together at the persisted position, so the data is correct on disk — only the live update is broken. ## Steps to reproduce 1. Open a board in tab A. 2. Open the same board in tab B via the shared link. 3. Confirm both tabs are connected (cursor presence visible). 4. In tab A, place a webframe pointing at any URL (e.g. `https://example.com`). Wait for the iframe to load in both tabs. 5. In tab A, drag the webframe to a new position on the canvas. 6. Observe tab B. ## Expected In tab B the iframe DOM overlay (live website content) follows the Konva group as it moves, the same way other objects' visuals do. ## Actual In tab B the Konva placeholder card moves but the iframe overlay stays at the old screen position. The two visually drift apart on every drag. Reloading tab B brings them back together at the persisted location. ## Root cause (likely) `webframe.js` positions the iframe via an absolutely-positioned `<div>` overlay outside the Konva stage. `updateOverlayPosition(id, group)` is the only function that writes the overlay's screen `left`/`top`. It is currently only called on: - local `dragstart` / `dragend` of the group itself, - the stage's `dragstart` / `dragend` / `wheel` (pan and zoom). The `applySyncUpdate` branch for `type === 'webframe'` in `sync.js` only patches `existing.url` — it does NOT call `WhiteboardWebframe.updateOverlayPosition(id, node)` after the position write at the top of `applySyncUpdate`. So a remote position update moves the Konva group but the iframe overlay is never repositioned. ## Suggested fix Two complementary changes: 1. Expose `WhiteboardWebframe.updateOverlayPosition(id, group)` (already exported on the module) and have `applySyncUpdate` call it for any webframe whose position/dimensions changed in the incoming payload, after the Konva group's `x`/`y`/`width`/`height` have been written. Alternatively, have webframe.js install a `node.on('xChange yChange transform')` listener that re-runs `updateOverlayPosition` on every Konva position change — this is the most general fix and also covers programmatic transforms (resize from the Properties panel, undo/redo, etc.). 2. Ensure the same hook fires when the stage's transform changes programmatically (not just via user drag/wheel). The current `stage.on('dragstart/dragend/wheel')` listeners miss `WhiteboardCanvas.setZoom(...)` and `stage.position(...)` calls — see the related presentation-mode bug for the same root cause manifestation. ## Acceptance criteria - [ ] In tab B, the iframe DOM overlay tracks the Konva placeholder during a remote drag, ending at the same screen position as in tab A. - [ ] Same for resize: changing the webframe's width/height via the Properties panel in tab A updates the iframe size in tab B live. - [ ] No regression on local drag/resize/zoom in tab A. - [ ] No double-apply or flicker on the originator (existing echo skip stays correct).
Author
Member

Spec — Issue #101: Webframe iframe overlay must follow remote position/size updates

Objective

Make the iframe DOM overlay (#wf-overlay-<id>) on a receiving client follow the Konva placeholder when a remote user drags or resizes a webframe. Persistence is already correct; only the live (debounced WebSocket) update path is broken.

Requirements

  • When a remote user drags a webframe, the receiving client's iframe overlay must reposition in lock-step with the placeholder card on each object.updated broadcast.
  • When a remote user resizes a webframe (via the canvas transformer), the receiving client's iframe overlay must resize to match.
  • The receiver must keep the iframe visible during remote movement/resizing — only the originator hides their own overlay during their local interaction.
  • No new transports, no new dependencies, no server changes, no polling loop.
  • Smallest possible diff.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js — the only file that needs a real change.

Files to Leave Alone

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js — already exposes refreshOverlay(id) which calls updateOverlayPosition internally and reads dimensions from the .bg rect (the source of truth after a sync writes the geometry). It also exposes updateUrl(id, url) which updates the iframe src and the .label Konva text.
  • properties.js, objects.js, server crates — out of scope.

Diagnosis

applySyncUpdate in sync.js:

  • Writes node.x(obj.x) / node.y(obj.y) near the top.
  • Writes bg.width/height, header width, placeholder/label widths in the dimensions block.
  • The type === 'webframe' branch only patches existing.url; never touches the overlay.

There is no call to WhiteboardWebframe.refreshOverlay / updateOverlayPosition anywhere in applySyncUpdate, which is why a full reload (which reconstructs via createWebframe) brings them back together but live updates do not.

Mechanism — Option A (chosen)

Add a single WhiteboardWebframe.refreshOverlay(strId) call inside applySyncUpdate's existing type === 'webframe' branch. Run it after the existing geometry writes earlier in the function. Optionally also call WhiteboardWebframe.updateUrl(strId, data.url) so a remote URL change propagates to the iframe src and .label without a reload (same root-cause class — currently only existing.url updates locally).

Reject Option B (Konva xChange/yChange/transform listeners on the group): more general but invasive — fires on every transient transform tick during a local drag/transform, creating extra DOM writes per frame on the originator.

Reject Option C (polling): explicitly out of scope.

Step-by-Step Plan

Step 1 — Patch applySyncUpdate in sync.js

File: crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

Locate the existing webframe branch (around lines 666-669):

} else if (type === 'webframe') {
    if (data && data.url) {
        existing.url = data.url;
    }
}

Replace with:

} else if (type === 'webframe') {
    if (data && data.url) {
        existing.url = data.url;
        if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.updateUrl) {
            WhiteboardWebframe.updateUrl(strId, data.url);
        }
    }
    if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshOverlay) {
        WhiteboardWebframe.refreshOverlay(strId);
    }
}

Dependencies: strId is already in scope; the bg/position writes earlier in the function are unconditional and run before this branch.

Step 2 — Verify the fix relies only on already-exported pieces

  • WhiteboardWebframe.refreshOverlay is on the module's public API — no export change needed.
  • WhiteboardWebframe.updateUrl is on the module's public API — no export change needed.
  • updateOverlayPosition reads bg dims as source of truth, so the dimensions block writing to bg.width/height before our branch means the overlay picks up the new size on the same call.
  • _hiddenForInteraction defaults to undefined/false on the receiver because the receiver never calls hideOverlay(id) for a remote drag — stage.on('dragstart.wf_<id>') fires only on local user stage drag, not on programmatic node mutation in applySyncUpdate.

Acceptance Criteria

  • Open the board in two tabs (A and B). In A, drop a webframe, then drag it. On B, both the Konva placeholder card and the live iframe move together — no overlay parked at the old screen coordinates.
  • In A, select the webframe and resize via the canvas transformer. On B, both placeholder and iframe dimensions update to match.
  • Reload of B continues to work (regression check).
  • Originator (A) behaviour during its own drag is unchanged: A's overlay still hides on dragstart and reappears on dragend.
  • Pan and zoom on B with a webframe present continue to work.
  • Stretch: a remote URL change via the URL modal updates the iframe src and .label text on the receiver without a reload.

Notes / Caveats

  • During remote resize, the iframe is not reloaded — refreshOverlay only updates the wrapper's style.left/top/width/height; the iframe inherits via width: 100%; height: 100%. Live page state is preserved.
  • Receivers see WS-throttled updates (~5 Hz), so no perf concern from extra refreshes. Polling is intentionally avoided.
  • Rotation desync (if any) is out of scope — the iframe overlay has never tracked rotation. Separate ticket if reported.
  • No change to the originator's hide-on-drag UX.
  • Do NOT add Konva xChange/yChange/transform listeners on the group at create time. Option A is sufficient and safer.
# Spec — Issue #101: Webframe iframe overlay must follow remote position/size updates ## Objective Make the iframe DOM overlay (`#wf-overlay-<id>`) on a receiving client follow the Konva placeholder when a remote user drags or resizes a webframe. Persistence is already correct; only the live (debounced WebSocket) update path is broken. ## Requirements - When a remote user drags a webframe, the receiving client's iframe overlay must reposition in lock-step with the placeholder card on each `object.updated` broadcast. - When a remote user resizes a webframe (via the canvas transformer), the receiving client's iframe overlay must resize to match. - The receiver must keep the iframe visible during remote movement/resizing — only the originator hides their own overlay during their local interaction. - No new transports, no new dependencies, no server changes, no polling loop. - Smallest possible diff. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` — the only file that needs a real change. ## Files to Leave Alone - `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` — already exposes `refreshOverlay(id)` which calls `updateOverlayPosition` internally and reads dimensions from the `.bg` rect (the source of truth after a sync writes the geometry). It also exposes `updateUrl(id, url)` which updates the iframe `src` and the `.label` Konva text. - `properties.js`, `objects.js`, server crates — out of scope. ## Diagnosis `applySyncUpdate` in `sync.js`: - Writes `node.x(obj.x)` / `node.y(obj.y)` near the top. - Writes `bg.width/height`, header width, placeholder/label widths in the dimensions block. - The `type === 'webframe'` branch only patches `existing.url`; never touches the overlay. There is no call to `WhiteboardWebframe.refreshOverlay` / `updateOverlayPosition` anywhere in `applySyncUpdate`, which is why a full reload (which reconstructs via `createWebframe`) brings them back together but live updates do not. ## Mechanism — Option A (chosen) Add a single `WhiteboardWebframe.refreshOverlay(strId)` call inside `applySyncUpdate`'s existing `type === 'webframe'` branch. Run it after the existing geometry writes earlier in the function. Optionally also call `WhiteboardWebframe.updateUrl(strId, data.url)` so a remote URL change propagates to the iframe `src` and `.label` without a reload (same root-cause class — currently only `existing.url` updates locally). Reject Option B (Konva `xChange`/`yChange`/`transform` listeners on the group): more general but invasive — fires on every transient transform tick during a local drag/transform, creating extra DOM writes per frame on the originator. Reject Option C (polling): explicitly out of scope. ## Step-by-Step Plan ### Step 1 — Patch `applySyncUpdate` in `sync.js` File: `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` Locate the existing webframe branch (around lines 666-669): ```js } else if (type === 'webframe') { if (data && data.url) { existing.url = data.url; } } ``` Replace with: ```js } else if (type === 'webframe') { if (data && data.url) { existing.url = data.url; if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.updateUrl) { WhiteboardWebframe.updateUrl(strId, data.url); } } if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshOverlay) { WhiteboardWebframe.refreshOverlay(strId); } } ``` Dependencies: `strId` is already in scope; the bg/position writes earlier in the function are unconditional and run before this branch. ### Step 2 — Verify the fix relies only on already-exported pieces - `WhiteboardWebframe.refreshOverlay` is on the module's public API — no export change needed. - `WhiteboardWebframe.updateUrl` is on the module's public API — no export change needed. - `updateOverlayPosition` reads bg dims as source of truth, so the dimensions block writing to `bg.width/height` before our branch means the overlay picks up the new size on the same call. - `_hiddenForInteraction` defaults to `undefined`/`false` on the receiver because the receiver never calls `hideOverlay(id)` for a remote drag — `stage.on('dragstart.wf_<id>')` fires only on local user stage drag, not on programmatic node mutation in `applySyncUpdate`. ## Acceptance Criteria - [ ] Open the board in two tabs (A and B). In A, drop a webframe, then drag it. On B, both the Konva placeholder card and the live iframe move together — no overlay parked at the old screen coordinates. - [ ] In A, select the webframe and resize via the canvas transformer. On B, both placeholder and iframe dimensions update to match. - [ ] Reload of B continues to work (regression check). - [ ] Originator (A) behaviour during its own drag is unchanged: A's overlay still hides on `dragstart` and reappears on `dragend`. - [ ] Pan and zoom on B with a webframe present continue to work. - [ ] Stretch: a remote URL change via the URL modal updates the iframe `src` and `.label` text on the receiver without a reload. ## Notes / Caveats - During remote resize, the iframe is not reloaded — `refreshOverlay` only updates the wrapper's `style.left/top/width/height`; the iframe inherits via `width: 100%; height: 100%`. Live page state is preserved. - Receivers see WS-throttled updates (~5 Hz), so no perf concern from extra refreshes. Polling is intentionally avoided. - Rotation desync (if any) is out of scope — the iframe overlay has never tracked rotation. Separate ticket if reported. - No change to the originator's hide-on-drag UX. - Do NOT add Konva `xChange`/`yChange`/`transform` listeners on the group at create time. Option A is sufficient and safer.
Author
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo test --workspace: pass
  • node --check sync.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - cargo clippy --workspace -- -D warnings: pass - cargo test --workspace: pass - node --check sync.js: pass
Author
Member

Implementation Summary

One file changed (sync.js), +7/-0 inside the existing webframe branch of applySyncUpdate.

Root cause

applySyncUpdate writes node.x/y and the bg dimensions on every incoming object.updated, but its type === 'webframe' branch only patched existing.url. The iframe DOM overlay (an absolutely-positioned <div> outside the Konva stage) was never repositioned/resized on the receiver — only the originator's local dragstart/dragend and stage wheel listeners called updateOverlayPosition. Reload masked the bug because createWebframe reconstructs the overlay from scratch.

crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

Inside the existing webframe branch of applySyncUpdate:

  • Call WhiteboardWebframe.updateUrl(strId, data.url) when the URL changes (so a remote URL change updates the iframe src and the .label Konva text live, not only on reload).
  • Call WhiteboardWebframe.refreshOverlay(strId) unconditionally at the end of the branch. refreshOverlay reads dimensions from the .bg rect (which the dimensions block earlier in applySyncUpdate has already updated) and writes them onto the wrapper's style.left/top/width/height. The iframe inherits via width: 100%; height: 100%, so live page state is preserved across resize.

Both helpers were already on WhiteboardWebframe's public API — no export changes needed.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • cargo clippy --workspace -- -D warnings: clean
  • cargo test --workspace: pass
  • node --check sync.js: clean

Manual smoke test

  1. Open the same board in two tabs.
  2. In tab A, drop a webframe and load it.
  3. In tab A, drag the webframe — tab B's iframe overlay tracks the placeholder live.
  4. In tab A, resize the webframe via the canvas transformer — tab B's iframe overlay matches the new size live.
  5. In tab A, double-click the webframe and change the URL — tab B picks up the new URL without a reload.
  6. Pan/zoom in tab B with a webframe present continues to behave as before.

Notes / scope

  • Receiver-only fix. The originator's hide-on-drag UX is unchanged.
  • Receivers see WS-throttled (~5 Hz) updates, so no perf concern from the extra refresh per remote tick. Polling is intentionally avoided.
  • Rotation desync (if ever observed) is out of scope — the iframe overlay has never tracked rotation. Separate ticket if reported.
## Implementation Summary One file changed (`sync.js`), +7/-0 inside the existing webframe branch of `applySyncUpdate`. ### Root cause `applySyncUpdate` writes `node.x/y` and the bg dimensions on every incoming `object.updated`, but its `type === 'webframe'` branch only patched `existing.url`. The iframe DOM overlay (an absolutely-positioned `<div>` outside the Konva stage) was never repositioned/resized on the receiver — only the originator's local `dragstart/dragend` and stage `wheel` listeners called `updateOverlayPosition`. Reload masked the bug because `createWebframe` reconstructs the overlay from scratch. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` Inside the existing webframe branch of `applySyncUpdate`: - Call `WhiteboardWebframe.updateUrl(strId, data.url)` when the URL changes (so a remote URL change updates the iframe `src` and the `.label` Konva text live, not only on reload). - Call `WhiteboardWebframe.refreshOverlay(strId)` unconditionally at the end of the branch. `refreshOverlay` reads dimensions from the `.bg` rect (which the dimensions block earlier in `applySyncUpdate` has already updated) and writes them onto the wrapper's `style.left/top/width/height`. The iframe inherits via `width: 100%; height: 100%`, so live page state is preserved across resize. Both helpers were already on `WhiteboardWebframe`'s public API — no export changes needed. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `cargo clippy --workspace -- -D warnings`: clean - `cargo test --workspace`: pass - `node --check sync.js`: clean ### Manual smoke test 1. Open the same board in two tabs. 2. In tab A, drop a webframe and load it. 3. In tab A, drag the webframe — tab B's iframe overlay tracks the placeholder live. 4. In tab A, resize the webframe via the canvas transformer — tab B's iframe overlay matches the new size live. 5. In tab A, double-click the webframe and change the URL — tab B picks up the new URL without a reload. 6. Pan/zoom in tab B with a webframe present continues to behave as before. ### Notes / scope - Receiver-only fix. The originator's hide-on-drag UX is unchanged. - Receivers see WS-throttled (~5 Hz) updates, so no perf concern from the extra refresh per remote tick. Polling is intentionally avoided. - Rotation desync (if ever observed) is out of scope — the iframe overlay has never tracked rotation. Separate ticket if reported.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#101
No description provided.