From 4e123941682ace8b6235df1585ed978cc4965d71 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 3 Jun 2026 19:50:26 +0700 Subject: [PATCH] fix: add error handling to analytics getTopics query and fix mascot tooltip positioning --- .../modules/analytics/analytics.repository.ts | 101 +++++++++--------- services/frontend/src/widgets/Sidebar.tsx | 8 +- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/services/backend/src/modules/analytics/analytics.repository.ts b/services/backend/src/modules/analytics/analytics.repository.ts index f5a224e..caf90b4 100644 --- a/services/backend/src/modules/analytics/analytics.repository.ts +++ b/services/backend/src/modules/analytics/analytics.repository.ts @@ -315,56 +315,61 @@ export class AnalyticsRepository { const pool = getPool(); const filter = buildTimeFilter(guildId, channelId, hours); - const { rows } = await pool.query( - ` - WITH word_list AS ( + try { + const { rows } = await pool.query( + ` + WITH word_list AS ( + SELECT + LOWER(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) AS word, + ai_moderation_score + FROM messages, + LATERAL regexp_split_to_table(content, E'\\\\s+') AS word + ${filter.where} + AND content IS NOT NULL + AND content != '' + AND LENGTH(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) >= 3 + AND word !~ '^<.+:\\d+>$' + AND word !~ '^https?://' + AND word !~ '^discord\\.(gg|app|com)' + AND word !~ '^cdn\\.discord' + ) SELECT - LOWER(TRIM(BOTH '.,!?;:\"'()[]{}' FROM word)) AS word, - ai_moderation_score - FROM messages, - LATERAL regexp_split_to_table(content, E'\\\\s+') AS word - ${filter.where} - AND content IS NOT NULL - AND content != '' - AND LENGTH(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) >= 3 - -- Skip sticker & custom emoji (<:name:id> or ) - AND word !~ '^$' - -- Skip URLs - AND word !~ '^https?://' - AND word !~ '^discord\\.(gg|app|com)' - -- Skip common Discord embed artifacts - AND word !~ '^cdn\\.discord' - ) - SELECT - word AS topic, - COUNT(*)::int AS count, - COALESCE(AVG(ai_moderation_score), 0)::real AS score - FROM word_list - WHERE word NOT IN ( - 'yang', 'dan', 'di', 'ke', 'dari', 'dengan', 'untuk', 'pada', 'ini', 'itu', - 'ada', 'akan', 'telah', 'sudah', 'bisa', 'dapat', 'tidak', 'nggak', 'enggak', - 'gak', 'gk', 'ga', 'aku', 'saya', 'kamu', 'dia', 'kami', 'kita', 'mereka', - 'iya', 'ya', 'yah', 'oh', 'ah', 'eh', 'lah', 'pun', 'juga', 'masih', - 'saja', 'hanya', 'sama', 'atau', 'tapi', 'namun', 'sedang', 'sangat', - 'begitu', 'karena', 'sebab', 'kalau', 'jika', 'maka', 'lalu', 'setelah', - 'seperti', 'antara', 'oleh', 'sebagai', 'secara', 'melalui', 'dalam', - 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'has', - 'was', 'were', 'been', 'like', 'just', 'that', 'this', 'with', 'your', - 'from', 'they', 'have', 'what', 'when', 'where', 'which', 'their', - 'about', 'would', 'could', 'should', 'very', 'also', 'than', 'then' - ) - GROUP BY word - ORDER BY count DESC - LIMIT 10 - `, - filter.params, - ); + word AS topic, + COUNT(*)::int AS count, + COALESCE(AVG(ai_moderation_score), 0)::real AS score + FROM word_list + WHERE word NOT IN ( + 'yang','dan','di','ke','dari','dengan','untuk','pada','ini','itu', + 'ada','akan','telah','sudah','bisa','dapat','tidak','nggak','enggak', + 'gak','gk','ga','aku','saya','kamu','dia','kami','kita','mereka', + 'iya','ya','yah','oh','ah','eh','lah','pun','juga','masih', + 'saja','hanya','sama','atau','tapi','namun','sedang','sangat', + 'begitu','karena','sebab','kalau','jika','maka','lalu','setelah', + 'seperti','antara','oleh','sebagai','secara','melalui','dalam', + 'the','and','for','are','but','not','you','all','can','has', + 'was','were','been','like','just','that','this','with','your', + 'from','they','have','what','when','where','which','their', + 'about','would','could','should','very','also','than','then' + ) + GROUP BY word + ORDER BY count DESC + LIMIT 10 + `, + filter.params, + ); - return rows.map((r) => ({ - topic: (r.topic as string) ?? "", - count: Number(r.count ?? 0), - score: Number(r.score ?? 0), - })); + return rows.map((r) => ({ + topic: (r.topic as string) ?? "", + count: Number(r.count ?? 0), + score: Number(r.score ?? 0), + })); + } catch (error) { + logger.error( + { error: error instanceof Error ? error.message : String(error), guildId }, + "getTopics query failed — returning empty", + ); + return []; + } } // ── New endpoints ───────────────────────────────────────────────────────── diff --git a/services/frontend/src/widgets/Sidebar.tsx b/services/frontend/src/widgets/Sidebar.tsx index 330a9f6..a62424f 100644 --- a/services/frontend/src/widgets/Sidebar.tsx +++ b/services/frontend/src/widgets/Sidebar.tsx @@ -116,7 +116,7 @@ export function Sidebar({ initial={{ opacity: 0, x: -8, y: 6, scale: 0.96 }} animate={{ opacity: 1, x: 0, y: 0, scale: 1 }} transition={{ type: "spring", stiffness: 280, damping: 24 }} - className="pointer-events-none absolute bottom-full left-1/2 z-[9999] mb-3 w-64 -translate-x-1/2" + className="pointer-events-none fixed bottom-[100px] left-[72px] z-[9999] w-64" >

@@ -126,10 +126,10 @@ export function Sidebar({ Halo! Saya mascot mu. Klik aku kalau mau tanya soal obrolan atau moderasi ✨

- {/* Outer: border layer — triangle pointing down at center */} -
+ {/* Outer: border layer — triangle pointing left at center-right */} +
{/* Inner: fill layer — matches bubble bg */} -
+
)}