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>
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user