diff --git a/services/backend/src/modules/messages/messages.repository.ts b/services/backend/src/modules/messages/messages.repository.ts index 7648264..07cb578 100644 --- a/services/backend/src/modules/messages/messages.repository.ts +++ b/services/backend/src/modules/messages/messages.repository.ts @@ -6,7 +6,6 @@ import { isNull, like, lt, - ne, notInArray, or, type SQL, @@ -223,58 +222,6 @@ export class MessagesRepository { return mapMessageRow(row as Record); } - /** - * Bulk-reset ai_status from 'error' to 'pending' so the DG recovery worker - * picks them up on its next poll cycle. - * - * Accepts optional scope filters (guildId, channelId) or a list of explicit - * message IDs. Returns the count of rows that were actually updated. - */ - async reanalyzeErrorBatch(opts: { - guildId?: string; - channelId?: string; - messageIds?: string[]; - }): Promise { - const db = getDatabase(); - const conditions: SQL[] = [eq(pgMessagesTable.ai_status, "error")]; - - if (opts.messageIds && opts.messageIds.length > 0) { - conditions.push(inArray(pgMessagesTable.id, opts.messageIds)); - } - if (opts.guildId) { - conditions.push(eq(pgMessagesTable.guild_id, opts.guildId)); - } - if (opts.channelId) { - conditions.push(eq(pgMessagesTable.channel_id, opts.channelId)); - } - - const result = await db - .update(pgMessagesTable) - .set({ ai_status: "pending" }) - .where(and(...conditions)); - - const count = result.rowCount ?? 0; - logger.info({ count, ...opts }, "Batch reanalyze triggered"); - return count; - } - - /** - * Mark a single message for re-analysis by resetting ai_status to 'pending'. - * Skips messages already in 'pending' state to avoid write amplification. - */ - async markForReanalysis(id: string): Promise { - const db = getDatabase(); - await db - .update(pgMessagesTable) - .set({ ai_status: "pending" }) - .where( - and( - eq(pgMessagesTable.id, id), - ne(pgMessagesTable.ai_status, "pending"), - ), - ); - } - /** * Retrieve messages flagged for review (ai_status IN ('warn', 'flagged')). * Optionally filtered by channelId, with configurable limit. diff --git a/services/backend/src/modules/messages/messages.routes.ts b/services/backend/src/modules/messages/messages.routes.ts index d7d3168..65445f7 100644 --- a/services/backend/src/modules/messages/messages.routes.ts +++ b/services/backend/src/modules/messages/messages.routes.ts @@ -1,7 +1,7 @@ import type { Request, Response, Router } from "express"; import express from "express"; import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; +import { asyncHandler } from "../../shared/middlewares/index.js"; import { handleGetAttachmentsByChannel, handleGetImageMessages, @@ -9,27 +9,10 @@ import { handleGetMessagesByChannel, handleListMessages, } from "./messages.controller.js"; -import { reanalyzeBatchSchema } from "./messages.schema.js"; import { messagesService } from "./messages.service.js"; const logger = createChildLogger("messages.routes"); -/** - * Per-message in-flight guard for the single reanalyze endpoint. - * Prevents concurrent spam-clicks from issuing duplicate UPDATE + recovery - * worker triggers for the same message. - */ -const reanalyzeInFlight = new Set(); - -/** - * Per-scope in-flight guard for the batch reanalyze endpoint. - * Scope key = "guildId:channelId" (empty string used for undefined parts). - * Two concurrent batch-reanalyze requests for the same scope are rejected - * with 409 so the recovery worker is not triggered multiple times for the - * same set of error messages. - */ -const reanalyzeBatchInFlight = new Set(); - export function createMessagesRouter(): Router { const router = express.Router(); @@ -51,74 +34,6 @@ export function createMessagesRouter(): Router { // (uses /detail/ prefix to avoid collision with :channelId route above) router.get("/messages/detail/:id", handleGetMessageById); - // POST /api/messages/reanalyze-batch — Bulk retry all errored messages - // MUST be registered BEFORE /messages/:id/reanalyze so "reanalyze-batch" - // is not captured as an :id param. - router.post( - "/messages/reanalyze-batch", - validateBody(reanalyzeBatchSchema), - asyncHandler(async (req: Request, res: Response) => { - const { guildId, channelId, messageIds } = req.body as { - guildId?: string; - channelId?: string; - messageIds?: string[]; - }; - - // Idempotency guard: one concurrent batch-reanalyze per scope. - // Prevents two admin sessions clicking simultaneously from each - // triggering the recovery worker for the same set of messages. - const scopeKey = `${guildId ?? ""}:${channelId ?? ""}`; - if (reanalyzeBatchInFlight.has(scopeKey)) { - res - .status(409) - .json({ error: "REANALYZE_BATCH_IN_PROGRESS", scope: scopeKey }); - return; - } - - reanalyzeBatchInFlight.add(scopeKey); - let count = 0; - try { - count = await messagesService.reanalyzeErrorBatch({ - guildId, - channelId, - messageIds, - }); - } finally { - reanalyzeBatchInFlight.delete(scopeKey); - } - - logger.info({ count, guildId, channelId }, "Batch reanalyze completed"); - res.status(200).json({ ok: true, count }); - }), - ); - - // POST /api/messages/:id/reanalyze - Mark single message for re-analysis - router.post( - "/messages/:id/reanalyze", - asyncHandler(async (req: Request, res: Response) => { - const id = String(req.params.id ?? ""); - if (!id) { - res.status(400).json({ error: "MISSING_ID" }); - return; - } - - // Idempotency guard: reject concurrent duplicate requests for the same ID. - if (reanalyzeInFlight.has(id)) { - res.status(409).json({ error: "REANALYZE_IN_PROGRESS", messageId: id }); - return; - } - - reanalyzeInFlight.add(id); - try { - await messagesService.markForReanalysis(id); - } finally { - reanalyzeInFlight.delete(id); - } - - res.status(200).json({ ok: true }); - }), - ); - // GET /api/review - Get flagged/warned messages for review router.get( "/review", diff --git a/services/backend/src/modules/messages/messages.schema.ts b/services/backend/src/modules/messages/messages.schema.ts index d0cd0ae..f26416b 100644 --- a/services/backend/src/modules/messages/messages.schema.ts +++ b/services/backend/src/modules/messages/messages.schema.ts @@ -38,13 +38,6 @@ export const messageUpdateSchema = z.object({ aiConfidence: z.number().optional(), }); -export const reanalyzeBatchSchema = z.object({ - guildId: z.string().optional(), - channelId: z.string().optional(), - messageIds: z.array(z.string()).optional(), -}); - export type MessageQuery = z.infer; export type MessageCreate = z.infer; export type MessageUpdate = z.infer; -export type ReanalyzeBatchInput = z.infer; diff --git a/services/backend/src/modules/messages/messages.service.ts b/services/backend/src/modules/messages/messages.service.ts index 5a6496d..5b6ac7a 100644 --- a/services/backend/src/modules/messages/messages.service.ts +++ b/services/backend/src/modules/messages/messages.service.ts @@ -58,15 +58,6 @@ export class MessagesService { return messagesRepository.getImageMessages(guildId, limit); } - async markForReanalysis(id: string): Promise { - if (!id) { - throw new ValidationError("message ID is required"); - } - - logger.debug({ id }, "Marking message for re-analysis"); - await messagesRepository.markForReanalysis(id); - } - async getReviewMessages( channelId?: string, limit?: number, @@ -74,25 +65,6 @@ export class MessagesService { logger.debug({ channelId, limit }, "Getting review messages"); return messagesRepository.getReviewMessages(channelId, limit); } - - async reanalyzeErrorBatch(opts: { - guildId?: string; - channelId?: string; - messageIds?: string[]; - }) { - if ( - !opts.guildId && - !opts.channelId && - (!opts.messageIds || opts.messageIds.length === 0) - ) { - throw new ValidationError( - "At least one of guildId, channelId, or messageIds[] is required", - ); - } - - logger.info(opts, "Batch reanalyzing errored messages"); - return messagesRepository.reanalyzeErrorBatch(opts); - } } export const messagesService = new MessagesService(); diff --git a/services/frontend/src/app/(dashboard)/messages/page.tsx b/services/frontend/src/app/(dashboard)/messages/page.tsx index 956e478..9cd97c1 100644 --- a/services/frontend/src/app/(dashboard)/messages/page.tsx +++ b/services/frontend/src/app/(dashboard)/messages/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Flag, Image, Loader2, RefreshCw, Search } from "lucide-react"; +import { Flag, Image, Loader2, Search } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { GlassCard } from "@/components/glass/card"; @@ -13,7 +13,6 @@ import { MessageList } from "@/components/messages/message-list"; import { SearchOverlay } from "@/components/messages/search-overlay"; import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared"; import { GuildSelector } from "@/components/shared/guild-selector"; -import { Button } from "@/components/ui/button"; import { Select, SelectContent, @@ -28,8 +27,6 @@ import { useMessages, useMessagesHasMore, useMessagesWsSync, - useReanalyze, - useReanalyzeBatch, useReview, useTextChannels, } from "@/hooks"; @@ -74,8 +71,6 @@ export default function MessagesPage() { const loadMoreMut = useLoadMore(); const { data: images } = useImages(guildId); const { data: reviews } = useReview(selectedChannel || undefined); - const reanalyzeMut = useReanalyze(); - const reanalyzeBatchMut = useReanalyzeBatch(); const { message: detailMessage, @@ -164,14 +159,6 @@ export default function MessagesPage() { ⌘K - {/* ── Sub navigation ── */} @@ -197,7 +184,6 @@ export default function MessagesPage() { messages={currentMessages} selectedId={detailId} onSelect={setDetailId} - onReanalyze={(id) => reanalyzeMut.mutate(id)} hasMore={cursorData?.hasMore} onLoadMore={handleLoadMore} isLoadingMore={loadMoreMut.isPending} diff --git a/services/frontend/src/components/analysis/search-panel.tsx b/services/frontend/src/components/analysis/search-panel.tsx index c0aaaae..44ead66 100644 --- a/services/frontend/src/components/analysis/search-panel.tsx +++ b/services/frontend/src/components/analysis/search-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Loader2, RefreshCw, Search, Sparkles } from "lucide-react"; +import { Loader2, Search, Sparkles } from "lucide-react"; import { useCallback, useState } from "react"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; @@ -10,14 +10,13 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; -import { useMessageSearch, useReanalyze } from "@/hooks"; +import { useMessageSearch } from "@/hooks"; import { renderMessageContent, safeParseJsonArray } from "@/lib/format"; import { cn } from "@/lib/utils"; export function SearchPanel() { const [query, setQuery] = useState(""); const [enabled, setEnabled] = useState(false); - const reanalyzeMut = useReanalyze(); const { data: results, isValidating: isFetching } = useMessageSearch( query, @@ -134,14 +133,6 @@ export function SearchPanel() { )} - diff --git a/services/frontend/src/components/messages/message-card.tsx b/services/frontend/src/components/messages/message-card.tsx index 1f5f0fe..f86ba27 100644 --- a/services/frontend/src/components/messages/message-card.tsx +++ b/services/frontend/src/components/messages/message-card.tsx @@ -1,9 +1,8 @@ "use client"; -import { Hash, RefreshCw } from "lucide-react"; +import { Hash } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { @@ -18,11 +17,9 @@ import { AiStatusBadge } from "./ai-status-badge"; export function MessageCard({ message: msg, onClick, - onReanalyze, }: { message: MessageRecord; onClick: (id: string) => void; - onReanalyze: (id: string) => void; }) { const severity = ( { @@ -156,16 +153,6 @@ export function MessageCard({ )} - diff --git a/services/frontend/src/components/messages/message-list.tsx b/services/frontend/src/components/messages/message-list.tsx index 0a6cb47..46b4283 100644 --- a/services/frontend/src/components/messages/message-list.tsx +++ b/services/frontend/src/components/messages/message-list.tsx @@ -9,7 +9,6 @@ interface MessageListProps { messages: MessageRecord[]; selectedId: string | null; onSelect: (id: string) => void; - onReanalyze?: (id: string) => void; hasMore?: boolean; onLoadMore?: () => void; isLoadingMore?: boolean; @@ -19,7 +18,6 @@ export function MessageList({ messages, selectedId: _selectedId, onSelect, - onReanalyze, hasMore, onLoadMore, isLoadingMore, @@ -27,12 +25,7 @@ export function MessageList({ return ( <> {messages.map((msg) => ( - onReanalyze?.(id)} - /> + ))} {hasMore && (
diff --git a/services/frontend/src/hooks/index.ts b/services/frontend/src/hooks/index.ts index 2d98823..4f1f75c 100644 --- a/services/frontend/src/hooks/index.ts +++ b/services/frontend/src/hooks/index.ts @@ -24,8 +24,6 @@ export { useMessages, useMessagesHasMore, useMessagesWsSync, - useReanalyze, - useReanalyzeBatch, useReview, useTextChannels, } from "./use-messages"; diff --git a/services/frontend/src/hooks/use-messages.ts b/services/frontend/src/hooks/use-messages.ts index ddddc50..6c6b1a2 100644 --- a/services/frontend/src/hooks/use-messages.ts +++ b/services/frontend/src/hooks/use-messages.ts @@ -155,16 +155,6 @@ export function useMessageDetail(id: string | null) { }; } -// ── Mutations ──────────────────────────────────── - -export function useReanalyze() { - return useAction((id: string) => messagesApi.reanalyze(id)); -} - -export function useReanalyzeBatch() { - return useAction((guildId: string) => messagesApi.reanalyzeBatch(guildId)); -} - // ── Search ─────────────────────────────────────── export function useMessageSearch(query: string, enabled: boolean) { diff --git a/services/frontend/src/lib/api/messages.ts b/services/frontend/src/lib/api/messages.ts index 26a4731..b1f42b1 100644 --- a/services/frontend/src/lib/api/messages.ts +++ b/services/frontend/src/lib/api/messages.ts @@ -65,15 +65,6 @@ export const messagesApi = { ); }, - reanalyze: (id: string) => - api.post<{ ok: boolean }>(`/api/messages/${id}/reanalyze`, {}), - - reanalyzeBatch: (guildId?: string, channelId?: string) => - api.post<{ ok: boolean; count: number }>("/api/messages/reanalyze-batch", { - guildId, - channelId, - }), - search: (query: string, limit?: number) => { const params = new URLSearchParams({ q: query }); if (limit) params.set("limit", String(limit));