feat: add "Retry All Errors" batch reanalyze button
Backend: - POST /api/messages/reanalyze-batch — bulk reset error messages to pending - messages.repository.reanalyzeErrorBatch() — scoped by guildId/channelId/messageIds - messages.service.reanalyzeErrorBatch() — validation layer Frontend: - reanalyzeErrorBatch() API client function - useMessages.reanalyzeAllErrors() — optimistic state + batch call - MessagesPanel: destructive button visible when error count > 0 - Shows confirmation text with queued count Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d81aaf2b4a
commit
6da8a32c9b
@@ -216,6 +216,50 @@ export class MessagesRepository {
|
||||
return mapMessageRow(rows[0] as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<number> {
|
||||
const pool = getPool();
|
||||
const clauses: string[] = ["ai_status = 'error'"];
|
||||
const params: (string | number)[] = [];
|
||||
let p = 1;
|
||||
|
||||
if (opts.messageIds && opts.messageIds.length > 0) {
|
||||
const placeholders = opts.messageIds.map((_, i) => `$${p + i}`);
|
||||
clauses.push(`id IN (${placeholders.join(", ")})`);
|
||||
params.push(...opts.messageIds);
|
||||
p += opts.messageIds.length;
|
||||
}
|
||||
if (opts.guildId) {
|
||||
clauses.push(`guild_id = $${p}`);
|
||||
params.push(opts.guildId);
|
||||
p++;
|
||||
}
|
||||
if (opts.channelId) {
|
||||
clauses.push(`channel_id = $${p}`);
|
||||
params.push(opts.channelId);
|
||||
p++;
|
||||
}
|
||||
|
||||
const where = clauses.join(" AND ");
|
||||
const { rowCount } = await pool.query(
|
||||
`UPDATE messages SET ai_status = 'pending' WHERE ${where}`,
|
||||
params,
|
||||
);
|
||||
|
||||
logger.info({ count: rowCount ?? 0, ...opts }, "Batch reanalyze triggered");
|
||||
return rowCount ?? 0;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const pool = getPool();
|
||||
const { rowCount } = await pool.query(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
handleGetMessagesByChannel,
|
||||
handleListMessages,
|
||||
} from "./messages.controller.js";
|
||||
import { messagesService } from "./messages.service.js";
|
||||
|
||||
const logger = createChildLogger("messages.routes");
|
||||
|
||||
@@ -28,7 +29,30 @@ export function createMessagesRouter(): Router {
|
||||
// (uses /detail/ prefix to avoid collision with :channelId route above)
|
||||
router.get("/messages/detail/:id", handleGetMessageById);
|
||||
|
||||
// POST /api/messages/:id/reanalyze - Mark message for re-analysis
|
||||
// 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",
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { guildId, channelId, messageIds } = (req.body ?? {}) as {
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
messageIds?: string[];
|
||||
};
|
||||
|
||||
const count = await messagesService.reanalyzeErrorBatch({
|
||||
guildId,
|
||||
channelId,
|
||||
messageIds,
|
||||
});
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -45,6 +45,21 @@ export class MessagesService {
|
||||
logger.debug({ channelId, query }, "Getting attachments by channel");
|
||||
return messagesRepository.getAttachmentsByChannel(channelId, query);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user