feat(web): live mesh dashboard — real data through extracted MeshStream
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled

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>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 14:51:14 +01:00
parent 64ca600195
commit 5bffdb1d30
9 changed files with 745 additions and 300 deletions

View File

@@ -139,6 +139,53 @@ export type CreateMyInviteResponse = z.infer<typeof createMyInviteResponseSchema
// List my invites (pending + sent)
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Live mesh stream (presences + recent envelopes + recent audit events)
// ---------------------------------------------------------------------
export const getMyMeshStreamResponseSchema = z.object({
presences: z.array(
z.object({
id: z.string(),
memberId: z.string(),
displayName: z.string().nullable(),
sessionId: z.string(),
pid: z.number(),
cwd: z.string(),
status: z.enum(["idle", "working", "dnd"]),
statusSource: z.enum(["hook", "manual", "jsonl"]),
statusUpdatedAt: z.coerce.date(),
lastPingAt: z.coerce.date(),
disconnectedAt: z.coerce.date().nullable(),
}),
),
envelopes: z.array(
z.object({
id: z.string(),
senderMemberId: z.string(),
senderDisplayName: z.string().nullable(),
targetSpec: z.string(),
priority: z.enum(["now", "next", "low"]),
ciphertextPreview: z.string(),
size: z.number(),
createdAt: z.coerce.date(),
deliveredAt: z.coerce.date().nullable(),
}),
),
auditEvents: z.array(
z.object({
id: z.string(),
eventType: z.string(),
actorPeerId: z.string().nullable(),
targetPeerId: z.string().nullable(),
createdAt: z.coerce.date(),
}),
),
});
export type GetMyMeshStreamResponse = z.infer<
typeof getMyMeshStreamResponseSchema
>;
export const getMyInvitesResponseSchema = z.object({
sent: z.array(
z.object({