The demo-dashboard embedded MeshStream with a fixed min-h-[480px] grid
+ overflow-y-auto on the message <ol>. Browsers capture every wheel
event that fires over a scrollable container — so hovering the demo
section froze page scroll until the user moved the cursor off.
Landing demo has only 6 messages, never needs internal scroll. The
fixed viewport only makes sense in the live dashboard where envelope
count can exceed the box.
Added `scrollable?: boolean` prop to MeshStream (default false).
- demo-dashboard (landing): no prop → intrinsic height, no overflow,
wheel events propagate to the page
- live-stream-panel (/dashboard/meshes/[id]/live): scrollable → keeps
the chat-style fixed viewport with scroll
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clickable HTTPS invite URLs replace the raw ic://join/<token> as the
primary share format. Someone receiving a link in Slack now lands on
a friendly page with install instructions, not a dead-end.
Backend:
- createMyInvite returns a new joinUrl field
(https://claudemesh.com/join/<token>) alongside the existing
ic://join/<token> inviteLink and raw token. Schema + Hono route
updated. ic:// scheme stays — CLI parses both.
- New GET /api/public/invite/:token in packages/api/src/modules/public/
(unauthed). Decodes the base64url payload, verifies ed25519
signature against owner_pubkey using the same canonicalInvite()
contract the broker enforces on join, then joins mesh/invite/user
to return the shape needed by the landing page. Does NOT mutate
usedCount — this is a read-only preview.
- Error taxonomy: malformed | bad_signature | expired | revoked |
exhausted | mesh_archived | not_found. Each returned with any
metadata we CAN surface (meshName, inviterName, expiresAt) so the
error page can be specific ("ask Jordan for a new one").
- cache-control: public max-age=30 on valid invites, no-store on
errors (reasons flip as state changes).
Frontend:
- New public route /[locale]/join/[token] (no auth). Server
Component fetches the preview endpoint, branches on valid/invalid,
renders a minimal landing-design-language shell (wordmark header,
clay accents, serif headlines, mono commands).
- Valid-invite view: "You're invited to {meshName}", inviter +
role + member-count lede, install-toggle component.
- Invalid-invite view: per-reason error copy + inviter name when
available + link back to /.
- InstallToggle client component: three-way state
(unknown/yes/no). Asks "first time / already set up?", then shows
either the 3-step install+init+join path with per-step copy
buttons, or the single claudemesh join <token> command for users
who have the CLI. Every code block has copy-to-clipboard.
- Security footer: "ed25519 keypair generated locally, you keep
your keys, broker sees ciphertext only, leave anytime with
claudemesh leave <mesh-slug>".
Invite generator (/dashboard/meshes/[id]/invite):
- QR code now encodes the HTTPS joinUrl instead of ic:// (phone
cameras land on the web page → friendly path).
- Primary CTA copies the HTTPS URL. Secondary "Copy CLI command"
for fast-path users. Footer explanation updated.
CLI coordination note: dispatched to broker/db lane — claudemesh CLI
needs to accept BOTH ic://join/<token> AND
https://claudemesh.com/join/<token> (extract <token> from pathname).
Server side already returns both.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires the Discord-style demo UI to real user data. Users with 1+ meshes
now get situational awareness: who's online, what's in the queue, what
the broker saw recently — polling every 4s, all E2E encrypted.
Extraction pass:
- New `<MeshStream peers messages channelLabel footer>` renderer at
modules/marketing/home/mesh-stream.tsx — pure presentation, no
playback engine, no data fetching. Handles peer filter, hover-for-
ciphertext tooltip, animated message list.
- demo-dashboard.tsx refactored to use it: keeps the playback loop,
traffic-light chrome, and script-driven messages; passes everything
to MeshStream via props. ~120 LOC shorter.
Backend:
- new GET /api/my/meshes/:id/stream in packages/api (same authz gate
as /my/meshes/:id — owner OR non-revoked member). Returns:
- up to 20 live presences (disconnectedAt IS NULL), joined to
meshMember for displayName
- up to 50 most-recent message_queue envelopes with metadata only:
sender + displayName, targetSpec, priority, createdAt, deliveredAt,
byte size, and a 24-char ciphertext preview (this IS what the
broker sees — no plaintext anywhere in the response)
- up to 20 recent audit events
- getMyMeshStreamResponseSchema in schema/mesh-user.ts matches exactly.
Frontend:
- new LiveStreamPanel client component at modules/mesh/live-stream-panel.tsx
— react-query with refetchInterval: 4000ms, refetchIntervalInBackground
false. Maps presences + envelopes to MeshStream's Peer/Message shape,
classifies targetSpec into message type ("tag:*" → ask_mesh, "*" →
broadcast, else direct). Passes through the ciphertextPreview as the
hover content — no fake ciphertext in live view.
- new route /dashboard/meshes/[id]/live with server-side authz preflight
via /my/meshes/:id. Mounts LiveStreamPanel inside a dashboard page
shell with breadcrumb back to mesh detail.
- Mesh detail page gets a new "Live" pill button (clay-pulsing dot)
next to "Generate invite link" in the header.
- paths config gets dashboard.user.meshes.live(id).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New user signs in → /dashboard (user) → hits server-side getMyMeshes → 0
results → redirects to /dashboard/meshes/new?onboarding=1. Create-mesh
page renders a welcome banner explaining what a mesh is. After submit,
if ?onboarding=1 was set, the form bounces to
/dashboard/meshes/[id]/invite?onboarding=1 instead of the mesh detail
page. Invite page renders a "🎉 Mesh created" banner with the
`claudemesh join <link>` CLI snippet.
The onboarding flag is URL-driven — no persistence needed, dismissal
happens naturally when the user navigates away.
Also rewrites the /dashboard (user) home page from the placeholder
"Welcome to your Dashboard" TurboStarter card grid to a claudemesh-
native view: top 6 meshes with badges, All meshes / New mesh CTAs.
Removes the unused Card/Icons imports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>