From ead945734561af608d31d4c568125d496dfaa2aa Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 23:11:38 -0700 Subject: [PATCH] feat(platform+admin-web): implement 4 missing backend endpoints + re-enable frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend endpoints added: - POST /delivery/logs/:id/retry — re-dispatches failed email deliveries (Q1) - POST /reviews/:id/flag — flags review item with reason + admin metadata (Q2) - DELETE /agent-evals/suites/:id — deletes evaluation suite with 204 response (Q3) Frontend re-enabled: - delivery: retry button for failed entries - reviews: flag dropdown menu item - agent-evals: delete dropdown menu item + Trash2 icon Frontend fixed: - webhooks: per-subscription delivery loading via GET /subscriptions/:id/deliveries (Q4) --- .../src/app/(dashboard)/agent-evals/page.tsx | 21 +++++++----- .../src/app/(dashboard)/delivery/page.tsx | 19 +++++++---- .../src/app/(dashboard)/reviews/page.tsx | 14 ++++---- .../src/app/(dashboard)/webhooks/page.tsx | 29 ++++++++++++---- .../src/modules/agent-evals/repository.ts | 4 +++ .../src/modules/agent-evals/routes.ts | 8 +++++ .../src/modules/delivery/repository.ts | 4 +++ .../src/modules/delivery/routes.ts | 34 +++++++++++++++++++ .../src/modules/reviews/routes.ts | 21 ++++++++++++ 9 files changed, 126 insertions(+), 28 deletions(-) diff --git a/dashboards/admin-web/src/app/(dashboard)/agent-evals/page.tsx b/dashboards/admin-web/src/app/(dashboard)/agent-evals/page.tsx index de38c919..527b03c0 100644 --- a/dashboards/admin-web/src/app/(dashboard)/agent-evals/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/agent-evals/page.tsx @@ -8,6 +8,7 @@ import { Plus, MoreHorizontal, Play, + Trash2, CheckCircle2, XCircle, Clock, @@ -107,13 +108,11 @@ export default function AgentEvalsPage() { loadData(); } - // TODO Q3: Backend has no DELETE /agent-evals/suites/:id endpoint. - // When suite deletion is implemented, uncomment this. - // async function handleDelete(id: string) { - // if (!confirm('Delete this evaluation suite?')) return; - // await apiFetch(`suites/${id}`, { method: 'DELETE' }); - // loadData(); - // } + async function handleDelete(id: string) { + if (!confirm('Delete this evaluation suite?')) return; + await apiFetch(`suites/${id}`, { method: 'DELETE' }); + loadData(); + } const passedCount = suites.filter(s => s.lastRunStatus === 'passed').length; const failedCount = suites.filter(s => s.lastRunStatus === 'failed').length; @@ -245,7 +244,13 @@ export default function AgentEvalsPage() { Run - {/* TODO Q3: Delete disabled — no backend endpoint yet */} + handleDelete(s.id)} + className="text-red-600" + > + + Delete + diff --git a/dashboards/admin-web/src/app/(dashboard)/delivery/page.tsx b/dashboards/admin-web/src/app/(dashboard)/delivery/page.tsx index a2463658..48ba094e 100644 --- a/dashboards/admin-web/src/app/(dashboard)/delivery/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/delivery/page.tsx @@ -71,12 +71,10 @@ export default function DeliveryPage() { void loadData(); }, [loadData]); - // TODO Q1: Backend has no retry endpoint for delivery log entries. - // When /delivery/logs/:id/retry is implemented, uncomment this. - // async function handleRetry(id: string) { - // await apiFetch(`logs/${id}/retry`, { method: 'POST' }); - // loadData(); - // } + async function handleRetry(id: string) { + await apiFetch(`logs/${id}/retry`, { method: 'POST' }); + loadData(); + } const deliveredCount = entries.filter(e => e.status === 'delivered').length; const failedCount = entries.filter(e => e.status === 'failed').length; @@ -191,7 +189,14 @@ export default function DeliveryPage() { {formatDate(e.createdAt)} - {/* TODO Q1: Retry button disabled — backend has no retry endpoint yet */} + {e.status === 'failed' && ( + + )} ); diff --git a/dashboards/admin-web/src/app/(dashboard)/reviews/page.tsx b/dashboards/admin-web/src/app/(dashboard)/reviews/page.tsx index 28483646..536ca94b 100644 --- a/dashboards/admin-web/src/app/(dashboard)/reviews/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/reviews/page.tsx @@ -88,12 +88,10 @@ export default function ReviewsPage() { loadData(); } - // TODO Q2: Backend has no /reviews/:id/flag endpoint. - // When flagging is implemented, uncomment this. - // async function handleFlag(id: string) { - // await apiFetch(`${id}/flag`, { method: 'POST' }); - // loadData(); - // } + async function handleFlag(id: string) { + await apiFetch(`${id}/flag`, { method: 'POST' }); + loadData(); + } const pendingCount = reviews.filter(r => r.status === 'pending').length; const flaggedCount = reviews.filter(r => r.flagged).length; @@ -253,7 +251,9 @@ export default function ReviewsPage() { Reject )} - {/* TODO Q2: Flag button disabled — no backend endpoint yet */} + handleFlag(r.id)}> + Flag + diff --git a/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx b/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx index c87049f8..54c1df01 100644 --- a/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx @@ -74,12 +74,29 @@ export default function WebhooksPage() { const loadData = useCallback(async () => { setLoading(true); const sData = await apiFetch('subscriptions'); - setSubs( - Array.isArray(sData?.subscriptions) ? sData.subscriptions : Array.isArray(sData) ? sData : [] - ); - // TODO Q4: Backend has no top-level GET /webhooks/deliveries. Deliveries are per-subscription - // via GET /webhooks/subscriptions/:id/deliveries. Wire up per-subscription delivery loading. - setDeliveries([]); + const subsList: WebhookSub[] = Array.isArray(sData?.subscriptions) + ? sData.subscriptions + : Array.isArray(sData) + ? sData + : []; + setSubs(subsList); + + // Load deliveries per-subscription (backend only has GET /webhooks/subscriptions/:id/deliveries) + const allDeliveries: Delivery[] = []; + for (const sub of subsList.slice(0, 10)) { + try { + const dData = await apiFetch(`subscriptions/${sub.id}/deliveries`); + const items: Delivery[] = Array.isArray(dData?.deliveries) + ? dData.deliveries + : Array.isArray(dData) + ? dData + : []; + allDeliveries.push(...items); + } catch { + // best-effort + } + } + setDeliveries(allDeliveries.sort((a, b) => b.createdAt.localeCompare(a.createdAt))); setLoading(false); }, []); diff --git a/services/platform-service/src/modules/agent-evals/repository.ts b/services/platform-service/src/modules/agent-evals/repository.ts index e7767637..3b0fe605 100644 --- a/services/platform-service/src/modules/agent-evals/repository.ts +++ b/services/platform-service/src/modules/agent-evals/repository.ts @@ -62,6 +62,10 @@ export async function updateSuite( return updated; } +export async function deleteSuite(id: string, productId: string): Promise { + await suiteCollection().delete(id, productId); +} + export async function createCase(doc: EvaluationCaseDoc): Promise { return caseCollection().create(doc); } diff --git a/services/platform-service/src/modules/agent-evals/routes.ts b/services/platform-service/src/modules/agent-evals/routes.ts index 7d345a30..c672f267 100644 --- a/services/platform-service/src/modules/agent-evals/routes.ts +++ b/services/platform-service/src/modules/agent-evals/routes.ts @@ -88,6 +88,14 @@ export async function agentEvalRoutes(app: FastifyInstance) { return repo.updateSuite(id, access.productId, parsed.data); }); + app.delete('/agent-evals/suites/:id', async (req, reply) => { + const access = requireAdmin(req); + const { id } = req.params as { id: string }; + await repo.getSuite(id, access.productId); // throws NotFoundError if missing + await repo.deleteSuite(id, access.productId); + return reply.code(204).send(); + }); + app.get('/agent-evals/suites/:id/cases', async req => { requireAdmin(req); const { id } = req.params as { id: string }; diff --git a/services/platform-service/src/modules/delivery/repository.ts b/services/platform-service/src/modules/delivery/repository.ts index 4478e6b7..29edb590 100644 --- a/services/platform-service/src/modules/delivery/repository.ts +++ b/services/platform-service/src/modules/delivery/repository.ts @@ -14,6 +14,10 @@ export async function updateDeliveryLog(doc: DeliveryLogDoc): Promise { + return collection().findById(id, pk); +} + export async function listDeliveryLogs( productId: string, options?: { channel?: string; status?: string; limit?: number } diff --git a/services/platform-service/src/modules/delivery/routes.ts b/services/platform-service/src/modules/delivery/routes.ts index 0befe311..b897a434 100644 --- a/services/platform-service/src/modules/delivery/routes.ts +++ b/services/platform-service/src/modules/delivery/routes.ts @@ -121,6 +121,40 @@ export async function deliveryRoutes(app: FastifyInstance) { }); }); + // Retry a failed delivery + app.post('/delivery/logs/:id/retry', async req => { + await extractAuth(req); + const { id } = req.params as { id: string }; + const query = req.query as Record; + const pk = query.pk || ''; + + // Look up the original log entry + const original = await repo.getDeliveryLog(id, pk); + if (!original) throw new BadRequestError(`Delivery log '${id}' not found`); + if (original.status !== 'failed') { + throw new BadRequestError( + `Can only retry failed deliveries (current status: ${original.status})` + ); + } + + // Re-dispatch based on channel + if (original.channel === 'email' && original.templateId) { + const result = await dispatchEmail( + { + to: original.to, + templateId: original.templateId, + variables: (original.metadata as Record) ?? {}, + productId: original.productId, + userId: original.userId, + }, + req.log + ); + return { success: result.success, messageId: result.messageId, error: result.error }; + } + + throw new BadRequestError(`Retry not supported for channel '${original.channel}'`); + }); + // Get delivery stats app.get('/delivery/stats', async req => { await extractAuth(req); diff --git a/services/platform-service/src/modules/reviews/routes.ts b/services/platform-service/src/modules/reviews/routes.ts index 2373b3d0..4cc88d6b 100644 --- a/services/platform-service/src/modules/reviews/routes.ts +++ b/services/platform-service/src/modules/reviews/routes.ts @@ -167,6 +167,27 @@ export async function reviewRoutes(app: FastifyInstance) { return { succeeded, failed, total: body.ids.length }; }); + // ── Flag ──────────────────────────────────────────────── + + app.post('/reviews/:id/flag', async req => { + const access = requireAdmin(req); + const { id } = req.params as { id: string }; + const body = req.body as { reason?: string }; + + const review = await repo.getById(id, access.productId); + const updated = await repo.update(id, access.productId, { + metadata: { + ...(review.metadata ?? {}), + flagged: true, + flaggedBy: access.userId, + flagReason: body.reason ?? 'Flagged by admin', + flaggedAt: new Date().toISOString(), + }, + }); + + return updated; + }); + // ── Delegation ────────────────────────────────────────── app.post('/reviews/:id/delegate', async req => {