feat: real guild/channel names from Discord + remove selectors in Messages/Analytics

- discord-gateway: add redis command handlers for guilds:list, guilds:text-channels, voice:channels
- backend: replace postgres synthetic names with redis commands to gateway (with fallback)
- frontend: remove guild/channel dropdowns from messages and analytics tabs
- frontend: auto-load all channels from monitor guild via guildId query param
- frontend: show guild name in messages/analytics headers instead of selector
- live tab: keeps guild/channel selectors with real discord names

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-02 11:02:54 +07:00
co-authored by Claude Opus 4.8
parent f1ddca5eee
commit 5f419a6f0f
7 changed files with 243 additions and 182 deletions
@@ -12,8 +12,6 @@ export function mergeMessages(
for (const message of incoming) {
byId.set(message.id, { ...byId.get(message.id), ...message });
}
// Removed .slice(0, 200) cap — let the message list grow unbounded.
// Infinite scroll handles the data volume via cursor pagination.
return Array.from(byId.values()).sort(
(a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id),
);
@@ -26,26 +24,25 @@ export function useMessages() {
const [error, setError] = useState<string | null>(null);
const [cursor, setCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(false);
const currentChannel = useRef<string | null>(null);
const currentGuild = useRef<string | null>(null);
const fetchMessages = useCallback(async (channelId?: string) => {
if (!channelId) {
const fetchMessages = useCallback(async (guildId?: string) => {
if (!guildId) {
setMessages([]);
setCursor(null);
setHasMore(false);
return [];
}
currentChannel.current = channelId;
currentGuild.current = guildId;
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({
limit: String(PAGE_SIZE),
channelId,
guildId,
});
const result = await listMessages(params);
// Only update state if we're still on the same channel (avoid race conditions)
if (currentChannel.current === channelId) {
if (currentGuild.current === guildId) {
setMessages(result.data);
setCursor(result.nextCursor);
setHasMore(!!result.nextCursor);
@@ -61,30 +58,23 @@ export function useMessages() {
}, []);
const loadMore = useCallback(async () => {
if (!cursor || !currentChannel.current || loadingMore) return;
if (!cursor || !currentGuild.current || loadingMore) return;
setLoadingMore(true);
try {
const params = new URLSearchParams({
limit: String(PAGE_SIZE),
channelId: currentChannel.current,
guildId: currentGuild.current,
cursor,
});
const result = await listMessages(params);
// Only update if still on the same channel
if (
currentChannel.current === result.data[0]?.channel_id ||
currentChannel.current
) {
setMessages((prev) => [...prev, ...result.data]);
setCursor(result.nextCursor);
setHasMore(!!result.nextCursor);
}
setMessages((prev) => [...prev, ...result.data]);
setCursor(result.nextCursor);
setHasMore(!!result.nextCursor);
} finally {
setLoadingMore(false);
}
}, [cursor, loadingMore]);
// BUG 5 FIX: reanalyze returns Promise<void> so callers can await it
const reanalyze = useCallback(async (id: string): Promise<void> => {
setMessages((prev) =>
prev.map((message) =>
@@ -1,16 +1,14 @@
import { Filter, Search, X } from "lucide-react";
import { useMemo, useState } from "react";
import type { Channel, Guild, MessageRecord } from "../../shared/api/client";
import type { MessageRecord } from "../../shared/api/client";
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Input,
Select,
Tabs,
TabsContent,
TabsList,
@@ -20,13 +18,8 @@ import { ImageGrid } from "./components/ImageGrid";
import { MessageFeed } from "./components/MessageFeed";
interface MessagesPanelProps {
guilds: Guild[];
channels: Channel[];
selectedGuild: string;
selectedChannel: string;
guildName: string | null;
messages: MessageRecord[];
onGuildChange: (guildId: string) => void;
onChannelChange: (channelId: string) => void;
onReanalyze: (id: string) => Promise<void>;
onLoadMore?: () => void;
hasMore?: boolean;
@@ -36,13 +29,8 @@ interface MessagesPanelProps {
type AiFilter = "all" | "clean" | "warn" | "flagged" | "error" | "pending";
export function MessagesPanel({
guilds,
channels,
selectedGuild,
selectedChannel,
guildName,
messages,
onGuildChange,
onChannelChange,
onReanalyze,
onLoadMore,
hasMore,
@@ -63,11 +51,7 @@ export function MessagesPanel({
}
setIsSearching(true);
try {
const params = new URLSearchParams({
q: searchQuery,
...(selectedChannel && { channelId: selectedChannel }),
limit: "50",
});
const params = new URLSearchParams({ q: searchQuery, limit: "50" });
const response = await fetch(`/api/analysis/search?${params}`);
if (!response.ok) throw new Error("Search failed");
const data = await response.json();
@@ -110,24 +94,19 @@ export function MessagesPanel({
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Message Source</CardTitle>
<CardDescription>
Pick a guild and channel/thread to inspect captures.
</CardDescription>
<CardTitle>Messages</CardTitle>
{guildName && (
<p className="text-sm text-muted-foreground">
Monitoring all text channels in{" "}
<span className="font-medium text-foreground">{guildName}</span>
</p>
)}
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<Select
value={selectedGuild}
onChange={(e) => onGuildChange(e.target.value)}
placeholder="Select text guild"
options={guilds.map((g) => ({ value: g.id, label: g.name }))}
/>
<Select
value={selectedChannel}
onChange={(e) => onChannelChange(e.target.value)}
placeholder="Select channel or thread"
options={channels.map((c) => ({ value: c.id, label: c.name }))}
/>
<CardContent>
<p className="text-xs text-muted-foreground">
Messages are automatically captured from all text channels in the
monitored guild. Real-time updates arrive via WebSocket.
</p>
</CardContent>
</Card>
@@ -257,9 +236,7 @@ export function MessagesPanel({
emptyText={
showSearch
? "No messages found matching your search."
: selectedChannel
? "No captures yet."
: "Select a channel to view captures."
: "No captures yet."
}
onLoadMore={showSearch ? undefined : onLoadMore}
hasMore={showSearch ? false : hasMore}