Commit graph

358 commits

Author SHA1 Message Date
714909d7ee feat(billing): Task 1 — Razorpay subscription schema + migration
Three tables + lifecycle enum for Razorpay subscriptions:
- subscription_plans: maps razorpay_plan_id -> grants (price/storage/member/child)
- family_subscriptions: per-family sub state mirrored from Razorpay
- razorpay_webhook_events: append-only log, razorpay_event_id = idempotency key
- subscription_status_enum: mirrors Razorpay's lifecycle states exactly
- partial unique index family_live_sub_idx: at most one non-terminal sub/family

Notes:
- Raw-SQL + Drizzle schema both added (repo uses raw sql`` at runtime;
  schema file keeps drizzle-kit + type inference working)
- child_limit added to plan (not in original handoff) since premium lifts the
  free 1-baby cap to 3 per product decision
- Migration 0012 idempotent; also added to debug-migration hot-apply steps
- when=1780100000000 (> last entry, per journal drift rule)

Entitlement will sync onto families.tier (Task 3/5) so existing quota.ts
guards stay unchanged — these tables are audit + Razorpay state mirror.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:10:53 +05:30
87e795c837 feat: show user phone in admin users page
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>
2026-06-02 21:48:57 +05:30
0f3e87b67a feat: home nudge for existing users with no phone number
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>
2026-06-02 21:44:58 +05:30
38bb5af01c feat: collect optional user phone number (onboarding + profile)
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>
2026-05-31 21:25:00 +05:30
5083961c6b fix(mockup): clean PNG avatar, local memory photos
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>
2026-05-31 12:33:59 +05:30
14db731ed4 fix(mockup): Viradhya name, Tia portrait avatar, real photos in memories grid
- Baby name: Arjun → Viradhya (greeting + card name + img alt)
- Baby card avatar: 👶 emoji → tia-portrait.jpg (circle crop, object-top)
- Memories grid: emoji color blocks → real photos
    Slot 1: tia-portrait.jpg (First smile 😊)
    Slot 2: Unsplash baby photo (Bath time 🛁)
    Slot 3: family-illustration.jpg (With family 🌸)
    Slot 4: Unsplash baby photo (8 months! 🎉)
  Each card has dark gradient overlay so white caption is readable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:23:05 +05:30
2608c7a146 fix(marketing): hero subtitle readable, footer links distinct from headings
Homepage hero subtitle:
- Remove font-light (was hard to read on gradient bg)
- Change text-gray-500 → text-gray-800 (dark, legible)
- Keep font-newsreader and leading-relaxed

Footer:
- Brand tagline → font-newsreader
- All footer links → font-normal text-gray-500 (explicit weight prevents
  Fraunces from blending with semibold headings at gray-700)
- globals.css: pin .text-gray-500 { color: var(--color-gray-500) } so
  links read clearly lighter than Company/Legal/Contact headings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:07:01 +05:30
af0dad6922 fix(marketing): Newsreader hero subtitle, Fraunces nav+footer links
- Hero subtitle paragraph → font-newsreader (warm reading serif)
- Nav About/Blog links → font-fraunces (via navLinkClass base string)
- Footer column headings (Company/Legal/Contact) → font-fraunces
- Footer all links and contact details → font-fraunces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:52:16 +05:30
c523533531 feat(marketing): editorial fonts (Fraunces/Newsreader/JetBrains Mono) site-wide
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>
2026-05-31 11:32:44 +05:30
2a450c7644 fix(about): align accent colors with rose/pink brand theme
- --accent: #c98a2b (marigold) → #f43f5e (rose-500) — cascades to
  pull-quote borders, creed numbers, eyebrow lines, ✦ divider,
  callout quote mark, place name highlights
- --accent-2: #b8503e (dusty rose) → #e11d48 (rose-600) — cascades to
  drop cap, display highlight line, closing display text, .tia-pull.accent
- .tia-hero: add rose-50→amber-50→rose-50 gradient (matches homepage hero)
- .tia-cta .big: rose-500 bg + white text + rose shadow; hover → rose-600
- .tia-ps: background tint changed from marigold to faint rose
- Cream paper body, fonts, layout, grain overlay — all unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:01:53 +05:30
69caff5226 fix(about): remove dark manifesto band, flow What We Believe on cream paper
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>
2026-05-31 10:49:14 +05:30
a3d89ee37c feat(about): heirloom letter redesign ported from About.html prototype
Design system (cream paper, keepsake direction):
- --paper #f7f1e6 · --ink #1f1b16 · --accent #c98a2b (marigold)
- --accent-2 #b8503e (dusty rose) · --surface #fbf6ea
- Fonts: Fraunces (headings, italic) · Newsreader (body 19px/1.75)
         JetBrains Mono (labels) · Caveat (hand/caption, already loaded)
