feat(fe): surface fetched-but-hidden data — voice bridges, recorder last-activity, hourly flow
Closes the three highest-value gaps from the FE data-flow audit:
- voice/view.tsx: render VoiceStatus.connections (multi-guild bridge roster)
which was fetched but never shown — channel name, guild/channel id,
connected time, ACTIVE marker for the primary bridge (falls back to the
active channel when connections is empty).
- recordings/view.tsx: show SpeakerSummary.last_at ('active <relative>') on
the speaker leaderboard row (data was fetched but hidden).
- dashboard/view.tsx: render activity.hourly as an 'Hourly Flow · Last 24h'
24-bar volume strip with per-hour tooltip and total msgs/flagged header
(hourly distribution was fetched but only daily was charted).
Verified: tsc clean, biome clean (127 files), pnpm build green.
This commit is contained in:
@@ -258,6 +258,54 @@ export function DashboardView({
|
|||||||
</GlassPanel>
|
</GlassPanel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hourly Flow — last-24h message volume by hour */}
|
||||||
|
{activity?.hourly && activity.hourly.length > 0 && (
|
||||||
|
<GlassPanel className="linear-tile">
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="Cadence"
|
||||||
|
title="Hourly Flow · Last 24h"
|
||||||
|
action={
|
||||||
|
<span className="mono text-xs text-[#8a8f98]">
|
||||||
|
{activity.hourly.reduce((a, h) => a + h.messages, 0)} MSGS ·{" "}
|
||||||
|
{activity.hourly.reduce((a, h) => a + h.flagged, 0)} FLAGGED
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex h-20 items-end gap-[3px]">
|
||||||
|
{activity.hourly.map((h) => {
|
||||||
|
const max = Math.max(
|
||||||
|
...activity.hourly.map((x) => x.messages),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const pct = Math.max(4, (h.messages / max) * 100);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={h.hour}
|
||||||
|
className="group relative flex h-full min-w-0 flex-1 flex-col justify-end"
|
||||||
|
title={`${String(h.hour).padStart(2, "0")}:00 — ${h.messages} msgs, ${h.flagged} flagged`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full rounded-t-[3px] bg-signal/80 transition-all group-hover:bg-signal"
|
||||||
|
style={{ height: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
<div className="mt-1 truncate text-center font-mono text-[7px] text-ink-faint">
|
||||||
|
{String(h.hour).padStart(2, "0")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex justify-between font-mono text-[9px] text-ink-faint">
|
||||||
|
<span>00:00</span>
|
||||||
|
<span>06:00</span>
|
||||||
|
<span>12:00</span>
|
||||||
|
<span>18:00</span>
|
||||||
|
<span>23:00</span>
|
||||||
|
</div>
|
||||||
|
</GlassPanel>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Top Channels + Engagement Row */}
|
{/* Top Channels + Engagement Row */}
|
||||||
<div className="grid gap-3 lg:grid-cols-2">
|
<div className="grid gap-3 lg:grid-cols-2">
|
||||||
{/* Top Channels Ranking */}
|
{/* Top Channels Ranking */}
|
||||||
|
|||||||
@@ -799,6 +799,9 @@ function Leaderboard({
|
|||||||
<div className="text-ink-faint">
|
<div className="text-ink-faint">
|
||||||
{s.transcribed > 0 ? `${s.words} words` : "no transcript"}
|
{s.transcribed > 0 ? `${s.words} words` : "no transcript"}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-ink-faint" suppressHydrationWarning>
|
||||||
|
{s.last_at ? `active ${formatRelativeTime(s.last_at)}` : "—"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Cable,
|
||||||
Mic,
|
Mic,
|
||||||
PhoneOff,
|
PhoneOff,
|
||||||
Radio,
|
Radio,
|
||||||
@@ -231,6 +232,99 @@ export function VoiceView({
|
|||||||
</GlassPanel>
|
</GlassPanel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Connected Bridges — multi-guild voice bridge roster */}
|
||||||
|
<GlassPanel className="voice-tile">
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="Bridges"
|
||||||
|
title="Connected Voice Bridges"
|
||||||
|
action={
|
||||||
|
<span className="mono text-xs text-[#8a8f98]">
|
||||||
|
{status?.connections?.length ?? (connected ? 1 : 0)} ACTIVE
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-3">
|
||||||
|
{status?.connections && status.connections.length > 0 ? (
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
{status.connections.map((conn) => {
|
||||||
|
const isActive =
|
||||||
|
conn.guildId === status.activeGuildId &&
|
||||||
|
conn.channelId === status.activeChannelId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${conn.guildId}-${conn.channelId}`}
|
||||||
|
className={`flex items-center gap-2.5 rounded-[8px] border p-2.5 ${
|
||||||
|
isActive
|
||||||
|
? "border-success/40 bg-success/5"
|
||||||
|
: "border-hairline bg-surface-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex size-8 shrink-0 items-center justify-center rounded-full border ${
|
||||||
|
isActive
|
||||||
|
? "border-success/40 bg-success/10 text-success"
|
||||||
|
: "border-hairline bg-surface text-ink-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Cable className="size-3.5" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-xs font-medium text-ink">
|
||||||
|
{conn.channelName || "#" + conn.channelId.slice(0, 8)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 flex items-center gap-1.5 font-mono text-[9px] text-ink-faint">
|
||||||
|
<span className="truncate">
|
||||||
|
G:{conn.guildId.slice(0, 8)} · C:
|
||||||
|
{conn.channelId.slice(0, 8)}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="rounded bg-success/15 px-1 text-success">
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="shrink-0 font-mono text-[9px] text-ink-faint"
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
{new Date(conn.connectedAt).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : connected ? (
|
||||||
|
<div className="flex items-center gap-2.5 rounded-[8px] border border-success/30 bg-success/5 p-2.5">
|
||||||
|
<span className="flex size-8 items-center justify-center rounded-full border border-success/40 bg-success/10 text-success">
|
||||||
|
<Cable className="size-3.5" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-xs font-medium text-ink">
|
||||||
|
{status?.activeChannelName || "Voice channel"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 font-mono text-[9px] text-ink-faint">
|
||||||
|
Active bridge · G:
|
||||||
|
{(status?.activeGuildId ?? guildId ?? "").slice(0, 8)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="rounded bg-success/15 px-1 font-mono text-[9px] text-success">
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-[8px] border border-dashed border-hairline p-4 text-center">
|
||||||
|
<span className="font-mono text-[10px] text-ink-faint">
|
||||||
|
No active voice bridges
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GlassPanel>
|
||||||
|
|
||||||
{/* Voice Stage Grid */}
|
{/* Voice Stage Grid */}
|
||||||
<div className="grid gap-3 lg:grid-cols-3">
|
<div className="grid gap-3 lg:grid-cols-3">
|
||||||
<GlassPanel className="voice-tile lg:col-span-2">
|
<GlassPanel className="voice-tile lg:col-span-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user