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
@@ -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(
{