Commit graph

392 commits

Author SHA1 Message Date
2bd45bd4fd feat(billing): Tasks 4-5 — create-subscription + webhook routes
Task 4 — POST /api/subscriptions/create:
- family_id from session (requireFamily) — IDOR-safe, never from body
- rejects if a live sub exists (also enforced by partial unique index)
- creates RZP sub via fetch Basic auth, total_count 120, notes carry family_id
- inserts family_subscriptions row 'created'; returns subscriptionId + keyId only
- key_secret never sent to client

Task 5 — POST /api/webhooks/razorpay (source of truth):
- RAW body, timing-safe HMAC over webhook secret
- idempotency: unique insert on x-razorpay-event-id; duplicate -> 200 bail
- routes events -> family_subscriptions status + syncs families.tier:
    authenticated/activated/charged/resumed/pending -> grantPremium (pending=grace)
    halted/cancelled/completed/expired/paused -> revokeToFree
- 400 bad sig, 200 success/duplicate/unknown, 500 processing error (retry)

middleware: /api/subscriptions protected; /api/webhooks/razorpay intentionally
public (authenticates via HMAC, not cookie).

Verified locally: HMAC valid/tampered, unix->date, event routing maps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:18:00 +05:30
6a1aaa38a2 feat(billing): Tasks 2-3 — config, plan seed, entitlement sync
Task 2:
- lib/billing/config.ts: reads 4 Razorpay env vars (throws at call time, not
  boot), premium grant constants (50GB / 6 members / 3 children / ₹199),
  razorpayAuthHeader() Basic-auth helper
- POST /api/admin/seed-plan: admin-only idempotent upsert of the Premium plan
  row from env + constants (GET shows current plans). Re-runnable, no DB shell

Task 3:
- lib/billing/entitlements.ts: grantPremium() / revokeToFree() sync onto
  families.tier + max_members + max_children. Existing quota.ts guards UNCHANGED
  — they already read these via isPaidFamily(). revoke = limit downgrade only,
  data untouched (freeze-not-demote)
- ENTITLED_STATUSES (active/authenticated/pending=grace) + TERMINAL_STATUSES

No guard refactor needed: chosen "sync to families.tier" approach means the
3 existing guards (storage/member/child) keep working as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:14:47 +05:30
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
a89ab96a12 fix: add analytics.manohargupta.com to CSP connect-src
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>
2026-05-30 21:30:54 +05:30
27b07a5cfc fix: patch umami.js to work with Next.js async script injection
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>
2026-05-30 21:21:53 +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
67f7c4836d docs: warn that migration journal when must exceed the last applied
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>
2026-05-30 09:55:35 +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