feat(web): live mesh dashboard — real data through extracted MeshStream
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user