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

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