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:
@@ -82,6 +82,52 @@ export class DashboardRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async getActivity(days: number) {
|
||||
const db = getDatabase();
|
||||
const sinceMs = Date.now() - days * 86400000;
|
||||
const dayAgoMs = Date.now() - 86400000;
|
||||
|
||||
// Daily buckets (last N days)
|
||||
const daily = await db.execute(sql`
|
||||
SELECT
|
||||
to_char(to_timestamp(created_at / 1000), 'YYYY-MM-DD') AS day,
|
||||
COUNT(*)::int AS messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||
COUNT(DISTINCT user_id)::int AS active_users
|
||||
FROM ${pgMessagesTable}
|
||||
WHERE created_at >= ${sinceMs}
|
||||
GROUP BY day
|
||||
ORDER BY day
|
||||
`);
|
||||
|
||||
// Hourly distribution (last 24h)
|
||||
const hourly = await db.execute(sql`
|
||||
SELECT
|
||||
EXTRACT(HOUR FROM to_timestamp(created_at / 1000))::int AS hour,
|
||||
COUNT(*)::int AS messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged
|
||||
FROM ${pgMessagesTable}
|
||||
WHERE created_at >= ${dayAgoMs}
|
||||
GROUP BY hour
|
||||
ORDER BY hour
|
||||
`);
|
||||
|
||||
return {
|
||||
days,
|
||||
daily: (daily.rows as Record<string, unknown>[]).map((r) => ({
|
||||
day: String(r.day),
|
||||
messages: Number(r.messages),
|
||||
flagged: Number(r.flagged),
|
||||
active_users: Number(r.active_users),
|
||||
})),
|
||||
hourly: (hourly.rows as Record<string, unknown>[]).map((r) => ({
|
||||
hour: Number(r.hour),
|
||||
messages: Number(r.messages),
|
||||
flagged: Number(r.flagged),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async listUsers(query: ListUsersQuery) {
|
||||
const db = getDatabase();
|
||||
const limit = query.limit ?? 20;
|
||||
|
||||
Reference in New Issue
Block a user