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>
- Moved guidelines row ABOVE the 4-day strip (correct order)
- 4-day strip: each chip is now tappable
→ opens a day-detail bottom sheet showing all logs for that day
→ each log row has ‹ › arrow; tap opens edit/delete action sheet
→ empty state tells mama to use Generate sample history to pre-fill
→ quick-add row at bottom (+ Feed / + Sleep / + Diaper) for fast logging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove redundant daily summary bar (Today chip in 4-day strip covers it)
- 4-day strip: reversed to oldest→newest order (Wed→Thu→Yest.→Today)
- 4-day strip: switched from flex to grid grid-cols-4 so all 4 chips
fill the full row width evenly instead of floating left
- Today chip highlighted in rose-400 to stand out from past days
- Guidelines: corrected 9-12 mo (feeds 3→4, sleep 12→14h, diapers 3→4)
per AAP; 12-18 mo sleep 11→13h, diapers 3→4; 18-24 mo sleep 11→12h,
diapers 2→3; all now match mid-range AAP recommendations
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>
Reverted fixed viewport positioning back to absolute — the parent cards
no longer have overflow-hidden so absolute works correctly now. Both
dropdowns (profile share and per-product share) appear directly below/above
their trigger button instead of floating at a random screen corner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: parent div had overflow-hidden which clipped absolute-positioned
dropdowns, making them render but be invisible (looked like click did nothing).
- Removed overflow-hidden from profile card container
- Share dropdowns now use fixed positioning to escape any parent overflow
- Merged chevron into the expand button so the whole left area is one tap target
- Per-product share sheet also changed to fixed bottom-28 right-4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Each product row now has an ↗ share button (product URL shared, not profile)
WhatsApp message: "Found this for our baby — [title]: [url]"
- Only one product share sheet open at a time; closes on backdrop tap
- Profile page share (↗) moved to the profile card header row where it
contextually belongs — shares the /m/slug link, not a product
- Removed the share button that was on the Products section header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapsible profile card:
- Starts collapsed if profile already exists (shows name + URL slug)
- Expands/collapses via chevron toggle row
- Auto-collapses after Save Profile succeeds
Share sheet on Product Recommendations:
- ↗ Share button appears once a valid slug is set
- Options: Copy link (clipboard, shows ✅ feedback), WhatsApp deep link,
and native Web Share API ("More options") when browser supports it
- Backdrop click closes the sheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings/Sign Out used absolute bottom-0, overlapping the bottom of the
menu items list on shorter screens. Converted to normal flow with mt-4
and added pb-28 so the list clears the bottom nav bar.
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>
- Convert Quick Log from grid-cols-4 to horizontal scroll strip so
it scales to any number of actions without layout breakage
- Add Wardrobe 👗 shortcut linking directly to /wardrobe/add,
saving mama the Menu → Wardrobe → Add Garment 3-tap journey
- Fix: replace non-existent `no-scrollbar` class with `scrollbar-hide`
across page.tsx, wardrobe pages, memories page, and TabBar component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Production runs Postgres 18; the dev compose file pinned pg16. A pg_dump
from prod (v18) cannot be restored by a v16 pg_restore — the dump header
is rejected. Matching the major version fixes restores and removes a
latent source of dev/prod behaviour drift.
Also adopts the pg18 image's data-directory convention: the volume now
mounts at /var/lib/postgresql (the image places data in a version
subdirectory), and drops the obsolete compose 'version' key.
Makes schema changes deploy automatically: edit schema -> db:generate ->
commit -> push -> Dokploy redeploys -> migrations apply on container start.
No more Dokploy database terminal.
Components:
- src/db/migrate.ts: standalone migrator (single short-lived connection,
fails loud on error so a bad migration crashes the container instead of
letting the app serve a half-migrated schema)
- scripts/build-migrator.mjs: esbuild bundles migrate.ts -> dist/migrate.mjs
with drizzle-orm + postgres inlined. Needed because Next.js standalone
output keeps neither as a separate node_modules package.
- Dockerfile: builder runs db:build-migrator; runner copies migrate.mjs +
drizzle/; CMD is 'node migrate.mjs && node server.js'
- package.json: db:generate / db:migrate / db:studio / db:pull /
db:build-migrator scripts; esbuild promoted to an explicit devDependency
- pnpm-lock.yaml resynced
BUG FIX: .dockerignore had 'drizzle/' — migration SQL was excluded from the
build context, so even a correct Dockerfile COPY would have found nothing.
This was the second half (with the .gitignore bug in commit 1) of why the
migration pipeline never worked. Now only _archived/_introspected are
excluded.
Verified: full docker build succeeds; runner image contains migrate.mjs +
drizzle baseline; migrator tested end-to-end against a scratch DB (35
tables created, __drizzle_migrations populated, idempotent on rerun).
drizzle-kit generate against the now-prod-aligned schema produces a
single baseline migration covering all 35 tables.
VERIFIED: 0000_baseline_prod_2026_05_19.sql was compared column-for-column
and type-for-type against the drizzle-kit pull introspection of tia_prod.
Table sets identical, all columns and types match. The baseline is a
faithful representation of production.
This baseline will be marked as already-applied in prod's
__drizzle_migrations table (done out-of-band, not in git), so the migrator
runs nothing on the next deploy. It exists purely as the reference point
for future schema diffs.
Adds drizzle/README.md documenting the baseline reset and the migration
workflow going forward.
The drizzle/ folder was in .gitignore (line 34) — likely confused with
the build 'out/' dir. Effect: migration SQL never reached the server on
deploy, so the migration pipeline could never have worked. Only 7 of 18
files were ever force-tracked; 0000-0010 + most of manual/ were untracked.
- Remove drizzle/ from .gitignore; document why it must be tracked
- Archive legacy hand-rolled migrations 0000-0015 + manual/ to
_archived_pre_baseline_2026-05-19/ (kept on disk; history retains old copies)
- Archive stale meta/ (knew of only 3 of 16 migrations)
- Baseline regeneration follows in subsequent commits
- Remove 📥 export button from growth page header (less clutter)
- Add "Export Growth Data" row in settings with child name and CSV download
- Fetches growth records on demand, shows loading state while exporting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Measurement form now appears directly below the header (not buried
after WHO card) so it's immediately visible when + Add is tapped
- Removed `&& latest` guard — form now works even with zero records
- Goals and Add are mutually exclusive: opening one closes the other
- 3-column grid for measurement inputs (weight / height / head on one row)
- Sticky header with backdrop-blur, smaller title (text-sm), icon-style
export and goals buttons
- All cards use rounded-2xl + shadow-sm for consistent look
- "Add First Measurement" in empty state scrolls to top and opens the form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- grid-cols-4 (~40% smaller than 3-col) with gap-1 and rounded-xl corners
- Hover: scale-110 image + dark overlay + expand icon (⤢)
- Subtle shadow + ring border on each tile
- Folder emoji + name shown below each tile (when assigned)
- Filter out tiles with no URL and remove tiles that fail to load (onError → null)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header title reduced to text-xs (~50% smaller)
- Tap image in viewer to zoom 2x; tap again to zoom out
- Tap outside image area to toggle UI chrome (top bar / captions)
- "New folder" pill at end of folder row and in upload picker
- Custom folders saved to localStorage (tia_custom_folders)
- Custom folders appear in folder pills, upload picker, and move-to-folder sheet
- Emoji picker (15 options) + name input for new folders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Direct PUT to R2 presigned URL is cross-origin, causing "Failed to fetch"
in browsers without R2 CORS configured. Use the existing PUT /api/upload
proxy handler instead — file goes client → Next.js → R2 server-side.
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>