From 8c45e440df33981a1eb893e11dedec37060fd387 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 22:56:22 -0700 Subject: [PATCH] feat(admin-web): add experiments + ab-testing proxy routes, fix webhooks deliveries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New proxy routes: - /api/experiments → rewrites to /api/ab-testing/experiments (base + catch-all) - /api/ab-testing/[...path] → /api/ab-testing/* (for suggestions, hypotheses) Bug fix: - B20: webhooks page called GET /webhooks/deliveries (404) — removed broken call, backend only has GET /webhooks/subscriptions/:id/deliveries (TODO Q4) --- .../src/app/(dashboard)/webhooks/page.tsx | 8 ++-- .../src/app/api/ab-testing/[...path]/route.ts | 48 +++++++++++++++++++ .../app/api/experiments/[...path]/route.ts | 48 +++++++++++++++++++ .../src/app/api/experiments/route.ts | 39 +++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 dashboards/admin-web/src/app/api/ab-testing/[...path]/route.ts create mode 100644 dashboards/admin-web/src/app/api/experiments/[...path]/route.ts create mode 100644 dashboards/admin-web/src/app/api/experiments/route.ts diff --git a/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx b/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx index bc3774c6..c87049f8 100644 --- a/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/webhooks/page.tsx @@ -73,13 +73,13 @@ export default function WebhooksPage() { const loadData = useCallback(async () => { setLoading(true); - const [sData, dData] = await Promise.all([apiFetch('subscriptions'), apiFetch('deliveries')]); + const sData = await apiFetch('subscriptions'); setSubs( Array.isArray(sData?.subscriptions) ? sData.subscriptions : Array.isArray(sData) ? sData : [] ); - setDeliveries( - Array.isArray(dData?.deliveries) ? dData.deliveries : Array.isArray(dData) ? dData : [] - ); + // 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([]); setLoading(false); }, []); diff --git a/dashboards/admin-web/src/app/api/ab-testing/[...path]/route.ts b/dashboards/admin-web/src/app/api/ab-testing/[...path]/route.ts new file mode 100644 index 00000000..3bc4df89 --- /dev/null +++ b/dashboards/admin-web/src/app/api/ab-testing/[...path]/route.ts @@ -0,0 +1,48 @@ +/** + * A/B Testing API proxy — forwards requests to platform-service. + * + * /api/ab-testing/* → platform-service /api/ab-testing/* + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUserFromRequest } from '@/lib/auth-server'; +import { logError } from '@/lib/logger'; + +const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'; + +async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + try { + const caller = await getCurrentUserFromRequest(req); + if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { path } = await params; + const targetPath = `/api/ab-testing/${path.join('/')}`; + const qs = new URL(req.url).search; + const headers: Record = { + 'Content-Type': 'application/json', + 'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(), + 'x-user-id': caller.id, + 'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai', + }; + const fetchOptions: RequestInit = { method: req.method, headers }; + if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text(); + const res = await fetch(`${PLATFORM_URL}${targetPath}${qs}`, fetchOptions); + const data = await res.json().catch(() => null); + return NextResponse.json(data ?? { error: res.statusText }, { status: res.status }); + } catch (error) { + logError('A/B Testing proxy error', error); + return NextResponse.json({ error: 'Service unavailable' }, { status: 502 }); + } +} + +export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} diff --git a/dashboards/admin-web/src/app/api/experiments/[...path]/route.ts b/dashboards/admin-web/src/app/api/experiments/[...path]/route.ts new file mode 100644 index 00000000..336f956e --- /dev/null +++ b/dashboards/admin-web/src/app/api/experiments/[...path]/route.ts @@ -0,0 +1,48 @@ +/** + * Experiments API proxy — forwards requests to platform-service. + * + * /api/experiments/* → platform-service /api/ab-testing/experiments/* + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUserFromRequest } from '@/lib/auth-server'; +import { logError } from '@/lib/logger'; + +const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'; + +async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + try { + const caller = await getCurrentUserFromRequest(req); + if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { path } = await params; + const targetPath = `/api/ab-testing/experiments/${path.join('/')}`; + const qs = new URL(req.url).search; + const headers: Record = { + 'Content-Type': 'application/json', + 'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(), + 'x-user-id': caller.id, + 'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai', + }; + const fetchOptions: RequestInit = { method: req.method, headers }; + if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text(); + const res = await fetch(`${PLATFORM_URL}${targetPath}${qs}`, fetchOptions); + const data = await res.json().catch(() => null); + return NextResponse.json(data ?? { error: res.statusText }, { status: res.status }); + } catch (error) { + logError('Experiments proxy error', error); + return NextResponse.json({ error: 'Service unavailable' }, { status: 502 }); + } +} + +export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} +export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { + return proxy(req, ctx); +} diff --git a/dashboards/admin-web/src/app/api/experiments/route.ts b/dashboards/admin-web/src/app/api/experiments/route.ts new file mode 100644 index 00000000..7d5a96cc --- /dev/null +++ b/dashboards/admin-web/src/app/api/experiments/route.ts @@ -0,0 +1,39 @@ +/** + * Experiments base route — handles GET /api/experiments. + * Rewrites to platform-service GET /api/ab-testing/experiments. + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUserFromRequest } from '@/lib/auth-server'; +import { logError } from '@/lib/logger'; + +const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'; + +async function proxyBase(req: NextRequest) { + try { + const caller = await getCurrentUserFromRequest(req); + if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const qs = new URL(req.url).search; + const headers: Record = { + 'Content-Type': 'application/json', + 'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(), + 'x-user-id': caller.id, + 'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai', + }; + const fetchOptions: RequestInit = { method: req.method, headers }; + if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text(); + const res = await fetch(`${PLATFORM_URL}/api/ab-testing/experiments${qs}`, fetchOptions); + const data = await res.json().catch(() => null); + return NextResponse.json(data ?? { error: res.statusText }, { status: res.status }); + } catch (error) { + logError('Experiments base proxy error', error); + return NextResponse.json({ error: 'Service unavailable' }, { status: 502 }); + } +} + +export async function GET(req: NextRequest) { + return proxyBase(req); +} +export async function POST(req: NextRequest) { + return proxyBase(req); +}