feat(gmw): public features #2-#6 — live moderation feed, toxic topic trends, channel timeline, CSV export, activity heatmap
- Live Moderation Feed: gateway publishes discord:moderation:action (Redis) → backend WS emits moderation_action → public web shows realtime stream. - Toxic Topic Trends: backend moderation.trends aggregates categories/severity/action_type (read-only) → SVG bar + donut. - Channel Timeline: messages view gets Feed/Timeline toggle with date-grouped separators. - CSV Export: client-side downloadCsv for moderation actions (no backend write scope). - Activity Heatmap: backend messages.activity (per-hour volume by channel) → pure-SVG grid. User reputation deliberately excluded — no such feature exists in the codebase. All read-only / public-facing / fully automatic per project rules.
This commit is contained in:
@@ -30,10 +30,13 @@ export {
|
||||
useReview,
|
||||
useSemanticSearch,
|
||||
useTextChannels,
|
||||
useMessageActivity,
|
||||
} from "./use-messages";
|
||||
export {
|
||||
useLiveModeration,
|
||||
useModerationActions,
|
||||
useModerationStats,
|
||||
useModerationTrends,
|
||||
} from "./use-moderation";
|
||||
export {
|
||||
useDeleteRecording,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { messagesApi, voiceApi } from "@/lib/api";
|
||||
import type {
|
||||
AttachmentRecord,
|
||||
Channel,
|
||||
MessageActivityBucket,
|
||||
MessageRecord,
|
||||
SemanticSearchResult,
|
||||
} from "@/lib/types";
|
||||
@@ -374,3 +375,9 @@ export function useMessagesStream(
|
||||
|
||||
return { streaming, error };
|
||||
}
|
||||
|
||||
export function useMessageActivity(days = 30) {
|
||||
return useSWR<MessageActivityBucket[]>(["activity", days], () =>
|
||||
messagesApi.getActivity(days),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { moderationApi } from "@/lib/api";
|
||||
import type { ModerationAction, ModerationStats } from "@/lib/types";
|
||||
import type {
|
||||
ModerationAction,
|
||||
ModerationStats,
|
||||
ModerationTrends,
|
||||
} from "@/lib/types";
|
||||
import { useWebSocket } from "@/lib/ws/context";
|
||||
|
||||
export function useModerationStats(initialData?: ModerationStats) {
|
||||
return useSWR<ModerationStats>(
|
||||
@@ -33,3 +39,46 @@ export function useModerationActions(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live moderation feed: merges the initial SWR list with actions pushed over
|
||||
* the WebSocket in real time. Returns a capped, newest-first buffer.
|
||||
* Read-only / public — no write actions.
|
||||
*/
|
||||
export function useLiveModeration(
|
||||
initialData: ModerationAction[] = [],
|
||||
cap = 50,
|
||||
) {
|
||||
const { on: subscribe } = useWebSocket();
|
||||
const [live, setLive] = useState<ModerationAction[]>(initialData);
|
||||
const seen = useRef<Set<string>>(new Set(initialData.map((a) => a.id)));
|
||||
|
||||
useEffect(() => {
|
||||
setLive(initialData);
|
||||
seen.current = new Set(initialData.map((a) => a.id));
|
||||
}, [initialData]);
|
||||
|
||||
const handle = useCallback(
|
||||
(action: ModerationAction) => {
|
||||
if (seen.current.has(action.id)) return;
|
||||
seen.current.add(action.id);
|
||||
setLive((prev) => [action, ...prev].slice(0, cap));
|
||||
},
|
||||
[cap],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = subscribe("moderation_action", handle);
|
||||
return unsub;
|
||||
}, [subscribe, handle]);
|
||||
|
||||
return live;
|
||||
}
|
||||
|
||||
export function useModerationTrends(days = 30, initialData?: ModerationTrends) {
|
||||
return useSWR<ModerationTrends>(
|
||||
["moderation-trends", days],
|
||||
() => moderationApi.getTrends(days),
|
||||
{ fallbackData: initialData },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user