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

@@ -0,0 +1,69 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getMyMeshResponseSchema } from "@turbostarter/api/schema";
import { handle } from "@turbostarter/api/utils";
import { Badge } from "@turbostarter/ui-web/badge";
import { buttonVariants } from "@turbostarter/ui-web/button";
import { pathsConfig } from "~/config/paths";
import { api } from "~/lib/api/server";
import { getMetadata } from "~/lib/metadata";
import {
DashboardHeader,
DashboardHeaderDescription,
DashboardHeaderTitle,
} from "~/modules/common/layout/dashboard/header";
import { LiveStreamPanel } from "~/modules/mesh/live-stream-panel";
export const generateMetadata = getMetadata({
title: "Live mesh",
description: "Real-time situational awareness of your mesh.",
});
export default async function LiveMeshPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Authz gate — same endpoint the detail page uses
const data = await handle(api.my.meshes[":id"].$get, {
schema: getMyMeshResponseSchema,
})({ param: { id } }).catch(() => null);
if (!data || !data.mesh) notFound();
const { mesh } = data;
return (
<>
<DashboardHeader>
<div className="flex w-full items-start justify-between gap-4">
<div>
<DashboardHeaderTitle>
<span className="flex items-center gap-3">
{mesh.name}
<Badge variant="outline" className="font-mono text-xs">
live
</Badge>
</span>
</DashboardHeaderTitle>
<DashboardHeaderDescription>
Real-time view of presences and envelope routing across this
mesh. Broker sees ciphertext only.
</DashboardHeaderDescription>
</div>
<Link
href={pathsConfig.dashboard.user.meshes.mesh(mesh.id)}
className={buttonVariants({ variant: "outline" })}
>
Mesh detail
</Link>
</div>
</DashboardHeader>
<LiveStreamPanel meshId={id} />
</>
);
}

View File

@@ -55,12 +55,21 @@ export default async function MeshPage({
· tier {mesh.tier} · {mesh.visibility} · {mesh.transport}
</DashboardHeaderDescription>
</div>
<Link
href={pathsConfig.dashboard.user.meshes.invite(mesh.id)}
className={buttonVariants({ variant: "default" })}
>
Generate invite link
</Link>
<div className="flex gap-2">
<Link
href={pathsConfig.dashboard.user.meshes.live(mesh.id)}
className={buttonVariants({ variant: "outline" })}
>
<span className="mr-1.5 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--cm-clay)]" />
Live
</Link>
<Link
href={pathsConfig.dashboard.user.meshes.invite(mesh.id)}
className={buttonVariants({ variant: "default" })}
>
Generate invite link
</Link>
</div>
</div>
</DashboardHeader>