merge development into main #124

Merged
AhmedHanafy725 merged 215 commits from development into main 2026-04-30 13:17:00 +00:00
Member
No description provided.
Signed-off-by: mik-tf
- Add popover retry logic to connection-status.js (handles CDN bootstrap delay)
- Fix proxy path issues (BASE_PATH env vars, base tags, relative paths)
- Fix iframe-hidden navbar in redis (fixed-position dot for iframe mode)
- Add connection status to hero_auth (inline poller with UI/Backend breakdown)
- Add status dot to whiteboard web view and collab chat page
- Fix foundry base_global.html paths and navbar_global.html dot element
- Fix RPC method names for inspector and browser
- Add popover deps smoke tests
- Fix redis SSO failure redirect and static file packaging

Signed-off-by: mik-tf
Replace subcommand tree with --start/--stop flags. Running without flags
enters foreground mode. Register health check endpoints with ServiceBuilder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Binary uses --start/--stop flags (not positional start/stop arguments).
Also fix status target to use hero_proc list instead of calling binary
with unsupported status subcommand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused `hyper_util::rt::TokioIo` import from hero_whiteboard_sdk
- Add `#[allow(dead_code)]` to unimplemented DB model structs (WorkspaceMember,
  BoardCollaborator, BoardSnapshot, ActivityLogEntry, VoteSession, Vote, Timer,
  Template) — these are schema-mapped models for planned features
- Add `#[allow(dead_code)]` to `batch_update_objects` utility in queries.rs

`cargo build --workspace` now produces zero warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add new crates/hero_whiteboard binary crate with --start/--stop and build_service_definition()
- Simplify hero_whiteboard_server: remove clap/--start/--stop, plain foreground daemon
- Simplify hero_whiteboard_ui: remove clap/--start/--stop, plain foreground daemon
- Makefile: call hero_whiteboard --start / hero_whiteboard --stop
- buildenv.sh: add hero_whiteboard to BINARIES list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Marks all server/UI actions as persistent processes so hero_proc
treats any unexpected exit as a failure and triggers restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Include hero_whiteboard alongside hero_whiteboard_server and hero_whiteboard_ui
in the install loop so the main binary is copied and made executable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UI routes: add X-Forwarded-Prefix middleware to inject WB_BASE for reverse-proxy sub-path support
- JS (rpc.js, sync.js, markdown.js): prefix all fetch/WebSocket/navigation URLs with WB_BASE
- Server RPC: add /health, /.well-known/heroservice.json, and /openrpc.json HTTP endpoints
- main.rs: add kill_other socket cleanup for server and UI actions on service restart
- Makefile: split logs into logs (server) and logs-ui (UI); add logs-ui target
- README: update crate table, sockets section, and API domain overview
- Add .forgejo/workflows/ci.yml for CI pipeline
- Add crates/hero_whiteboard_examples/tests/integration.rs integration test suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: add missing JS files to web assets for status-dot popover
Some checks failed
CI / build (push) Failing after 1s
ce101f0f81
The web view HTML references connection-status.js and bootstrap.bundle.min.js
but these files only existed in the admin static directory (static/js/),
not the web asset directory (static/web/js/). Copied to fix HTTP 404.

Signed-off-by: mik-tf
Add scripts/rhai/run.rhai for hero_proc service lifecycle
Some checks failed
CI / build (push) Failing after 2s
2654b8e971
Rhai script that builds, installs, registers, and starts the service
via hero_proc using the proper action_set() + service_register() model.

Usage: hero_do scripts/rhai/run.rhai  (from repo root)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
validate: scripts cleanup and service registration pattern check
Some checks failed
CI / build (push) Failing after 4s
550856a34e
- Remove all rhai scripts and non-essential scripts (build.rhai, clean.rhai,
  download-assets.sh, install.rhai, install.sh, restart.rhai, run.rhai,
  status.rhai, stop.rhai, test.rhai); keep only scripts/build_lib.sh
- Fix main.rs: change lifecycle::start_service() to lifecycle::restart_service()
  so --start is idempotent (stops old instance before restarting)
- Rewrite Makefile to use build_lib.sh directly and call hero_whiteboard --start/--stop
  for run/stop targets instead of deleted rhai scripts
- Fix buildenv.sh: add required PROJECT_NAME and ALL_FEATURES variables for build_lib.sh
- Update Cargo.lock: bump hero_proc_sdk to commit 2209ea5 which includes restart_service()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add VERSION and PATCHLEVEL to buildenv.sh
Some checks failed
CI / build (push) Failing after 4s
ab83223737
Required by build_lib.sh for version management (ship-binary, release targets,
sync_cargo_version). VERSION=0.1.0 matches workspace Cargo.toml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix hero_proc_sdk git reference and simplify selfstart CLI
Some checks failed
CI / build (push) Failing after 2s
123846b63b
- Restore correct git URL (forge.ourworld.tf/lhumina_code/hero_proc.git)
- Remove redundant socket existence check before connecting (connect_socket handles errors)
- Clean up whitespace in self_start/self_stop functions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: migrate sockets to hero_sockets per-service directory layout
Some checks failed
CI / build (push) Failing after 2s
a664328298
- Server: binds to $HERO_SOCKET_DIR/hero_whiteboard/rpc.sock (was hero_whiteboard_server.sock at socket root)
- UI: binds to $HERO_SOCKET_DIR/hero_whiteboard/ui.sock (was hero_whiteboard_ui.sock at socket root)
- Both services now read HERO_SOCKET_DIR env var with correct default ($HOME/hero/var/sockets)
- CLI binary: socket kill/health-check paths updated to match new per-service directory
- routes.rs: replaced base_path_middleware with hero_context_middleware that reads all three context headers (X-Forwarded-Prefix, X-Hero-Context, X-Hero-Claims)
- Discovery manifests: corrected name to "hero_whiteboard", added "socket" field, fixed protocol to "ui" for UI sockets
- Makefile: echo paths reflect HERO_SOCKET_DIR and new socket names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrate CLI to hero_proc_factory() and update dependencies
Some checks failed
CI / build (push) Failing after 1s
990a2b80bc
- Switch self_start/self_stop to use hero_proc_factory() instead of manual
  lifecycle::restart_service/stop_service with raw HeroProcRPCAPIClient
