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>

View File

@@ -95,6 +95,7 @@ const pathsConfig = {
new: `${DASHBOARD_PREFIX}/meshes/new`,
mesh: (id: string) => `${DASHBOARD_PREFIX}/meshes/${id}`,
invite: (id: string) => `${DASHBOARD_PREFIX}/meshes/${id}/invite`,
live: (id: string) => `${DASHBOARD_PREFIX}/meshes/${id}/live`,
},
invites: `${DASHBOARD_PREFIX}/invites`,
settings: {

View File

@@ -1,5 +1,4 @@
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Reveal, SectionIcon } from "./_reveal";
import {
@@ -9,67 +8,33 @@ import {
SCRIPT,
SCRIPT_DURATION_MS,
type DemoMessage,
type Peer,
type PeerStatus,
} from "./demo-dashboard-script";
import { MeshStream, type StreamMessage, type StreamPeer } from "./mesh-stream";
const STATUS_DOT: Record<PeerStatus, string> = {
idle: "bg-emerald-500",
working: "bg-[var(--cm-clay)] animate-pulse",
offline: "bg-[var(--cm-fg-tertiary)]",
};
const toStreamMessage = (
m: DemoMessage,
loopKey: number,
): StreamMessage => ({
key: `${loopKey}-${m.t}`,
from: m.from,
to: m.to,
type: m.type,
text: m.text,
ciphertext: m.ciphertext,
});
const SURFACE_ICON: Record<Peer["surface"], React.ReactNode> = {
terminal: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
<path d="M6 9l3 3-3 3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
),
phone: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="7" y="2" width="10" height="20" rx="2" stroke="currentColor" strokeWidth="2" />
<circle cx="12" cy="18" r="1" fill="currentColor" />
</svg>
),
slack: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="10" y="3" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="12" y="15" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="3" y="10" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="15" y="12" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
</svg>
),
};
const TYPE_GLYPH = (type: DemoMessage["type"]) => {
if (type === "ask_mesh")
return (
<span className="inline-flex items-center gap-1 rounded-[4px] border border-[var(--cm-border)] bg-[var(--cm-bg)] px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-[var(--cm-clay)]">
broadcast
</span>
);
if (type === "self_nominate")
return (
<span className="inline-flex items-center gap-1 rounded-[4px] border border-emerald-500/40 bg-emerald-500/10 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-emerald-500">
hand-raise
</span>
);
return (
<span className="inline-flex items-center gap-1 rounded-[4px] border border-[var(--cm-border)] bg-[var(--cm-bg)] px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-[var(--cm-fg-secondary)]">
direct
</span>
);
};
type VisibleMessage = DemoMessage & { seq: number };
const STREAM_PEERS: StreamPeer[] = PEERS.map((p) => ({
id: p.id,
name: p.name,
status: p.status,
machine: p.machine,
surface: p.surface,
}));
export const DemoDashboard = () => {
const [elapsed, setElapsed] = useState(0);
const [playing, setPlaying] = useState(true);
const [focusedPeer, setFocusedPeer] = useState<string | null>(null);
const [loopCount, setLoopCount] = useState(0);
const [hoveredMessage, setHoveredMessage] = useState<number | null>(null);
const startRef = useRef<number>(0);
const rafRef = useRef<number | null>(null);
@@ -99,19 +64,13 @@ export const DemoDashboard = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playing, tick]);
const visible = useMemo<VisibleMessage[]>(() => {
return SCRIPT.filter((m) => m.t <= elapsed).map((m, i) => ({
...m,
seq: loopCount * 100 + i,
}));
}, [elapsed, loopCount]);
const filtered = useMemo(() => {
if (!focusedPeer) return visible;
return visible.filter(
(m) => m.from === focusedPeer || m.to === focusedPeer,
);
}, [visible, focusedPeer]);
const messages = useMemo<StreamMessage[]>(
() =>
SCRIPT.filter((m) => m.t <= elapsed).map((m) =>
toStreamMessage(m, loopCount),
),
[elapsed, loopCount],
);
const handleRestart = () => {
setElapsed(0);
@@ -119,8 +78,29 @@ export const DemoDashboard = () => {
setLoopCount((c) => c + 1);
};
const peerName = (id: string) =>
PEERS.find((p) => p.id === id)?.name ?? id;
const footer = (
<>
<div
className="h-[2px] bg-[var(--cm-clay)] transition-[width] duration-[100ms] ease-linear"
style={{
width: `${Math.min(100, (elapsed / SCRIPT_DURATION_MS) * 100)}%`,
}}
/>
<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} / {SCRIPT.length} messages
</span>
<span>
loop #{loopCount + 1} · {Math.floor(elapsed / 1000)}s /{" "}
{Math.floor(SCRIPT_DURATION_MS / 1000)}s
</span>
<span>{playing ? "▶ playing" : "⏸ paused"}</span>
</div>
</>
);
return (
<section
@@ -196,227 +176,14 @@ export const DemoDashboard = () => {
</button>
</div>
</div>
<div className="grid grid-cols-[200px_220px_1fr] min-h-[480px]">
{/* server sidebar */}
<aside
className="hidden border-r border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/40 p-4 md:block"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
<div
className="mb-3 text-[10px] uppercase tracking-[0.18em] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
your meshes
</div>
<ul className="space-y-1">
<li className="rounded-[var(--cm-radius-xs)] px-2.5 py-1.5 text-[13px] text-[var(--cm-fg-tertiary)] hover:bg-[var(--cm-bg)]">
smoke-test
</li>
<li className="rounded-[var(--cm-radius-xs)] bg-[var(--cm-clay)]/15 px-2.5 py-1.5 text-[13px] font-medium text-[var(--cm-clay)]">
{MESH_NAME}
</li>
<li className="rounded-[var(--cm-radius-xs)] px-2.5 py-1.5 text-[13px] text-[var(--cm-fg-tertiary)] hover:bg-[var(--cm-bg)]">
home-lab
</li>
</ul>
<button
className="mt-3 w-full rounded-[var(--cm-radius-xs)] border border-dashed border-[var(--cm-border)] px-2.5 py-1.5 text-left text-[12px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg-tertiary)] hover:text-[var(--cm-fg-secondary)]"
disabled
>
+ join mesh
</button>
</aside>
{/* peers */}
<aside
className="border-r border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/20 p-4"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
<div
className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.18em] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<span>peers · {PEERS.filter((p) => p.status !== "offline").length} online</span>
{focusedPeer && (
<button
onClick={() => setFocusedPeer(null)}
className="text-[var(--cm-clay)] hover:underline"
aria-label="Clear filter"
>
clear
</button>
)}
</div>
<ul className="space-y-1">
{PEERS.map((p) => {
const active = focusedPeer === p.id;
return (
<li key={p.id}>
<button
onClick={() =>
setFocusedPeer(active ? null : p.id)
}
className={
"group flex w-full items-center gap-2.5 rounded-[var(--cm-radius-xs)] px-2 py-1.5 text-left transition-colors " +
(active
? "bg-[var(--cm-clay)]/15"
: "hover:bg-[var(--cm-bg)]")
}
>
<span
className={
"h-2 w-2 flex-shrink-0 rounded-full " +
STATUS_DOT[p.status]
}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span
className={
"truncate text-[13px] " +
(active
? "font-medium text-[var(--cm-clay)]"
: "text-[var(--cm-fg)]")
}
>
{p.name}
</span>
<span className="text-[var(--cm-fg-tertiary)]">
{SURFACE_ICON[p.surface]}
</span>
</div>
<div
className="truncate text-[10px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{p.machine}
</div>
</div>
</button>
</li>
);
})}
</ul>
</aside>
{/* message stream */}
<div
className="relative flex flex-col"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
<div
className="flex items-center gap-2 border-b border-[var(--cm-border)] px-4 py-2.5"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<span className="text-[var(--cm-clay)]">#</span>
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
live-stream
</span>
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
{focusedPeer
? `filtered: ${peerName(focusedPeer)}`
: "all peers · E2E encrypted"}
</span>
</div>
<ol className="flex-1 space-y-3 overflow-y-auto p-4">
<AnimatePresence initial={false}>
{filtered.map((m) => (
<motion.li
key={`${m.seq}-${m.t}`}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.4,
ease: [0.22, 0.61, 0.36, 1],
}}
onMouseEnter={() => setHoveredMessage(m.seq)}
onMouseLeave={() => setHoveredMessage(null)}
className="group relative"
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 pt-0.5">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[var(--cm-bg-elevated)] text-[10px] font-medium uppercase text-[var(--cm-fg-secondary)]">
{peerName(m.from).slice(0, 2)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
{peerName(m.from)}
</span>
{m.to && (
<>
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
</span>
<span
className="text-[12px] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{m.to.startsWith("tag:")
? m.to
: peerName(m.to)}
</span>
</>
)}
{TYPE_GLYPH(m.type)}
</div>
<p
className="text-[14px] leading-[1.55] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
{m.text}
</p>
{hoveredMessage === m.seq && (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 rounded-[var(--cm-radius-xs)] border border-dashed border-[var(--cm-clay)]/40 bg-[var(--cm-bg-elevated)]/50 px-3 py-2"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<div className="mb-1 text-[9px] uppercase tracking-wider text-[var(--cm-clay)]">
broker sees only this
</div>
<code className="block break-all text-[11px] text-[var(--cm-fg-tertiary)]">
{m.ciphertext}
</code>
</motion.div>
)}
</div>
</div>
</motion.li>
))}
</AnimatePresence>
</ol>
{/* progress bar */}
<div className="border-t border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/30">
<div
className="h-[2px] bg-[var(--cm-clay)] transition-[width] duration-[100ms] ease-linear"
style={{
width: `${Math.min(100, (elapsed / SCRIPT_DURATION_MS) * 100)}%`,
}}
/>
<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>
{visible.length} / {SCRIPT.length} messages
</span>
<span>
loop #{loopCount + 1} · {Math.floor(elapsed / 1000)}s /{" "}
{Math.floor(SCRIPT_DURATION_MS / 1000)}s
</span>
<span>
{playing ? "▶ playing" : "⏸ paused"}
{focusedPeer && ` · filtered`}
</span>
</div>
</div>
</div>
</div>
{/* unused var to silence lint on LOOP_PAUSE_MS if dead-code elimination hits */}
<span hidden>{LOOP_PAUSE_MS}</span>
<MeshStream
peers={STREAM_PEERS}
messages={messages}
channelLabel="live-stream"
footer={footer}
/>
</div>
</Reveal>
@@ -429,9 +196,6 @@ export const DemoDashboard = () => {
broker routes ciphertext, never plaintext
</p>
</Reveal>
{/* prevent eslint exhaustive-deps hook warning from dead var */}
{loopCount < -1 && <span />}
</div>
</section>
);

