feat(workspace): auto-add creator to workspace_members + polished first-run UI #23

Merged
sameh-farouk merged 1 commit from feat/workspace-creator-membership-and-first-run-ui into development 2026-04-26 17:42:11 +00:00
Member

Closes #22.

Summary

Two paired fixes for the operator's first-run experience after a fresh deployment:

  1. Server: workspace.create now auto-adds the creator to workspace_members so per-workspace operations (canvas.create, etc.) actually work for the workspace creator.
  2. Client: replace the silent "General" auto-create in pickUser with a polished first-run setup modal asking the operator to name their workspace + first channel.

Coupled because either alone leaves the first-run path broken — the modal needs the membership row to work, and the membership fix without the modal still ends up with a workspace named "General" that the operator didn't choose.

Changes

File Change
crates/hero_collab_server/src/handlers/workspace.rs After the workspace INSERT, INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, data) for caller_id (when set). Wraps in if let Some(caller) so dev-mode unauth path silently skips (canvas.create's membership check is also dev-mode-bypassed). Failure logged but doesn't fail workspace.create.
crates/hero_collab_ui/static/js/chat-app.js init() proxy-mode branch awaits workspace.list (was fire-and-forget). pickUser zero-workspace branch calls showFirstRunSetup(user) instead of silent auto-create. New showFirstRunSetup polished modal (~290 LOC) — full description below.

Net: +312 lines, -6 lines.

First-run modal — polish details

The modal is forced (no dismiss-without-submit) because the chat is unusable without a workspace. All built via createElement for safety; only user-derived value (user.name) set via textContent, no innerHTML on user data.

Feature Why
Backdrop rgba(0,0,0,0.85) + backdrop-filter: blur(4px) The earlier draft reused .user-picker-overlay's 50%-black backdrop, which let the chat sidebar's avatar widget bleed through. Higher opacity + blur fixes the visual artifact.
Submit button starts disabled + grey; enables only when both fields validate Honest button state matching form validity. Pre-fix: button looked enabled with empty form, click → server error.
Live per-field validation Workspace name: required, max 80. Channel name: client-side mirror of server's validate_channel_slug regex (^[a-z0-9][a-z0-9_-]*$). Catches My Channel before the server roundtrip.
Workspace name pre-filled with ${user.name}'s Workspace Selected on focus so it's one-character-overwrite to clear if the operator wants something else. One-click happy path.
Inline animated spinner during submit + lock both inputs Visual motion = "still working" on slow connections. Pre-fix was just text change to "Creating…".
Per-field hint/error line + separate form-level error span Errors point to the field that failed, not just one generic row at the bottom.
Tighter rhythm (gap: 14px), narrower modal (max-width: 420px) Felt boxy when reusing the user-picker dialog. 2-field form deserves a focused 2-field modal.
Keyboard: Enter advances field-to-field; Cmd/Ctrl+Enter submits; Esc clears form-level error Picker users won't move their hand to the mouse for a 2-field form.
"Switch user" link visible only when state.users.length > 1 Dev mode (picker scenario) gets an exit; proxy mode locks the user as before since the picker isn't an option there.

Behaviour matrix

Scenario Today After this PR
Proxy mode, deployer's first request, fresh DB silent "General" workspace appears with no naming choice first-run modal asks for name; default ${user.name}'s Workspace pre-filled
Dev mode (picker), fresh DB after delete same silent "General" same modal + "Switch user" link visible
Joining user (proxy mode, populated DB) unchanged — sees existing workspace unchanged — modal doesn't fire (state.workspaces non-empty)
Workspace creator tries canvas.create Not a member of workspace N (PermissionDenied) succeeds — membership row was added by workspace.create
Operator types My Channel for first channel server returns generic error after roundtrip client shows red hint immediately, blocks submit
Empty workspace name server returns error after roundtrip submit button disabled + grey

Test plan

  • Cargo build clean (server + UI)
  • Dev mode, fresh DB + dev_seed users + deleted workspace: picker shows (PR 3 dev-mode shim), pick Alice → polished first-run modal appears with "Alice's Workspace" pre-filled → submit → land in workspace → canvas creation works
  • Proxy mode, fresh DB: user.me auto-provisions sameh + grants admin (PR 3 federation), first-run modal appears with "Sameh's Workspace" pre-filled, no "Switch user" link (single user known) → submit → land in workspace → canvas creation works
  • Live validation: clearing workspace name greys out submit; typing My Channel shows red slug-rule hint and disables submit
  • Backdrop hides chat layout fully (no avatar bleed-through)
  • Spinner animates during submit
  • Keyboard navigation (Enter advances; Cmd/Ctrl+Enter submits) works
  • "Switch user" link only present in dev mode (state.users > 1)

Notes

  • Depends on PR #20 (admin bootstrap) logically — the modal needs the operator to have permission to create a workspace, which requires admin or workspace_manage right. PR #20 grants it via proxy.is_admin federation. This PR can land before/after but tests cleanly with PR #20 active.
  • Does NOT change dev-mode behavior in any way that surprises a user who was using the silent "General" path. Dev mode + dev_seed still creates Alice/Bob/Carol/Dave; the seed's "General" workspace exists and pickUser sees state.workspaces=[General], so the modal doesn't fire. The modal only fires on a truly empty state.workspaces.
  • Does NOT change the auth contract or any RPC method signatures. Pure data-layer + UI change.
Closes #22. ## Summary Two paired fixes for the operator's first-run experience after a fresh deployment: 1. **Server**: `workspace.create` now auto-adds the creator to `workspace_members` so per-workspace operations (canvas.create, etc.) actually work for the workspace creator. 2. **Client**: replace the silent "General" auto-create in `pickUser` with a polished first-run setup modal asking the operator to name their workspace + first channel. Coupled because either alone leaves the first-run path broken — the modal needs the membership row to work, and the membership fix without the modal still ends up with a workspace named "General" that the operator didn't choose. ## Changes | File | Change | |---|---| | `crates/hero_collab_server/src/handlers/workspace.rs` | After the workspace INSERT, `INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, data)` for `caller_id` (when set). Wraps in `if let Some(caller)` so dev-mode unauth path silently skips (canvas.create's membership check is also dev-mode-bypassed). Failure logged but doesn't fail `workspace.create`. | | `crates/hero_collab_ui/static/js/chat-app.js` | `init()` proxy-mode branch awaits `workspace.list` (was fire-and-forget). `pickUser` zero-workspace branch calls `showFirstRunSetup(user)` instead of silent auto-create. New `showFirstRunSetup` polished modal (~290 LOC) — full description below. | Net: +312 lines, -6 lines. ## First-run modal — polish details The modal is forced (no dismiss-without-submit) because the chat is unusable without a workspace. All built via `createElement` for safety; only user-derived value (`user.name`) set via `textContent`, no `innerHTML` on user data. | Feature | Why | |---|---| | Backdrop `rgba(0,0,0,0.85)` + `backdrop-filter: blur(4px)` | The earlier draft reused `.user-picker-overlay`'s 50%-black backdrop, which let the chat sidebar's avatar widget bleed through. Higher opacity + blur fixes the visual artifact. | | Submit button starts disabled + grey; enables only when both fields validate | Honest button state matching form validity. Pre-fix: button looked enabled with empty form, click → server error. | | Live per-field validation | Workspace name: required, max 80. Channel name: client-side mirror of server's `validate_channel_slug` regex (`^[a-z0-9][a-z0-9_-]*$`). Catches `My Channel` before the server roundtrip. | | Workspace name pre-filled with `${user.name}'s Workspace` | Selected on focus so it's one-character-overwrite to clear if the operator wants something else. One-click happy path. | | Inline animated spinner during submit + lock both inputs | Visual motion = "still working" on slow connections. Pre-fix was just text change to "Creating…". | | Per-field hint/error line + separate form-level error span | Errors point to the field that failed, not just one generic row at the bottom. | | Tighter rhythm (`gap: 14px`), narrower modal (`max-width: 420px`) | Felt boxy when reusing the user-picker dialog. 2-field form deserves a focused 2-field modal. | | Keyboard: Enter advances field-to-field; Cmd/Ctrl+Enter submits; Esc clears form-level error | Picker users won't move their hand to the mouse for a 2-field form. | | "Switch user" link visible only when `state.users.length > 1` | Dev mode (picker scenario) gets an exit; proxy mode locks the user as before since the picker isn't an option there. | ## Behaviour matrix | Scenario | Today | After this PR | |---|---|---| | Proxy mode, deployer's first request, fresh DB | silent "General" workspace appears with no naming choice | first-run modal asks for name; default `${user.name}'s Workspace` pre-filled | | Dev mode (picker), fresh DB after delete | same silent "General" | same modal + "Switch user" link visible | | Joining user (proxy mode, populated DB) | unchanged — sees existing workspace | unchanged — modal doesn't fire (state.workspaces non-empty) | | Workspace creator tries `canvas.create` | `Not a member of workspace N` (PermissionDenied) | succeeds — membership row was added by `workspace.create` | | Operator types `My Channel` for first channel | server returns generic error after roundtrip | client shows red hint immediately, blocks submit | | Empty workspace name | server returns error after roundtrip | submit button disabled + grey | ## Test plan - [x] Cargo build clean (server + UI) - [x] **Dev mode, fresh DB + dev_seed users + deleted workspace**: picker shows (PR 3 dev-mode shim), pick Alice → polished first-run modal appears with "Alice's Workspace" pre-filled → submit → land in workspace → canvas creation works - [x] **Proxy mode, fresh DB**: user.me auto-provisions sameh + grants admin (PR 3 federation), first-run modal appears with "Sameh's Workspace" pre-filled, no "Switch user" link (single user known) → submit → land in workspace → canvas creation works - [x] Live validation: clearing workspace name greys out submit; typing `My Channel` shows red slug-rule hint and disables submit - [x] Backdrop hides chat layout fully (no avatar bleed-through) - [x] Spinner animates during submit - [x] Keyboard navigation (Enter advances; Cmd/Ctrl+Enter submits) works - [x] "Switch user" link only present in dev mode (state.users > 1) ## Notes - **Depends on PR #20 (admin bootstrap)** logically — the modal needs the operator to have permission to create a workspace, which requires `admin` or `workspace_manage` right. PR #20 grants it via `proxy.is_admin` federation. This PR can land before/after but tests cleanly with PR #20 active. - **Does NOT change** dev-mode behavior in any way that surprises a user who was using the silent "General" path. Dev mode + dev_seed still creates Alice/Bob/Carol/Dave; the seed's "General" workspace exists and pickUser sees `state.workspaces=[General]`, so the modal doesn't fire. The modal only fires on a truly empty `state.workspaces`. - **Does NOT change** the auth contract or any RPC method signatures. Pure data-layer + UI change.
Two paired fixes for the operator-deployed bootstrap path:

## Server (handlers/workspace.rs)

`workspace.create` now `INSERT OR IGNORE INTO workspace_members` for
the creator after the workspace insert. Without this row, every
per-workspace-gated operation (canvas.create, channel.member.add,
...) fails with `Not a member of workspace N` even though the
creator passed the workspace.create permission check.
`channel.create` and `canvas.create` already auto-add the creator
with `role=admin`/`role=owner` — workspace.create was the only one
missing the equivalent.

The INSERT only runs when `caller_id` is set (proxy-mode + dev-mode
with picker). In dev-mode unauth path, the existing
`if !is_dev_mode()` guard inside canvas.create's membership check
makes the missing row a non-issue.

## Client (chat-app.js)

`init()` proxy-mode branch now `await`s `workspace.list` instead of
fire-and-forget. Previously `pickUser` ran with a racy empty
`state.workspaces`, which (pre-fix) silently created a workspace
named "General" — confusing.

`pickUser`'s zero-workspace branch calls `showFirstRunSetup(user)`
instead of the silent auto-create. Slack-pattern first-run
onboarding: forces the operator to name their workspace + first
channel before unlocking the chat.

`showFirstRunSetup` polish (vs. an earlier draft that reused the
.user-picker-overlay class wholesale):

  * Backdrop bumped to rgba(0,0,0,0.85) + backdrop-filter blur(4px)
    so the underlying chat layout (sidebar avatar etc.) is fully
    hidden — fixes the "A circle bleeding through the workspace
    input" visual bug.
  * Submit button starts disabled + grey; enables only when both
    fields validate. Honest button state matching form validity.
  * Live per-field validation:
      - workspace name: required, max 80 chars
      - channel name: client-side mirror of server's
        validate_channel_slug (`^[a-z0-9][a-z0-9_-]*$`) — catches
        bad names before the server roundtrip.
  * Workspace name pre-filled with `${user.name}'s Workspace`
    (selected on focus so it can be overwritten immediately).
  * Inline animated spinner during submit + lock both inputs
    (visual motion = "still working").
  * Per-field hint/error line + form-level error span.
  * Tighter vertical rhythm (gap:14px), narrower modal (max-width
    420px) since it's only 2 fields.
  * Keyboard: Enter advances field-to-field; Cmd/Ctrl+Enter submits
    from anywhere; Esc clears form-level error.
  * "Switch user" link visible only when state.users.length > 1
    (i.e. dev mode with picker). Proxy mode locks the user as
    before.

DOM built via createElement throughout — only user-derived value
(user.name) is set via textContent, no innerHTML on user data.

## Why a prompt instead of a "General" default

hero_collab is a single-tenant component of a self-hosted OS, not a
SaaS. The operator owns the deployment; subsequent users join
existing workspaces by invite. The operator should name their
workspace once, on install, like every modern self-hosted product.

Subsequent users provisioning via user.me land without admin (the
existing permissions model) and never see this screen because
state.workspaces is non-empty for them.
sameh-farouk force-pushed feat/workspace-creator-membership-and-first-run-ui from 5b34e83ba7 to 1c008b47d9 2026-04-26 17:42:03 +00:00 Compare
sameh-farouk merged commit f5964c2688 into development 2026-04-26 17:42:11 +00:00
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_collab!23
No description provided.