Commit graph

5 commits

Author SHA1 Message Date
51e36633b9 Add step-by-step upload progress UI across all three upload points
Each upload now shows a persistent card with 3 labelled steps and their
live status (pending → active → done / error). Errors include the exact
HTTP status code + raw response body (handles non-JSON from Traefik,
nginx, etc. that return HTML error pages). The card stays visible after
failure so the user can read the diagnostic before dismissing.

Changes per surface:
- src/components/UploadProgress.tsx — new shared step-tracker component
- profile/page.tsx — step card rendered below avatar; safeResponseText()
  reads raw body so a Traefik 413 shows "HTTP 413: <html>..." not just
  "Upload failed"
- memories/page.tsx — fixed toast expands to show all 3 steps; dismissible
  after done/error; same safeResponseText pattern
- home/page.tsx (baby photo) — same fixed toast as memories; 3 steps with
  HTTP codes and raw body on error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:17:01 +05:30
ccae6d85d2 Fix broken memories count + silent upload failures
Memories "count 2 but no images" root cause:
- DB rows exist with processing_status='failed'/'uploading' from aborted uploads
  whose R2 objects never actually landed. The img onError fires and hides the
  tile, but the count still includes these orphaned rows.
- Fix: GET /api/memories now excludes failed rows and uploading rows older than
  30 min from both the SELECT and the count. Also fires a background DELETE to
  clean up orphaned rows so they stop accumulating.

Profile / memories upload silent failures:
- Some Android cameras return file.type="" which caused the avatar API to reject
  the upload with a 400 error. Error was caught but shown in a small text node
  buried below the form — invisible when looking at the avatar area.
- Fix: added resolveContentType() helper (used in profile, memories, home) that
  falls back to extension-based detection when file.type is empty/octet-stream.
- Fix: profile page now uses a separate uploadMsg state rendered immediately
  below the avatar so errors/success are always visible on mobile without scroll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:48:28 +05:30
ef30f27e9c Fix FAB z-index/position, broken image fallbacks, and upload progress UI
- Bug 2: raise all FABs (activity, memories, circle) to z-50 and bottom-24
  so they sit above the bottom nav (z-40, ~56px tall)
- Bug 1: add onError handlers to baby photo (home) and parent avatar (profile)
  so a broken R2 URL shows the 👶 / initials placeholder instead of a broken
  browser icon; reset error flag after a successful upload
- Bug 3: replace  emoji spinner with a proper CSS spinner on home page baby
  photo; add a fixed upload-progress toast to the memories page that appears
  at the top of the screen for the full duration of the upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:15:39 +05:30
0c7f37fd12 feat(quota): storage quota + family-member limits for free tier
Feature A — Storage quota (1 GiB per family):
- src/lib/quota.ts: enforcement library with pure functions (fully unit-tested)
  and DB-bound helpers; isPaidFamily() is the single payment abstraction gate
- src/lib/format-bytes.ts: extracted formatBytes() — safe for client imports
- POST /api/upload: quota check before presigned URL issuance (HTTP 402 + reason code)
- POST /api/memories/[id]/confirm: HeadObject reconciles actual R2 size; deletes
  over-quota objects and marks row failed rather than silently exceeding limit
- GET /api/storage-usage: storage info endpoint for UI meter
- src/components/StorageMeter.tsx: meter bar + StorageQuotaBanner + MemberLimitBanner
- memories/page.tsx: quota banner, FAB disabled (⊘) when exceeded, compact meter in header
- settings/page.tsx: always-visible StorageMeter + MemberLimitBanner in invite section

Feature B — Member limit (2 per family, free tier):
- invites/route.ts: replaced ad-hoc inline check with checkMemberLimit() from quota lib
  Structured 403 response: { reason, currentCount, limit }
- Freeze rule: paid→free downgrade leaves all members intact; only new invites blocked

Migration:
- drizzle/0007_subscription_status.sql: ADD COLUMN subscription_status varchar(20)
- debug-migration/route.ts: step added for hot-apply without full redeploy
- src/db/schema/family.ts: subscriptionStatus field added to Drizzle schema

Tests: 44 unit tests in src/__tests__/quota.test.ts, all passing
- Pure function tests (no DB): isPaidFamily, wouldExceedQuota, isAtMemberLimit, formatBytes
- DB-bound tests (mocked @/db): getFamilyStorageUsage, checkStorageQuota,
  checkMemberLimit, getStorageInfo, tenant isolation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:21:11 +05:30
2a09c027fa feat(marketing): public homepage replacing / → /login redirect
- Add (marketing) route group: /, /pricing, /privacy, /terms
- Add (app) route group: moves all authenticated pages, app home → /home
- Root / is now a static marketing page (zero DB imports, zero auth)
- NavAuthButton client component: shows "Open Tia →" if logged in, else "Continue with Google"
- Plausible analytics hook in marketing layout
- Auto-generated OG image via opengraph-image.tsx
- Middleware updated to allowlist marketing routes
- All /-redirects updated to /home (login, onboarding, invite, circle join)
- BottomNav home tab updated: / → /home

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:26:26 +05:30
Renamed from src/app/memories/page.tsx (Browse further)