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>
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>
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>
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>
- 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>
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>
- FAB raised to bottom-20 to clear fixed bottom nav
- Branded loading: bouncing 🍼😴🚼 emojis
- Back button: white pill with shadow (matches other pages)
- Generate History moved to ⋯ overflow menu (keeps header clean)
- Filter pills: emoji labels (🍼 Feed / 😴 Sleep / 🚼 Diaper) + scrollbar-hide
- Daily summary bar: today's feed/diaper/sleep counts at a glance
- 4-day overview strip: quick multi-day snapshot above timeline
- Collapsible guidelines card: collapsed shows x/target fractions,
expands to progress bars with actual/target display
- Today/Yesterday/weekday labels in timeline; Today styled in rose
- Better empty state with emoji and "Tap + to start logging" CTA
- Tap any log row → action sheet with Edit and Delete
Edit: pre-fills LogModal with same values, deletes old log on save
Delete: inline confirmation (no browser confirm()), then refresh
- New DELETE + PATCH handlers at /api/logs/[id] with family ownership check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add persistent bottom navigation bar (Home / Activity / AI / Menu)
- Fix TodaySummary bug: last-log times now show today's events only
- Replace 6 hardcoded AI chips with 3 AI-generated context-aware chips
- Show child's real profile photo in baby card (fallback to 👶 emoji)
- Recent Activity limited to 3 items with "See all →" link to /activity
- "Suggested now" promoted to prominent amber banner with "Log it →" CTA
- Offline pending banner is now a tappable retry button
- Branded loading state with bouncing emoji (🍼😴🚼👶)
- Remove unused Button import from page.tsx
- Expose image_url via /api/children and Child type/FamilyProvider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract offline queue to src/lib/offline-queue.ts
- Extract shared LogModal with time presets (Just now/5/15/30min/Custom)
and smart default pre-fill from last log of same type
- Replace ActivityScroller with TodaySummary (today's counts + last time)
- Fix activity page: GET /api/logs without type param now returns all logs merged
- Fix field naming: log.loggedAt / log.amount (camelCase throughout)
- Add FAB to activity page for zero-navigation quick logging
- Recent Activity shows 5 most recent entries with correct field names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. GET /api/logs required a `type` param; activity page calls without one,
always receiving {error} → empty list. Fixed: when type is omitted,
merge feeds + diapers_logs + sleeps, sort by time, return camelCase
fields (loggedAt, subType, amount) matching the activity page interface.
2. Type-specific queries (used by homepage) still return raw snake_case
rows unchanged so the homepage "Recent Activity" section is unaffected.
3. LogModal initialised subType as "breast_milk" regardless of log type.
Adding a useEffect to reset it (breast_milk / wet / nap) when type
changes prevents bad enum values from being sent for diaper/sleep logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire Resend to /api/auth/reset-request with fallback for dev
- Install: sharp, recharts, next-pwa, resend, @react-pdf/renderer, @types/sharp
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add auth to /api/ai via requireFamily middleware
- Remove /api/ai and /api/auth/debug from public routes
- Delete debug/test routes that expose internal state
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add PUT and DELETE endpoints for growth records
- Add CSV export for pediatrician visits
- Add goal tracking with localStorage persistence
- Color-coded percentiles (green/yellow/red zones)
- Show WHO percentile lines (15th, 50th, 85th) on chart
- Growth velocity indicator (kg/month between readings)
- Enhanced WHO standards card with actual vs target + goal progress
- Better empty state with encouraging prompt
- Fix UUID type for growth record IDs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AI route was reading LITELLM_URL/LITELLM_KEY but env vars are named
LITELLM_BASE_URL/LITELLM_API_KEY — causing 503 on every request.
Home page chat now creates a fresh session each time the modal opens
and resets homeSessionId on close, so conversations don't pile into
the same old session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simple hash_ format stored by admin PATCH was incompatible with bcrypt
verification in /api/auth/signin, causing "Invalid password" on login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Route was returning { test: "ok", count: N } instead of the expected
overview/conversions/growth/childrenByAge structure, crashing the dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix redirect from /admin/login to /admin-login
- Add DELETE endpoint for logout
- Connect logout button to API
- Remove unused admin state/localStorage
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Login page checks cookie on load via useEffect, no need for
localStorage token. More secure this way.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The API was setting the session token as a cookie but not
returning it in the JSON response. Login page expects token
in the response to store in localStorage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>