feat(fe): add Member Intelligence (/users) page wired to full reputation data
- New /users route: member roster (trust score, clean/flagged counts, message volume) + inspector detail (trust breakdown, clean streak, infractions, AI profile, recent messages) with live search - Backend dashboard.listUsers/getUserDetail now JOIN user_reputations to expose trust_score, clean_message_streak, total_infractions, last_infraction_at + warn_count/clean_count breakdowns - Frontend types updated to match; useUsers refactored to PaginatedUsers shape, added useUserSearch for the old search behavior - Nav rail + mobile nav: add Users entry
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
pgChannelCulturesTable,
|
||||
pgMessagesTable,
|
||||
pgUserProfilesTable,
|
||||
pgUserReputationsTable,
|
||||
pgVoiceRecordingsTable,
|
||||
} from "../../shared/index.js";
|
||||
import type { ListUsersQuery } from "./dashboard.service.js";
|
||||
@@ -153,8 +154,14 @@ export class DashboardRepository {
|
||||
m.username,
|
||||
m.avatar_url,
|
||||
p.profile_summary,
|
||||
r.trust_score,
|
||||
r.clean_message_streak,
|
||||
r.total_infractions,
|
||||
r.last_infraction_at,
|
||||
m.total_messages,
|
||||
m.flagged_count,
|
||||
m.clean_count,
|
||||
m.warn_count,
|
||||
m.last_message_at
|
||||
FROM (
|
||||
SELECT
|
||||
@@ -163,11 +170,14 @@ export class DashboardRepository {
|
||||
avatar_url,
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warn_count,
|
||||
MAX(created_at) AS last_message_at
|
||||
FROM ${pgMessagesTable}
|
||||
GROUP BY user_id, username, avatar_url
|
||||
) m
|
||||
LEFT JOIN ${pgUserProfilesTable} p ON p.user_id = m.user_id
|
||||
LEFT JOIN ${pgUserReputationsTable} r ON r.user_id = m.user_id
|
||||
${whereClause}
|
||||
ORDER BY m.last_message_at DESC NULLS LAST
|
||||
LIMIT ${limit + 1}
|
||||
@@ -180,8 +190,20 @@ export class DashboardRepository {
|
||||
username: r.username as string | null,
|
||||
avatar_url: r.avatar_url as string | null,
|
||||
profile_summary: r.profile_summary as string | null,
|
||||
trust_score: r.trust_score != null ? Number(r.trust_score) : null,
|
||||
clean_message_streak:
|
||||
r.clean_message_streak != null
|
||||
? Number(r.clean_message_streak)
|
||||
: null,
|
||||
total_infractions:
|
||||
r.total_infractions != null ? Number(r.total_infractions) : null,
|
||||
last_infraction_at: r.last_infraction_at
|
||||
? Number(r.last_infraction_at)
|
||||
: null,
|
||||
total_messages: Number(r.total_messages),
|
||||
flagged_count: Number(r.flagged_count),
|
||||
clean_count: Number(r.clean_count),
|
||||
warn_count: Number(r.warn_count),
|
||||
last_message_at: r.last_message_at ? Number(r.last_message_at) : null,
|
||||
}));
|
||||
|
||||
@@ -429,8 +451,13 @@ export class DashboardRepository {
|
||||
m.total_messages,
|
||||
m.flagged_count,
|
||||
m.clean_count,
|
||||
m.warn_count,
|
||||
p.profile_summary,
|
||||
p.last_analyzed_at
|
||||
p.last_analyzed_at,
|
||||
r.trust_score,
|
||||
r.clean_message_streak,
|
||||
r.total_infractions,
|
||||
r.last_infraction_at
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
@@ -438,12 +465,14 @@ export class DashboardRepository {
|
||||
avatar_url,
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warn_count
|
||||
FROM ${pgMessagesTable}
|
||||
WHERE user_id = ${userId}
|
||||
GROUP BY user_id, username, avatar_url
|
||||
) m
|
||||
LEFT JOIN ${pgUserProfilesTable} p ON p.user_id = m.user_id
|
||||
LEFT JOIN ${pgUserReputationsTable} r ON r.user_id = m.user_id
|
||||
`);
|
||||
|
||||
const row = userResult.rows[0] as Record<string, unknown> | undefined;
|
||||
@@ -466,10 +495,21 @@ export class DashboardRepository {
|
||||
total_messages: Number(row.total_messages),
|
||||
flagged_count: Number(row.flagged_count),
|
||||
clean_count: Number(row.clean_count),
|
||||
warn_count: Number(row.warn_count),
|
||||
profile_summary: row.profile_summary as string | null,
|
||||
last_analyzed_at: row.last_analyzed_at
|
||||
? Number(row.last_analyzed_at)
|
||||
: null,
|
||||
trust_score: row.trust_score != null ? Number(row.trust_score) : null,
|
||||
clean_message_streak:
|
||||
row.clean_message_streak != null
|
||||
? Number(row.clean_message_streak)
|
||||
: null,
|
||||
total_infractions:
|
||||
row.total_infractions != null ? Number(row.total_infractions) : null,
|
||||
last_infraction_at: row.last_infraction_at
|
||||
? Number(row.last_infraction_at)
|
||||
: null,
|
||||
recent_messages: (recent.rows as Record<string, unknown>[]).map((r) => ({
|
||||
id: String(r.id),
|
||||
content: String(r.content),
|
||||
|
||||
Reference in New Issue
Block a user