From d0453881b8913b6e7d88c170c791152f20a0e54b Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 2 Sep 2026 00:12:52 +0700 Subject: [PATCH] fix(users): drop dead user_reputations JOIN that broke Members page + audit Root cause: gateway migration 0016 removed the per-user reputation feature (DROP TABLE user_reputations), but the backend still LEFT JOINed pgUserReputationsTable in dashboard listUsers/getUserDetail and the chatbot get_user_reputation tool. Against the live DB these queries raised 42P01 (undefined_table) -> the new /users page could never load (oRPC WS error). Fix: - dashboard.repository.ts: remove reputation columns/join from listUsers and getUserDetail (data sourced only from messages + user_profiles). - chatbot.tools.ts: get_user_reputation now returns honest 'unavailable' (feature removed) instead of querying the dropped table. - chatbot.toolDefs.ts: update tool description so the LLM doesn't advertise a dead feature. - frontend types/users view: drop trust_score/clean_message_streak/ total_infractions/last_infraction_at; derive risk label from real flagged% and add warn_count to the inspector (real available data). Verified: backend+frontend tsc clean, biome clean, 37 unit tests pass, query tested against live DB (returns real members), build green. --- .../src/modules/chatbot/chatbot.toolDefs.ts | 2 +- .../src/modules/chatbot/chatbot.tools.ts | 31 +++----- .../modules/dashboard/dashboard.repository.ts | 33 +------- .../src/app/(dashboard)/users/view.tsx | 78 ++++++++++--------- services/frontend/src/lib/types/dashboard.ts | 6 -- 5 files changed, 56 insertions(+), 94 deletions(-) diff --git a/services/backend/src/modules/chatbot/chatbot.toolDefs.ts b/services/backend/src/modules/chatbot/chatbot.toolDefs.ts index 880113d5..2c4eca6e 100644 --- a/services/backend/src/modules/chatbot/chatbot.toolDefs.ts +++ b/services/backend/src/modules/chatbot/chatbot.toolDefs.ts @@ -152,7 +152,7 @@ export const tools: ToolDef[] = [ function: { name: "get_user_reputation", description: - "Ambil skor trust, jumlah infraction, dan streak pesan bersih seorang user dari user_reputations. Untuk 'berapa trust score si A?' / riwayat pelanggaran. Butuh user_id.", + "Cek status skor reputasi per-user. CATATAN: fitur trust score per-user telah dihapus dari sistem — tool ini menjawab 'unavailable' dan menyarankan rasio flag vs total sebagai pengganti.", parameters: { type: "object", properties: { diff --git a/services/backend/src/modules/chatbot/chatbot.tools.ts b/services/backend/src/modules/chatbot/chatbot.tools.ts index 14d2d800..dfc31169 100644 --- a/services/backend/src/modules/chatbot/chatbot.tools.ts +++ b/services/backend/src/modules/chatbot/chatbot.tools.ts @@ -6,7 +6,6 @@ import { pgMessageReviewsTable, pgMessagesTable, pgUserProfilesTable, - pgUserReputationsTable, pgVoiceRecordingsTable, } from "../../shared/index.js"; @@ -282,26 +281,18 @@ async function userProfile(userId?: string, guildId?: string): Promise { } async function userReputation( - userId?: string, - guildId?: string, + _userId?: string, + _guildId?: string, ): Promise { - const db = getDatabase(); - if (!userId) return JSON.stringify({ error: "userId wajib" }); - const conds = [eq(pgUserReputationsTable.user_id, userId)]; - if (guildId) conds.push(eq(pgUserReputationsTable.guild_id, guildId)); - const rows = await db - .select({ - user_id: pgUserReputationsTable.user_id, - guild_id: pgUserReputationsTable.guild_id, - trust_score: pgUserReputationsTable.trust_score, - clean_message_streak: pgUserReputationsTable.clean_message_streak, - total_infractions: pgUserReputationsTable.total_infractions, - last_infraction_at: pgUserReputationsTable.last_infraction_at, - }) - .from(pgUserReputationsTable) - .where(and(...conds)) - .limit(1); - return JSON.stringify(rows[0] ?? { error: "reputasi tidak ditemukan" }); + // The per-user reputation feature (trust scores, streaks, infractions) was + // removed from the gateway (migration 0016 drops user_reputations). Return + // an honest "unavailable" answer from the derived moderation signals instead + // of querying the now-dropped table. + return JSON.stringify({ + available: false, + message: + "Skor trust/skala reputasi per-user telah dihapus dari sistem. Gunakan rasio pesan ter-flag vs total untuk menilai risiko (lihat dashboard Users).", + }); } async function channelCulture(channelId?: string): Promise { diff --git a/services/backend/src/modules/dashboard/dashboard.repository.ts b/services/backend/src/modules/dashboard/dashboard.repository.ts index 41907860..f443ec19 100644 --- a/services/backend/src/modules/dashboard/dashboard.repository.ts +++ b/services/backend/src/modules/dashboard/dashboard.repository.ts @@ -5,7 +5,6 @@ import { pgChannelCulturesTable, pgMessagesTable, pgUserProfilesTable, - pgUserReputationsTable, pgVoiceRecordingsTable, } from "../../shared/index.js"; import type { ListUsersQuery } from "./dashboard.service.js"; @@ -154,10 +153,6 @@ 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, @@ -177,7 +172,6 @@ export class DashboardRepository { 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} @@ -190,16 +184,6 @@ 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), @@ -453,11 +437,7 @@ export class DashboardRepository { m.clean_count, m.warn_count, p.profile_summary, - p.last_analyzed_at, - r.trust_score, - r.clean_message_streak, - r.total_infractions, - r.last_infraction_at + p.last_analyzed_at FROM ( SELECT user_id, @@ -472,7 +452,6 @@ export class DashboardRepository { 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 | undefined; @@ -500,16 +479,6 @@ export class DashboardRepository { 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[]).map((r) => ({ id: String(r.id), content: String(r.content), diff --git a/services/frontend/src/app/(dashboard)/users/view.tsx b/services/frontend/src/app/(dashboard)/users/view.tsx index 585d1e69..0169ff74 100644 --- a/services/frontend/src/app/(dashboard)/users/view.tsx +++ b/services/frontend/src/app/(dashboard)/users/view.tsx @@ -38,20 +38,18 @@ import type { } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; -function trustTone(score?: number | null) { - if (score == null) return "neutral"; - if (score >= 70) return "signal"; - if (score >= 45) return "amber"; - return "vermilion"; +function trustTone(flagPct: number) { + if (flagPct >= 20) return "vermilion"; + if (flagPct >= 8) return "amber"; + if (flagPct >= 3) return "signal"; + return "neutral"; } -function trustLabel(score?: number | null) { - if (score == null) return "UNKNOWN"; - if (score >= 80) return "TRUSTED"; - if (score >= 60) return "LOW RISK"; - if (score >= 45) return "MONITOR"; - if (score >= 30) return "WATCH"; - return "HIGH RISK"; +function riskLabel(flagPct: number) { + if (flagPct >= 20) return "HIGH RISK"; + if (flagPct >= 8) return "WATCH"; + if (flagPct >= 3) return "MONITOR"; + return "LOW RISK"; } export function UsersView({ initialUsers }: { initialUsers?: PaginatedUsers }) { @@ -191,7 +189,7 @@ function UserRow({ user.total_messages > 0 ? (user.flagged_count / user.total_messages) * 100 : 0; - const tone = trustTone(user.trust_score); + const tone = trustTone(flagPct); return (