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:
1 parent
36363fa3db
commit
9b3134d767
26 files changed
+854
-44
No files matched your search
@@ -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