View File

@@ -0,0 +1,324 @@
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
export type PeerStatus = "idle" | "working" | "dnd" | "offline";
export type MessageType = "ask_mesh" | "self_nominate" | "direct" | "broadcast";
export interface StreamPeer {
id: string;
name: string;
status: PeerStatus;
/** e.g. "macOS · payments-api" or "iOS · push-relay" */
machine: string;
surface?: "terminal" | "phone" | "slack";
}
export interface StreamMessage {
/** stable unique key */
key: string;
/** peer id or display name */
from: string;
/** peer id, "tag:xxx", "*", or null (self-nominate) */
to: string | null;
type: MessageType;
/** plaintext for demo, undefined for live (broker never sees it) */
text?: string;
/** truncated base64url — what the broker actually sees */
ciphertext: string;
/** absolute time, optional — used by live dashboard */
createdAt?: Date;
}
const STATUS_DOT: Record<PeerStatus, string> = {
idle: "bg-emerald-500",
working: "bg-[var(--cm-clay)] animate-pulse",
dnd: "bg-[#c46686]",
offline: "bg-[var(--cm-fg-tertiary)]",
};
const TYPE_CHIP: Record<MessageType, { label: string; className: string }> = {
ask_mesh: {
label: "⟐ broadcast",
className:
"border-[var(--cm-border)] bg-[var(--cm-bg)] text-[var(--cm-clay)]",
},
broadcast: {
label: "⟐ broadcast",
className:
"border-[var(--cm-border)] bg-[var(--cm-bg)] text-[var(--cm-clay)]",
},
self_nominate: {
label: "← hand-raise",
className: "border-emerald-500/40 bg-emerald-500/10 text-emerald-500",
},
direct: {
label: "→ direct",
className:
"border-[var(--cm-border)] bg-[var(--cm-bg)] text-[var(--cm-fg-secondary)]",
},
};
const surfaceGlyph = (s?: StreamPeer["surface"]) => {
if (s === "phone")
return (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="7" y="2" width="10" height="20" rx="2" stroke="currentColor" strokeWidth="2" />
<circle cx="12" cy="18" r="1" fill="currentColor" />
</svg>
);
if (s === "slack")
return (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="10" y="3" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="12" y="15" width="2" height="6" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="3" y="10" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
<rect x="15" y="12" width="6" height="2" rx="1" stroke="currentColor" strokeWidth="2" />
</svg>
);
return (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
<path d="M6 9l3 3-3 3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
};
const resolveName = (id: string, peers: StreamPeer[]) =>
peers.find((p) => p.id === id)?.name ?? id;
export interface MeshStreamProps {
peers: StreamPeer[];
messages: StreamMessage[];
/** text shown in stream header, right of # */
channelLabel?: string;
/** override the "N peers online" hint */
peersHint?: string;
/** override empty-state message */
emptyLabel?: string;
/** footer content (stats / progress bar / timers) */
footer?: React.ReactNode;
}
export const MeshStream = ({
peers,
messages,
channelLabel = "live-stream",
peersHint,
emptyLabel = "Waiting for messages…",
footer,
}: MeshStreamProps) => {
const [focusedPeer, setFocusedPeer] = useState<string | null>(null);
const [hoveredKey, setHoveredKey] = useState<string | null>(null);
const onlineCount = peers.filter((p) => p.status !== "offline").length;
const filtered = focusedPeer
? messages.filter((m) => m.from === focusedPeer || m.to === focusedPeer)
: messages;
return (
<div className="grid min-h-[480px] grid-cols-1 md:grid-cols-[220px_1fr]">
{/* peers sidebar */}
<aside
className="border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/20 p-4 md:border-b-0 md:border-r"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
<div
className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.18em] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<span>{peersHint ?? `peers · ${onlineCount} online`}</span>
{focusedPeer && (
<button
onClick={() => setFocusedPeer(null)}
className="text-[var(--cm-clay)] hover:underline"
aria-label="Clear filter"
>
clear
</button>
)}
</div>
{peers.length === 0 ? (
<p
className="text-[12px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
no peers online
</p>
) : (
<ul className="space-y-1">
{peers.map((p) => {
const active = focusedPeer === p.id;
return (
<li key={p.id}>
<button
onClick={() => setFocusedPeer(active ? null : p.id)}
className={
"group flex w-full items-center gap-2.5 rounded-[var(--cm-radius-xs)] px-2 py-1.5 text-left transition-colors " +
(active
? "bg-[var(--cm-clay)]/15"
: "hover:bg-[var(--cm-bg)]")
}
>
<span
className={
"h-2 w-2 flex-shrink-0 rounded-full " +
STATUS_DOT[p.status]
}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span
className={
"truncate text-[13px] " +
(active
? "font-medium text-[var(--cm-clay)]"
: "text-[var(--cm-fg)]")
}
>
{p.name}
</span>
<span className="text-[var(--cm-fg-tertiary)]">
{surfaceGlyph(p.surface)}
</span>
</div>
<div
className="truncate text-[10px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{p.machine}
</div>
</div>
</button>
</li>
);
})}
</ul>
)}
</aside>
{/* message stream */}
<div
className="relative flex flex-col"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
<div
className="flex items-center gap-2 border-b border-[var(--cm-border)] px-4 py-2.5"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<span className="text-[var(--cm-clay)]">#</span>
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
{channelLabel}
</span>
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
{focusedPeer
? `filtered: ${resolveName(focusedPeer, peers)}`
: "all peers · E2E encrypted"}
</span>
</div>
<ol className="flex-1 space-y-3 overflow-y-auto p-4">
{filtered.length === 0 && (
<li
className="py-8 text-center text-[13px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{emptyLabel}
</li>
)}
<AnimatePresence initial={false}>
{filtered.map((m) => (
<motion.li
key={m.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.4,
ease: [0.22, 0.61, 0.36, 1],
}}
onMouseEnter={() => setHoveredKey(m.key)}
onMouseLeave={() => setHoveredKey(null)}
className="group relative"
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 pt-0.5">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[var(--cm-bg-elevated)] text-[10px] font-medium uppercase text-[var(--cm-fg-secondary)]">
{resolveName(m.from, peers).slice(0, 2)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="text-[13px] font-medium text-[var(--cm-fg)]">
{resolveName(m.from, peers)}
</span>
{m.to && (
<>
<span className="text-[11px] text-[var(--cm-fg-tertiary)]">
</span>
<span
className="text-[12px] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{m.to.startsWith("tag:") || m.to === "*"
? m.to
: resolveName(m.to, peers)}
</span>
</>
)}
<span
className={
"inline-flex items-center gap-1 rounded-[4px] border px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider " +
TYPE_CHIP[m.type].className
}
>
{TYPE_CHIP[m.type].label}
</span>
{m.createdAt && (
<span
className="text-[10px] text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
{m.createdAt.toLocaleTimeString()}
</span>
)}
</div>
{m.text && (
<p
className="text-[14px] leading-[1.55] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
{m.text}
</p>
)}
{hoveredKey === m.key && (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 rounded-[var(--cm-radius-xs)] border border-dashed border-[var(--cm-clay)]/40 bg-[var(--cm-bg-elevated)]/50 px-3 py-2"
style={{ fontFamily: "var(--cm-font-mono)" }}
>
<div className="mb-1 text-[9px] uppercase tracking-wider text-[var(--cm-clay)]">
broker sees only this
</div>
<code className="block break-all text-[11px] text-[var(--cm-fg-tertiary)]">
{m.ciphertext}
{m.ciphertext && !m.text && "…"}
</code>
</motion.div>
)}
</div>
</div>
</motion.li>
))}
</AnimatePresence>
</ol>
{footer && (
<div className="border-t border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]/30">
{footer}
</div>
)}
</div>
</div>
);
};

