Schema: WeightLog, VetVisit, Vaccine, HealthCard models
Router: health.ts — CRUD for weight/vet/vaccines + health card token management
Pages:
- /pets/[petId]/health — owner-only dashboard (weight, vet visits, vaccines)
- /health-card/[token] — public shareable health card (no auth required)
Profile: Health button added for own pets; Edit + Health side by side
Health data is owner-private; the Health Card link is the only public
access point, shareable via token URL (vet, friends, sitters).
Token can be regenerated to revoke access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add pets.getProfile procedure (no ownership check) so any auth'd user can view any pet profile
- Profile page now uses getProfile; Edit link and owner attribution only shown for own pets
- PostCard header: wrap PawRing + pet name in Link to /pets/[petId]
- Search Pets tab + People tab pet rows: wrap avatar/name in Link to /pets/[petId]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PostCard: add variant="detail" (object-contain + black bg) for profile sheet — full image in original format
- VideoCard: getByPostId polls Mux API directly when PROCESSING, self-heals without webhooks in dev
- PawRing: gray ring for all-seen stories, orange for unseen; reads viewerPetId from ActivePetContext
- Search: add People tab via new search.byOwner (Clerk username search → owner's pets)
- Notifications: rows now navigate to actor profile (FOLLOW) or pet profile (REACTION/COMMENT)
- Notifications router: include post.petId for navigation target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Schema:
- Add NotificationType enum (FOLLOW, REACTION, COMMENT, MESSAGE)
- Add Notification model with recipientPet, actorPet, post, comment
relations; index on [recipientPetId, read, createdAt desc]
Backend:
- notifications router: list (cursor-paginated), unreadCount,
markRead, markAllRead
- follows.create: fire-and-forget FOLLOW notification to followee
- reactions.toggle: REACTION notification on create (skip self)
- comments.create: COMMENT notification with commentId (skip self)
All triggers use .catch(()=>{}) to never fail the main action
UI:
- NotificationBell component with orange badge, polls every 30s
- /notifications page: list with avatar, type icon, text, thumbnail,
read/unread dot; auto-marks all read on visit
- Sidebar: Notifications link activated with NotificationBell
- MobileNav: Bell replaces Search tab (Search still at /search)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PawRing containers are size+RING_PAD*2=72px tall while the Add Story
button was only 56px, causing vertical misalignment. Wrap the button
in a matching 72px container so all tray items share the same height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mainCy=58 shifted the ring below the flex-centered avatar, causing the
image to appear above/half-inside the ring. Setting cy=50 aligns the
ring with the container center; toe beans (cy≈1-10) overflow upward via
SVG overflow:visible, which is the correct paw-print anatomy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feed.getFeed: when Redis is unavailable, fall back to a direct
Postgres query (followed pets + own posts, cursor-based) instead of
returning an empty array — feed now works in dev without Upstash
- ProfileTabs: wrap grid thumbnails in <button> + open a bottom Sheet
with the full PostCard on tap; previously tapping a photo did nothing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MultiImageUpload: move onKeysChange out of setImages updater into
useEffect to fix React "Cannot update a component while rendering"
warning; same fix applied to handleRemove
- feed.getFeed: wrap getFeedPage in try/catch so Redis failure
(Upstash not configured) returns empty feed instead of 500
- posts.create + reposts.create: wrap fanOutPost with .catch(() => {})
so Redis failure never rolls back an already-committed post
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next.js static rendering requires useSearchParams() to be inside a
Suspense boundary. Splits page into ResetPasswordContent (inner) +
ResetPasswordPage (Suspense wrapper). Caught by npm run build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- assertPetOwnership enforces petId ownership (T-02-20)
- getFeedPage reads Redis sorted set with cursor pagination
- Block filter applied at Prisma hydration layer (T-02-21)
- Post order restored to Redis score order after Prisma findMany (Pitfall 7)
- FEED-01 tests green
- Empty feed: returns [] and null nextCursor for pet with no follows
- Cursor pagination: nextCursor passed through from getFeedPage
- Block filter: blocked pets excluded from hydration
- Ownership: throws FORBIDDEN when petId is not owned by requester
vi.mock() is vitest-hoisted at runtime but TypeScript requires explicit import.
Adds: import { vi } from "vitest" to all 7 new test scaffolds.
npx tsc --noEmit now exits 0.
- Test 1: getPresignedUrl generates URL with correct contentType and avatars/-scoped key
- Test 2: getPresignedUrl rejects non-owner with FORBIDDEN (T-4-01)
- Test 3: getPresignedUrl rejects image/gif (Zod enum validation, T-4-02)
- Test 4: getPresignedUrl rejects 11MB file (T-4-06 fileSize limit)
- Test 5: confirmUpload writes avatarKey to DB with correct update call
- Test 6: confirmUpload rejects path traversal key (T-4-05)
- Bonus: confirmUpload rejects non-owner with FORBIDDEN (T-4-01)
- Mock getPresignedUploadUrl via vi.mock — no real R2 calls
- pets/[petId]/page.tsx: replace /api/avatar stub with NEXT_PUBLIC_R2_PUBLIC_URL CDN URL
- pets/[petId]/edit/page.tsx: pass initialAvatarKey to PetForm so AvatarUpload shows current photo
- Sidebar.tsx: add avatarKey to Pet type; show AvatarImage from CDN for active pet and dropdown list
- MobileNav.tsx: same CDN avatar pattern for mobile pet switcher
- next.config.ts: add images.remotePatterns for R2 CDN hostname (T-4-04 safeguard)
- Create AvatarUpload.tsx: idle/dragging/uploading/error/idle-with-avatar states
- XHR PUT to R2 presigned URL with progress tracking (Content-Type must match presignedUrl)
- Two-step upload: getPresignedUrl -> XHR PUT -> confirmUpload -> CDN URL display
- Client-side validation: size <= 10MB, type jpeg/png/webp
- Disabled state in create mode (no petId) with tooltip
- Drag and drop: dragOver/dragLeave/onDrop handlers; orange border in dragging state
- Wire AvatarUpload into PetForm replacing AvatarUploadZone stub
- Add initialAvatarKey prop and currentAvatarKey state to PetForm
- onUploadComplete invalidates React Query cache (pets.byId + pets.list)