Answer to "is phone visible in admin?": it wasn't — added it.
- /api/admin/users: SELECT u.phone, return phone in the DTO
- admin users page: new Phone column (tap-to-call tel: link, "—" when empty),
searchable by phone, included in CSV export (now properly quoted so
commas/empty cells don't shift columns)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Existing users (signed up before phone collection) won't have a number.
Show a dismissable home banner prompting them to add one.
- Checks /api/auth/profile on load; shows banner only if user.phone is empty
- "Add" links to /profile; ✕ dismisses
- Dismissal remembered in localStorage (tia_phone_nudge_dismissed) so it
never nags repeatedly
- Dark-mode styled, sits below the vaccine reminder banner
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Google OAuth cannot provide phone numbers (no scope returns them reliably),
so we collect it ourselves. Optional, stored unverified.
- Migration 0011: users.phone text column (+ debug-migration hot-apply step)
- schema/auth.ts: add phone field
- onboarding: optional phone input on step 1; saved to users.phone via the
onboarding API (normalised: leading + then digits, 8-15 digit validation)
- profile page: editable Phone field; loaded from + saved to /api/auth/profile
- /api/auth/profile: GET returns phone; POST accepts & normalises it
(empty string clears, undefined leaves untouched)
Capture point covers both Google and email/password signups since both land
on onboarding. Verification (OTP) and marketing-consent flag intentionally
deferred per product decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avatar (baby card):
- Switch tia-portrait.jpg → tia-portrait.png (white/transparent bg, no dark edges)
- bg-white on container so PNG transparency shows correctly
- object-position: 65% top to shift face right and center it in the circle
Memories grid (all local, no external URLs):
- memory-bath.jpg: baby feet in towel (downloaded from Unsplash, stored locally)
- memory-milestone.jpg: baby in pool float with sunglasses (local)
- Captions updated: 'Tiny toes 🛁' and 'Summer fun 😎'
About page P.S.:
- tia-portrait.jpg → tia-portrait.png (clean white background)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Loading:
- Fraunces, Newsreader, JetBrains_Mono added to (marketing)/layout.tsx
as CSS variables -- one load point, cached for all marketing pages
- 3 utility classes added to globals.css: .font-fraunces, .font-newsreader,
.font-jetbrains
- about/page.tsx: removed duplicate font loading (now from layout)
Font roles applied:
Fraunces → all h1/h2/h3 headings + blog card titles + hero h1 (italic)
Newsreader → long prose blocks: TheProblem, FounderStory card, Privacy
intro, HeirloomVision description, blog article paragraphs,
blog post excerpt
JetBrains → all small uppercase eyebrow labels across every section
Geist → nav, buttons, feature card body, short UI text (unchanged)
Pages updated: homepage, blog listing, blog articles, pricing, partners
About page: fonts resolve identically via layout variables, no visual change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The dark #1f1b16 full-bleed section broke the continuous letter feel.
Replaced with a standard letter article on var(--paper):
- tia-subhead 'What we believe' (same as other subheadings)
- tia-pull.accent for the opening line
- Paragraphs in tia-col as usual
- Creed list (01-06) retains mono numbers in marigold + ink body text,
hairline borders — now reads as part of the letter, not a separate block
Removed all .tia-mani* CSS rules and --mani-bg/fg tokens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Content: complete "A Letter to Every New Parent" letter by Yashika &
Manohar — four named sections (Why We Named the App, Modern Families,
What We Believe, To the Parent Reading This) + signature
Images:
- /images/family-illustration.jpg — watercolour illustration of the
family in the hero, 2-col layout at lg (copy left, image right)
- /images/tia-portrait.jpg — Tia's portrait cropped to circle in
the closing signature
Design:
- Hero: rose→amber gradient, 2-col at lg, family illustration bottom-aligned
- Breadcrumb below hero intro
- Letter body max-w-2xl, generous leading, section borders
- "Nani in Lucknow / Dadu in Jaipur" in amber callout card
- Beliefs rendered as rose dot list
- Key line "Tia is here to help you remember your baby" in rose-700
- Closing parenthetical about Tia in a rose-50 italic card
- CTA: Get started → with hover lift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The home banner read fields (.status, .vaccineName) that the DB-backed
notifications API never returns — its DTO exposes {type, title, message,
metadata,...}. So .vaccineName was undefined → "undefined due today", and
.status was undefined → the ternary always fell to the "due today" branch
even for overdue/nudge items.
Two issues, both fixed:
1. Contract mismatch: render the real `message` field instead of the
non-existent `.vaccineName`/`.status`. message is already correctly built
server-side ("BCG is due today" / "BCG is 3 days overdue").
2. Wrong source set: the banner consumed the FIRST notification of ANY type.
Since the API also returns log/memory/garment nudges, a nudge could land
at index 0 and render under the vaccine header. Now filtered to
type starting with "vaccine_" before use.
Also hardened the vaccine upsert: ON CONFLICT DO NOTHING -> DO UPDATE SET
title/message/action_url/metadata, so future copy/day-count changes refresh
existing rows instead of freezing the first version (preserves id/is_read).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace flex+ml-auto with grid-cols-[1fr_auto_1fr]:
- Col 1 (1fr): logo, left-aligned
- Col 2 (auto): nav links, truly centered regardless of button width
- Col 3 (1fr): auth button, flex justify-end
Works on mobile too: About sits perfectly centered between logo and button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Both About and Blog: text-rose-500 at rest, no border, no background
- Hover: border-rose-300 + bg-rose-50 + text-rose-600 (border space
always pre-reserved via border-transparent to prevent layout shift)
- Active page (usePathname): font-semibold text-rose-600, no visual
container — bold text only, clean
- About: always visible on all breakpoints including mobile
- Blog: hidden on mobile, sm:inline-flex on desktop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MarketingNav:
- About: always visible on all breakpoints; styled as outlined rose pill
(border-rose-300, rose-600 text) — pairs naturally with the solid rose
CTA button beside it; hover fills bg-rose-50
- Blog: hidden on mobile (sm:inline-flex), plain text gray-600 → rose-600
- Use ml-auto mr-3 on nav so links sit right of center, next to the button
Homepage FounderStory:
- Remove 👨💻 / Manohar Gupta / Founder block — author details move to About
- Story content and amber card remain unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MarketingNav:
- Add center nav with About and Blog links (hidden on mobile xs,
visible from sm: breakpoint)
- Logo + nav + auth button now use justify-between with gap-4
Homepage FounderStory section:
- Replace [PLACEHOLDER] blocks with Manohar's real founder story
- Section opens with bold "TIA began with a promise." heading
- Key line "TIA isn't here to track your child. It's here to help
you remember them." highlighted in rose-700
- Closes with "Built by parents. Inspired by our daughter. Made for
families." in a bordered amber footer strip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two stacked causes:
1. ROOT CAUSE (home page field mismatch): the DB-backed notifications rewrite
(678cf65) changed the API response shape, but the home page banner still read
the OLD fields. It checked `notif.status === "overdue"` (no longer returned —
now it's `title`) so it always fell to the else branch `${notif.vaccineName}
due today`, and `vaccineName` no longer exists at top level (now in
`metadata.vaccineName`) → rendered "undefined due today".
Fix: render the complete server-built `message` ("BCG is N days overdue" /
"BCG is due today"). Also filter to vaccine_* notifications so the banner can't
show a log/memory/garment nudge under a "Vaccine Reminder" header.
2. Stale message data: an earlier generator had stored "undefined …" in the
message itself, frozen by ON CONFLICT DO NOTHING. Delete legacy "undefined%"
rows and switch vaccine upsert to DO UPDATE so messages (and overdue
day-counts) stay correct instead of freezing the first version. is_read / id /
created_at preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Launch-critical monitoring wiring — alerts go to tiaBaby_Bot via Telegram.
- src/lib/alert.ts: sendAlert(level, title, detail?, {fields, silent}) — HTML
formatted, IST timestamped, best-effort (never throws). Env: TELEGRAM_BOT_TOKEN,
TELEGRAM_CHAT_ID
- GET /api/healthz: public, no-auth liveness probe (200 ok / 503 down) for
Uptime Kuma + Dokploy healthcheck. No sensitive detail
- cron/backup: alert on failure (fatal), warn if dump < 1KB (empty), silent
success confirmation with file + size
- cron/monitor: error-spike rising-edge detection (last 1h > 5 and > 2x prior
hour — stateless, no re-alert on flat rate), DB/migrations/integration checks.
?test=1 sends a Telegram test ping
- cron/visitor-summary: polls Umami REST API (login -> stats/metrics/active),
posts visitor digest to Telegram. ?hours=N window (default 24)
- CLAUDE.md: new env vars + Monitoring & Alerting section
Health up/down flip detection is delegated to Uptime Kuma (pings /api/healthz);
this code covers what Kuma can't see from outside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Browser was blocking the POST to analytics.manohargupta.com/api/send
with 'Failed to fetch' because the Content-Security-Policy connect-src
only listed plausible.io (old analytics tool, now removed).
- connect-src: replace plausible.io with analytics.manohargupta.com
- script-src: remove plausible.io (no longer needed, Umami script is self-hosted)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
document.currentScript is always null for async scripts (which is how
next/script afterInteractive works). Umami's first check was:
const{currentScript:l}=c; if(!l)return;
This caused an immediate exit — zero tracking.
Fix: fall back to querySelector('script[data-website-id]') when
currentScript is null, so Umami finds its own script tag and reads
the data-website-id / data-host-url attributes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
analytics.manohargupta.com/script.js returns 503 when loaded as a
browser sub-resource from tia.manohargupta.com (same Cloudflare Bot
Management issue as R2 images). Fix: serve the script from /umami.js
(same origin, no cross-origin block) and use data-host-url to tell it
where to POST events back to the Umami instance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drizzle's migrator applies a migration only when its journal `when` is greater
than the max created_at already recorded. Entries 0003-0010 were given 2025-era
timestamps (smaller than the 2026 baseline), so drizzle silently skipped them —
they only applied via the debug-migration hot-apply endpoint. Document the rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-family R2 storage usage (the basis for usage billing), computed from
SUM(size_bytes) over memories + attachments — same basis as the quota system,
so admin numbers match what users are charged on.
- GET /api/admin/storage: per-family bytes/objects (sorted by heaviest),
totals, over/approaching free-limit counts, est. R2 cost, 30-day upload trend.
- /admin/storage page: summary cards, daily upload chart, per-family table with
% of free quota bar and paid/over-limit flags.
- Sidebar: added Storage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each probe now runs independently (pgvector check first and standalone) and
reads the drizzle journal from the correct "drizzle" schema, so the endpoint
returns a usable diagnostic instead of crashing on the first missing relation.
Also reports whether error_events exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migration 0010 (error_events) didn't land via the drizzle journal on prod, so
add an idempotent CREATE TABLE + indexes to the /api/debug-migration steps so
the table can be created instantly via the documented hot-apply endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The baseline schema needs the `vector` extension (memories.vision_embedding),
but nothing created it on deploy — a fresh DB hit "could not access file
vector" and migrations failed.
- migrate.ts now runs CREATE EXTENSION IF NOT EXISTS vector (superuser) before
applying migrations, with a clear error if the Postgres image lacks pgvector.
- /api/debug-migration GET now reports pgvector status (binaryAvailable /
installed) so the image/extension can be checked from the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Analytics showed blank despite real data because one failing sub-query made
the whole route fall back to an empty shape, and the page dropped the error.
- Each sub-query (adoption, families, AI, daily) now runs in its own
try/catch; failures are collected and returned in `error` while the other
sections still render (partial data instead of all-or-nothing).
- Cast all MAX() timestamps to timestamptz inside GREATEST() so mixed
timestamp/timestamptz columns can't error.
- Dedicated COUNT(*) for total families (robust denominator/summary total).
- Analytics page now surfaces the `error` string in a banner instead of
silently rendering empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the admin panel into a real monitoring tool so production bugs are
visible instead of silent.
- Error & crash tracking: error_events table (migration 0010) + logError()
helper + /api/errors ingest; global-error.tsx and (app)/error.tsx report
crashes automatically; /admin/errors viewer (recent + grouped, filters).
- Full audit-log viewer at /admin/audit over the existing audit_log (all
actions, not just auth) with action/resource/family/user/text filters.
- AI observability at /admin/ai over ai_usage: per-intent latency (avg/p95),
tokens, cost, daily trend, slowest calls, medical-redirect count.
- System health at /admin/health: DB latency, migration status, recent error
volume, and integration config presence.
- Sidebar updated with Health / Errors / Audit Log / AI Usage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /admin/analytics page calls /api/admin/engagement, whose per-family
query LEFT JOINed feeds × diapers_logs × sleeps × vaccinations × growth ×
chat_sessions before GROUP BY — a cartesian explosion that times out for
any family with real activity, 500ing the route. The page then ran
[...data.families] on the error object and white-screened.
- Pre-aggregate each log table to one row per child (CTEs) before joining,
eliminating the row explosion; memories aggregated per family.
- Route error fallback now returns the full shape (safe empties) not {error}.
- Page normalizes the response into the full EngagementData shape so a bad
response renders an empty state instead of crashing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Focusing an input with font-size <16px makes mobile browsers auto-zoom
the viewport, which distorted the home "Ask AI" popup UI. Enforce a 16px
minimum on form controls for touch devices via globals.css; desktop and
pinch-zoom accessibility are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Input component renders its own wrapper div, so flex-1 passed to
<Input> only hit the inner <input> (already w-full) while the wrapper
stayed content-width, leaving the box narrow with empty space beside it.
Wrap Input in a flex-1 div so the flex child actually stretches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add pb-16 to /ai root so the input clears the fixed BottomNav
(only page using h-screen instead of min-h-screen+pb-24).
- Normalize new sessions with messages:[] in createNewSession so
render no longer hits undefined.length and crashes on first chat.
- Make render defensive (messages?.length ?? 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add metadataBase to root layout so OG/Twitter/canonical URLs resolve
to absolute https URLs (fixes broken social previews)
- New src/lib/seo.ts with SITE_URL + JSON-LD builders
- New robots.ts (disallow api/admin/private app paths) and sitemap.ts
(marketing pages + blog posts with real lastmod dates)
- JSON-LD: Organization/WebSite/SoftwareApplication on home,
Blog+Breadcrumb on blog list, BlogPosting+Breadcrumb on posts
- Per-page canonical + Open Graph on all marketing pages; article OG
+ Twitter cards on blog posts; per-post dynamic OG image
- noindex on (app) and admin layouts; richer PWA manifest
- Fix CSP to allow plausible.io in script-src/connect-src (analytics
was silently blocked)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove continuous animate-cta-pulse ring — too distracting.
On hover: button lifts 0.5px, scales 1.03×, shadow grows with a
rose-200/60 tint. Fades back on mouse-out. Active state still
scales down to 0.95 for tactile press feel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the static placeholder with an auto-cycling mockup that shows
three real app screens — Home/Quick Log, Vaccinations (IAP), and
Memories — mirroring the actual app card/row UI patterns.
- 2500ms auto-advance, pauses on hover
- Smooth opacity crossfade (duration-500) between screens
- Active dot indicator stretches to pill shape (w-4)
- Floating label pill above phone changes with active screen
- All pure CSS/Tailwind — zero external assets, static page unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Homepage:
- Wrap "Continue with Google" in a relative group; add rose-300 pulse
ring (animate-cta-pulse) that fades out smoothly on hover
- @keyframes cta-pulse added to globals.css (2.6s, scale 1→1.22, opacity 0.45→0)
Blog listing (/blog):
- Remove breadcrumb from above the hero header
- Place it in a pt-5 strip directly above the 3-column content grid
so it reads as "you are here" navigation rather than floating chrome
Blog post (/blog/[slug]):
- Remove breadcrumb from inside the hero gradient section
- Place it in the same pt-5 strip between hero and 3-col grid for
consistent placement across listing + post pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration (0009): notifications table with unique daily/weekly slots,
is_read column, metadata JSONB for vaccine info. No more localStorage
for read state — syncs across devices.
API GET /api/notifications?childId=:
- Generates vaccine notifications (upsert, filtered by given vaccines at query time)
- log_nudge: if no feed/diaper/sleep logged today after noon IST
- memory_nudge: if no photo added to memories today
- garment_nudge: if wardrobe < 10 items (once per week slot)
- Returns unread first, then recent read, limit 60
API PATCH /api/notifications — mark all read for family+child
API PATCH /api/notifications/[id] — mark single notification read
Page /notifications:
- Fetches from real API (no hardcoded mock data)
- Optimistic mark-read on tap, navigates to actionUrl
- Colored cards per type (red=vaccine, amber=log, purple=memory, pink=garment)
- Unread badge + Mark all read button in sticky header
- Legend row at bottom
debug-migration: added notifications table CREATE IF NOT EXISTS for hot-apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replaced hardcoded mock data with real call to /api/notifications?childId=
- Read/unread state stored in localStorage (tia_read_notifications) since
notifications are computed on-the-fly from vaccination schedule, no DB row
- Tapping a notification marks it read individually
- Mark all read button appears in header only when there are unread items
- Unread count badge shown in header
- Empty state now says "All caught up!" instead of generic "No notifications"
- Shows 🚨 for overdue vaccines, 💉 for due-today
- Added due date display in IST-formatted date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old behavior: dismiss once -> never shown again forever.
New behavior:
- Only shows after 3+ sessions (user has seen value first)
- "Later" -> snoozes for 7 days, re-asks after that
- "No thanks" -> snoozes for 30 days
- Once actually installed (Android accepted) -> permanently hidden
- iOS: two buttons (Later / No thanks) instead of bare X, so intent is clear
- Android: tapping X now snoozes rather than permanently suppressing
- Dark mode support added to both banners
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/img: add garments/ to ALLOWED_PREFIXES so garment images proxy correctly
- garments upload: resolve Android empty MIME type from file extension; return
/api/img proxy URLs instead of raw pub-*.r2.dev (blocked by Cloudflare Bot Mgmt)
- garments route + [id] route: toDto() now builds /api/img?key= proxy URLs
- date-ist.ts: add toUTCDate() helper -- strings without Z/offset treated as UTC,
preventing browser local-time misinterpretation; used in fmtTime, fmtDate, dateIST
- LogModal: add editTime to SmartDefault; pre-fill time picker (custom preset) when
editing an existing log instead of defaulting to now
- activity page: pass editTime: log.loggedAt in handleEdit so LogModal pre-fills
- wardrobe/add: explicit Camera and Gallery buttons via separate hidden inputs
(one with capture=environment for direct camera, one without for media picker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wardrobe add page:
- Remove capture="environment" so Android shows the Camera/Gallery chooser
instead of opening the camera directly
- Vision AI no longer blocks the UI: photo preview + form appear instantly
after selecting an image; upload spinner shows on the thumbnail while R2
upload runs; vision AI fires in the background and fills in tags when done
without interrupting the user (they can pick size/occasions in parallel)
- "AI tagging…" pulsing badge in header while vision runs; "✨ AI pre-filled"
badge when done; form fields are only overwritten if the user hasn't already
typed/selected something (functional state updater with prev-value guard)
- Save button is disabled (with "Uploading photo…" label) until R2 upload
completes — prevents saving a garment with no imageKey
/api/time endpoint (GET, no auth):
- Returns { utc, istDate, istTime, ist, offsetMinutes } for the current server
time in Asia/Kolkata so the app can verify server clock and surface IST time
reliably (can be called from browser console at /api/time)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: postgres.js v3 parses `timestamp without timezone` columns as
`new Date("YYYY-MM-DD HH:mm:ss")` (space format, no Z). V8 treats this as
*local time*, so on a non-UTC server (Dokploy host = Europe/Helsinki UTC+3)
the parsed Date object is 3 hours off, making logged times show as server
time instead of the user's IST.
Fixes:
- db/index.ts: add custom `timestamp` type parser that forces UTC by
converting space-format to ISO with 'Z' before calling new Date().
Also set `connection: { TimeZone: "UTC" }` so PostgreSQL sessions always
store/return timestamps in UTC regardless of server OS timezone.
- CalendarView.tsx: use `dateIST()` for day grouping (fixes midnight-boundary
bug where a 12:30 AM IST entry appeared on the previous UTC day) and
`fmtTime()` for time display (replaces toLocaleTimeString without timezone).
- MedicineTab.tsx: replace toLocaleString() with fmtDate/fmtTime (IST-aware).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete UploadProgress component (was debug UI, no longer needed)
- All 3 pages (home, memories, profile) now use simple inline error state
instead of the step-by-step toast
- /api/img proxy: fetch R2 objects server-side to bypass Cloudflare Bot
Management 503s on pub-xxx.r2.dev cross-origin img requests
- All API responses (memories, children, profile) now return /api/img proxy
URLs via toProxyUrl() helper in src/lib/r2-proxy.ts
- Fix memory pipeline: vision failure now marks status='ready' instead of
'failed'; thumbnail failure no longer blocks vision via .catch() separation
- Reset stuck 'processing' memories via debug-migration endpoint
- memories page: replace full-screen overlay with small ⏳ badge on tile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>