View 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>
);
};

View File

@@ -10,7 +10,14 @@ import {
or,
sql,
} from "@turbostarter/db";
import { auditLog, invite, mesh, meshMember } from "@turbostarter/db/schema";
import {
auditLog,
invite,
mesh,
meshMember,
messageQueue,
presence,
} from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import type { GetMyMeshesInput } from "../../schema";
@@ -163,6 +170,100 @@ export const getMyMeshById = async ({
};
};
/**
* Live mesh stream — presences + recent message envelopes (metadata only) +
* recent audit events. Polled every 3-5s by the live dashboard. Authz:
* caller must own OR be a non-revoked member of the mesh.
*
* Envelopes expose a 24-char ciphertext preview so the UI can show
* "broker sees: <blob>" truthfully — this IS what the broker sees.
* Plaintext, nonces, full ciphertext are NEVER returned from here.
*/
export const getMyMeshStream = async ({
userId,
meshId,
}: {
userId: string;
meshId: string;
}) => {
// Authz check — same pattern as getMyMeshById
const [m] = await db
.select({ ownerUserId: mesh.ownerUserId })
.from(mesh)
.where(eq(mesh.id, meshId))
.limit(1);
if (!m) return null;
const isOwner = m.ownerUserId === userId;
if (!isOwner) {
const [membership] = await db
.select({ id: meshMember.id })
.from(meshMember)
.where(
and(
eq(meshMember.meshId, meshId),
eq(meshMember.userId, userId),
isNull(meshMember.revokedAt),
),
)
.limit(1);
if (!membership) return null;
}
const presences = await db
.select({
id: presence.id,
memberId: presence.memberId,
displayName: meshMember.displayName,
sessionId: presence.sessionId,
pid: presence.pid,
cwd: presence.cwd,
status: presence.status,
statusSource: presence.statusSource,
statusUpdatedAt: presence.statusUpdatedAt,
lastPingAt: presence.lastPingAt,
disconnectedAt: presence.disconnectedAt,
})
.from(presence)
.leftJoin(meshMember, eq(presence.memberId, meshMember.id))
.where(and(eq(meshMember.meshId, meshId), isNull(presence.disconnectedAt)))
.orderBy(desc(presence.lastPingAt))
.limit(20);
const envelopes = await db
.select({
id: messageQueue.id,
senderMemberId: messageQueue.senderMemberId,
senderDisplayName: meshMember.displayName,
targetSpec: messageQueue.targetSpec,
priority: messageQueue.priority,
ciphertextPreview: sql<string>`LEFT(${messageQueue.ciphertext}, 24)`,
size: sql<number>`OCTET_LENGTH(${messageQueue.ciphertext})`,
createdAt: messageQueue.createdAt,
deliveredAt: messageQueue.deliveredAt,
})
.from(messageQueue)
.leftJoin(meshMember, eq(messageQueue.senderMemberId, meshMember.id))
.where(eq(messageQueue.meshId, meshId))
.orderBy(desc(messageQueue.createdAt))
.limit(50);
const auditEvents = await db
.select({
id: auditLog.id,
eventType: auditLog.eventType,
actorPeerId: auditLog.actorPeerId,
targetPeerId: auditLog.targetPeerId,
createdAt: auditLog.createdAt,
})
.from(auditLog)
.where(eq(auditLog.meshId, meshId))
.orderBy(desc(auditLog.createdAt))
.limit(20);
return { presences, envelopes, auditEvents };
};
export const getMyExport = async ({ userId }: { userId: string }) => {
const meshesOwned = await db
.select({

View File

@@ -19,6 +19,7 @@ import {
getMyExport,
getMyInvitesSent,
getMyMeshById,
getMyMeshStream,
getMyMeshes,
} from "./queries";
@@ -47,6 +48,15 @@ export const myRouter = new Hono<Env>()
);
}
})
.get("/meshes/:id/stream", async (c) => {
const user = c.var.user;
return c.json(
(await getMyMeshStream({
userId: user.id,
meshId: c.req.param("id"),
})) ?? { presences: [], envelopes: [], auditEvents: [] },
);
})
.get("/meshes/:id", async (c) => {
const user = c.var.user;
return c.json(

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({