feat: surface AI analysis duration across gateway, backend, and FE

Adds per-message AI moderation analysis time (ai_analysis_duration_ms)
so operators can see how long the LLM took to moderate each message.

Gateway:
- messagesTable: new ai_analysis_duration_ms (bigint) column.
- AIAnalysisUpdate + buildAIAnalysisSet: carry analysisDurationMs through
  both single and bulk update paths.
- ai-analysis-worker: measure wall-clock time around runModerationAnalysis
  and attach it to every result in the batch.

Backend:
- Mirror schema column; messageMapper maps ai_analysis_duration_ms;
  moderation-types + MappedMessage expose it.

Frontend:
- message.ts type gains ai_analysis_duration_ms.
- AiBadge (messages view) shows 'status · 1.2s' when duration is present;
  analysis view badge mirrors the same formatting.

DB:
- scripts/add-ai-analysis-duration.sql (idempotent ADD COLUMN IF NOT EXISTS).

No behavior change for moderation logic; null until new gateway build
records values.
This commit is contained in:
asepharyana
2026-08-16 00:11:00 +07:00
parent 2d7c7f2c35
commit 6244e307a3
10 changed files with 64 additions and 5 deletions
@@ -62,6 +62,9 @@ export const pgMessagesTable = pgTable(
enum: ["none", "monitor", "warn", "review", "delete", "escalate"],
}),
ai_analyzed_at: pgBigint("ai_analyzed_at", { mode: "number" }),
ai_analysis_duration_ms: pgBigint("ai_analysis_duration_ms", {
mode: "number",
}),
ai_error: pgText("ai_error"),
},
(table) => ({
@@ -80,6 +80,7 @@ export interface MessageRecord {
ai_confidence?: number | null;
ai_recommended_action?: AIRecommendedAction | null;
ai_analyzed_at?: number | null;
ai_analysis_duration_ms?: number | null;
ai_error?: string | null;
}
@@ -24,6 +24,7 @@ export interface MappedMessage {
ai_confidence: number | null;
ai_recommended_action: string | null;
ai_analyzed_at: number | null;
ai_analysis_duration_ms: number | null;
ai_error: string | null;
is_reply: boolean | null;
is_forward: boolean | null;
@@ -58,6 +59,8 @@ export function mapMessageRow(row: Record<string, unknown>): MappedMessage {
ai_confidence: (row.ai_confidence as number | null) ?? null,
ai_recommended_action: (row.ai_recommended_action as string | null) ?? null,
ai_analyzed_at: (row.ai_analyzed_at as number | null) ?? null,
ai_analysis_duration_ms:
(row.ai_analysis_duration_ms as number | null) ?? null,
ai_error: (row.ai_error as string | null) ?? null,
is_reply: row.is_reply === null ? null : Boolean(row.is_reply),
is_forward: row.is_forward === null ? null : Boolean(row.is_forward),