- New families.pediatrician_name column (migration 0008)
- Settings card: Name input above Phone input, single Save
- Emergency page: shows doctor's name above the call button
- AI medical redirects: personalised "Call Dr. X: +91…" message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature A — Storage quota (1 GiB per family):
- src/lib/quota.ts: enforcement library with pure functions (fully unit-tested)
and DB-bound helpers; isPaidFamily() is the single payment abstraction gate
- src/lib/format-bytes.ts: extracted formatBytes() — safe for client imports
- POST /api/upload: quota check before presigned URL issuance (HTTP 402 + reason code)
- POST /api/memories/[id]/confirm: HeadObject reconciles actual R2 size; deletes
over-quota objects and marks row failed rather than silently exceeding limit
- GET /api/storage-usage: storage info endpoint for UI meter
- src/components/StorageMeter.tsx: meter bar + StorageQuotaBanner + MemberLimitBanner
- memories/page.tsx: quota banner, FAB disabled (⊘) when exceeded, compact meter in header
- settings/page.tsx: always-visible StorageMeter + MemberLimitBanner in invite section
Feature B — Member limit (2 per family, free tier):
- invites/route.ts: replaced ad-hoc inline check with checkMemberLimit() from quota lib
Structured 403 response: { reason, currentCount, limit }
- Freeze rule: paid→free downgrade leaves all members intact; only new invites blocked
Migration:
- drizzle/0007_subscription_status.sql: ADD COLUMN subscription_status varchar(20)
- debug-migration/route.ts: step added for hot-apply without full redeploy
- src/db/schema/family.ts: subscriptionStatus field added to Drizzle schema
Tests: 44 unit tests in src/__tests__/quota.test.ts, all passing
- Pure function tests (no DB): isPaidFamily, wouldExceedQuota, isAtMemberLimit, formatBytes
- DB-bound tests (mocked @/db): getFamilyStorageUsage, checkStorageQuota,
checkMemberLimit, getStorageInfo, tenant isolation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Favicon → 🌸 cherry blossom SVG (replaces smiley)
- Nav: removed Pricing/Privacy links; nav is now invisible at top and
slides in after scrolling 75% past the hero (scroll-reveal client component)
- Hero CTA: white background + proper 4-color Google G (matches login page)
- Hero subtitle: font-light, text-xl, leading-loose for a more editorial feel
- Feature cards: hover border highlight + emoji scale on group-hover
- Heirloom vision cards: hover border on hover
- Privacy items: bg-rose-50 on hover
- Final CTA button: hover shadow lift + active:scale-95
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add (marketing) route group: /, /pricing, /privacy, /terms
- Add (app) route group: moves all authenticated pages, app home → /home
- Root / is now a static marketing page (zero DB imports, zero auth)
- NavAuthButton client component: shows "Open Tia →" if logged in, else "Continue with Google"
- Plausible analytics hook in marketing layout
- Auto-generated OG image via opengraph-image.tsx
- Middleware updated to allowlist marketing routes
- All /-redirects updated to /home (login, onboarding, invite, circle join)
- BottomNav home tab updated: / → /home
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Primary sender: tia@tia.manohargupta.com (verified subdomain in Resend)
- sendWithFallback() retries with onboarding@resend.dev if Resend rejects
the primary domain (covers the window while SPF is still propagating)
- Both sendFamilyInviteEmail() and sendVerificationEmail() use the fallback
Update EMAIL_FROM in Dokploy to: Tia <tia@tia.manohargupta.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md:
- Add RESEND_API_KEY, EMAIL_FROM, NEXT_PUBLIC_APP_URL to required env vars
- Document DB migration pattern (journal + hot-fix via debug-migration POST)
- Document R2 3-step proxy upload pattern (CORS note)
- Document users.image (NOT avatar_url) and two separate photo features
- Document admin auth server-component pattern
- Document family_invites fix, invite flow, cancel invite
- Full data storage table with all localStorage keys
Invite route:
- Return emailStatus in POST response so caller can see if Resend fired
or why it failed (noKey / error message)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New DELETE /api/invites/[id] endpoint — only the owning family can delete
- Settings page shows expiry date on each pending invite
- Cancel button removes invite instantly from list (optimistic UI)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ALTER TABLE steps to debug-migration POST so the columns can be applied
immediately via the running app without waiting for a full Dokploy redeploy.
Also revert invites routes to use display_name/accepted_at now that the
hot-fix path exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The family_invites migration hasn't run yet on production. Work around by:
- Removing display_name from INSERT and SELECT (optional field anyway)
- Removing accepted_at IS NULL filter from GET and accept queries
- DELETE the invite row on accept instead of marking accepted_at — keeps
invites single-use without needing the extra column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add sendFamilyInviteEmail() to email.ts using existing Resend setup
- Wire into POST /api/invites — fetches inviter name + family name, sends
warm invite email with Accept Invitation button linking to /invite/{token}
- Email is non-fatal: invite is created even if email send fails
- Register 0006_family_invites_missing_cols in _journal.json so Dokploy
auto-applies the migration on next deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both columns are referenced by the invite API but were never in the table,
causing "column does not exist" errors when inviting family members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
users.avatar_url doesn't exist — the column is `image`. Querying/updating
a non-existent column caused a SQL error on every profile load (blank name
& email) and on every avatar save/delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch from broken FormData POST to 3-step flow: POST init → PUT /api/upload proxy → PATCH save
- Add remove photo option that clears DB and deletes R2 object (DELETE /api/auth/avatar)
- Add deleteOldAvatar() helper that cleans up old R2 object on every upload
- No orphaned objects in R2, no wasted storage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Tapping avatar when a photo exists shows a mini menu: Change photo / Remove photo
- Tapping avatar when no photo exists goes straight to file picker (no menu)
- handleRemovePhoto: PATCHes imageUrl=null, deletes file from R2, clears UI
- PATCH /api/children/[id]: fetches old image_url before update, deletes the
old R2 object (profiles/ prefix only) when it changes — no more orphaned files
- updateChildImage in FamilyProvider now accepts string | null
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Direct PUT to R2 presigned URL is cross-origin — browser blocks it.
Route the upload through the existing PUT /api/upload server proxy instead,
same pattern used for memories. Also return `key` from children POST so
the proxy call has the R2 object key without needing the presigned URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New POST /api/auth/avatar — accepts multipart FormData, uploads image to
R2 under avatars/{userId}/{ts}.ext, saves URL to users.avatar_url
- GET /api/auth/profile now returns avatarUrl field
- /profile page: real avatar display (image or initials fallback), hidden
file input wired to "Change Photo" button, spinner overlay while uploading,
inline success/error message; name save and photo upload are independent
NOTE: This is the parent user avatar (mama/daddy). The baby profile photo
on the homepage card is separate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Non-today rows: hover:bg-rose-50 / dark:hover:bg-gray-700
- Today (rose) rows: hover:bg-white/20 overlay so highlight still reads on rose bg
- transition-all for smooth fade
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add group + hover:bg-rose-50 + hover:shadow-sm to timeline log rows
- Add group + hover:bg-rose-50 to day-sheet log rows
- Chevron › turns rose-400 on hover (group-hover) in both places
- transition-all for smooth background + shadow animation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
External favicon URL (www.google.com/favicon.ico) fails to load in
production due to CSP/network restrictions. Inline SVG has no external
dependency and renders the correct Google logo at all sizes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Behind Traefik, request.url resolves to http://0.0.0.0:3000/... (the
internal Docker address). Using that as the redirect base sent browsers
to 0.0.0.0, causing ERR_SSL_PROTOCOL_ERROR. Switch all server-side
redirects in the Google callback and verify-email routes to use
NEXT_PUBLIC_APP_URL (with tia.manohargupta.com fallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Signup now creates unverified users and sends a verification email
(Resend); dev falls back to [VERIFY-LINK] console log
- /api/auth/verify-email: single-use token handler, mints tia_session
on success, redirects to /onboarding
- /api/auth/resend-verification: rate-limited (3/hr), enumeration-safe
- Sign-in gated on email_verified — unverified accounts get 403 with
needsVerification flag so the UI can show the resend button
- Google OAuth via arctic v3: PKCE + state anti-CSRF, find-or-create
user, writes accounts row, mints tia_session
- Login page: Google button, check-email screen, resend link on 403
- drizzle/0005_email_verification.sql: creates email_verifications
table + backfills all existing users as verified (runs automatically
on container start before app boots)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users page:
- Delete was silently failing with FK constraint violations — sessions,
accounts, support_tickets and family_members all have ON DELETE no action
refs to users. Now cascades in correct order before deleting the user.
- Added error/success toast notifications so failures are visible
- Delete button shows loading spinner while in-flight; all buttons
disabled during operation to prevent double-submit
- Always optimistically removes row on success (no full refetch needed)
Families page:
- Replaced browser prompt() for "New Family" with inline form — prompt()
is blocked in some environments (CSP, iframes, browser settings)
- Fixed role-before-email bug: role dropdown was silently lost when changed
before typing email, because onChange reset the whole addMember state.
Now uses per-family form state with stable field updates.
- Remove member button shows loading spinner; disabled during operation
- Tier change button shows loading; disabled during other tier changes
- Added error/success toast notifications for all actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /admin/activity page: live login events, failed attempts, active
sessions from audit_log + sessions tables; auto-refresh toggle
- New /api/admin/activity route: queries audit_log + sessions for
stats (active sessions, logins/failures 24h, signups 7d) and events
- Fix /api/admin/stats: real growth charts (families/users by day),
real children-by-age, real conversion rate, active sessions count,
and login/failure counts — was all hardcoded empty arrays before
- Fix /api/admin/analytics: avg logs per family now divides by actual
family count instead of hardcoded 1
- Dashboard: 6-card grid adding Active Sessions + Failed Logins 24h
with links to Activity Monitor; bar charts now show hover counts
- Families: inline tier upgrade/downgrade button (Pro ↑ / Free ↓)
wired to existing PATCH API; member panel polished
- Support: admin reply thread using support_responses table; Cmd+Enter
to send; conversation view with original message + admin replies;
auto-moves ticket to in_progress on first reply
- Settings: honest read-only display for env-var-controlled settings
(pricing, AI config); editable free-tier limits that write to DB
- New /api/admin/families/limits route for bulk free-tier limit update
- Sidebar: added Activity Monitor nav item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapsed state now fits entirely in one row:
[avatar] [name] · [truncated text] [tiny thumbnail] [▼]
No second row needed. Expanded state shows the normal two-line
author block (name + time) as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Collapsed preview (thumbnail + text) moved to its own row centered
below the author line instead of being pushed to the right
- Chevron ▼/▲ stays on the author row right side as the only element
- Edit post and Delete post now call setCollapsed(false) first so the
edit textarea / delete confirmation is always visible when triggered
from a collapsed card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Posts start collapsed showing author, timestamp, a thumbnail (if image)
and truncated text preview with ▼ chevron. Tap the author row to expand
the full post (body, full image, reactions, comments). Tap again to
collapse. Lets users scan many posts quickly and expand only what
interests them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove overflow-hidden from PostCard root so the ⋯ dropdown menu
is no longer clipped by the card boundary
- Add Edit Post option to menu (own posts only) with inline textarea
and Save/Cancel; calls new PATCH /api/circles/[id]/posts/[postId]
- Add PATCH endpoint: author-only text edit
- Fix image display: object-contain (no crop) instead of object-cover
- Add tap-to-fullscreen lightbox: clicking any post image opens a
full-screen black overlay with the image at natural size, ✕ to close
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Direct browser PUT to R2's S3 endpoint is blocked by CORS. Replace the
presigned-URL client-side upload with a server-side upload endpoint:
client sends FormData → server uploads to R2 with PutObjectCommand →
returns tmpKey. No browser-to-R2 connection needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Server: normalise empty/missing MIME type by sniffing file extension so
iOS HEIC/HEIF and camera photos (which send empty type) are accepted
- Server: add image/heif and image/gif to allowed types
- Server: return normalised contentType in presign response
- Client: check presignRes.ok before uploading; use server contentType
for the PUT to R2 so the header matches what was signed
- Client: show error message in modal instead of silent catch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin invites by entering email instead of copying a link
- If email matches existing Tia user → creates pending invite visible
on their Circles page with Accept/Decline buttons
- If email is not registered → sends Resend email with signup link
that lands them directly in the circle after account creation
- DB migration adds invited_email + invited_family_id to circle_invites
- New GET /api/circles/invites endpoint for pending invite banners
- Remove clipboard-copy approach entirely
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catches errors from the circle_members SELECT query and auth
that were escaping the narrower try-catch and returning empty 500s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues prevented 0003_circles.sql from running:
1. Missing -->statement-breakpoint markers (Drizzle splits SQL by these)
2. migrate.ts used DATABASE_URL (tia_app, no DDL privileges) instead of
DATABASE_URL_SUPERUSER — now prefers superuser URL with fallback to
DATABASE_URL for local dev
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SQL file existed but was missing from _journal.json so the
migrator skipped it on deploy. Adding the journal entry ensures
the circles tables are created on next container boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds full circle functionality — private social groups for trusted families
to share milestones, memories, and posts with reactions and comments.
- 7-table DB migration: circles, members, invites, posts, comments, reactions, reports
- 11 API routes: create/list circles, posts feed, comments, emoji reactions, invite tokens, join flow, member management, reporting
- 3 new pages: /circle (list), /circle/[id] (feed + PostCard + CreatePostModal), /circle/join/[token]
- Copy-on-share for memory photos (independent R2 objects, never references originals)
- Admin controls: invite generation, member promote/demote/remove, last-admin guard
- C9 privacy consent screen before first post
- Menu entry added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New PATCH /api/children/[id] — updates image_url in children table
- New POST /api/children/[id] — returns presigned R2 URL for profile
photos (stored under profiles/{childId}/ prefix, no memories row)
- FamilyProvider: expose updateChildImage() so UI updates instantly
without a full re-fetch after upload
- Home page baby card: photo avatar is now a separate tap target from
the growth link. Tap photo → file picker → upload to R2 → save URL.
Camera overlay (📷) appears on hover/tap; ⏳ shown while uploading.
Tapping name/age/arrow still navigates to growth as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each row in a day chip is now its own button. Tapping 🍼×2 on Thursday
opens a sheet scoped to feeds on Thursday only — not all logs for that day.
Sheet shows entries for that specific type, with edit/delete per entry and
a single focused "+ Add [type]" CTA at the bottom.
Rows showing ×0 render dimmed so missing entries stand out at a glance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>