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>
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>
pub-xxx.r2.dev returns 503 for cross-origin sub-resource requests (img tags,
fetch) due to Cloudflare Bot Management on the r2.dev dev domain. Direct
browser navigation works but programmatic loading fails, so all uploaded
images appeared as placeholders after upload.
Fix: route all image display through a same-origin /api/img?key=... proxy that
fetches from R2 via the S3 API server-side. API responses (profile, children,
memories) now return proxy URLs. After upload, UI state is updated with proxy
URLs directly rather than raw R2 URLs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>