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
@@ -459,6 +459,31 @@ export class MessagesRepository {
return { data: trimmed, nextCursor };
}
/**
* Per-hour message volume for the last `days` days, grouped by channel.
* Powers the public Activity Heatmap (read-only, no write scope).
* Returns a flat list of { channel_id, hour (0-23), count } buckets.
*/
async getActivity(days = 30) {
const db = getDatabase();
const since = Date.now() - days * 24 * 60 * 60 * 1000;
const result = await db.execute(sql`
SELECT channel_id,
EXTRACT(HOUR FROM to_timestamp(created_at / 1000))::int AS hour,
COUNT(*)::int AS c
FROM messages
WHERE created_at >= ${since}
GROUP BY channel_id, hour
ORDER BY channel_id, hour
`);
const rows = (result.rows as Record<string, unknown>[]) || [];
return rows.map((r) => ({
channelId: String(r.channel_id ?? "unknown"),
hour: Number(r.hour ?? 0),
count: Number(r.c ?? 0),
}));
}
}
export const messagesRepository = new MessagesRepository();