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:
asepharyana
2026-09-02 00:25:14 +07:00
parent 7e50c000dc
commit 82f1698043
3 changed files with 145 additions and 0 deletions
@@ -258,6 +258,54 @@ export function DashboardView({
</GlassPanel>
</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 */}
<div className="grid gap-3 lg:grid-cols-2">
{/* Top Channels Ranking */}
@@ -799,6 +799,9 @@ function Leaderboard({
<div className="text-ink-faint">
{s.transcribed > 0 ? `${s.words} words` : "no transcript"}
</div>
<div className="text-ink-faint" suppressHydrationWarning>
{s.last_at ? `active ${formatRelativeTime(s.last_at)}` : "—"}
</div>
</div>
</button>
))}
@@ -1,6 +1,7 @@
"use client";
import {
Cable,
Mic,
PhoneOff,
Radio,
@@ -231,6 +232,99 @@ export function VoiceView({
</GlassPanel>
</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 */}
<div className="grid gap-3 lg:grid-cols-3">
<GlassPanel className="voice-tile lg:col-span-2">