diff --git a/CLAUDE.md b/CLAUDE.md index e5d8829..d6429fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,10 +12,13 @@ pnpm start # Start production server # Note: If Turbopack fails, use: pnpm build -- --webpack # Database (direct SQL or docker) -docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia +docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia_dev ``` **Note:** Running from `/Users/manohar_air/MyProjects/Tia/tia` directory. +**Rule:** ALWAYS run `pnpm build` locally BEFORE git commit/push. + +--- ## Architecture @@ -23,10 +26,12 @@ docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia - **Framework:** Next.js 16 with App Router (src/app/) - **Database:** PostgreSQL 16 with pgvector + Drizzle ORM -- **Auth:** Database sessions with httpOnly cookies +- **Auth:** Database sessions with httpOnly cookies (`tia_session`) - **AI:** LiteLLM gateway → MiniMax model (minimax-2.7) - **Storage:** Cloudflare R2 for media uploads +- **Email:** Resend (transactional emails) - **Styling:** Tailwind CSS v4 +- **Deploy:** Dokploy (Docker-based, auto-runs migrations on deploy) ### Project Structure @@ -34,289 +39,265 @@ docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia src/ ├── app/ # Next.js App Router pages │ ├── api/ # API routes (auth, logs, ai, growth, etc.) -│ ├── page.tsx # Home (Quick Log + AI card) +│ ├── page.tsx # Home (Quick Log + AI card + baby card) │ ├── ai/ # AI chat page with sidebar │ ├── medical/ # Vaccination tracking │ ├── growth/ # Growth charts │ ├── memories/ # Photo gallery │ ├── menu/ # Navigation menu │ ├── onboarding/ # First-time setup -│ ├── settings/ # Settings with theme picker +│ ├── settings/ # Settings with theme picker, invite members +│ ├── profile/ # Parent user profile (avatar, name, email) │ ├── login/ # User login (email + password) -│ ├── admin/ # Admin panel -│ └── admin-login/ # Admin login (separate) +│ ├── admin/ # Admin panel (server component layout) +│ └── admin-login/ # Admin login (separate, NOT under admin layout) ├── ThemeProvider.tsx # Theme context (light/dark/system/time) ├── FamilyProvider.tsx # Family/child context (resolves from session) -drizzle/ # Database migrations -docs/ # Design docs +├── lib/ +│ ├── auth.ts # requireFamily(), requireOwnership() +│ ├── email.ts # sendVerificationEmail(), sendFamilyInviteEmail() +│ └── admin-auth.ts # requireAdmin() for API, verifyAdminSession() for server components +drizzle/ # Database migrations (SQL files) +drizzle/meta/_journal.json # Migration order — MUST update when adding new SQL files ``` -### Database +--- -- **Migrations:** SQL files in `drizzle/` (not using drizzle-kit push) -- **Apply:** `psql` directly or via docker-compose exec -- **RLS:** Row-level security for multi-family isolation +## Database Migrations -### Data Models +### How it works (Dokploy auto-deploy) +- `Dockerfile CMD`: `node migrate.mjs && node server.js` — migrations run first, app boots after +- `src/db/migrate.ts`: reads every SQL file in `drizzle/` and applies any not yet in `__drizzle_migrations` +- `drizzle/meta/_journal.json`: ordered list of migration files — **must be updated** when adding a new SQL file -- **Family:** Parent account container -- **Members:** Adults in family (mom, dad, etc.) via `family_members` -- **Children:** Baby profiles with birth date -- **Sessions:** Login sessions with httpOnly cookies -- **Logs:** Feed, sleep, diaper entries with timestamps -- **Vaccinations:** IAP schedule tracking -- **Growth:** Weight/height/head measurements over time -- **Memories:** Photos with R2 storage -- **Chat Sessions:** User conversations with AI (chat_sessions, chat_messages) +### Adding a new migration +1. Create `drizzle/NNNN_description.sql` with your SQL (use `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`) +2. Add an entry to `drizzle/meta/_journal.json` with the next `idx` and matching `tag` +3. Push to git → Dokploy auto-applies on next deploy -### Growth Page Features +### Hot-fix: apply migration without waiting for redeploy +Use the **debug-migration endpoint** (same pattern as the Circles implementation): -The growth page tracks child's physical development with WHO standards. - -**Components:** -- `formatAge(birthDate, measurementDate?)` - Shows age as "1y 4mo" or "8mo" -- Latest Reading card with velocity and percentile -- WHO Standards collapsible card (shows icons ⚖️📏⭕ when collapsed) -- Growth chart (collapsible inside WHO card) -- History (collapsible with scroll for many entries) -- Goals stored in localStorage (`tia_growth_goal_${childId}`) - -**State:** -- `showAdd` - Toggle add form (only shows if latest exists) -- `showGoals` - Toggle goals card -- `showWhoStandards` - Toggle WHO card expand/collapse -- `showChart` - Toggle chart inside WHO card -- `showHistory` - Toggle history expand/collapse - -**Color-coded percentiles:** -- 🟢 Normal (15th-85th percentile) -- 🟡 Watch (<15th or >85th) -- 🔴 Alert (<3rd or >97th) - -**Column order:** Goals → Latest → Add Form → WHO+Chart → History - -### Key Patterns - -**ThemeProvider:** Wrap app in ThemeProvider from layout.tsx. Use `useTheme()` hook in components. - -```typescript -import { useTheme } from "./ThemeProvider"; -const { theme, toggle, setMode } = useTheme(); -// theme: "light" | "dark" -// mode: "light" | "dark" | "system" | "time" +``` +POST /api/debug-migration +Header: x-run-migration: yes ``` -**FamilyProvider:** Resolves family from database session on login. +This runs SQL directly through the app's live DB connection. Steps use `IF NOT EXISTS` so safe to re-run. + +To add a new hot-fix: add `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` to the `steps` array in +`src/app/api/debug-migration/route.ts`, push, and immediately call the endpoint from Chrome: + +```javascript +// In browser console on tia.manohargupta.com +const res = await fetch('/api/debug-migration', { + method: 'POST', + headers: { 'x-run-migration': 'yes' } +}); +console.log(await res.json()); +``` + +The GET endpoint also shows which migrations have been applied and email config status. + +--- + +## Authentication + +### User auth (email + password) +1. User visits `/login` with email + password +2. `POST /api/auth/signin` verifies bcrypt hash, creates session in `sessions` table +3. Session token stored in **httpOnly cookie** `tia_session` (NOT localStorage!) +4. All data routes use `requireFamily()` from `@/lib/auth` + +### Admin auth +- Login at `/admin-login` (username: `admin`, password: from env) +- Sets `tia_admin_session` httpOnly cookie +- **Admin layout** (`src/app/admin/layout.tsx`) is a **server component** — calls `verifyAdminSession()` from `@/lib/admin-auth` and `redirect('/admin-login')` if not authed +- **Admin sub-pages** (`families`, `users`, `analytics`, etc.) use `credentials: 'include'` on all fetches — NO localStorage tokens, NO Bearer headers +- `AdminSidebar.tsx` is a separate `"use client"` component for the interactive sidebar + +### FamilyProvider +Resolves family from database session on login. Skip for admin routes. ```typescript import { useFamily } from "./FamilyProvider"; -const { familyId, familyName, child, children, tier, memberCount } = useFamily(); -// familyId: string | null -// familyName: string | null (from session) -// child: Child | null -// children: Child[] -// tier: "free" | "pro" -// memberCount: number (from family_members table) +const { familyId, familyName, child, children, tier, memberCount, updateChildImage } = useFamily(); +// updateChildImage(childId, imageUrl | null) — updates in-memory state after photo change ``` -**Offline Queue:** Uses localStorage (`tia_offline_queue`) for failed API calls, retries when online. +--- -**Session Validation:** All data API routes must use functions from `@/lib/auth`: +## Photo Uploads (R2) -```typescript -import { validateSession, requireFamily, requireOwnership } from "@/lib/auth"; +### Critical: CORS — never do a direct cross-origin PUT from the browser +Direct `PUT` to R2 presigned URLs is cross-origin and **blocked by the browser**. Always proxy through the server. -// Require family for user data -export async function GET(request: Request) { - const auth = await requireFamily(); - if (!auth.success) { - return NextResponse.json({ error: auth.error }, { status: auth.status }); - } +### 3-step upload pattern (used everywhere) +1. `POST /api/[init-endpoint]` with `{ contentType, filename }` → get `{ key, publicUrl }` +2. `PUT /api/upload?key=...&contentType=...` — server proxies the file to R2 +3. `PATCH /api/[save-endpoint]` with `{ [imageField]: publicUrl }` → saves URL to DB + deletes old R2 object - const familyId = auth.session!.familyId!; - // ... route logic -} +### Photo storage — two completely separate features -// Require ownership for specific resources -const ownership = await requireOwnership(childId, "children", "Child"); -if (!ownership.success) { - return NextResponse.json({ error: ownership.error }, { status: ownership.status }); -} -``` +| | **Parent profile photo** | **Baby card photo** | +|---|---|---| +| Page | `/profile` | Homepage (`/`) | +| DB column | `users.image` | `children.image_url` | +| API | `POST/PATCH/DELETE /api/auth/avatar` | `POST/PATCH /api/children/[id]` | +| R2 prefix | `avatars/{userId}/...` | `profiles/{childId}/...` | -**Chat Sessions:** Stored in localStorage (`tia_chat_sessions`) - shared between home page AI card and /ai page. Database tables: `chat_sessions`, `chat_messages`. +### Orphan cleanup pattern +Before overwriting an image URL in the DB, fetch the old URL, extract the R2 key, verify it's under a controlled prefix (`avatars/` or `profiles/`), then `DeleteObjectCommand`. Applied in both avatar and children routes. -**API Routes:** Return standard JSON `{ success: true, items: [...] }` format for lists. +### users.image — NOT users.avatar_url +The `users` table column is called **`image`** (not `avatar_url`). `avatar_url` belongs to `member_profiles`. +Always use `u.image` in SQL queries against the `users` table. -**AI Integration:** +--- -- Route: `/api/ai` → LiteLLM (set via `LITELLM_BASE_URL` env var) -- Model: `minimax-2.7` -- See `/docs/debugging.md` for troubleshooting - -## Authentication (Email + Password) - -1. User visits `/login` with email + password (or signs up for new account) -2. API `/api/auth/signin` verifies password hash and creates session in `sessions` table -3. Session token stored in **httpOnly cookie** (NOT localStorage!) -4. Password stored with simple hash in `users.password_hash` - -### Tables Used - -- **users:** User accounts (email, name) -- **families:** Family accounts (name, tier, limits) -- **family_members:** Links users to families (user_id, family_id, role) -- **children:** Child profiles (name, birth_date, family_id) -- **sessions:** Login sessions (session_token, user_id, expires) - -### NEVER use localStorage for: -- authentication tokens -- family_id after login -- Any data that should persist across devices - -### localStorage Acceptable For: -- Theme preference (user-specific display only) -- Temporary cache (offline queue for retry) -- Chat sessions local cache (synced from database) - -## Admin Panel - -Access at: `/admin-login` (username: `admin`, password: `admin123`) - -### Pages - -- `/admin` - Dashboard with clickable stat cards -- `/admin/families` - Manage families (create, view/add/remove members, set tier) -- `/admin/users` - Manage users (add to family, password status, delete) -- `/admin/children` - Manage children -- `/admin/revenue` - Revenue analytics -- `/admin/analytics` - Feature usage -- `/admin/support` - Support tickets -- `/admin/settings` - Platform settings - -## Data Storage Consistency - -### RULE: All user data must persist to database, NOT localStorage - -| Data Type | Storage | API Key | Persists After Refresh | Persists After Logout | -|----------|---------|--------|------------------------|-------------------| -| Children | Database | `/api/children` | ✅ Yes | ✅ Yes | -| Activity Logs | Database | `/api/logs` | ✅ Yes | ✅ Yes | -| Vaccinations | Database | `/api/vaccinations` | ✅ Yes | ✅ Yes | -| Growth Records | Database | `/api/growth` | ✅ Yes | ✅ Yes | -| User Profile | Database | `/api/auth/profile` | ✅ Yes | ✅ Yes | -| Memories/Photos | Database + R2 | `/api/upload` | ✅ Yes | ✅ Yes | -| Auth Session | Database + Cookie | `/api/auth/signin` | ✅ Yes | ✅ No | -| Theme | localStorage | `tia_theme` | ✅ Yes | ✅ Yes | -| Chat Sessions | Database | `/api/chat` | ✅ Yes | ✅ Yes | -| Offline Queue | localStorage | `tia_offline_queue` | ✅ Yes | ❌ No | - -## R2 Storage (Cloudflare) - -### Setup - -1. **Create bucket** in Cloudflare Dashboard → R2 -2. **Create API token** with "Object Read & Write" permissions -3. **Enable Public Development URL** in bucket settings (gives pub-*.r2.dev URL) - -### API Endpoint Format - -The S3 API endpoint is: `https://.r2.cloudflarestorage.com` - -For your bucket named "tia": -- Account ID: `e71f22a2f8614fb3ba6d9b28a264d8ce` -- S3 Endpoint: `https://e71f22a2f8614fb3ba6d9b28a264d8ce.r2.cloudflarestorage.com` -- Public URL: `https://pub-37a76fd657c94d1dbc521a109c087a11.r2.dev` (no bucket name in path!) - -### Code Example - -```typescript -const client = new S3Client({ - region: "auto", - endpoint: `https://${accountId}.r2.cloudflarestorage.com`, - credentials: { accessKeyId, secretAccessKey }, -}); - -// List objects -const command = new ListObjectsV2Command({ Bucket: "tia" }); -const res = await client.send(command); - -// Get presigned upload URL -const command = new PutObjectCommand({ Bucket: "tia", Key: key, ContentType }); -const url = await getSignedUrl(client, command, { expiresIn: 3600 }); -``` - -### Key Learnings - -1. **S3 API endpoint** does NOT include bucket name: `https://...r2.cloudflarestorage.com` -2. **Public URL** does NOT include bucket path: `https://pub-...r2.dev` (NOT `/tia/...`) -3. **CORS** needs GET and PUT methods for uploads -4. ListObjects needs bucket name in command, not endpoint - -## Environment Variables - -Set in `.env.local` for development, or in Dokploy dashboard for production. - -Required: - -- `DATABASE_URL` - PostgreSQL connection (as `tia_app` role after H2.1) -- `DATABASE_URL_SUPERUSER` - Superuser connection (for migrations only) -- `LITELLM_BASE_URL` - AI gateway URL (e.g., https://llm.manohargupta.com) -- `LITELLM_API_KEY` - AI API key -- `R2_ACCOUNT_ID` - Cloudflare R2 account ID -- `R2_ACCESS_KEY_ID` - R2 access key -- `R2_SECRET_ACCESS_KEY` - R2 secret key -- `R2_BUCKET_NAME` - R2 bucket name (e.g., "tia") -- `R2_PUBLIC_URL` - Public R2 URL -- `CRON_SECRET` - Secret for cron backup endpoint - -### Security Patterns - -All data API routes must validate sessions using `@/lib/auth`: +## Key Patterns +### Session validation in API routes ```typescript import { requireFamily, requireOwnership } from "@/lib/auth"; export async function GET(request: Request) { const auth = await requireFamily(); - if (!auth.success) { - return NextResponse.json({ error: auth.error }, { status: auth.status }); - } - // ... route logic + if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const familyId = auth.session!.familyId!; + // ... } ``` -### Current Security Status (May 2026) - -- **RLS (Row-Level Security):** DISABLED on family_members, children, and growth tables (app-level security via requireOwnership) -- **App-level security:** All routes use `requireFamily()` and `requireOwnership()` checks -- **This is secure because:** All API routes validate session before returning data -- **To re-enable RLS later:** Add proper INSERT bypass policy, keep RLS for SELECT only - -AI routes use medical guardrails from `@/lib/ai/medical-triggers`: - +### ThemeProvider ```typescript -import { detectMedicalIntent } from "@/lib/ai/medical-triggers"; - -const intent = detectMedicalIntent(query); -if (intent.isMedical) { - // Redirect to pediatrician -} +import { useTheme } from "./ThemeProvider"; +const { theme, toggle, setMode } = useTheme(); +// theme: "light" | "dark" | mode: "light" | "dark" | "system" | "time" ``` -## Known Issues +### Offline Queue +Uses localStorage (`tia_offline_queue`) for failed API calls, retries when online. + +### Chat Sessions +Stored in localStorage (`tia_chat_sessions`) — shared between home page AI card and `/ai` page. +Database tables: `chat_sessions`, `chat_messages`. + +--- + +## Email (Resend) + +Transactional emails via Resend. Functions in `src/lib/email.ts`: +- `sendVerificationEmail(email, token, userId)` — account verification +- `sendFamilyInviteEmail({ to, inviterName, familyName, token, role })` — family invite + +Emails fall back to console log if `RESEND_API_KEY` is not set (dev mode). + +--- + +## Family Invites + +Flow: +1. Settings → Invite → `POST /api/invites` → creates row in `family_invites`, sends email via Resend +2. Invitee clicks `/invite/{token}` link → logs in/signs up +3. `POST /api/invites/accept` → adds to `family_members`, **deletes** the invite row (single-use) +4. `DELETE /api/invites/[id]` — cancel a pending invite + +`family_invites` table columns: `id, family_id, email, role, token, expires_at, created_at, display_name, accepted_at` +(Note: `display_name` and `accepted_at` were missing from the original schema — added in migration `0006`) + +--- + +## Growth Page + +- `formatAge(birthDate, measurementDate?)` — shows age as "1y 4mo" or "8mo" +- Goals stored in localStorage (`tia_growth_goal_${childId}`) +- Column order: Goals → Latest Reading → Add Form → WHO+Chart → History +- Color-coded percentiles: 🟢 Normal (15–85th) · 🟡 Watch (<15 or >85) · 🔴 Alert (<3 or >97) + +--- + +## Admin Panel + +Access at `/admin-login`. + +Pages: `/admin`, `/admin/families`, `/admin/users`, `/admin/children`, `/admin/revenue`, `/admin/analytics`, `/admin/support`, `/admin/settings` + +Auth pattern: server component layout calls `verifyAdminSession()` → redirects to `/admin-login` if not authenticated. No client-side cookie checks. + +--- + +## Data Storage Rules + +| Data Type | Storage | API | +|----------|---------|-----| +| Children | Database | `/api/children` | +| Activity Logs | Database | `/api/logs` | +| Vaccinations | Database | `/api/vaccinations` | +| Growth Records | Database | `/api/growth` | +| User Profile | Database | `/api/auth/profile` | +| Parent Avatar | Database (`users.image`) + R2 | `/api/auth/avatar` | +| Baby Photo | Database (`children.image_url`) + R2 | `/api/children/[id]` | +| Memories/Photos | Database + R2 | `/api/upload` | +| Auth Session | Database + Cookie | `/api/auth/signin` | +| Theme | localStorage | `tia_theme` | +| Growth Goals | localStorage | `tia_growth_goal_${childId}` | +| Custom Folders | localStorage | `tia_custom_folders` | +| Offline Queue | localStorage | `tia_offline_queue` | + +**NEVER use localStorage for:** authentication tokens, family_id, or any data that should persist across devices. + +--- + +## Environment Variables + +Set in `.env.local` for development, Dokploy dashboard for production. + +| Variable | Required | Description | +|----------|----------|-------------| +| `DATABASE_URL` | ✅ | PostgreSQL connection | +| `DATABASE_URL_SUPERUSER` | ✅ | Superuser connection (migrations only) | +| `LITELLM_BASE_URL` | ✅ | AI gateway URL | +| `LITELLM_API_KEY` | ✅ | AI API key | +| `R2_ACCOUNT_ID` | ✅ | Cloudflare R2 account ID | +| `R2_ACCESS_KEY_ID` | ✅ | R2 access key | +| `R2_SECRET_ACCESS_KEY` | ✅ | R2 secret key | +| `R2_BUCKET_NAME` | ✅ | R2 bucket name (e.g. "tia") | +| `R2_PUBLIC_URL` | ✅ | Public R2 URL | +| `RESEND_API_KEY` | ✅ | Resend API key for transactional email | +| `EMAIL_FROM` | ✅ | Sender address (e.g. `Tia `) | +| `NEXT_PUBLIC_APP_URL` | ✅ | Full app URL (e.g. `https://tia.manohargupta.com`) | +| `CRON_SECRET` | ✅ | Secret for cron backup endpoint | + +--- + +## R2 Storage (Cloudflare) + +- Account ID: `e71f22a2f8614fb3ba6d9b28a264d8ce` +- S3 Endpoint: `https://e71f22a2f8614fb3ba6d9b28a264d8ce.r2.cloudflarestorage.com` +- Public URL: `https://pub-37a76fd657c94d1dbc521a109c087a11.r2.dev` (no bucket name in path!) + +Key learnings: +1. S3 API endpoint does NOT include bucket name +2. Public URL does NOT include `/tia/` bucket path +3. Direct browser PUT to presigned URL = CORS blocked → always proxy via `/api/upload` + +--- + +## Known Issues & Fixes Applied ### Turbopack Parsing Issue +Some files cause "Unterminated regexp literal" with Turbopack. Fix: `pnpm build -- --webpack` +Avoid patterns like `name: "TT/Td"` in arrays (the `/` can be misread as regex). -Some files may cause "Unterminated regexp literal" errors when building with Turbopack. If build fails: +### users.image vs users.avatar_url +The `users` table uses `image` for the profile photo column. Using `avatar_url` in SQL will throw +"column does not exist". `avatar_url` exists only on `member_profiles`. -1. Try building with Webpack instead: - - ```bash - pnpm build -- --webpack - ``` - -2. The issue is in SWC parser - avoid patterns like `name: "TT/Td"` in arrays as the `/` can be interpreted as regex - -3. Apply fixes one change at a time between builds - cumulative changes can confuse Turbopack's cache +### family_invites missing columns +Original schema was missing `display_name` and `accepted_at`. Added in `drizzle/0006_family_invites_missing_cols.sql`. +Hot-fixed via `/api/debug-migration` POST. +### Admin auth (httpOnly cookie) +`tia_admin_session` is httpOnly — `document.cookie` can never read it. Admin pages use a **server component layout** +that calls `verifyAdminSession()` server-side. Client pages use `credentials: 'include'` on fetches; no Bearer tokens. diff --git a/src/app/api/invites/route.ts b/src/app/api/invites/route.ts index a32e1b3..df1e1d4 100644 --- a/src/app/api/invites/route.ts +++ b/src/app/api/invites/route.ts @@ -65,30 +65,37 @@ export async function POST(request: Request) { [auth.session!.familyId, email, role || "caregiver", token, expiresAt.toISOString()] ); - // Fetch inviter name + family name for the email + // Send invite email — capture result so caller can see if it worked + let emailStatus: { sent: boolean; error?: string; noKey?: boolean } = { sent: false }; try { - const [meta] = await sql.unsafe( - `SELECT u.name as inviter_name, f.name as family_name - FROM family_members fm - JOIN users u ON u.id = fm.user_id - JOIN families f ON f.id = fm.family_id - WHERE fm.family_id = $1 AND fm.user_id = $2 - LIMIT 1`, - [auth.session!.familyId, auth.session!.userId] - ); - await sendFamilyInviteEmail({ - to: email, - inviterName: meta?.inviter_name || "Someone", - familyName: meta?.family_name || "your family", - token, - role: role || "caregiver", - }); + if (!process.env.RESEND_API_KEY) { + emailStatus = { sent: false, noKey: true }; + } else { + const [meta] = await sql.unsafe( + `SELECT u.name as inviter_name, f.name as family_name + FROM family_members fm + JOIN users u ON u.id = fm.user_id + JOIN families f ON f.id = fm.family_id + WHERE fm.family_id = $1 AND fm.user_id = $2 + LIMIT 1`, + [auth.session!.familyId, auth.session!.userId] + ); + await sendFamilyInviteEmail({ + to: email, + inviterName: meta?.inviter_name || "Someone", + familyName: meta?.family_name || "your family", + token, + role: role || "caregiver", + }); + emailStatus = { sent: true }; + } } catch (emailErr) { - console.error("[INVITE-EMAIL-ERROR]", emailErr); - // non-fatal — invite was created, email just didn't send + const msg = emailErr instanceof Error ? emailErr.message : String(emailErr); + console.error("[INVITE-EMAIL-ERROR]", msg); + emailStatus = { sent: false, error: msg }; } - return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}` }); + return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}`, emailStatus }); } catch (error) { console.error(error); return NextResponse.json({ error: String(error) }, { status: 500 });