diff --git a/app/api/history/[id]/route.ts b/app/api/history/[id]/route.ts index 278c0b4..f593d81 100644 --- a/app/api/history/[id]/route.ts +++ b/app/api/history/[id]/route.ts @@ -3,17 +3,18 @@ import { query } from '@/lib/db'; import logger from '@/lib/log'; export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { - logger.warn({ route: 'history.delete' }, 'unauthenticated'); + logger.warn({ durationMs: Date.now() - start }, 'history.delete.unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; - await query( + const result = await query( 'DELETE FROM chat_histories WHERE id = $1 AND user_email = $2', [id, session.user.email], ); - logger.info({ user: session.user.email, id }, 'history.delete'); + logger.info({ user: session.user.email, id, durationMs: Date.now() - start, deleted: result.rowCount ?? 0 }, 'history.delete'); return new Response(null, { status: 204 }); } diff --git a/app/api/history/route.ts b/app/api/history/route.ts index 83a9044..5098589 100644 --- a/app/api/history/route.ts +++ b/app/api/history/route.ts @@ -3,9 +3,10 @@ import { query } from '@/lib/db'; import logger from '@/lib/log'; export async function GET() { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { - logger.warn({ route: 'history.list' }, 'unauthenticated'); + logger.warn({ durationMs: Date.now() - start }, 'history.list.unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -13,25 +14,36 @@ export async function GET() { 'SELECT id, iv, ciphertext, updated_at FROM chat_histories WHERE user_email = $1 ORDER BY updated_at DESC', [session.user.email], ); - logger.info({ user: session.user.email, rows: result.rows.length }, 'history.list'); - return Response.json(result.rows); + const rows = result.rows; + const newestAt = rows[0]?.updated_at ?? null; + const oldestAt = rows[rows.length - 1]?.updated_at ?? null; + logger.info({ + user: session.user.email, + durationMs: Date.now() - start, + rows: rows.length, + newestAt, + oldestAt, + }, 'history.list'); + return Response.json(rows); } export async function POST(req: Request) { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { - logger.warn({ route: 'history.save' }, 'unauthenticated'); + logger.warn({ durationMs: Date.now() - start }, 'history.save.unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const { id, iv, ciphertext } = await req.json(); + const ciphertextBytes = Math.round((ciphertext?.length ?? 0) * 0.75); if (id) { await query( 'UPDATE chat_histories SET iv = $1, ciphertext = $2, updated_at = now() WHERE id = $3 AND user_email = $4', [iv, ciphertext, id, session.user.email], ); - logger.info({ user: session.user.email, id, op: 'update' }, 'history.save'); + logger.info({ user: session.user.email, id, op: 'update', durationMs: Date.now() - start, ciphertextBytes }, 'history.save'); return Response.json({ id }); } @@ -41,6 +53,6 @@ export async function POST(req: Request) { RETURNING id`, [session.user.email, iv, ciphertext], ); - logger.info({ user: session.user.email, id: result.rows[0].id, op: 'insert' }, 'history.save'); + logger.info({ user: session.user.email, id: result.rows[0].id, op: 'insert', durationMs: Date.now() - start, ciphertextBytes }, 'history.save'); return Response.json({ id: result.rows[0].id }); } diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 1f625c6..8bf314d 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -3,9 +3,10 @@ import { query } from '@/lib/db'; import logger from '@/lib/log'; export async function GET() { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { - logger.warn({ route: 'settings.get' }, 'unauthenticated'); + logger.warn({ route: 'settings.get', durationMs: Date.now() - start }, 'unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -14,7 +15,13 @@ export async function GET() { [session.user.email], ); const row = result.rows[0]; - logger.info({ user: session.user.email, hasKey: !!row?.key_jwk }, 'settings.get'); + logger.info({ + user: session.user.email, + durationMs: Date.now() - start, + hasKey: !!row?.key_jwk, + saveHistory: row?.save_history ?? false, + newUser: !row, + }, 'settings.get'); return Response.json({ systemPrompt: row?.system_prompt ?? '', saveHistory: row?.save_history ?? false, @@ -23,9 +30,10 @@ export async function GET() { } export async function PUT(req: Request) { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { - logger.warn({ route: 'settings.put' }, 'unauthenticated'); + logger.warn({ route: 'settings.put', durationMs: Date.now() - start }, 'unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -38,6 +46,11 @@ export async function PUT(req: Request) { key_jwk = COALESCE(EXCLUDED.key_jwk, user_settings.key_jwk)`, [session.user.email, systemPrompt ?? '', saveHistory ?? false, keyJwk ?? null], ); - logger.info({ user: session.user.email, saveHistory: saveHistory ?? false }, 'settings.put'); + logger.info({ + user: session.user.email, + durationMs: Date.now() - start, + saveHistory: saveHistory ?? false, + hasKey: !!keyJwk, + }, 'settings.put'); return new Response(null, { status: 204 }); } diff --git a/app/api/shares/[id]/route.ts b/app/api/shares/[id]/route.ts index 3747317..d4d481f 100644 --- a/app/api/shares/[id]/route.ts +++ b/app/api/shares/[id]/route.ts @@ -1,11 +1,15 @@ import { query } from '@/lib/db'; +import logger from '@/lib/log'; export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) { + const start = Date.now(); const { id } = await params; const result = await query( 'SELECT id, model, system_prompt, messages, created_at FROM shared_chats WHERE id = $1', [id], ); - if (result.rows.length === 0) return new Response('Not found', { status: 404 }); + const found = result.rows.length > 0; + logger.info({ id, found, durationMs: Date.now() - start }, 'share.get'); + if (!found) return new Response('Not found', { status: 404 }); return Response.json(result.rows[0]); } diff --git a/app/api/shares/route.ts b/app/api/shares/route.ts index 7f7d857..7380501 100644 --- a/app/api/shares/route.ts +++ b/app/api/shares/route.ts @@ -4,13 +4,17 @@ import { query } from '@/lib/db'; import logger from '@/lib/log'; export async function POST(req: NextRequest) { + const start = Date.now(); const session = await auth(); if (!session?.user?.email) { + logger.warn({ durationMs: Date.now() - start }, 'share.create.unauthenticated'); return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await req.json(); - if (JSON.stringify(body).length > 500_000) { + const bodyBytes = JSON.stringify(body).length; + if (bodyBytes > 500_000) { + logger.warn({ user: session.user.email, bodyBytes, durationMs: Date.now() - start }, 'share.create.too_large'); return Response.json( { error: 'Chat too large to share. Try removing image attachments first.' }, { status: 413 }, @@ -24,6 +28,6 @@ export async function POST(req: NextRequest) { [id, session.user.email, model, systemPrompt || null, JSON.stringify(messages)], ); - logger.info({ user: session.user.email, id, model, msgs: messages.length }, 'share.create'); + logger.info({ user: session.user.email, id, model, msgs: messages.length, bodyBytes, durationMs: Date.now() - start }, 'share.create'); return Response.json({ id }); } diff --git a/app/page.tsx b/app/page.tsx index 6438ef3..44cf639 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -112,7 +112,8 @@ export default function Home() { const saved = localStorage.getItem(MODEL_KEY); if (saved) setModel(saved); - fetch('/api/settings') + const ac = new AbortController(); + fetch('/api/settings', { signal: ac.signal }) .then(r => r.json()) .then(async ({ systemPrompt, saveHistory: sh, keyJwk }) => { if (systemPrompt) setSystemPrompt(systemPrompt); @@ -130,7 +131,7 @@ export default function Home() { }).catch(() => {}); } }) - .catch(() => { keyResolveRef.current?.(); }); + .catch(e => { if (e.name !== 'AbortError') keyResolveRef.current?.(); }); const fork = localStorage.getItem('gippidy-fork'); if (fork) { @@ -142,6 +143,7 @@ export default function Home() { localStorage.removeItem('gippidy-fork'); chatIdRef.current = null; // fork always starts a new history entry } + return () => ac.abort(); }, []); useEffect(() => {