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:
asepharyana
2026-08-18 17:43:02 +07:00
parent 36363fa3db
commit 9b3134d767
26 changed files with 854 additions and 44 deletions
@@ -25,6 +25,7 @@ import {
registerMessageCapture,
setEventBroadcaster as setMessageCaptureEventBroadcaster,
} from "../modules/message-capture/messageCapture.js";
import { setModerationEventBroadcaster } from "../modules/message-capture/moderationActionsDb.js";
import { registerReactionCapture } from "../modules/reaction-tracking/index.js";
import { registerThreadCapture } from "../modules/thread-tracking/index.js";
import { registerPresenceCapture } from "../modules/user-presence/index.js";
@@ -254,6 +255,7 @@ export async function initializeDiscordGateway() {
logger.info({ user: client.user?.tag }, "Bot logged in");
setMessageCaptureEventBroadcaster(eventBroadcaster);
setRecorderEventBroadcaster(eventBroadcaster);
setModerationEventBroadcaster(eventBroadcaster);
registerMessageCapture(client);
startPendingAIAnalysisWorker(client, eventBroadcaster);
@@ -293,6 +293,16 @@ export class EventBroadcaster {
});
}
async moderationAction(data: Record<string, unknown>): Promise<void> {
this.logger.debug({ data }, "Publishing moderation_action");
await this.publisher.publish(EventChannels.MODERATION_ACTION, {
type: "moderation_action",
data,
timestamp: Date.now(),
source: "discord-gateway",
});
}
async analysisQueueStatus(data: Record<string, unknown>): Promise<void> {
this.logger.debug({ data }, "Publishing analysis_queue_status");
await this.publisher.publish(EventChannels.ANALYSIS_QUEUE_STATUS, {
@@ -9,6 +9,7 @@ import {
DISCORD_MESSAGE_CREATED,
DISCORD_MESSAGE_DELETED,
DISCORD_MESSAGE_UPDATED,
DISCORD_MODERATION_ACTION,
DISCORD_PRESENCE_UPDATED,
DISCORD_REACTION_ADDED,
DISCORD_REACTION_REMOVED,
@@ -50,6 +51,7 @@ export const EventChannels = {
GUILD_MEMBER_ADDED: DISCORD_GUILD_MEMBER_ADDED,
GUILD_MEMBER_REMOVED: DISCORD_GUILD_MEMBER_REMOVED,
VOICE_ANALYZED: DISCORD_VOICE_ANALYZED,
MODERATION_ACTION: DISCORD_MODERATION_ACTION,
} as const;
export type EventChannelType =
@@ -4,8 +4,16 @@ import type * as schema from "../../shared/database/schema.js";
import { moderationActionsTable } from "../../shared/database/schema.js";
import { buildCursorCondition, pageResult } from "../../shared/index.js";
import { createChildLogger, type Logger } from "../../shared/logger/index.js";
import type { EventBroadcaster } from "../event-broadcaster/eventBroadcaster.js";
import type { ModerationAction, PageResult } from "../message-capture/types.js";
let _eventBroadcaster: EventBroadcaster | null = null;
/** Inject the gateway's event broadcaster so actions can be published live. */
export function setModerationEventBroadcaster(eb: EventBroadcaster): void {
_eventBroadcaster = eb;
}
// ─── ModerationActionsDb Class ──────────────────────────────────────────────
export class ModerationActionsDb {
@@ -38,7 +46,16 @@ export class ModerationActionsDb {
})
.returning();
return rows[0] as ModerationAction;
const created = rows[0] as ModerationAction;
// Fire-and-forget live broadcast (backend WS → frontend feed).
if (_eventBroadcaster) {
_eventBroadcaster
.moderationAction(created as unknown as Record<string, unknown>)
.catch(() => {});
}
return created;
} catch (error) {
this.logger.error(
{
@@ -30,6 +30,7 @@ export const DISCORD_CHANNEL_TOPIC_UPDATED = "discord:channel:topic_updated";
export const DISCORD_PRESENCE_UPDATED = "discord:presence:updated";
export const DISCORD_GUILD_MEMBER_ADDED = "discord:guild_member:added";
export const DISCORD_GUILD_MEMBER_REMOVED = "discord:guild_member:removed";
export const DISCORD_MODERATION_ACTION = "discord:moderation:action";
// ---------------------------------------------------------------------------
// Command channels (backend -> discord-gateway)