From eb360c4ab36faf9f95d4608f4521f2241566e835 Mon Sep 17 00:00:00 2001 From: Daniel Rosel Date: Thu, 13 Nov 2025 17:52:35 +0100 Subject: [PATCH] fixing small bugs and adding exepriments to tracking --- web/package-lock.json | 10 - web/src/app/admin/experiments/page.tsx | 199 ++++++++++++++++++ web/src/app/api/admin/experiments/route.ts | 15 ++ .../app/api/admin/experiments/start/route.ts | 43 ++++ .../app/api/admin/experiments/stop/route.ts | 39 ++++ web/src/components/ui/PriceDisplay.tsx | 8 +- 6 files changed, 303 insertions(+), 11 deletions(-) create mode 100755 web/src/app/admin/experiments/page.tsx create mode 100644 web/src/app/api/admin/experiments/route.ts create mode 100644 web/src/app/api/admin/experiments/start/route.ts create mode 100644 web/src/app/api/admin/experiments/stop/route.ts diff --git a/web/package-lock.json b/web/package-lock.json index 8de31b4..e773ffb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,7 +8,6 @@ "name": "web", "version": "0.1.0", "dependencies": { - "kafkajs": "^2.2.4", "next": "16.0.0", "react": "19.2.0", "react-dom": "19.2.0", @@ -1042,15 +1041,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/kafkajs": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", - "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", diff --git a/web/src/app/admin/experiments/page.tsx b/web/src/app/admin/experiments/page.tsx new file mode 100755 index 0000000..ef8f89e --- /dev/null +++ b/web/src/app/admin/experiments/page.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useSession } from '@/hooks/useSession'; + +type Experiment = { + id: string; + status: 'active' | 'stopped'; + sessionIds: string[]; + createdAt: number; +}; + +export default function ExperimentsAdmin() { + const { sessionId, isLoading: sessionLoading } = useSession(); + const [exps, setExps] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchExps = async () => { + try { + const res = await fetch('/api/admin/experiments'); + if (!res.ok) throw new Error(`fetch failed: ${res.status}`); + const data = await res.json(); + setExps(data.experiments || []); + } catch (err: any) { + setError(err.message); + } + }; + + useEffect(() => { + fetchExps(); + }, []); + + const handleStart = async () => { + if (!sessionId) { + setError('no session available'); + return; + } + + setLoading(true); + setError(null); + + try { + const res = await fetch('/api/admin/experiments/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId }), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'start failed'); + } + + await fetchExps(); // refresh list + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleStop = async (expId: string) => { + setLoading(true); + setError(null); + + try { + const res = await fetch('/api/admin/experiments/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ experimentId: expId }), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'stop failed'); + } + + await fetchExps(); // refresh list + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (sessionLoading) { + return ( +
+

loading session...

+
+ ); + } + + return ( +
+
+
+
+

+ Experiments +

+

+ current session: {sessionId || 'none'} +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + + + + + + + + + + {exps.length === 0 ? ( + + + + ) : ( + exps.map((exp) => ( + + + + + + + + )) + )} + +
+ experiment id + + status + + session count + + created + + action +
+ no experiments yet +
+ {exp.id.slice(0, 8)}... + + + {exp.status} + + + {exp.sessionIds.length} + + {new Date(exp.createdAt).toLocaleString()} + + {exp.status === 'active' && ( + + )} +
+
+
+
+ ); +} diff --git a/web/src/app/api/admin/experiments/route.ts b/web/src/app/api/admin/experiments/route.ts new file mode 100644 index 0000000..58fcede --- /dev/null +++ b/web/src/app/api/admin/experiments/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getAllExperiments } from '@/lib/sessionStore'; + +export async function GET() { + try { + const exps = getAllExperiments(); + return NextResponse.json({ experiments: exps }); + } catch (err: any) { + console.error('experiments list error:', err); + return NextResponse.json( + { error: err.message || 'unknown error' }, + { status: 500 } + ); + } +} diff --git a/web/src/app/api/admin/experiments/start/route.ts b/web/src/app/api/admin/experiments/start/route.ts new file mode 100644 index 0000000..2fb35e3 --- /dev/null +++ b/web/src/app/api/admin/experiments/start/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { randomUUID } from 'crypto'; +import { createExperiment, getSession } from '@/lib/sessionStore'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { sessionId } = body; + + if (!sessionId) { + return NextResponse.json( + { error: 'sessionId required' }, + { status: 400 } + ); + } + + // verify session exists + const session = getSession(sessionId); + if (!session) { + return NextResponse.json( + { error: 'session not found' }, + { status: 404 } + ); + } + + // generate and create experiment + const experimentId = randomUUID(); + const exp = createExperiment(sessionId, experimentId); + + return NextResponse.json({ + experimentId: exp.id, + sessionId, + status: exp.status, + createdAt: exp.createdAt, + }); + } catch (err: any) { + console.error('experiment start error:', err); + return NextResponse.json( + { error: err.message || 'unknown error' }, + { status: 500 } + ); + } +} diff --git a/web/src/app/api/admin/experiments/stop/route.ts b/web/src/app/api/admin/experiments/stop/route.ts new file mode 100644 index 0000000..521219b --- /dev/null +++ b/web/src/app/api/admin/experiments/stop/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { stopExperimentById, getExperiment } from '@/lib/sessionStore'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { experimentId } = body; + + if (!experimentId) { + return NextResponse.json( + { error: 'experimentId required' }, + { status: 400 } + ); + } + + // verify experiment exists + const existing = getExperiment(experimentId); + if (!existing) { + return NextResponse.json( + { error: 'experiment not found' }, + { status: 404 } + ); + } + + // stop the experiment + const exp = stopExperimentById(experimentId); + + return NextResponse.json({ + experimentId: exp!.id, + status: exp!.status, + }); + } catch (err: any) { + console.error('experiment stop error:', err); + return NextResponse.json( + { error: err.message || 'unknown error' }, + { status: 500 } + ); + } +} diff --git a/web/src/components/ui/PriceDisplay.tsx b/web/src/components/ui/PriceDisplay.tsx index bb20619..b340ab5 100644 --- a/web/src/components/ui/PriceDisplay.tsx +++ b/web/src/components/ui/PriceDisplay.tsx @@ -69,6 +69,12 @@ export default function PriceDisplay({ const { sessionId, experimentId } = sessionRef.current; + if (!sessionId) { + setError('Invalid session'); + setLoading(false); + return; + } + const params = new URLSearchParams({ productId, sessionId, @@ -122,7 +128,7 @@ export default function PriceDisplay({ {isStale && ( - prices maybe out outdated + prices may be outdated )}