feat(dashboard): message activity timeline + moderation donut

Backend:
- GET /api/dashboard/activity?days=1..90 — daily buckets (messages,
  flagged, active_users) + hourly distribution last 24h
- clamped days param, reuses existing indexes (idx_messages_created,
  ai_status_created)

Frontend:
- ActivityChart: area chart messages+flagged per day, 7/14/30d range
- HourlyActivityChart: 24h bars with peak highlight
- ModerationDonut: clean/flagged/warned/error breakdown with live
  summary line (server X% clean)
- Dashboard layout: activity 2/3 + donut 1/3, hourly + top channels

Verified: endpoint returns real data from prod DB (784/1897/8 msgs
per day), tsc clean backend+frontend.
This commit is contained in:
asepharyana
2026-08-01 23:06:00 +07:00
parent 7d2bd75f6c
commit 78d514b73d
11 changed files with 514 additions and 7 deletions
@@ -1,4 +1,5 @@
import type {
DashboardActivity,
DashboardChannelDetail,
DashboardStats,
DashboardUserDetail,
@@ -10,6 +11,9 @@ import { api } from "./client";
export const dashboardApi = {
getStats: () => api.get<DashboardStats>("/api/dashboard/stats"),
getActivity: (days = 14) =>
api.get<DashboardActivity>(`/api/dashboard/activity?days=${days}`),
listUsers: (limit?: number, cursor?: string, search?: string) => {
const params = new URLSearchParams();
if (limit) params.set("limit", String(limit));
@@ -28,6 +28,25 @@ export interface ModerationOverview {
error: number;
}
export interface DashboardActivity {
days: number;
daily: DailyActivityPoint[];
hourly: HourlyActivityPoint[];
}
export interface DailyActivityPoint {
day: string; // YYYY-MM-DD
messages: number;
flagged: number;
active_users: number;
}
export interface HourlyActivityPoint {
hour: number; // 0-23
messages: number;
flagged: number;
}
export interface DashboardUser {
user_id: string;
username?: string | null;