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:
120
apps/web/src/modules/mesh/live-stream-panel.tsx
Normal file
120
apps/web/src/modules/mesh/live-stream-panel.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
getMyMeshStreamResponseSchema,
|
||||
type GetMyMeshStreamResponse,
|
||||
} from "@turbostarter/api/schema";
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/client";
|
||||
import {
|
||||
MeshStream,
|
||||
type StreamMessage,
|
||||
type StreamPeer,
|
||||
} from "~/modules/marketing/home/mesh-stream";
|
||||
|
||||
const POLL_INTERVAL_MS = 4000;
|
||||
|
||||
const classifyTarget = (
|
||||
target: string,
|
||||
): "direct" | "ask_mesh" | "broadcast" => {
|
||||
if (target === "*") return "broadcast";
|
||||
if (target.startsWith("tag:")) return "ask_mesh";
|
||||
return "direct";
|
||||
};
|
||||
|
||||
const buildStream = (data: GetMyMeshStreamResponse) => {
|
||||
const peers: StreamPeer[] = data.presences.map((p) => ({
|
||||
id: p.memberId,
|
||||
name: p.displayName ?? p.memberId.slice(0, 8),
|
||||
status: p.status === "dnd" ? "dnd" : p.status,
|
||||
machine: p.cwd,
|
||||
surface: "terminal",
|
||||
}));
|
||||
|
||||
const messages: StreamMessage[] = data.envelopes
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((e) => ({
|
||||
key: e.id,
|
||||
from: e.senderMemberId,
|
||||
to: e.targetSpec,
|
||||
type: classifyTarget(e.targetSpec),
|
||||
ciphertext: e.ciphertextPreview,
|
||||
createdAt: new Date(e.createdAt),
|
||||
}));
|
||||
|
||||
return { peers, messages };
|
||||
};
|
||||
|
||||
export const LiveStreamPanel = ({ meshId }: { meshId: string }) => {
|
||||
const { data, isLoading, dataUpdatedAt, isFetching } = useQuery({
|
||||
queryKey: ["mesh", "stream", meshId],
|
||||
queryFn: () =>
|
||||
handle(api.my.meshes[":id"].stream.$get, {
|
||||
schema: getMyMeshStreamResponseSchema,
|
||||
})({ param: { id: meshId } }),
|
||||
refetchInterval: POLL_INTERVAL_MS,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
|
||||
const { peers, messages } = useMemo(
|
||||
() =>
|
||||
data ? buildStream(data) : { peers: [], messages: [] },
|
||||
[data],
|
||||
);
|
||||
|
||||
const secondsAgo = dataUpdatedAt
|
||||
? Math.max(0, Math.floor((Date.now() - dataUpdatedAt) / 1000))
|
||||
: null;
|
||||
|
||||
const footer = (
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2 text-[10px] text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
<span>
|
||||
{messages.length} envelopes · {peers.length} live peers
|
||||
</span>
|
||||
<span>
|
||||
{isFetching ? "▶ polling…" : `↻ ${secondsAgo ?? "—"}s ago`}
|
||||
{" · "}every {POLL_INTERVAL_MS / 1000}s
|
||||
</span>
|
||||
<span>read-only · E2E encrypted</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const emptyLabel = isLoading
|
||||
? "Connecting to mesh…"
|
||||
: "No envelopes yet. When your peers send messages they'll appear here.";
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[var(--cm-radius-lg)] border border-[var(--cm-border)] bg-[var(--cm-bg)]">
|
||||
<div
|
||||
className="flex items-center justify-between border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/60 px-4 py-3"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={
|
||||
"inline-block h-2 w-2 rounded-full " +
|
||||
(isFetching ? "bg-[var(--cm-clay)] animate-pulse" : "bg-emerald-500")
|
||||
}
|
||||
/>
|
||||
<span className="text-[11px] text-[var(--cm-fg-secondary)]">
|
||||
live · polling every {POLL_INTERVAL_MS / 1000}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<MeshStream
|
||||
peers={peers}
|
||||
messages={messages}
|
||||
channelLabel="live-stream"
|
||||
emptyLabel={emptyLabel}
|
||||
footer={footer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user