- Inline scoped <style> block — no globals pollution

Page structure (letter reading flow):
- Title-only hero with mono eyebrow — no byline, no photo (suspense)
- Opening: drop cap + family illustration floated right in a cream photo
  frame (washi tape + slight rotation + handwritten caption)
- 5 pull-quotes (side border + italic Fraunces) + centered variant
- Quiet italic subheadings with hairline rule
- Callout card: Nani/Dadu passage with marigold handwriting place names
- Dark manifesto band (#1f1b16): numbered creed 01–06
- Authorship revealed only at the end: closing display line → ✦ divider
  → signature "Yashika & Manohar" in Caveat
- P.S.: Tia portrait circle-floated left, playful note in tinted card

Interaction:
- AboutScrollReveal client component: elements above fold visible on
  first paint; below-fold get .tia-pre (opacity 0 + translateY 18px);
  scroll/resize handler removes .tia-pre as they enter viewport
- Respects prefers-reduced-motion — never hides content for users who
  prefer reduced motion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:35:14 +05:30
8f141883bb feat(about): full letter page with family illustration + Tia portrait
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>
2026-05-31 02:30:40 +05:30
1cbdd68756 fix(nav): mobile flex layout restored, desktop 3-col grid, gray→rose colors
Layout:
- Mobile: flex layout unchanged — [Logo] ml-auto [About] [Auth]
- Desktop sm+: 3-col grid [1fr Logo | auto nav centered | 1fr Auth]
  achieved by flex→sm:grid responsive switch + sm:ml-0 on nav

Colors:
- Default: text-gray-600 font-normal (matches app theme, not rose)
- Hover: border-rose-300 + bg-rose-50 + text-rose-500 (border reserved
  via border-transparent at rest to prevent layout shift)
- Active: text-rose-600 font-medium (slight weight bump, not semibold)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 02:22:16 +05:30
f5d21eea28 fix: home "Vaccine Reminder" showing "undefined due today"
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>
2026-05-31 02:15:51 +05:30
52a60a7cff fix(nav): center About/Blog using 3-col grid layout
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>
2026-05-31 02:11:32 +05:30
c5cb9570b2 fix(nav): rose text links, border on hover only, bold active state
- 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>
2026-05-31 02:05:37 +05:30
68a911c6db fix(nav+story): About pill always-on-mobile, Blog desktop-only, remove author block
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>
2026-05-31 01:51:19 +05:30
6df914ddf9 feat(marketing): About+Blog in nav, real founder story content
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>
2026-05-31 01:42:51 +05:30
c1e02249d6 fix: home page vaccine reminder showing "undefined due today"
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>
2026-05-30 22:52:05 +05:30
ab937c4e9d feat: Telegram alerting + public health probe + Umami visitor digest
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>
2026-05-30 22:01:18 +05:30
4e90064989 fix: self-host Umami script to bypass Cloudflare cross-origin 503
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>
2026-05-30 12:16:51 +05:30
7332bd1e8b Add admin Storage & Billing monitor
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>
2026-05-30 09:45:24 +05:30
05975b51a1 Make debug-migration GET a robust pgvector/migration diagnostic
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>
2026-05-30 08:59:16 +05:30
91c25b2c15 Add error_events to debug-migration hot-apply steps
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>
2026-05-30 01:11:01 +05:30
470df7fb9f Ensure pgvector extension in migrator + add pgvector diagnostic
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>
2026-05-30 00:49:43 +05:30
deaa1810d7 feat: add Umami self-hosted analytics with custom event tracking
- Root layout: load Umami script (afterInteractive) — covers all pages including
  SPA navigation auto-tracking
- Marketing layout: remove Plausible script (Umami now covers marketing pages too)
- src/lib/analytics.ts: type-safe track() wrapper + typed helpers for each event;
  window.umami declared globally; safe no-op on SSR/ad-block
- Custom events wired:
    log-created { logType }  — LogModal on successful save
    garment-added            — wardrobe/add after save
    memory-added             — memories after upload pipeline completes
    growth-logged            — growth page after measurement saved
    pwa-installed            — InstallPrompt when Android prompt accepted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 00:40:05 +05:30
cbbe8f24ac Make admin engagement feed resilient + self-diagnosing
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>
2026-05-30 00:37:53 +05:30
7a60132bb2 Add admin observability: error tracking, audit viewer, AI metrics, health
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>
2026-05-30 00:27:07 +05:30
94d9b234f8 Fix admin analytics crash — cartesian join + undefended render
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>
2026-05-30 00:03:48 +05:30
e53c51f044 Fix mobile zoom-on-focus in AI chat (and all inputs)
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>
2026-05-29 23:25:51 +05:30
45f9d6261b Fix AI chat input width — stretch to fill the row
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>
2026-05-29 22:23:05 +05:30
2b534d4c43 Fix AI chat: input blocked by bottom nav + first-chat crash
- 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>
2026-05-29 22:17:10 +05:30
dad0611350 SEO overhaul: metadata, robots, sitemap, structured data
- 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>
2026-05-29 11:03:04 +05:30
39b2787484 fix(hero): remove floating screen label above phone mockup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:39:29 +05:30
e309c91309 fix(hero): replace pulse ring with hover-only lift + rose glow on CTA
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>
2026-05-29 10:36:45 +05:30
261a9cbbcb feat(hero): CSS-animated 3-screen phone mockup carousel
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>
2026-05-29 10:32:39 +05:30
daf6b34281 feat(marketing): CTA pulse animation + breadcrumb repositioned below hero
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>
2026-05-29 10:23:28 +05:30
e5a59c5191 feat(blog): 3-col layout, breadcrumbs, TOC + footer 3-col bottom bar
Blog listing (/blog):
- Breadcrumb: Home › Blog
- Left sidebar: chronological timeline with dot + date + category badge
- Right sidebar: "Browse by topic" category counts, quick reads, CTA card
- Still single column on mobile (sidebars hidden)

Blog post (/blog/[slug]):
- Breadcrumb: Home › Blog › Post title
- Left sidebar: numbered Table of Contents (section headings as anchor links)
  with "← All articles" back link below
- Right sidebar: "More articles" list + "Filed under" category + CTA
- scroll-mt-28 on headings so sticky nav doesn't cover anchor targets
- Both back-to-listing links visible (sidebar + article footer)

Footer bottom bar:
- Split into 3-column grid: © left · privacy tagline center · ❤️ India right

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:06:14 +05:30
8be6bbe23f feat(blog): proper blog structure with 4 sample posts + footer polish
Blog:
- posts.ts data file with 4 authored posts: feeding by age, health
  warning signs, getting started guide, Telegram alerts feature
- /blog listing page with category pills, read time, hover cards
- /blog/[slug] post template: hero, sections (paragraphs/lists/tables/
  callouts), IAP schedule table, article footer CTA
- All 4 posts prerendered as SSG (generateStaticParams)

Footer:
- Update phone number to +91 95548 81799
- ❤️ grows on hover with inline-block + hover:scale-150 transition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 09:39:00 +05:30
678cf65d70 feat: DB-backed notification system with vaccine + activity nudges
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>
2026-05-29 09:33:51 +05:30
ee4bcc4498 fix: notifications page — wire to real API, make Mark all read functional
- 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>
2026-05-29 01:08:18 +05:30
e7a332cacd tweak: install prompt snooze — Later=2d, No thanks=7d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 01:05:00 +05:30
093903162e improve: smarter install prompt — visit gate + snooze instead of permanent dismiss
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>
2026-05-29 01:00:52 +05:30
3cfcbdc0ca fix: garment upload MIME/proxy, log edit time pre-fill, date-ist hardening, wardrobe camera+gallery
- /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>
2026-05-29 00:42:04 +05:30
e99a874309 Wardrobe: gallery picker + non-blocking vision AI; add /api/time endpoint
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>
2026-05-28 23:31:18 +05:30
cfb0f4b2eb Fix timestamp timezone — logs now always show in IST regardless of server TZ
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>
2026-05-28 23:13:57 +05:30
d8c9500949 Remove UploadProgress debug toast; fix R2 image proxy and memory pipeline
- 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>
2026-05-28 22:53:16 +05:30
27709dc851 Add one-time fix: reset stuck processing memories to ready
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:38:45 +05:30
f953963b3b Fix memories disappearing + always-processing state
Three bugs fixed:

1. Vision failure was marking memories as 'failed', triggering orphan
   cleanup that permanently deleted them. Vision is optional — on error,
   now marks 'ready' so the image stays visible without AI captions.

2. Thumbnail failure was blocking vision from running (rejected promise
   swallowed the chain). Fixed by catching thumbnail error separately so
   processMemoryVision always executes and always sets a final status.

3. /api/img proxy was rejecting thumbnail keys (families/{id}/thumbnails/…)
   because 'families/' was not in ALLOWED_PREFIXES. Added it.

Also: replaced the full-bleed 'Processing…' overlay with a small corner
badge so the uploaded photo is visible immediately after upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:37:46 +05:30