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 => {