diff --git a/.forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png b/.forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png new file mode 100644 index 0000000..c1720eb Binary files /dev/null and b/.forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png differ diff --git a/.forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png b/.forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png new file mode 100644 index 0000000..c1720eb Binary files /dev/null and b/.forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png differ diff --git a/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log b/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log new file mode 100644 index 0000000..fff9530 --- /dev/null +++ b/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log @@ -0,0 +1,3 @@ +[2026-04-20T13:45:28.457860900+00:00] Started execution for plan: 2d054b74-476c-4350-9b0d-01fe30d3dea3 +[2026-04-20T13:45:28.532783200+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T13:45:29.666195600+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log b/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log new file mode 100644 index 0000000..8006e1f --- /dev/null +++ b/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log @@ -0,0 +1,4 @@ +[2026-04-20T14:01:25.320370100+00:00] Started execution for plan: d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff +[2026-04-20T14:01:25.392901100+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T14:01:51.628467+00:00] [Provider] Refinement failed: Request failed: error sending request for url (https://api.mistral.ai/v1/chat/completions). Using fallback. +[2026-04-20T14:01:51.629282600+00:00] Resolved action: InspectFiles against diff --git a/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log b/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log new file mode 100644 index 0000000..7ccdad3 --- /dev/null +++ b/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log @@ -0,0 +1,3 @@ +[2026-04-20T13:44:37.566968200+00:00] Started execution for plan: 2d054b74-476c-4350-9b0d-01fe30d3dea3 +[2026-04-20T13:44:37.662824400+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T13:44:39.095675800+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log b/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log new file mode 100644 index 0000000..29d939c --- /dev/null +++ b/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log @@ -0,0 +1,3 @@ +[2026-04-20T14:33:38.557977700+00:00] Started execution for plan: d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff +[2026-04-20T14:33:38.637442100+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T14:33:40.691084300+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/state.json b/.forge/executions/state.json new file mode 100644 index 0000000..2e404dd --- /dev/null +++ b/.forge/executions/state.json @@ -0,0 +1,7 @@ +{ + "id": "c68159c7-9e6e-4271-90b6-73a6414bfba0", + "planId": "d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff", + "status": "running", + "mode": "step_by_step", + "currentStepId": "step_1" +} \ No newline at end of file diff --git a/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json b/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json new file mode 100644 index 0000000..e7eccad --- /dev/null +++ b/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json @@ -0,0 +1,42 @@ +{ + "id": "2d054b74-476c-4350-9b0d-01fe30d3dea3", + "taskId": "7fbdc7ca-d44b-47ca-9db1-30ad5c19b87c", + "status": "approved", + "title": "Plan for: what is this project about?", + "objective": "what is this project about?", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"plan_1\",\n \"taskId\": \"task_1\",\n \"status\": \"draft\",\n \"title\": \"Analyze Tera Project\",\n \"objective\": \"Understand the purpose and structure of the Tera project", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json b/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json new file mode 100644 index 0000000..d5b8ba7 --- /dev/null +++ b/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json @@ -0,0 +1,42 @@ +{ + "id": "d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff", + "taskId": "2c368bff-7a21-47c5-8640-dc0f68bd74b1", + "status": "approved", + "title": "Plan for: tell me what this project is about", + "objective": "tell me what this project is about", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d\",\n \"taskId\": \"project_analysis\",\n \"status\": \"draft\",\n \"title\": \"Analyze Tera Project\",\n \"objective\": \"Determine what t", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json b/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json new file mode 100644 index 0000000..684c2b2 --- /dev/null +++ b/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json @@ -0,0 +1,42 @@ +{ + "id": "e44ed371-65d0-4399-97a1-fd2bbf46de00", + "taskId": "5b12269e-62c9-41d1-af4d-a9d06bc811d5", + "status": "ready_for_review", + "title": "Plan for: what is Tera about", + "objective": "what is Tera about", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"c0f7b5e8-1234-5678-9abc-def123456789\",\n \"taskId\": \"what_is_tera_about\",\n \"status\": \"draft\",\n \"title\": \"Investigate Tera project to understand its purpose\",\n ", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/provider_config.json b/.forge/provider_config.json new file mode 100644 index 0000000..370e891 --- /dev/null +++ b/.forge/provider_config.json @@ -0,0 +1,6 @@ +{ + "kind": "openai_compatible", + "baseUrl": "https://api.mistral.ai", + "modelId": "mistral-small-latest", + "apiKeySet": true +} \ No newline at end of file diff --git a/.forge/provider_secret.key b/.forge/provider_secret.key new file mode 100644 index 0000000..8959165 --- /dev/null +++ b/.forge/provider_secret.key @@ -0,0 +1 @@ +v1Vphvx1drTK9OdsQBv1lsTVr4bsaBrv \ No newline at end of file diff --git a/app/actions/generate.ts b/app/actions/generate.ts index bc26957..d715dba 100644 --- a/app/actions/generate.ts +++ b/app/actions/generate.ts @@ -40,6 +40,11 @@ function isMissingColumnError(error: unknown, columnName: string) { return details.includes(columnName.toLowerCase()) && details.includes('column') } +function omitField, K extends keyof T>(payload: T, key: K): Omit { + const { [key]: _removed, ...rest } = payload + return rest +} + export async function generateAnswer({ prompt, tool, authorId, authorEmail, attachments = [], sessionId, chatId, enableWebSearch = false, researchMode = false }: GenerateProps) { // Get user profile and check limits let userProfile = await getUserProfileServer(authorId) @@ -234,24 +239,50 @@ export async function generateAnswer({ prompt, tool, authorId, authorEmail, atta title: title } - let { data, error } = await supabaseServer.from('chat_sessions').insert({ + let insertPayload: Record = { ...baseInsertPayload, token_usage: tokenCost, - }) - .select('id') - .single() + } + let data: { id: string } | null = null + let error: any = null - if (error && isMissingColumnError(error, 'token_usage')) { - const retryResult = await supabaseServer.from('chat_sessions').insert(baseInsertPayload) + for (let attempt = 0; attempt < 4; attempt += 1) { + const result = await supabaseServer + .from('chat_sessions') + .insert(insertPayload) .select('id') .single() - data = retryResult.data - error = retryResult.error + data = result.data + error = result.error + + if (!error) break + + if (isMissingColumnError(error, 'token_usage') && 'token_usage' in insertPayload) { + insertPayload = omitField(insertPayload, 'token_usage') + continue + } + + if (isMissingColumnError(error, 'session_id') && 'session_id' in insertPayload) { + insertPayload = omitField(insertPayload, 'session_id') + continue + } + + if (isMissingColumnError(error, 'title') && 'title' in insertPayload) { + insertPayload = omitField(insertPayload, 'title') + continue + } + + break } if (error) { - console.error('[chat_insert_failed]', { userId: authorId, sessionId: currentSessionId, error }) + console.error('[chat_insert_failed]', { + userId: authorId, + sessionId: currentSessionId, + error, + attemptedPayloadKeys: Object.keys(insertPayload) + }) persistenceWarning = 'We generated your response, but could not save this chat message.' } else if (data?.id) { savedChatId = data.id @@ -259,8 +290,10 @@ export async function generateAnswer({ prompt, tool, authorId, authorEmail, atta } } - // Increment chat counter after successful generation - await incrementChatsServer(authorId) + // Increment chat counter only after successful persistence to avoid analytics drift. + if (chatPersisted) { + await incrementChatsServer(authorId) + } // Increment web search counter if enabled if (enableWebSearch) { diff --git a/app/api/admin/analytics/route.ts b/app/api/admin/analytics/route.ts index 5501c91..810d208 100644 --- a/app/api/admin/analytics/route.ts +++ b/app/api/admin/analytics/route.ts @@ -3,6 +3,12 @@ import { auth } from '@/lib/auth' import { supabaseServer as supabase } from '@/lib/supabase-server' import { isAdminUser } from '@/lib/admin' +function throwIfSupabaseError(error: any, context: string) { + if (error) { + throw new Error(`[admin-analytics:${context}] ${error.message || 'Supabase query failed'}`) + } +} + export async function POST(req: NextRequest) { try { const session = await auth() @@ -33,44 +39,51 @@ async function getAnalyticsData() { // ===== USER METRICS ===== // Total users - const { count: totalUsers } = await supabase + const { count: totalUsers, error: totalUsersError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) + throwIfSupabaseError(totalUsersError, 'total-users') // New users today - const { count: newUsersToday } = await supabase + const { count: newUsersToday, error: newUsersTodayError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', todayStart) + throwIfSupabaseError(newUsersTodayError, 'new-users-today') // New users this week - const { count: newUsersWeek } = await supabase + const { count: newUsersWeek, error: newUsersWeekError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(newUsersWeekError, 'new-users-week') // New users this month - const { count: newUsersMonth } = await supabase + const { count: newUsersMonth, error: newUsersMonthError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', thirtyDaysAgo) + throwIfSupabaseError(newUsersMonthError, 'new-users-month') // Users who hit chat limit - const { count: chatLimitHits } = await supabase + const { count: chatLimitHits, error: chatLimitHitsError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .not('limit_hit_chat_at', 'is', null) + throwIfSupabaseError(chatLimitHitsError, 'chat-limit-hits') // Users who hit upload limit - const { count: uploadLimitHits } = await supabase + const { count: uploadLimitHits, error: uploadLimitHitsError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .not('limit_hit_upload_at', 'is', null) + throwIfSupabaseError(uploadLimitHitsError, 'upload-limit-hits') // Subscription breakdown - const { data: subscriptionBreakdown } = await supabase + const { data: subscriptionBreakdown, error: subscriptionBreakdownError } = await supabase .from('users') .select('subscription_plan') + throwIfSupabaseError(subscriptionBreakdownError, 'subscription-breakdown') const plans = { free: 0, pro: 0, plus: 0, school: 0 } subscriptionBreakdown?.forEach((user: any) => { @@ -82,41 +95,47 @@ async function getAnalyticsData() { // ===== CHAT SESSION METRICS ===== // Total chat sessions - const { count: totalChatSessions } = await supabase + const { count: totalChatSessions, error: totalChatSessionsError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) + throwIfSupabaseError(totalChatSessionsError, 'total-chat-sessions') // Chat sessions today - const { count: chatsToday } = await supabase + const { count: chatsToday, error: chatsTodayError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', todayStart) + throwIfSupabaseError(chatsTodayError, 'chats-today') // Chat sessions this week - const { count: chatsThisWeek } = await supabase + const { count: chatsThisWeek, error: chatsThisWeekError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(chatsThisWeekError, 'chats-this-week') // Active users today (users who had chats today) - const { data: activeUsersData } = await supabase + const { data: activeUsersData, error: activeUsersDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', todayStart) + throwIfSupabaseError(activeUsersDataError, 'active-users-today') const activeUsersToday = new Set(activeUsersData?.map((c: any) => c.user_id) || []).size // Active users this week - const { data: weeklyActiveData } = await supabase + const { data: weeklyActiveData, error: weeklyActiveDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(weeklyActiveDataError, 'active-users-week') const activeUsersWeek = new Set(weeklyActiveData?.map((c: any) => c.user_id) || []).size // ===== WEB SEARCH METRICS ===== // Total web searches (sum of monthly_web_searches column) - const { data: webSearchData } = await supabase + const { data: webSearchData, error: webSearchDataError } = await supabase .from('users') .select('monthly_web_searches') + throwIfSupabaseError(webSearchDataError, 'web-search-data') const totalWebSearches = webSearchData?.reduce((sum: number, u: any) => sum + (u.monthly_web_searches || 0), 0) || 0 // ===== DAILY ACTIVITY CHART DATA (Last 7 days) ===== @@ -126,17 +145,19 @@ async function getAnalyticsData() { dayStart.setHours(0, 0, 0, 0) const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000) - const { count: dayChats } = await supabase + const { count: dayChats, error: dayChatsError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', dayStart.toISOString()) .lt('created_at', dayEnd.toISOString()) + throwIfSupabaseError(dayChatsError, `daily-chats-${i}`) - const { count: dayUsers } = await supabase + const { count: dayUsers, error: dayUsersError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', dayStart.toISOString()) .lt('created_at', dayEnd.toISOString()) + throwIfSupabaseError(dayUsersError, `daily-users-${i}`) dailyActivity.push({ date: dayStart.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), @@ -146,10 +167,11 @@ async function getAnalyticsData() { } // ===== TOP ACTIVE USERS ===== - const { data: topUsersData } = await supabase + const { data: topUsersData, error: topUsersDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(topUsersDataError, 'top-users-data') const userChatCount: Record = {} topUsersData?.forEach((chat: any) => { @@ -164,10 +186,11 @@ async function getAnalyticsData() { .slice(0, 10) .map(([id]) => id) - const { data: topUsersInfo } = await supabase + const { data: topUsersInfo, error: topUsersInfoError } = await supabase .from('users') .select('id, email, subscription_plan, created_at') .in('id', topUserIds.length > 0 ? topUserIds : ['none']) + throwIfSupabaseError(topUsersInfoError, 'top-users-info') const topActiveUsers = topUsersInfo?.map((user: any) => ({ ...user, @@ -175,32 +198,36 @@ async function getAnalyticsData() { })).sort((a: any, b: any) => b.chatCount - a.chatCount) || [] // ===== RECENT SIGNUPS ===== - const { data: recentSignups } = await supabase + const { data: recentSignups, error: recentSignupsError } = await supabase .from('users') .select('id, email, subscription_plan, created_at') .order('created_at', { ascending: false }) .limit(10) + throwIfSupabaseError(recentSignupsError, 'recent-signups') // Users who upgraded after hitting limit - const { data: upgradedAfterLimit } = await supabase + const { data: upgradedAfterLimit, error: upgradedAfterLimitError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at, created_at') .neq('subscription_plan', 'free') .or('limit_hit_chat_at.not.is.null, limit_hit_upload_at.not.is.null') + throwIfSupabaseError(upgradedAfterLimitError, 'upgraded-after-limit') // Users still locked out - const { data: lockedOutUsers } = await supabase + const { data: lockedOutUsers, error: lockedOutUsersError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at') .or(`limit_hit_chat_at.gt.${oneDayAgo}, limit_hit_upload_at.gt.${oneDayAgo}`) + throwIfSupabaseError(lockedOutUsersError, 'locked-out-users') // Recent limit hits (last 7 days) - const { data: recentLimitHits } = await supabase + const { data: recentLimitHits, error: recentLimitHitsError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at, created_at') .or(`limit_hit_chat_at.gte.${sevenDaysAgo}, limit_hit_upload_at.gte.${sevenDaysAgo}`) .order('created_at', { ascending: false }) .limit(50) + throwIfSupabaseError(recentLimitHitsError, 'recent-limit-hits') // Upgrade rate calculation const upgradedCount = (upgradedAfterLimit || []).length diff --git a/migrations/add_chat_sessions_session_id_and_title.sql b/migrations/add_chat_sessions_session_id_and_title.sql new file mode 100644 index 0000000..9ba2bd9 --- /dev/null +++ b/migrations/add_chat_sessions_session_id_and_title.sql @@ -0,0 +1,20 @@ +-- Ensure chat_sessions has session_id/title required by server actions and admin analytics. +-- Keep this copy aligned with supabase/migrations for environments using the root migrations folder. + +ALTER TABLE chat_sessions +ADD COLUMN IF NOT EXISTS session_id UUID, +ADD COLUMN IF NOT EXISTS title TEXT; + +UPDATE chat_sessions +SET session_id = gen_random_uuid() +WHERE session_id IS NULL; + +ALTER TABLE chat_sessions +ALTER COLUMN session_id SET DEFAULT gen_random_uuid(), +ALTER COLUMN session_id SET NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id +ON chat_sessions (session_id); + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_session_created_at +ON chat_sessions (user_id, session_id, created_at DESC); diff --git a/mobile/metro.config.js b/mobile/metro.config.js new file mode 100644 index 0000000..73ba303 --- /dev/null +++ b/mobile/metro.config.js @@ -0,0 +1,7 @@ +const { getDefaultConfig } = require('expo/metro-config'); + +const config = getDefaultConfig(__dirname); + +config.resolver.useWatchman = false; + +module.exports = config; \ No newline at end of file diff --git a/mobile/package.json b/mobile/package.json index 02d0bac..8d69a0a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,37 +1 @@ -{ - "name": "tera-mobile", - "version": "1.0.0", - "description": "Tera - Your AI Learning Companion for Anything (Mobile App)", - "main": "expo-router/entry", - "scripts": { - "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "test": "jest", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@expo/metro-runtime": "^6.1.2", - "@react-native-async-storage/async-storage": "^2.2.0", - "@tanstack/react-query": "^5.62.7", - "expo": "^54.0.33", - "expo-constants": "^18.0.13", - "expo-font": "^14.0.11", - "expo-router": "^6.0.23", - "expo-secure-store": "^15.0.8", - "expo-splash-screen": "^31.0.13", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-native": "0.81.5", - "react-native-gesture-handler": "^2.28.0", - "react-native-web": "^0.21.2", - "zod": "^3.24.1", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@types/node": "^20.10.6", - "@types/react": "~19.1.10", - "typescript": "^5.9.3" - } -} +{ "name": "tera-mobile", "version": "1.0.0", "description": "Tera - Your AI Learning Companion for Anything (Mobile App)", "main": "expo-router/entry", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "test": "jest", "typecheck": "tsc --noEmit" }, "dependencies": { "@expo/metro-runtime": "^6.1.2", "@react-native-async-storage/async-storage": "^2.2.0", "@tanstack/react-query": "^5.62.7", "expo": "^54.0.33", "expo-constants": "^18.0.13", "expo-font": "^14.0.11", "expo-router": "^6.0.23", "expo-secure-store": "^15.0.8", "expo-splash-screen": "^31.0.13", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "^2.28.0", "react-native-web": "^0.21.2", "zod": "^3.24.1", "zustand": "^5.0.2" }, "devDependencies": { "@types/node": "^20.10.6", "@types/react": "~19.1.10", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql b/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql new file mode 100644 index 0000000..7300def --- /dev/null +++ b/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql @@ -0,0 +1,21 @@ +-- Ensure chat_sessions has session_id/title required by server actions and admin analytics. +-- This migration is idempotent and safe for environments that already applied these columns. + +ALTER TABLE chat_sessions +ADD COLUMN IF NOT EXISTS session_id UUID, +ADD COLUMN IF NOT EXISTS title TEXT; + +-- Backfill old rows so downstream queries and grouping by session_id remain reliable. +UPDATE chat_sessions +SET session_id = gen_random_uuid() +WHERE session_id IS NULL; + +ALTER TABLE chat_sessions +ALTER COLUMN session_id SET DEFAULT gen_random_uuid(), +ALTER COLUMN session_id SET NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id +ON chat_sessions (session_id); + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_session_created_at +ON chat_sessions (user_id, session_id, created_at DESC);