From 82f169804350e9b720c3f88005f33aab9eb03515 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 2 Sep 2026 00:25:14 +0700 Subject: [PATCH] =?UTF-8?q?feat(fe):=20surface=20fetched-but-hidden=20data?= =?UTF-8?q?=20=E2=80=94=20voice=20bridges,=20recorder=20last-activity,=20h?= =?UTF-8?q?ourly=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ') 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. --- .../src/app/(dashboard)/dashboard/view.tsx | 48 ++++++++++ .../src/app/(dashboard)/recordings/view.tsx | 3 + .../src/app/(dashboard)/voice/view.tsx | 94 +++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index 7e98c8b7..fe3f4a82 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -258,6 +258,54 @@ export function DashboardView({ + {/* Hourly Flow — last-24h message volume by hour */} + {activity?.hourly && activity.hourly.length > 0 && ( + + + {activity.hourly.reduce((a, h) => a + h.messages, 0)} MSGS ·{" "} + {activity.hourly.reduce((a, h) => a + h.flagged, 0)} FLAGGED + + } + /> +
+ {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 ( +
+
+
+ {String(h.hour).padStart(2, "0")} +
+
+ ); + })} +
+
+ 00:00 + 06:00 + 12:00 + 18:00 + 23:00 +
+ + )} + {/* Top Channels + Engagement Row */}
{/* Top Channels Ranking */} diff --git a/services/frontend/src/app/(dashboard)/recordings/view.tsx b/services/frontend/src/app/(dashboard)/recordings/view.tsx index 7cf7c7cd..85d4a5b1 100644 --- a/services/frontend/src/app/(dashboard)/recordings/view.tsx +++ b/services/frontend/src/app/(dashboard)/recordings/view.tsx @@ -799,6 +799,9 @@ function Leaderboard({
{s.transcribed > 0 ? `${s.words} words` : "no transcript"}
+
+ {s.last_at ? `active ${formatRelativeTime(s.last_at)}` : "—"} +
))} diff --git a/services/frontend/src/app/(dashboard)/voice/view.tsx b/services/frontend/src/app/(dashboard)/voice/view.tsx index 12319033..a7889b2c 100644 --- a/services/frontend/src/app/(dashboard)/voice/view.tsx +++ b/services/frontend/src/app/(dashboard)/voice/view.tsx @@ -1,6 +1,7 @@ "use client"; import { + Cable, Mic, PhoneOff, Radio, @@ -231,6 +232,99 @@ export function VoiceView({
+ {/* Connected Bridges — multi-guild voice bridge roster */} + + + {status?.connections?.length ?? (connected ? 1 : 0)} ACTIVE + + } + /> +
+ {status?.connections && status.connections.length > 0 ? ( +
+ {status.connections.map((conn) => { + const isActive = + conn.guildId === status.activeGuildId && + conn.channelId === status.activeChannelId; + return ( +
+ + + +
+
+ {conn.channelName || "#" + conn.channelId.slice(0, 8)} +
+
+ + G:{conn.guildId.slice(0, 8)} · C: + {conn.channelId.slice(0, 8)} + + {isActive && ( + + ACTIVE + + )} +
+
+ + {new Date(conn.connectedAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + +
+ ); + })} +
+ ) : connected ? ( +
+ + + +
+
+ {status?.activeChannelName || "Voice channel"} +
+
+ Active bridge · G: + {(status?.activeGuildId ?? guildId ?? "").slice(0, 8)} +
+
+ + ACTIVE + +
+ ) : ( +
+ + No active voice bridges + +
+ )} +
+
+ {/* Voice Stage Grid */}