- Remove dirs dependency from CLI crate (replaced with std::env::var("HOME"))
- Run cargo update: hero_proc_sdk v0.4.1→v0.4.4, tokio v1.50→v1.51, hyper v1.8→v1.9,
  and various other crate updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Performance (#5):
- Replace ~83K individual Konva.Circle grid nodes with single custom Shape
  using sceneFunc for direct canvas 2D drawing (massive memory + render savings)
- Add viewport tracking with rAF-debounced grid redraws and isInViewport culling
- WebSocket message batching: coalesce rapid broadcasts into single batch messages
- Remote cursor presence: real-time cursor broadcasting, rendering, auto-expire
- SQLite batch_update: wrap in transaction (BEGIN IMMEDIATE/COMMIT/ROLLBACK)
- Add partial_update_object() using COALESCE to skip SELECT per object

Miro features (#6):
- 10 new shape types: triangle, pentagon, hexagon, star, cloud, callout,
  cylinder, parallelogram, cross, rounded_rect
- Image object support: paste from clipboard, drag-drop files, base64 storage
- Snap alignment guides: 8px threshold, horizontal/vertical snap lines
- Layer ordering: bring to front, send to back, move forward/backward
- Lock/unlock toggle for objects
- Shape picker dropdown expanded from 4 to 14 options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove /rpc proxy from hero_whiteboard_ui — dashboard only
Some checks failed
CI / build (push) Failing after 3s
c945c87fe8
Remove admin_rpc_proxy, web_rpc_proxy, openrpc_spec handlers and their
route registrations. The forward_rpc helper (only used by admin_rpc_proxy)
is also removed. The call_rpc helper is retained as it is used by
web_shared_board for server-side share-token resolution.

RPC routing is exclusively handled by hero_router_server.

Ref: hero_router#16

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge branch 'development_5' into development
Some checks failed
CI / build (push) Failing after 2s
5a8aebb0e5
fix: update socket paths to per-service directory layout
Some checks failed
CI / build (push) Failing after 2s
f7c4dac403
- Update hero_proc_sdk dependency
- Update hero_rpc_derive and hero_rpc_openrpc dependencies
- Migrate socket paths from /tmp/hero_proc.sock to /tmp/hero_proc/rpc.sock

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update Rust edition to 2024 and refresh cargo lockfile
Some checks failed
CI / build (push) Failing after 1s
cf73cc04d3
Bumped workspace edition from 2021 to 2024 per Hero toolchain conventions.
Ran cargo update to pick up latest hero_proc_sdk (0.5.0) and hero_rpc crates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 18 native dioxus-bootstrap-css _app crates with ConnectionStatusIndicator
- Proxy → hero_router routing model (/<service>/<socket_type>/<path>)
- Socket convention migration (per-service directories)
- OSIS auth dispatch fix (OsisAppWrapper::handle_custom)
- Build parallelism limits for thermal safety
- hero_services → hero_zero rename
- Service TOML URL fixes for new routing pattern

Signed-off-by: mik-tf
Switch whiteboard CSS body reset to apply globally instead of only in wb-standalone mode, and update Cargo.lock to remove unused patch entries while adding git sources for hero_proc_sdk, hero_rpc_derive, and hero_rpc_openrpc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: update SDK default socket to directory convention
Some checks failed
CI / build (push) Failing after 2s
f2b1ecefb4
lhumina_code/home#116

Signed-off-by: mik-tf
Support eraser and fix the ci (#8)
All checks were successful
CI / build (push) Successful in 2m18s
2dc094bdbe
Reviewed-on: #8
Keep the kanban changes persisten
All checks were successful
CI / build (pull_request) Successful in 2m12s
da6b41ba43
Reviewed-on: #9
Fix the sharing buttons
Some checks failed
CI / build (pull_request) Failing after 58s
f2f63e88e0
Reviewed-on: #10
Fix the movement and editing of the object on brave browser
Some checks failed
CI / build (push) Failing after 2s
7a6991024a
- Change hit overlay fill from fully transparent to rgba(0,0,0,0.001) for Brave compatibility
  Hit detection on Brave may not work reliably with fully transparent fills; using imperceptible
  opacity ensures consistent behavior while remaining visually invisible
- Add listening: false to background rect and placeholder to prevent interference
- Remove draggable(false) override in refreshFromServer that was breaking locally-created documents
  This was incorrectly marking synced objects as non-draggable, causing issues when server state
  was refreshed or documents were reloaded

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fix zooming after clicking the fit button
Some checks failed
CI / build (push) Failing after 2s
b4b9274416
Fix the markdown docs right click
Some checks failed
CI / build (push) Failing after 2s
56e08c5c68
Update the Readme for brave users
Some checks failed
CI / build (push) Failing after 46s
937e0f5b1f
Adjust the paste behavior
Some checks failed
CI / build (push) Failing after 54s
28cb7bf5a3
Add button for open the url on a new tab for web frame
Some checks failed
CI / build (push) Failing after 2s
d598c55839
Fix the web frame resizing
Some checks failed
CI / build (push) Failing after 48s
84de5428ac
Fix the kanban resizing
Some checks failed
CI / build (push) Failing after 52s
18766f4a95
Fix the shape resizing
Some checks failed
CI / build (push) Failing after 1m3s
540ced1eee
Fix the resizing content size
All checks were successful
CI / build (push) Successful in 2m11s
5858437315
Fix the kanban 3 dots menu
All checks were successful
CI / build (push) Successful in 2m10s
74f990695f
Use fixed size for the eraser based on the scale not in pixels
All checks were successful
CI / build (push) Successful in 2m39s
a2c38d3a6b
Keep the zoom and pan position across reloads
All checks were successful
CI / build (push) Successful in 2m11s
0164d0f794
Fix change the shapes from the properties sidebar
All checks were successful
CI / build (push) Successful in 2m10s
dd09ce7c35
Fix mind map leaf right clicks
All checks were successful
CI / build (push) Successful in 2m12s
de034b1246
Fix the connector deletion
All checks were successful
CI / build (push) Successful in 2m10s
0d0230bbea
Presist dimensions after reloading
All checks were successful
CI / build (push) Successful in 2m50s
59e50e61e8
Add limit for the dimensions and allow the opposite resizing
Some checks failed
CI / build (push) Failing after 3s
a42931d746
Make the rotation persistent
All checks were successful
CI / build (push) Successful in 2m9s
0b6b3f27da
Make connector selectable
All checks were successful
CI / build (push) Successful in 2m16s
e8b6d5ac35
Don't resize on double click on the calendar
All checks were successful
CI / build (push) Successful in 2m12s
f8672c0021
fix(webframe): hide iframe overlay during drag/pan/zoom to kill desync
All checks were successful
CI / build (pull_request) Successful in 2m11s
92df0ad454
Eliminates the visual glitch where the HTML iframe body lagged behind the
Konva-rendered header during webframe drag, stage pan, or wheel-zoom.

- Add hide/show helpers and an _hiddenForInteraction gate in webframe.js
- Swap per-frame dragmove overlay repositioning for dragstart/dragend
- Debounce wheel-zoom (150 ms) and use dragstart/dragend on stage pan
- Hide all overlays around transformer events so resize uses the Konva
  group as the sole visual, then show them after applyTransform writes
  final bg dimensions
- Keep placeholder and label widths in sync with bg on webframe resize

#16
Reviewed-on: #23
fix(calendar): live-redraw during resize and view-aware minimums
All checks were successful
CI / build (pull_request) Successful in 2m18s
9bc0641173
Fixes two visible resize glitches on the calendar widget:

- Children no longer look stretched mid-drag. A transformer 'transform'
  handler now applies the live scale to bg, clamps to per-view minimums,
  resets group scale to 1, and redraws the calendar children every tick.
- Per-view minimum sizes prevent broken renders at odd aspect ratios
  (month 280x260, week 320x220, day 200x280), exposed via a new
  WhiteboardCalendar.getMinSize helper and consumed by both the live
  transform handler and the existing applyTransform release path.
- Defensive Math.max floors on cellH and hourH keep rows from collapsing
  even if a legacy persisted bg lands under the minimum.

#18
Reviewed-on: #24
fix(calendar): persist view mode and current date on every mutation
All checks were successful
CI / build (pull_request) Successful in 2m8s
2806e38f8c
Previously only the double-click handler called WhiteboardSync.onUpdate,
so changing the view via the properties panel selector, pressing Prev /
Today / Next, or clicking the in-header nav arrows mutated local state
and redrew the calendar but never reached the server. After reload the
calendar reverted to the default month view on today's date.

Call WhiteboardSync.onUpdate from every mutation path:
- calendar.js: navigateNext, navigatePrev, cycleViewMode, navigateCalendar
- properties.js: prop-cal-viewmode change, prop-cal-today click
(prop-cal-prev / prop-cal-next go through navigatePrev / navigateNext
so they piggy-back on those fixes.)

#25
Reviewed-on: #26
fix(tools): expand transformer anchor hit area for easier grabbing
All checks were successful
CI / build (pull_request) Successful in 2m12s
a8757454e5
The resize/rotate anchors were 8px (smaller than Konva's 10px default)
and the hit region matched the visual, so clicks within a few pixels of
the corner missed and selected the underlying object instead.

- Bump visible anchorSize 8 -> 10
- Install a padded hitFunc on every anchor (6px padding -> ~22x22 pick
  area) via a helper that re-runs on transform, transformer.nodes(), and
  transformer.forceUpdate() so the padding survives anchor rebuilds

#21
Reviewed-on: #27
fix(app): persist view after Fit / mindmap auto-fit
All checks were successful
CI / build (pull_request) Successful in 2m24s
ff29e4de0a
zoomReset (the Fit toolbar button) and fitMindmapToView both set
stage.scale and stage.position directly, but never called
WhiteboardCanvas.saveView. The resulting view was lost on reload — the
board came back at the previously-saved wheel/drag zoom instead of the
fitted view.

Call saveView at every exit point of zoomReset (empty-board path, empty
bounding-box path, and the normal fit path) and at the end of
fitMindmapToView.

#28
Reviewed-on: #29
fix(mindmap): refresh transformer outline on direction change
All checks were successful
CI / build (pull_request) Successful in 2m17s
a997be93b3
flipDirection (in-widget toggle + L shortcut) and the properties panel
direction select both re-laid-out the mindmap but never told the
transformer, so its outline and anchors stuck to the old orientation's
bounding box until the user deselected and re-selected.

- mindmap.js flipDirection: forceUpdate the transformer when the group
  is the selected node.
- properties.js prop-mm-direction change: same forceUpdate, and also
  call WhiteboardSync.onUpdate so the new direction actually persists
  (it wasn't being saved at all from this entry point).

#30
Reviewed-on: #31
fix(comments): popover position, rubber-band inclusion, Escape to close
All checks were successful
CI / build (pull_request) Successful in 2m9s
ad58bb4153
Three small UX fixes on the comment tool:

- positionPopover was double-counting the stage transform (markerPos from
  getAbsolutePosition is already stage-container pixels; the code then
  multiplied by scaleX and added stage.x/y). At zoom-out the popover
  landed far from its marker. Drop the extra terms.
- Rubber-band selection in tools.js iterated only '.object' nodes, so
  comment markers (name 'comment-marker') were never picked up; a group
  drag left them behind. Also walk '.comment-marker' on mouse up.
- No Escape handler on the thread popover. Added a document-level
  keydown handler installed when either popover opens and removed in
  hidePopover, so Esc now dismisses both the thread and new-comment
  popovers.

#32
#33
#34
fix(comments): eraser, properties pane, un-resolve, right-click, persistence
All checks were successful
CI / build (pull_request) Successful in 2m16s
c2e48f97db
Batch of six comment-related UX fixes:

- Eraser drag over a comment now deletes it (tools.js walks
  '.comment-marker' and routes to WhiteboardComments.deleteComment).
- Properties panel renders a comment pane with Resolve/Unresolve + Delete
  when a comment marker is selected (properties.js).
- Server comment.resolve takes an optional `resolved: bool` (defaults to
  true); client exposes unresolveComment and the popover/properties pane
  both render an Unresolve button when the comment is resolved.
- Right-click on a comment no longer opens the thread popover (click
  handler filters e.evt.button !== 0); the canvas context menu still
  fires normally.
- Comments now carry rotation and scale columns (migration 005),
  persisted through comment.update and comment.create, restored in
  addCommentMarker, and baked on transformer transformend via a new
  WhiteboardComments.onTransformEnd helper.

#36
#37
#38
#39
#40
#41
Reviewed-on: #35
fix(comments): register migration 005 in the runtime migration list
All checks were successful
CI / build (pull_request) Successful in 2m8s
7d5994e840
The ALTER TABLE migration that adds rotation and scale columns was on
disk but missing from the Migrations::new vec in db/mod.rs, so it never
ran against existing databases and comment.create failed with
"table comments has no column named rotation".

#40
#41
fix(comments): properties pane polish + Delete key route
All checks were successful
CI / build (pull_request) Successful in 2m9s
84be2dad0e
Follow-ups on the batch comment PR:

- Comment properties Delete button now matches the generic objects'
  Delete styling (full-width danger-bordered button on its own row).
- Clicking Resolve/Unresolve in the properties pane re-renders the pane
  so the button flips to its counterpart without having to re-select.
- Deleting a comment now strips the marker from the transformer and
  hides the properties pane before destroying the Konva group, so no
  stale outline or empty-pane artifact is left behind.
- Delete key on a selected comment marker now routes through
  WhiteboardComments.deleteComment (previously it silently failed
  because comments aren't in the WhiteboardObjects store).

#37
#38
Reviewed-on: #42
feat(emoji): Miro-style emoji picker tool
All checks were successful
CI / build (pull_request) Successful in 2m12s
44c5c02202
New emoji-smile toolbar button opens a picker popover with search, 9
category tabs (~300 curated emojis), and click-to-place. After picking
an emoji the next canvas click drops it at that point as a first-class
'emoji' object (Konva.Text inside a Group with an invisible .bg hit rect
so the generic transformer / rubber-band / eraser / applyTransform paths
all work without special cases).

- New module: emoji_picker.js (IIFE, no external deps).
- Uniform resize: applyTransform takes min(scaleX, scaleY) to keep the
  glyph square.
- Persistence: reuses the existing objects table — type='emoji',
  data={char, fontSize}. No server changes.
- Keyboard: ':' opens the picker (when no input is focused); Escape
  closes it without falling through to deselect.
- Styling: new .wb-emoji-picker CSS; theme vars re-used.

#43
fix(emoji): don't close picker on canvas click — let the stage place first
All checks were successful
CI / build (pull_request) Successful in 2m10s
57e72e0e23
The outside-click handler ran on the document capture phase and fired
before the Konva stage mousedown bubbled, so close() cleared emojiChar
via setEmojiChar(null) before the 'emoji' branch in tools.js onMouseDown
could read it. Result: clicking the canvas placed nothing.

Skip closing when the click target is inside #whiteboard-container.
The stage mousedown drops the emoji and then tools.js calls
WhiteboardEmojiPicker.close() explicitly.

#43
feat(emoji): add Recent tab backed by localStorage
All checks were successful
CI / build (pull_request) Successful in 2m11s
e1ae79b1cf
Adds a 'Recent' category as the first tab in the emoji picker, backed
by localStorage key wb.emoji.recent (cap 24). Every time an emoji is
placed on the canvas, it moves to the front (dedupe + LRU). The Recent
tab becomes the default active tab when history is non-empty.

Shows a helper message in the grid when Recent is active and empty.

#43
Reviewed-on: #44
feat(kanban): drag-and-drop cards between and within columns
All checks were successful
CI / build (pull_request) Successful in 3m0s
898f32d9e5
#45
Reviewed-on: #46
fix(kanban): refresh transformer handles after content-driven re-renders
All checks were successful
CI / build (pull_request) Successful in 2m9s
dd1e15693e
#49
Reviewed-on: #59
fix(kanban): vertically center column color indicator against title text
All checks were successful
CI / build (pull_request) Successful in 2m9s
4e78ad3e03
#53
Reviewed-on: #61
fix(kanban): select card with click and delete with Delete/Backspace
All checks were successful
CI / build (pull_request) Successful in 2m10s
77dc160f88
#48
fix(kanban): ensure kanban is transformer-selected on card/bg click
All checks were successful
CI / build (pull_request) Successful in 2m8s
d046a13be6
Inside the click handlers, renderKanban() destroys the event target
before the stage-level handler in tools.js can walk its ancestor chain
to transformer-select the kanban group. As a result, a first card click
set the internal selection but did not select the kanban outline, so
Delete did nothing. Now the handlers set the transformer themselves
(mirroring the stage handler's behavior) before re-rendering.

#48
fix(kanban): let pointer events on card text pass through to the card rect
Some checks failed
CI / build (pull_request) Failing after 1s
7a6ed6f5a6
The card text node intercepted clicks and drags, so clicking on the
text (vs. empty card area) neither selected nor dragged the card. Set
listening: false on the text so all pointer events fall through to the
cardRect underneath, which owns the click/drag/dblclick handlers. The
now-redundant dblclick handler on cardText is removed.

#48
fix(kanban): drag whole card as a unit so text follows the rect
All checks were successful
CI / build (pull_request) Successful in 2m10s
a09b8d15f4
Wrap each card's rect, text, and menu in a per-card Konva.Group that
is the draggable unit. Previously only cardRect was draggable, so the
text and menu stayed at their old positions during drag and only
snapped into place on dragend.

#48
Reviewed-on: #62
fix(kanban): eliminate resize snapback and lower shrink minimums
All checks were successful
CI / build (pull_request) Successful in 2m9s
1686120930
Extend the existing transform live-redraw handler to also cover kanban
so that scale is converted into colWidth/cardHeight state on every
tick. Previously only colWidth/cardHeight were scaled into state on
transformend, while the padding/header/gap constants were scaled
uniformly during drag and reverted on release — producing a visible
size jump. Align the minimum clamps in applyTransform and the
property-panel sliders at colWidth 100 and cardHeight 22 so the two
resize paths stay consistent.

#50
Reviewed-on: #63
fix(admin): coerce IDs to int in dashboard RPC calls
All checks were successful
CI / build (push) Successful in 3m32s
5ab6686f58
Admin dashboard was passing workspace/board/user/group IDs as strings
from onclick attributes and input.value; server handlers use
`.as_u64()` and rejected them with "missing 'id'" — same class of bug
as home.html (fixed in 6374655). Normalize at entry points and add a
`getSelectedIntValues()` helper so group `workspace_ids` arrays stop
being silently dropped by the server's `filter_map(as_u64)`.
fix(kanban): scale card and column font sizes with resize state
All checks were successful
CI / build (pull_request) Successful in 2m6s
079a6bd397
Card text and the 3-dot menu glyph scale with cardHeight; column
titles and the +Add card button scale with colWidth. All formulas
are calibrated to produce the previous hardcoded values at the
default sizes (colWidth 200, cardHeight 44) so existing kanbans
do not visually shift on load. A cardH <= 48 branch preserves
exact pixel parity for card-text and menu-glyph positioning at
default card heights while vertically centering glyphs on larger
cards.

#51
fix(kanban): restore card text editing via double-click and menu
All checks were successful
CI / build (pull_request) Successful in 2m9s
b60852bd4a
Two regressions from the card-select/drag work:

- Double-click no longer opened the inline editor because the card's
  click handler called renderKanban, which destroyed the cardRect
  between the two clicks. Konva's dblclick detector requires the
  second click to hit the same node as the first, so it never fired.
  Update stroke/strokeWidth directly on click and track the accented
  cardRect on the group, preserving its identity so dblclick works.
  renderCard re-attaches the ref during full re-renders triggered
  by state changes.

- The 3-dot menu "Edit" action walked group.children looking for the
  Konva.Text matching the card, but the text now lives inside the
  per-card cardGroup introduced with drag-as-a-unit. Pass cardText
  into showCardMenu so Edit has a direct reference.
Reviewed-on: #64
Reviewed-on: #65
feat(kanban): wrap card text and support multi-line editing
All checks were successful
CI / build (pull_request) Successful in 2m11s
1a3e61fdc2
Card text now word-wraps instead of truncating with an ellipsis, and
cards auto-grow vertically to fit wrapped or explicitly multi-line
content. Base cardHeight in state remains the slider/transformer-
controlled minimum and is not affected by the new auto-grow; per-card
actual heights are re-derived every render from the text, font size,
and text width.

renderKanban now does a two-pass layout: it measures every column's
cards up-front, stores {heights[], offsets[]} per column in a module-
level WeakMap, and computes the column-background height from the
tallest stack. renderColumn and renderCard read the layout back, and
the dragend slot calculation uses per-card midpoints so reordering
works correctly when cards have different heights.

editInline gains a multiline flag. When true it renders a <textarea>
with auto-grow on input, Shift+Enter for newlines, Enter/blur to
commit, and Escape to cancel. Single-line <input> path is preserved
for board and column titles. Both card edit entry points (double-click
and the 3-dots menu Edit item) now pass multiline: true.

#52
fix(kanban): match editor font to card and disable spellcheck squiggles
Some checks failed
CI / build (pull_request) Failing after 2s
9d2fd64486
The inline textarea inherited the browser's default font family and
spellcheck, so editing a card showed a different typeface than the
Konva-rendered text and red squiggles under every word. Pick up the
text node's own fontFamily (defaulting to Arial to match Konva's own
default) and turn off spellcheck / autocomplete / autocorrect /
autocapitalize on the overlay.
fix(kanban): grow card live while editing so following cards slide down
All checks were successful
CI / build (pull_request) Successful in 2m9s
9a6b4c90bd
The edit textarea auto-grew to fit the typed text, but the underlying
card rect only resized on commit, so mid-typing the textarea visually
overflowed onto the next card. Push the current textarea value into
card.text on each input event and re-render the kanban so the card
rect expands and cards below move down in real time. Sync still
fires only on commit, so intermediate keystrokes do not trigger
network writes.
fix(kanban): match Konva card-text line-height to editor so card grows
All checks were successful
CI / build (pull_request) Successful in 2m8s
65f2847ecb
Konva.Text defaults lineHeight to 1 while the edit textarea uses
line-height 1.3, so the Konva-measured height for the same content
was ~30 percent shorter than the browser actually rendered. The
live-grow code trusted the Konva measurement and left the card
visibly too short while typing. Set lineHeight 1.3 on both the
rendered cardText node and the measureCardTextHeight temp node so
both sides agree.
Reviewed-on: #66
fix(resize): keep pivot corner fixed across all object types
Some checks failed
CI / build (pull_request) Has been cancelled
788a7ccb0a
Konva's Transformer sets node.x / node.y each tick assuming the visible
size equals oldSize * scale. When our applyTransform and live-redraw
paths round or clamp the committed size to something other than
oldSize * scale, left/top-anchored drags shift the pivot corner by the
delta, producing visible drift (most obvious on calendar past its
minimum).

Introduce a shared correctResizeDrift helper in tools.js that shifts
node.x by (expectedW - newW) for left anchors and node.y by
(expectedH - newH) for top anchors. Wire it into the calendar and
kanban live-redraw branches and into every applyTransform branch that
mutates size: sticky, shape (Rect, Ellipse, RegularPolygon, Star,
Path, Line), frame, document, image, webframe, emoji, calendar,
kanban, and mindmap. Text and drawing branches are skipped because
they do not resize.

Mindmap is handled by reading the intrinsic bbox via
getClientRect({skipTransform: true}) and comparing expectedW =
intrinsic.width * scaleX against newW = intrinsic.width *
finalClampedScale, since mindmap bakes a uniform scale onto the group
instead of resetting scale to 1.

The shape else-fallback is tightened to `else if (cls === 'Rect')`
so unknown classes fall through without a bogus drift correction;
all six currently used shape classes are explicitly handled.

#58
fix(resize): compute expected size from transformstart snapshot
Some checks failed
CI / build (pull_request) Failing after 2s
c6bca71e35
Konva's Transformer computes each tick's scale relative to the bounding
box sampled at transformstart, not the current mutated bg. The previous
drift fix computed expectedW = currentBgWidth * scaleX, which matched
Konva on tick 1 but diverged on later ticks once our live-redraw had
mutated bg. Once the clamp engaged, the mismatch between our expected
and Konva's internal expected compounded each tick into visible drift.

Snapshot bg.width / bg.height (and the kanban colWidth / cardHeight)
at transformstart, use those as the basis for expectedW / expectedH on
every tick, and clear the snapshot at transformend. Scale commits now
always derive from the original values so correctResizeDrift sees the
true Konva-expected size and compensates for clamp overshoot exactly.
fix(resize): re-anchor node to snapshotted pivot instead of scale-derived math
All checks were successful
CI / build (pull_request) Successful in 2m10s
a3b7052781
The previous drift correction shifted node.x by (expectedW - newW), which
required expectedW to match Konva's internal per-tick scale math. For
left-anchor drags past the minimum, Konva keeps committing ever-smaller
scales while our clamp keeps committing the same min size, so the
difference (expectedW - newW) grows each tick and the shift compounded
into visible drift regardless of whether we sampled bg width at tick
start or at transformstart.

Snapshot the initial visible right edge (node.x + bg.width) and bottom
edge at transformstart, and after each live redraw set node.x =
pivotX - newW and node.y = pivotY - newH directly. The new node
position no longer depends on Konva's scale math, so clamping at the
minimum keeps the pivot exactly where it was when the drag began.
fix(resize): enforce per-type minimum via boundBoxFunc instead of clamping
All checks were successful
CI / build (pull_request) Successful in 2m10s
231c1cf054
The previous approach clamped bg.width / bg.height inside the transform
handler after Konva had already committed a smaller scale. Konva kept
tracking the cursor past our minimum while we held the size fixed, so
the user saw the object continue to translate across the canvas even
though the box had stopped shrinking. Position adjustments in the
handler could not fix this because Konva re-computed node.x/y from the
active pointer on every subsequent tick.

Move the minimum enforcement into Konva's own boundBoxFunc. When the
user drags past a calendar's view-specific minimum or a kanban's
computed minimum, boundBoxFunc returns the old box; Konva refuses the
transform entirely and the node and transformer handles both freeze
until the cursor moves back above the minimum. The transform handler
now applies only the scale -> bg conversion (with Math.round for sub
pixel cleanup) — no clamp, no drift.
fix(resize): let Konva handle pivot, enforce min via boundBoxFunc only
All checks were successful
CI / build (pull_request) Successful in 2m14s
3a76c8a24e
Previous attempts tried to correct drift by mutating node.x / node.y
inside the transform handler based on initW * sx. That math did not
match what Konva commits per tick (sx is measured against the node's
current width, which we were mutating every tick), so enlarging
produced the wrong size and shrinking below the minimum still drifted.

Drop all of the init-size snapshotting, pivot tracking, and
correctResizeDrift helper. The handler now just commits bg.width =
round(bg.width() * sx) (same as the original working code) and
resets scale to 1 — Konva's own math keeps the pivot fixed because
our committed width matches Konva's expected width for that tick.

Per-type minimums stay enforced inside boundBoxFunc. When the user
drags past a calendar's view-specific minimum or a kanban's computed
minimum, boundBoxFunc returns oldBox; Konva refuses the transform
entirely, so position and scale freeze until the cursor moves back
above the minimum.

Also revert the per-branch correctResizeDrift wiring in objects.js —
no longer needed now that Konva handles the pivot during drag and
applyTransform at transformend already commits size the same way the
original code did.
Reviewed-on: #68
fix(shape): persist stroke width / stroke color / fill across reload
All checks were successful
CI / build (pull_request) Successful in 2m17s
319977588d
The Properties-panel handlers for prop-stroke-input, prop-fill-input,
and prop-stroke-width mutated the Konva bg and redrew the layer but
never called WhiteboardSync.onUpdate. Without that call the server
never heard the edit, so reloading reset the shape to its pre-edit
style. Add the onUpdate call to each of the three handlers, matching
the pattern already used by every other object-type style handler in
the same file. No schema or RPC changes; style.strokeWidth, .stroke,
and .fill already round-trip through the existing wire format.

#67
Reviewed-on: #70
feat(kanban): drag column title to reorder columns
All checks were successful
CI / build (pull_request) Successful in 2m17s
8727eb76ea
Capture per-column horizontal bounds on the group each render, mark
colTitle as draggable, and wire dragstart/dragend handlers. The
dragstart cancels bubble (preventing the whole-kanban drag from also
firing), snapshots history, moves the title to the top, and records
the source index. The dragend converts the pointer to group-local
coordinates, finds the target column via the stored layout, splices
state.columns, re-renders, and calls WhiteboardSync.onUpdate. Snap
back, single-column, and same-slot drops are no-ops and do not emit
sync traffic.

Column order is already the array order of state.columns and already
round-trips through the existing serializer, so no schema or sync
changes are required.

#69
feat(kanban): drag whole column as a unit during reorder
All checks were successful
CI / build (pull_request) Successful in 3m13s
f50e7cd694
Wrap each column's children in a per-column colGroup and make that the
draggable unit, mirroring the per-card cardGroup introduced for card
drag. Header, cards, indicator, delete button, and +Add card button
now all move with the cursor during a reorder instead of just the
title text.

Target-slot detection in dragend switched from range inclusion to
closest-center matching, so dropping in the padding gap between two
columns lands on the nearer slot instead of clamping to the far end.

cardGroup.dragstart now reparents the dragged card to the kanban group
(preserving its absolute position) so cross-column card drags render
above all columns. renderKanban on dragend destroys and rebuilds,
so no explicit cleanup is needed.
fix(kanban): allow reorder past the last column and tighten drop threshold
All checks were successful
CI / build (pull_request) Successful in 2m15s
1dc3f97e48
The prior dragend logic picked the closest of the N existing column
centers, so there was no way to signal "past the last column" and a
middle column could never become the new rightmost. It also required
dragging roughly a full column width before any swap, which read as
"sometimes it doesn't move".

Replace with a body-overlap check: whenever the dragged column's
center falls inside another column's body (extended by half the
inter-column padding so the gap never eats the drop), that column
becomes the pivot — swap with it if it is to the source's side, or
insert after it if the drag went the other way. Drops past the last
column's right edge or before the first column's left edge map to
the "past end" / "before start" slots. No-op detection stays
(targetSlot equal to srcIdx or srcIdx+1). Reorder threshold is now
roughly half a column width, which matches the visual cue.
Reviewed-on: #71
feat(document): internal scroll and stop auto-growing on edit
All checks were successful
CI / build (pull_request) Successful in 2m11s
cf8bf4d9f8
Document elements now respect the size the user chose and expose a
vertical scroll for overflowing content, via three changes in
renderDocumentContent and createDocument:

- Auto-grow removed. renderDocumentContent no longer mutates bg.height
  to fit content. User size (from the default or from the transformer
  resize) is authoritative; editMarkdownText, rerenderDocument, and
  remote sync re-renders all stop enlarging the element.

- Scroll state on the group. createDocument initialises _scrollY and
  _contentHeight and attaches _docMaxScroll / _docSetScrollY /
  _docClampScroll. SetScrollY clamps, repositions every md-line-*
  child by its stamped _baseY minus _scrollY, updates the scrollbar
  thumb, and batchDraws.

- Wheel interception. A wheel handler on the group consumes deltaY
  when the document overflows (and cancels bubbling so the stage pan
  does not fire); passes through otherwise.

- Scrollbar. renderDocumentContent now rebuilds a track + draggable
  thumb on the right edge when content overflows, with a dragBoundFunc
  constraining the thumb to the track. On dragmove the thumb position
  is converted back into _scrollY. Hides when content fits.

- Resize re-clamp. applyTransform for documents drops the now
  no-op skipAutoGrow flag and calls _docClampScroll after re-render
  so enlarging pulls scroll back to 0 and shrinking reveals hidden
  content.

#73
Reviewed-on: #75
fix(calendar): scale text and chrome with widget size
Some checks failed
CI / build (push) Failing after 0s
8b9d9bdeef
#76
#77
fix(tools): clamp resize floor per-axis instead of rejecting whole box
All checks were successful
CI / build (push) Successful in 2m15s
4faefca0f0
Hitting the width floor no longer freezes height changes, and vice versa.
The transformer's boundBoxFunc now keeps the violating axis at oldBox
(both size and position so the anchored edge stays in place) while
letting the other axis through.
fix(sync): relayout content when a remote user resizes an object
Some checks failed
CI / build (push) Failing after 0s
56cd4bb356
applySyncUpdate now detects bg dimension changes from a remote user and
re-runs the matching content layout — text rewrap for sticky/text/shape/
document, widget redraw for calendar/kanban/mindmap/group, and child
sizing for image/webframe — so text and children no longer drift outside
the resized object in the receiving window.

#78
The topbar accent rect was created once in createDocument and never
resized, so when the document shrinks the accent is clipped on the
right (its rounded corner disappears) and when it grows there's a
visible gap. renderDocumentContent now stretches the topbar to the
current bg width on every call, so all resize paths (local, sync,
scroll, theme) keep it in sync.
fix(sync): refresh transformer when a synced node is currently selected
Some checks failed
CI / build (push) Failing after 0s
67d9df544c
When a remote user resizes / moves / rotates an object that is selected
in this window, the transformer's cached bounding rect kept the outline
and anchors glued to the pre-sync geometry. Calling forceUpdate after
applySyncUpdate makes the selection outline track the new geometry.
Mindmap auto-sizes from its tree layout, so non-uniform resize squashes
the visual without reflowing content (the transformend handler already
averages scaleX/scaleY back to a single scale). Enabling keepRatio on
the transformer whenever a mindmap is in the selection makes the corner
anchors resize uniformly during drag — matching what the user already
gets manually with Shift on other objects.
The transformer's boundBoxFunc already enforces a 40x30 hard floor in
world units, which is enough to keep the mindmap from collapsing. There
was no good reason to cap the upper end either — let the user resize
freely.
fix(sync): apply mindmap scale on remote update
All checks were successful
CI / build (push) Successful in 2m12s
14b33c4a00
Mindmap resizes via group scaleX/scaleY rather than bg width/height, so
the generic dimsChanged path never picked it up — and the existing
mindmap branch in applySyncUpdate rebuilt _mmState without preserving
or applying data.scale. The receiving window now reads data.scale (or
keeps the previous local scale as a fallback) and applies it to the
group, so a resize in one window is mirrored to the other.
Replace the silent New Board flow with a modal that asks for board name
and target workspace, with an inline "+ New workspace..." option that
calls workspace.create before board.create. Add the same sentinel to
the page-level filter dropdown so users can create a workspace without
leaving the home page (workspace.create was previously only reachable
from the admin dashboard). Cache the workspace list so the modal can
populate without an extra RPC.

#79
Drop the "(no workspace)" option from the new-board modal; a board must
belong to a workspace. When the user has no workspaces yet, "+ New
workspace..." is the only option (auto-selected, so the inline name
input appears immediately). submitNewBoard now bails with an alert if
no workspace id was resolved.
Wrap each label/input pair in a block with consistent margin-top, give
labels display:block + margin-bottom so they sit above their input with
breathing room, and add box-sizing:border-box to the inputs/select so
width:100% respects the inner padding instead of overflowing toward the
modal edge.
The filter dropdown's "+ New workspace..." path used window.prompt /
window.alert which look out of place next to the new-board modal that
already exists on the same page. Wire it through a dedicated modal
matching the new-board modal style: name input, inline error display,
Cancel + Create buttons, Enter/Escape support. Cancel still reverts
the dropdown to the previous selection so it doesn't get stuck on the
sentinel.
The native <select> arrow on Chromium-Linux is rendered flush against
the field's right border, which makes the modal look cramped next to
the rounded corners. Replace it with appearance:none plus an inline
SVG chevron positioned 12px from the right edge, with padding-right:36px
so the value text stays clear of the arrow. Class wb-modal-select keeps
this scoped so the rest of the app's selects are unaffected.
fix(home): persist target workspace after creating a board
All checks were successful
CI / build (push) Successful in 2m9s
c5107cac1c
submitNewBoard navigated to the new board without updating the page's
currentWorkspaceId / localStorage entry, so when the user came back to
the home page the filter still showed the previous workspace and the
freshly-created board (which lives in a different workspace, especially
when the user used "+ New workspace...") was hidden from the listing.
Persist the target workspace before navigating so the home page lands
on it on return.
feat(webframe): replace prompt() URL editor with an in-page modal
All checks were successful
CI / build (push) Successful in 2m9s
bca268c679
Double-clicking a Web Frame opened a window.prompt that couldn't be
themed, was easy to dismiss accidentally, and looked alien next to the
rest of the board. Wire the dblclick handler through a themed modal
matching the home page's pattern (display:block labels, box-sizing
inputs, inline error region, Cancel + Save, Enter/Escape support).
Extract the side effects into applyNewUrl so the legacy native-prompt
fallback (used when the modal markup isn't rendered) and the modal
share the same code path. applyNewUrl now also calls
WhiteboardSync.onUpdate explicitly so the change propagates to other
windows.

#80
feat(home): replace browser confirm/alert in board delete with a modal
Some checks failed
CI / build (push) Failing after 0s
9ded668e89
The delete-board flow used window.confirm for the destructive
confirmation and window.alert for RPC failure, both of which ignore
the page theme and look out of place next to the page's other modals.
Add a themed delete-board-modal with the board name in the prompt, a
red Delete button (var(--wb-error)), inline error reporting, and
Enter/Escape support consistent with the rest of the page's modals.
deleteBoard(id, name) keeps the same call signature so the per-card
button doesn't need to change.

#81
style(home): tidy rename-board modal field spacing
All checks were successful
CI / build (push) Successful in 2m9s
def04fd3d3
Match the new-board modal's structure: wrap each label/input in a div,
make labels display:block with margin-bottom:4px, add a missing "Name"
label above the first input, and add box-sizing:border-box so width:100%
respects the inner padding instead of pushing inputs to the modal edge.
Bumped the description block's margin-top from 8px to 12px to match the
other modals on the page.
feat(home): replace inline share panel with a proper modal
All checks were successful
CI / build (push) Successful in 2m13s
f8e258bee3
Clicking share on a board card used to expand an inline panel inside
the card with two URL <code> elements as click-to-copy targets and a
custom × close glyph. Replace it with a centered themed modal showing
the board name, two read-only inputs (View / Edit) each with an
explicit Copy button, and a single Close button at the bottom right.
RPC failures show inline; Escape closes. Cache resolved share URLs
per board id so re-opening the modal doesn't re-issue share.list /
share.create.

#82
board.delete is a soft delete (sets deleted_at) but every other layer
treated the board as live, so other windows kept editing it
indefinitely after a delete.

Server:
- get_board now filters deleted_at IS NULL.
- New is_board_live / is_comment_board_live / is_connector_board_live
  predicates.
- object.{create,list,update,delete,batch_update}, comment.{create,
  list,update,resolve,delete}, connector.{create,list,update,delete}
  and share.create reject mutations against a soft-deleted parent
  board with "board has been deleted".
- Added a regression test (in-memory SQLite + handler exercise) that
  asserts board.delete then object.create returns an error.

WebSocket broadcast:
- ws::broadcast_to_board helper sends a server-originated message
  (sender_id 0; real connection ids start at 1).
- The UI's rpc_proxy sniffs successful board.delete responses and
  broadcasts {type:"board.deleted", board_id} to all subscribers of
  that board's channel.

Client:
- sync.js handles the broadcast: closes the WS, cancels the reconnect
  timer, drops pending updates, and short-circuits onCreate/onUpdate/
  onDelete via a boardDeleted flag. connectWebSocket also bails when
  the flag is set, so the deletion is final.
- board.html exposes window.showBoardDeletedNotice() and a centered
  themed overlay matching the rest of the page's modals, with a
  "Back to home" link.

#83
The server-originated WS broadcast is best-effort: if the share-link
viewer's connection dropped or the page loads after the deletion (a
hard refresh on a deleted board), the broadcast never arrives and the
editor stays interactive on a board the server now rejects every
write to.

Make rpcCall the safety net: any RPC error containing "board has been
deleted" calls WhiteboardSync.markBoardDeleted(), which now lives at
the module level (refactored out of handleWsMessage) and is exposed
on the public API. That tears down the WS / pending updates / sync
loop and shows the same overlay regardless of whether the trigger
was the broadcast or an RPC failure.

Also align board.get's error wording: queries::get_board already
filters deleted_at IS NULL, so a soft-deleted board surfaces as
QueryReturnedNoRows; translate that to "board has been deleted" so
the client fallback recognizes it on the next refresh.

#83
fix(board): say "deleted by its owner" instead of "by another user"
Some checks failed
CI / build (push) Failing after 43s
1060881104
Only the board owner can delete a board, so "another user" is vague
and slightly misleading. Make the wording specific.
feat(board): enforce unique board name per workspace
Some checks failed
CI / build (push) Failing after 45s
e0f7a10690
Two boards in the same workspace were allowed to share the same name,
producing visually indistinguishable cards on the home grid. Enforce
uniqueness end-to-end:

- Migration 006 dedupes any pre-existing collisions among live rows
  by appending " (1)", " (2)", ... and creates a partial unique index
  on (workspace_id, name) WHERE deleted_at IS NULL. Soft-deleted
  rows stay exempt so a name can be reused after deletion (matches
  is_board_live semantics).
- New board_name_taken(workspace_id, name, except_id) helper.
- board.create / board.update reject duplicates with
  "A board named \"<name>\" already exists in this workspace".
- Renaming a board to its current name is a no-op (no error).
- Regression test asserts the duplicate-create rejection and that
  the name becomes reusable after soft-delete.

UI:
- New Board modal gains an inline #new-board-error region; submit
  now surfaces every error path (including the duplicate-name one)
  inline instead of via window.alert.
- Rename modal gains #rename-error and the same inline-error flow.

#84
style: cargo fmt across queries / object / routes
All checks were successful
CI / build (push) Successful in 2m13s
349d6b7c5c
Three files committed across the recent server-gating + share modal
work had stray multi-line formatting that rustfmt collapsed. Run
cargo fmt --all over them so the CI fmt check passes.
feat(home): delete workspace from the home page, cascading to its boards
All checks were successful
CI / build (push) Successful in 2m10s
5ef6c426ec
The home page had no way to delete a workspace, so the user had to go
to the admin dashboard. The DB schema already declares ON DELETE
CASCADE for boards.workspace_id, so the cascade hard-removes the
boards — but no broadcast fired, so any open editor on one of those
boards kept trying to sync until rpc.js's fallback caught the
"board not found" error.

Server:
- queries::live_board_ids_in_workspace snapshots live board ids in a
  workspace.
- workspace::delete now snapshots those ids before issuing the delete
  and returns {deleted: 1, board_ids: [...]} so the UI proxy can
  broadcast.
- Regression test seeds two boards in a workspace, deletes the
  workspace, asserts both ids are returned in board_ids and that
  is_board_live is false for each.

UI proxy:
- rpc_proxy now also recognizes workspace.delete (in addition to
  board.delete) and broadcasts board.deleted to every channel listed
  in result.board_ids. The single-board path is unchanged.

UI:
- Trash button next to the workspace filter dropdown, visible only
  for real workspaces.
- delete-workspace-modal matching the delete-board-modal styling:
  workspace name in the prompt, best-effort cascade count line,
  inline error region, red destructive Delete button, Cancel.
- After success: filter resets to "All Workspaces", localStorage
  entry cleared, dropdown + board list refreshed.
- loadWorkspaces falls back to "All Workspaces" if the cached
  currentWorkspaceId is no longer in the list.

#85
When the filter is set to a specific workspace the parent is implied,
but in the all-view the flat grid mixed boards from every workspace
with no visual cue. Match what Miro / Trello / Linear do: when no
specific workspace is selected, render one section per workspace with
a "<name> · <count>" header and a sub-grid of boards. Sections are
ordered by the workspace dropdown order; boards within a section
preserve updated_at DESC. Specific-workspace view stays a flat grid.

Boards whose workspace is no longer in workspacesCache (e.g. a stale
cache after a delete) get an orphan "Workspace #<id>" section at the
end so they aren't lost.

UI-only; the workspacesCache populated by loadWorkspaces already gives
the section names, no server change required.

#86
style(home): make workspace section header bigger, count in (N)
All checks were successful
CI / build (push) Successful in 2m10s
398b649572
The 13px header sat just below the 14px board title weight, so visually
the hierarchy collapsed and the section name read as another card meta
line. Bump it to 18px / weight 600 with normal text color so it reads
as a section heading. Move the count to a parenthesized inline span at
13px / muted, e.g. "Sprint planning (4)" — the count is secondary
information and shouldn't compete with the workspace name.
WhiteboardFrames was fully implemented but the Present button was
hardcoded to alert("Presentation mode coming soon"). Wire it through
to togglePresentation, hide the chrome via a body.wb-presenting class,
and add a global keydown handler so Right/Space/PageDown advance,
Left/PageUp rewind, and Escape exits. startPresentation now snapshots
the current zoom + pan; stopPresentation restores it. The legacy
alert(...) for the no-frames case is replaced with a themed toast
that auto-dismisses.

#87
Frames could only be renamed via the right-side property panel; every
other titled object on the board has an inline rename via dblclick.
Wire dblclick on the frame label to the legacy editText helper (which
was defined but never called) and teach editText to be history- and
sync-aware so the rename round-trips to other windows and is undoable.
Enter commits, Escape reverts and skips the no-op sync.

#88
Frames looked like containers but were just visually-overlapping
rectangles. Dragging the frame moved only its dashed bg + label,
leaving any sticky/shape/etc. visually inside the frame stranded.

createFrame now snapshots the frame's bounding box at dragstart and
captures every other top-level object whose bounding box is fully
inside it. dragmove applies the frame's drag delta to each captured
child and fires `dragmove` on the child so connector tracking
listeners run. dragend wraps the per-child commitUpdate calls in a
single WhiteboardHistory.batch so one Ctrl+Z reverts the entire
group move. Each child's new position syncs via WhiteboardSync.onUpdate.

history.js gains a generic batch(work) helper plus a `batch` action
type that undo/redo recurse into. push() collects into a buffer
while batch is active so the captured actions ship as one undo entry.

#89
The frame group is draggable; mousedowns on the label bubbled up to
the group's auto-drag, so the cursor jitter between the two clicks of
a double-click drifted the frame a few pixels mid-rename.

Stop mousedown propagation at the label so single mousedowns on the
label never reach the parent group. Single-click selection still
works because that's driven by stage 'click', not by mousedown.

Also suspend the group's draggable while the inline editor is open
so any stray mousedown that does reach the group during the edit
can't start a new drag. wasDraggable is captured at editText entry
and restored unconditionally on close.
focusFrame had a hardcoded 40 px gutter that left visible space around
every frame, and the only way to navigate was the keyboard — no slide
counter, no buttons. Drop the padding so frames fill the viewport
edge-to-edge (aspect-ratio fit preserved). Add a floating bottom-center
control bar with prev / X-of-N counter / next / exit; subscribe it to
a new WhiteboardFrames.setOnChange listener so the counter and disabled
states stay in sync regardless of input source.

#93
fix(present): mask outside-frame content and freeze canvas while presenting
All checks were successful
CI / build (push) Successful in 2m18s
25f575b77a
Edge-to-edge fit only fills one axis; the perpendicular axis still showed
adjacent canvas content (other frames, stickies, etc.). And the canvas
remained pannable / wheel-zoomable mid-presentation, so the slide could
slide out from under the user.

frames.js now computes the focused frame's screen-pixel rect after each
focusFrame and ships it through the setOnChange callback. board.html
positions a #pres-spotlight overlay over that rect; a 9999px box-shadow
fills the rest of the viewport with a dark backdrop, hiding any content
outside the slide. body.wb-presenting also locks pointer-events on
#whiteboard-container so wheel/drag pan are disabled for the duration.
The control bar lives in its own DOM and is unaffected.

#93
Frames had no explicit order — getFrames returned them in z-index order
which the user couldn't control. Now each frame stores data.order on
its Konva node (frameOrder attr); getFrames sorts by (order, id); a
small numbered badge sits next to the title showing the deck position.

Reorder is exposed two ways:
- Property panel: Up / Down buttons under a new "Slide order" block.
- Right-click on a frame: Move slide up / Move slide down entries.

Both call WhiteboardFrames.moveFrameUp / moveFrameDown which swap the
adjacent frames' order, refresh badges everywhere, and fire onUpdate so
other windows pick up the change. The presentation control bar's
counter and prev/next disabled state automatically reflect the new
order because they query WhiteboardFrames live each tick.

Backward compatibility: legacy frames missing data.order fall back to
Number(id) at sort time, so they don't jump around. The first reorder
writes explicit orders that survive subsequent reads.

#95
fix(ui): copy share link works on non-secure HTTP / IPv6 origins
All checks were successful
CI / build (push) Successful in 2m12s
7cb726d1bf
Falls back to document.execCommand('copy') via an off-screen textarea
when navigator.clipboard is unavailable (any non-secure context, e.g.
plain-HTTP IPv6) or when its writeText promise rejects. Surfaces a red
error toast if both paths fail.

#99
fix(theme): broadcast theme changes to other connected viewers
All checks were successful
CI / build (push) Successful in 2m9s
7f5f60a0ac
#90
fix(shape): broadcast and apply shape type changes to other connected viewers
All checks were successful
CI / build (push) Successful in 2m12s
a4ea9ed163
#91
fix(connector): broadcast style changes and re-anchor endpoints on remote moves
All checks were successful
CI / build (push) Successful in 2m9s
f4119a6f10
#92
fix(mindmap): persist and broadcast root node color and text edits
All checks were successful
CI / build (push) Successful in 2m15s
3f130a3109
#94
#96
#101
#102
#104
#103
fix(frame): include calendar, kanban, mindmap, webframe in id-based frame capture
All checks were successful
CI / build (push) Successful in 2m7s
b1c0b71b9a
#105
fix(webframe): refresh iframe overlays on programmatic zoom
Some checks failed
CI / build (push) Failing after 4s
a3c174bffd
#106
#97
Adds a Share button to the in-presentation control bar and includes the
Presentation link row in the inline board.html share dialog that was
missed when the home-page modal and ui-helpers.js dialog gained it.

#97
Adds the spotlight, pointer-events lockout, and a Counter+Exit control bar
to the read-only board template so the shared presentation link gets the
same audience experience as the presenter. nextFrame/prevFrame now broadcast
the current slide index over the existing WebSocket fan-out, and receivers
in presentation mode re-focus the matching frame without echoing.

#97
When a new client joins, anyone in presentation mode broadcasts their current
slide so the joiner doesn't get stranded on slide 1. If the broadcast arrives
before the joiner has finished loading and auto-started, the index is cached
in _pendingRemoteSlide and applied at the end of startPresentation.

#97
#97
feat(present): floating emoji reactions broadcast to all participants
All checks were successful
CI / build (push) Successful in 2m9s
82f242459e
Adds five reaction buttons (thumbs up, heart, clap, party, wow) to the
presentation control bar in both the editor and viewer templates. Clicks
spawn a floating emoji locally and broadcast presentation.reaction over
the existing per-board WebSocket fan-out; receivers in presentation mode
play the same animation. Throttled to ~6/sec per sender, ephemeral, no
persistence.

#98
feat(webframe): pre-flight URL check + non-embeddable card
Some checks failed
CI / build (push) Failing after 2s
c84e76eb67
Replace the broken "refused to connect" iframe with a clear card and
"Open in new tab" button when the target site sends X-Frame-Options or
Content-Security-Policy: frame-ancestors that block embedding. The check
runs server-side because browsers can't read those headers cross-origin.

#74
Hide the Exit button and ignore Esc when the URL has ?present=1 so a
recipient of a presentation share link can't drop into the full board.
The presenter (no ?present=1) keeps the normal exit affordances. Prev,
Next, reactions, share, and the slide counter remain accessible.

#107
fix(presentation): align spotlight and fit math with rotated frames
All checks were successful
CI / build (push) Successful in 2m10s
49764e677c
Use the rotated frame's stage-coord AABB for fit-to-viewport, and pass
the frame's rotation through the screen-rect payload so the host
template can rotate #pres-spotlight via CSS transform around the same
pivot. The spotlight cutout now matches a rotated frame's outline
instead of leaving an axis-aligned hole.

#108
perf(polling): pause /health + /rpc when island isn't focused
Some checks failed
CI / build (pull_request) Failing after 2s
157ec84bad
chore(deps): bump hero_archipelagos for use_focus_poll
Some checks failed
CI / build (pull_request) Failing after 3s
dc813340bb
style: cargo fmt
All checks were successful
CI / build (pull_request) Successful in 3m24s
04a11f95f6
Reviewed-on: #112
style: default light theme for admin UI templates
Some checks failed
CI / build (push) Failing after 2s
8de03e3897
Demo polish — flip data-bs-theme="dark" → "light" across the admin UI
templates so the dashboards default to light mode.

Signed-off-by: mik-tf
The WS dispatcher in sync.js routed comment.created/updated/resolved/
deleted to the comments handler but dropped comment.unresolved. The
receiving handler was already prepared for both resolve and unresolve,
so adding the missing message type to the dispatch arm makes
un-resolve apply live in presentation, shared, and side-by-side editor
tabs the same way resolve already did.

#111
loadBoard was gating the initial theme apply on !readonlyMode, so the
viewer share (and the ?present=1 presentation share, which is also
readonly) always rendered with the default theme on first paint. The
live-sync path in handleWsMessage was not gated, so a subsequent
owner-side change applied — only the initial load was wrong.

The theme picker UI lives in the navbar, which board_view.html already
hides via CSS, so the viewer still cannot change the theme; this only
fixes displaying the owner's chosen theme.

#114
fix(theme): make whiteboard chrome follow the active theme
All checks were successful
CI / build (push) Successful in 2m10s
4bfa631ffd
applyTheme only repainted the canvas; navbar / toolbar / properties /
zoom / minimap stayed in light mode regardless of theme. Now the
chrome's --wb-* CSS variables are derived from the theme's canvas-bg
and text-color and pushed onto :root, so each theme produces its own
coherent chrome (Ocean blue, Warm brown, Dark grey, Light/Minimal
near-white).

The accent color (--wb-primary) follows the theme too, paired with a
new --wb-primary-text that picks black or white from the accent's
luminance — Ocean's cyan and Warm's amber now sit under dark text
instead of unreadable white.

#115
feat(theme): wire kanban, mindmap, connectors, sticky/shape defaults to theme
All checks were successful
CI / build (push) Successful in 2m14s
b31e4a89c5
Only the canvas, the chrome (after #115), and calendars (always) were
following the active theme. Kanban / mindmap / connectors / sticky
palette / shape defaults all hard-coded the Dark palette regardless of
which theme was selected.

themes.js now carries 9 mindmap-* tokens with per-theme overrides and
expands the post-canvas redraw loop to walk every kanban / mindmap /
calendar and call its redraw helper, then asks connectors.js to recolor
default-stroke connectors. kanban.js and mindmap.js read the theme
tokens at render time. connectors.js tracks an _themeDefault flag so
user-customized strokes are preserved on theme change. objects.js
sources the sticky palette and the default text / shape / drawing
colors from the theme at create time; existing persisted objects keep
their colors.

#116
fix(connector): idempotent delete + clean orphan connectors on object delete
All checks were successful
CI / build (push) Successful in 2m19s
e7a80a9055
connectors.from_id and to_id are FK ON DELETE CASCADE, so deleting an
endpoint object removes its connectors at the SQL layer. The client
kept the connector visible, and clicking delete on it called
connector.delete on a row that was already gone. is_connector_board_live
returned false (no row), so the handler bailed with
"board has been deleted" — which the rpc client maps to the deletion
overlay. The board was alive; the message was misleading.

Server: new tri-state BoardLinkStatus (Missing / BoardDeleted / Live)
plus connector_board_status / comment_board_status helpers. delete
handlers now treat Missing as a successful no-op ({deleted: 0}) and
keep the bail only for BoardDeleted. Same fix on comment.delete for
cross-tab races.

Client: WhiteboardObjects.deleteObject now walks the connector
registry and deletes any connector whose endpoint matches the object
being removed, so the orphan never appears in the first place. The
existing connector.deleted broadcast still propagates to other tabs.

#117
zoomReset bypasses setZoom and mutates stage.scale and stage.position
directly, so the iframe-overlay refresh that setZoom does internally
never fires on Fit. Frame moves to the fitted position; iframe stays
where it was. Add the same refreshAllOverlays call to the two
zoomReset branches that mutate stage directly. The empty-board branch
already routes through setZoom and is untouched.

#118
fix(emoji): one-side stretch resize works in both directions
Some checks failed
CI / build (push) Failing after 2s
c258106438
Math.min(scaleX, scaleY) was picking the unmoved axis whenever the
user dragged a single side handle outward — the perpendicular axis
stays at scale 1, and min(>1, 1) collapses to 1 with no resize.
Shrinks worked because min(<1, 1) = <1. Pick whichever axis moved
further from 1 instead, so the dragged axis always wins. Aspect
ratio stays 1:1 across all paths.

#119
fix(presentation): hide subtoolbar and other floating editor UI
All checks were successful
CI / build (push) Successful in 2m10s
1c30f4df4e
The body.wb-presenting hide rule covered navbar, toolbar, minimap,
zoom, and properties — but not the sub-toolbar, emoji picker,
right-click menu, or theme panel. Any of those left open at the
moment the user pressed Present floated above the slide.

Add .wb-subtoolbar / .wb-emoji-picker / .wb-context-menu / #theme-panel
to the hide list in both templates. Pure CSS, no DOM teardown, so the
user returns to where they left off after exiting presentation.

#120
fix(presence): converge avatar list on every join and leave
All checks were successful
CI / build (push) Successful in 2m54s
a17b086b66
The avatar count diverged between owner and shared-link tabs because
existing tabs never replied to a new tab's `join` with their own
identity, so the new tab only learned about other users when those
users happened to broadcast a cursor or edit. Closing a tab also
left stale entries because no `leave` was sent on unload.

handleWsMessage's `join` arm now replies with our identity when we
first see an unknown joiner, gated by an `alreadyKnown` check so the
handshake stops after one round-trip. The `beforeunload` handler now
emits `wsSend({ type: 'leave' })` (best-effort) so other tabs prune
us via the existing `leave` receiver. Server-side disconnect leave
remains a future improvement for crashes / network drops.

#109
fix(theme): default new boards to Light so picker matches render
Some checks failed
CI / build (push) Failing after 2s
734b0c4b92
themes.js initialized currentThemeName to 'Dark' and never applied a
theme at init. New boards (no saved theme) rendered Light via CSS
fallback while the picker showed Dark highlighted. Apply Light
explicitly at init with _fromSync=true so the picker's selected
indicator agrees with what is actually on screen, without saving or
broadcasting on a passive page load. loadBoard's subsequent
loadTheme call still overrides for boards with a saved theme.

#121
feat(draw): black swatch + theme-aware first swatch in freehand palette
Some checks failed
CI / build (push) Failing after 3s
14e78c6832
Add a black option to the freehand drawing color row, and make the
leftmost swatch automatically match the active theme's draw-stroke
token: #dee2e6 on Dark, #0f172a on Light, #64ffda on Ocean,
#f59e0b on Warm, #374151 on Minimal. The first swatch is tagged
data-theme-default; toolbar.js syncs it via WhiteboardThemes.get,
and themes.js::applyTheme calls the refresh on theme switch so the
swatch and (when it's the active selection) the live draw color
update without a reload.

#122
fix(emoji): give selection outline headroom on top so glyphs don't clip
All checks were successful
CI / build (push) Successful in 2m10s
683cf670fd
Some emoji (the heart, etc.) render slightly above the Konva.Text
layout box, so the transformer outline visibly clipped their top at
large sizes. Shift the text node down by 10% of fontSize and grow
the bg by the same amount so the outline gains headroom on top.
Bottom and sides stay flush with the layout box.

Avoided lineHeight (pads all four sides), measureText().actualBoundingBox*
(reports the font metric box for emoji, over-pads height), and a
pixel-scan + textNode.width/height/x/y override (broke Konva.Text
rendering). Pure y/height tweak only.

#123
ci: add release workflow that builds and uploads Linux binaries on tag push
Some checks failed
CI / build (push) Successful in 2m7s
Build Linux / build-linux (linux-amd64-musl, false, x86_64-unknown-linux-musl) (push) Failing after 1s
Build Linux / build-linux (linux-arm64-gnu, true, aarch64-unknown-linux-gnu) (push) Failing after 2s
CI / build (pull_request) Successful in 2m59s
01057bb881
Hero-pattern matrix (amd64-musl + arm64-gnu) using scripts/build_lib.sh
and root buildenv.sh. Installs Rust 1.93 (workspace edition 2024),
clears the cargo git cache to keep the hero_proc_sdk branch dep
fresh, and gates release creation on the tag commit being reachable
from main. Uploads BINARIES from buildenv.sh as release assets via
the Forgejo API. CI continues to validate branches via ci.yml.
Sign in to join this conversation.
No reviewers
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!124
No description provided.