From 0cec8487ba0fdea514ab3fdb78b80acf6d5d82dc Mon Sep 17 00:00:00 2001 From: Daniel Rosel Date: Thu, 6 Nov 2025 18:08:19 +0100 Subject: [PATCH] tentative session storage with maybe using airtable --- web/src/app/api/session/route.ts | 10 ++- web/src/hooks/useSession.ts | 38 ++++++++++++ web/src/lib/sessionStore.ts | 102 +++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 web/src/hooks/useSession.ts create mode 100644 web/src/lib/sessionStore.ts diff --git a/web/src/app/api/session/route.ts b/web/src/app/api/session/route.ts index 455f589..7f5b4c6 100644 --- a/web/src/app/api/session/route.ts +++ b/web/src/app/api/session/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { randomUUID } from 'crypto'; +import { getSession, createSession } from '@/lib/sessionStore'; const COOKIE_NAME = 'phantom_session_id'; const isProd = process.env.NODE_ENV === 'production'; @@ -10,13 +11,18 @@ export async function GET(req: NextRequest) { const existingSession = req.cookies.get(COOKIE_NAME)?.value; if (existingSession) { - return NextResponse.json({ sessionId: existingSession }); + const sessionData = getSession(existingSession); + return NextResponse.json({ + sessionId: existingSession, + experimentId: sessionData?.experimentId, + }); } // mint new session id const sessionId = randomUUID(); + createSession(sessionId); - const res = NextResponse.json({ sessionId }); + const res = NextResponse.json({ sessionId, experimentId: undefined }); // set httpOnly cookie with security flags res.cookies.set({ diff --git a/web/src/hooks/useSession.ts b/web/src/hooks/useSession.ts new file mode 100644 index 0000000..d2f48eb --- /dev/null +++ b/web/src/hooks/useSession.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; + +type SessionState = { + sessionId: string | null; + experimentId: string | null; + isLoading: boolean; +}; + +export const useSession = () => { + const [state, setState] = useState({ + sessionId: null, + experimentId: null, + isLoading: true, + }); + + useEffect(() => { + const fetchSession = async () => { + try { + const res = await fetch('/api/session'); + if (!res.ok) throw new Error(`fetch failed: ${res.status}`); + + const data = await res.json(); + setState({ + sessionId: data.sessionId || null, + experimentId: data.experimentId || null, + isLoading: false, + }); + } catch (err) { + console.error('session fetch error:', err); + setState({ sessionId: null, experimentId: null, isLoading: false }); + } + }; + + fetchSession(); + }, []); + + return state; +}; diff --git a/web/src/lib/sessionStore.ts b/web/src/lib/sessionStore.ts new file mode 100644 index 0000000..769cfd7 --- /dev/null +++ b/web/src/lib/sessionStore.ts @@ -0,0 +1,102 @@ +type SessionData = { + experimentId?: string; + startedAt: number; + status: 'active' | 'stopped'; +}; + +type ExperimentData = { + id: string; + status: 'active' | 'stopped'; + sessionIds: string[]; + createdAt: number; +}; + +const store = new Map(); +const experiments = new Map(); + +const cfg = { + key: process.env.AIRTABLE_API_KEY, + base: process.env.AIRTABLE_BASE_ID, + table: process.env.AIRTABLE_TABLE_NAME || 'Sessions', +}; + +// sync session to airtable if credentials present +const syncToAirtable = async (sid: string, data: SessionData) => { + if (!cfg.key || !cfg.base) return; // skip if not configured + + try { + const url = `https://api.airtable.com/v0/${cfg.base}/${encodeURIComponent(cfg.table)}`; + await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${cfg.key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fields: { + sessionId: sid, + experimentId: data.experimentId || '', + startedAt: new Date(data.startedAt).toISOString(), + status: data.status, + }, + }), + }); + } catch (err) { + console.error('airtable sync failed:', err); + } +}; + +export const getSession = (sid: string) => store.get(sid); + +export const createSession = (sid: string) => { + const data: SessionData = { startedAt: Date.now(), status: 'active' }; + store.set(sid, data); + syncToAirtable(sid, data); // async fire-and-forget + return data; +}; + +export const setExperiment = (sid: string, expId: string) => { + const data = store.get(sid) || createSession(sid); + data.experimentId = expId; + store.set(sid, data); + syncToAirtable(sid, data); + return data; +}; + +export const stopExperiment = (sid: string) => { + const data = store.get(sid); + if (data) { + data.status = 'stopped'; + store.set(sid, data); + syncToAirtable(sid, data); + } + return data; +}; + +// experiment-level operations +export const createExperiment = (sid: string, expId: string) => { + const exp: ExperimentData = { + id: expId, + status: 'active', + sessionIds: [sid], + createdAt: Date.now(), + }; + experiments.set(expId, exp); + setExperiment(sid, expId); // link session to experiment + console.log(`experiment ${expId} started with session ${sid}`); + return exp; +}; + +export const stopExperimentById = (expId: string) => { + const exp = experiments.get(expId); + if (exp) { + exp.status = 'stopped'; + experiments.set(expId, exp); + console.log(`experiment ${expId} stopped`); + } + return exp; +}; + +export const getExperiment = (expId: string) => experiments.get(expId); + +export const getAllExperiments = () => Array.from(experiments.values());