admin and Claude Sonnet 4.6
80559c3b3f
feat(health): health card share link, PDF export, weight chart in card
...
- ShareDialog: expiry selector (7/14/30/60/90 days, default 14), shows
expiry date after generation, "Als PDF öffnen / drucken" button
- WeightChart extracted to standalone component (WeightChart.tsx) so it
can be shared between HealthDashboard and the public health card page
- Health card page: renders WeightChart when 2+ weight entries exist
- AutoPrint client component: auto-triggers window.print() when card is
opened with ?print=1 (600ms delay for render to settle)
- README: Phase 6 + Phase 7 marked complete with full feature summaries;
project structure and router list updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-15 06:06:28 +02:00
admin and Claude Sonnet 4.6
f1c798a4b0
feat(07): Phase 7 — Health & Vet Tracking
...
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 >
2026-06-15 05:07:56 +02:00
admin and Claude Sonnet 4.6
5d396a7bb4
fix: make pet profiles clickable from feed, search, and notifications
...
- 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 >
2026-06-15 04:52:45 +02:00
admin and Claude Sonnet 4.6
e2e932804b
feat(06): Phase 6 — Direct Messages
...
- Schema: DmPolicy enum (EVERYONE/FOLLOWERS_ONLY), dmPolicy on Pet, Conversation + Message models
- conversationsRouter: list (inbox), getOrCreate (with DmPolicy + block check), unreadCount
- messagesRouter: list (3s poll, auto mark-read), send (+ MESSAGE notification), markRead
- /messages inbox page with unread badges + last message preview
- /messages/[conversationId] thread with bubble UI, day dividers, Enter to send
- Message button on pet profiles (ProfileActions) → getOrCreate → navigate to thread
- Sidebar: Messages link activated
- Pet edit page: DM Privacy setting (Everyone / Only pets I follow)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-14 09:45:45 +02:00
admin and Claude Sonnet 4.6
da0f9f2bb7
fix: resolve 5 bugs from dev testing
...
- 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 >
2026-06-14 08:41:46 +02:00
admin and Claude Sonnet 4.6
60366691e8
feat(05): Phase 5 — short-form video via Mux
...
Schema:
- Add VideoPost model (muxUploadId, muxAssetId, muxPlaybackId,
status, durationSecs, aspectRatio) linked 1-to-1 with Post
- Add VideoStatus enum (PROCESSING/READY/ERROR)
- Add PostType.VIDEO
Backend:
- src/lib/mux.ts — singleton Mux client + webhook secret
- videos router: createUpload (Mux direct upload URL + PROCESSING post),
getByPostId (status polling), updateCaption
- POST /api/webhooks/mux: verifies signature; handles
video.upload.asset_created (store muxAssetId),
video.asset.ready (set READY + playbackId + fan-out to feed),
video.asset.errored (set ERROR)
- feed.ts + posts.byPetId: include videoPost in all post queries
UI:
- VideoUploadForm: drop zone → XHR PUT to Mux upload URL with
progress bar; caption field; transitions to "Processing…" on success
- VideoCard: PROCESSING shows spinner; READY renders MuxPlayer
(HLS, orange accent); polls every 5s until status changes
- PostTypeSheet: "Video Post" option added (between Photo and Milestone)
- PostCard: VIDEO type renders VideoCard instead of image carousel
Env vars: MUX_TOKEN_ID, MUX_TOKEN_SECRET, MUX_WEBHOOK_SECRET,
NEXT_PUBLIC_APP_URL — placeholders added to .env.local
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 13:27:43 +02:00
admin and Claude Sonnet 4.6
08fd0dfb71
feat(04): Phase 4 — in-app notifications
...
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 >
2026-06-13 12:59:23 +02:00
admin and Claude Sonnet 4.6
a9886948b8
fix: empty feed + unclickable profile grid
...
- 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 >
2026-06-13 12:38:53 +02:00
admin and Claude Sonnet 4.6
5af2a40bb3
fix: resolve three runtime errors found in dev smoke test
...
- 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 >
2026-06-13 12:33:14 +02:00
admin and Claude Sonnet 4.6
847ac97bde
feat(03-06): Hashtag search end-to-end — Phase 3 complete
...
- hashtag-helpers.ts: extractHashtags (regex+dedup) + saveHashtags (upsert)
- parse-caption.ts: parseCaption → CaptionSegment[] for client rendering
- search.byHashtag GREEN: hashtag.findUnique → postHashtag.findMany + pagination
- posts.create + reposts.create: extract+save hashtags after post creation
- /hashtag/[tag]: infinite scroll post grid + useInView sentinel
- /search Tags tab: byHashtag query wired, 3-col post grid
- PostCard: parseCaption replaces raw caption — hashtags are orange Link to /hashtag/[tag]
- STATE.md: Phase 3 complete, 3/7 phases done
Tests: 67 passing, 0 failing (5 todo), tsc clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 12:16:03 +02:00
admin and Claude Sonnet 4.6
59a3be27b0
feat(03-05): Explore + pet search — routers GREEN + pages + nav
...
- explore.listPets: publicProcedure, speciesId/breedId filter, cursor pagination
- search.pets: protectedProcedure, ILIKE name+breed OR, cursor pagination
- ExploreCard: avatar, breed badge, follower count, FollowButton
- /explore: species tabs + breed filter Select + infinite pet grid
- /search: debounced input, Pets tab with FollowButton rows, Tags stub
- Sidebar: Explore activated, Search added
- MobileNav: Explore activated, Notifications placeholder → Search
Tests: explore 3/3 GREEN, search.pets 2/2 GREEN; 65 total passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 12:09:24 +02:00
admin and Claude Sonnet 4.6
8507fe9489
feat(03-04): Paw-Back/Repost — reposts router GREEN + RepostCard + PawBackButton
...
- reposts.create: ownership + repost-of-repost guard + idempotency + fanOutPost
- reposts.delete: ownership + cascade via post.delete
- reposts.isReposted: compound-key lookup, returns boolean
- PawBackButton: optimistic toggle, orange when reposted, invalidates feed
- RepostCard: attribution header + original pet/images/caption, no action row
- PostCard: REPOST type dispatches to RepostCard; stub replaced with PawBackButton
- feed.ts + posts.ts: repost include added to all post hydration queries
- PostCardPost type: OriginalPostContent + repost field
Tests: 3/3 reposts GREEN, 60 total passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 12:01:58 +02:00
admin and Claude Sonnet 4.6
6fbb99e7c9
feat(03-02): Paw reactions GREEN — toggle/count router + PawButton on PostCard
...
- reactions.toggle: assertPetOwnership → findUnique(postId_petId) → create or delete, return {reacted}
- reactions.hasReacted, getCount implemented
- feed.ts + posts.byPetId: add _count{reactions,comments} to hydration include
- PawButton: optimistic toggle, filled/outline PawPrint icon, count badge
- PostCard: replace "0 likes/0 comments" placeholder with action row (PawButton + comment chip + Paw-Back stub)
- PostCardPost type: add optional _count field
- 4/4 reactions tests GREEN; 56 passing total (+3 from 53)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 11:43:31 +02:00
admin and Claude Sonnet 4.6
73666a3916
feat(03-01): Phase 3 schema foundation — Reaction/Comment/Repost/Hashtag models + router stubs RED
...
- Prisma: add Reaction, Comment, Repost, Hashtag, PostHashtag models; PostType.REPOST enum
- Back-relations on Post and Pet for all Phase 3 models (named to avoid ambiguous-relation errors)
- 5 tRPC router stubs: reactions, comments, reposts, explore, search — all registered in _app.ts
- Extend prisma-mock.ts with Phase 3 model mocks
- 5 test scaffold files: 14 failing RED + 3 FORBIDDEN passing; 53 Phase 2 tests untouched
- Phase 3 planning: CONTEXT, RESEARCH, UI-SPEC, VALIDATION, 6 PLANs, ROADMAP + STATE updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-13 11:31:48 +02:00
admin
554ca1deae
test(02-06): add milestones tests RED → implement milestonesRouter GREEN — MILE-01
2026-06-13 10:21:40 +02:00
admin
e30531f834
feat(02-05): storiesRouter GREEN + PawRing + StoryViewer + StoryTray + StoryForm
2026-06-13 10:08:23 +02:00
admin
99bff88545
test(02-05): add stories tests RED — STORY-01-03
2026-06-13 10:08:16 +02:00
admin
fa9b33ad7e
refactor(feed): use PrismaClient type directly in assertPetOwnership
2026-06-13 10:02:17 +02:00
admin and Claude Sonnet 4.6
75d1e80665
feat(02-04): implement postsRouter GREEN — PHOTO-01–04
...
- getPresignedUrl: scopes key to posts/{ownerId}/{petId}/, ownership check (T-02-30)
- create: ownership assertion + Post+PostImage nested write + fanOutPost (PHOTO-01)
- create: imageKeys.max(10) enforces carousel limit (PHOTO-02, T-02-32)
- imageKeys.startsWith('posts/') prevents path traversal (T-02-31)
- updateCaption: assertPostOwnership via post→pet→ownerId (PHOTO-03, T-02-34)
- delete: assertPostOwnership + prisma.post.delete, PostImages cascade (PHOTO-04)
- byPetId: cursor-paginated query with images+pet+milestone include
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-07 12:43:35 +02:00
admin
f85230a074
feat(02-02): implement blocksRouter + reportsRouter (GREEN)
...
- blocks.create: assertPetOwnership + self-block rejection + prisma.block.create (SAFE-01)
- blocks.list: assertPetOwnership + prisma.block.findMany scoped by blockerPetId (T-02-12)
- reports.create: assertPetOwnership + both-null target rejection + prisma.report.create (SAFE-02)
- ReportReason enum: SPAM, HARASSMENT, INAPPROPRIATE, MISINFORMATION, OTHER (T-02-13)
- Blocks authoritative in Postgres only; never Redis (RESEARCH anti-pattern avoided)
2026-06-07 12:33:42 +02:00
admin
7bdb2989ca
feat(02-02): implement followsRouter (GREEN)
...
- follows.create: assertPetOwnership + self-follow rejection + prisma.follow.create + backfillOnFollow
- follows.delete: assertPetOwnership + prisma.follow.delete by composite key
- follows.isFollowing: prisma.follow.findUnique → boolean
- follows.listFollowers: scoped by followeePetId, includes follower display fields
- follows.listFollowing: scoped by followerPetId, includes followee display fields
- All mutations use protectedProcedure; social graph is pet-to-pet never ownerId (SOCL-01/02/03)
2026-06-07 12:31:50 +02:00
admin
56f976e3c4
feat(02-03): implement feed.getFeed router with Redis + Prisma hydration
...
- 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
2026-06-07 12:31:36 +02:00
admin
c435404b2d
feat(02-01): shared libs + 7 tRPC router stubs registered in _app.ts
...
- src/lib/redis.ts: @upstash/redis singleton (server-only)
- src/lib/feed-helpers.ts: feedKey, fanOutPost, backfillOnFollow, getFeedPage, trimFeed, FEED_RETENTION_MS
- src/lib/media-url.ts: getMediaUrl (mirrors getAvatarUrl signature, client-safe)
- src/lib/milestone-thresholds.ts: FOLLOWER_MILESTONE_THRESHOLDS constant (D-09, A3)
- Router stubs: posts, stories, milestones, follows, blocks, reports, feed (all NOT_IMPLEMENTED)
- All 9 routers registered in _app.ts (pets, media + 7 new)
- feed.getFeed input includes cursor: z.number().nullish() per Pitfall 6
- Rule 1 fix: @upstash/redis zrange requires {offset, count} not just count
- npx tsc --noEmit exits 0
2026-06-07 12:23:10 +02:00
admin
e3c279d6a4
feat(01-04): switch media storage from Cloudflare R2 to Supabase Storage
2026-06-06 11:32:17 +02:00
admin
485877a541
feat(01-04): tRPC media router with getPresignedUrl and confirmUpload
...
- Create src/trpc/routers/media.ts with server-only import (T-4-03)
- getPresignedUrl: ownership assertion, contentType enum (jpeg/png/webp), fileSize <= 10MB, UUID-scoped R2 key
- confirmUpload: ownership assertion, key.startsWith('avatars/') validation (T-4-05)
- Register media: mediaRouter in _app.ts appRouter
- Fix pre-existing TS error in PetForm.tsx: setSelectedSpeciesId(val ?? '') (Rule 1 bug)
2026-06-06 11:07:09 +02:00
admin
69553b103b
feat(01-03): extend pets router (update/delete/listSpecies/listBreeds) + breed seed
...
- Add assertPetOwnership helper (T-3-01: ownership check before every mutation)
- Add pets.update with Zod limits (name<=30, bio<=150, adoptionStory<=500)
- Add pets.delete with ownership assertion
- Add pets.listSpecies (protectedProcedure — T-3-04 scraping prevention)
- Add pets.listBreeds filtered by speciesId
- Extend seed with 45 breeds: 20 dog, 15 cat, 10 bird (idempotent upserts)
- Install react-hook-form + @hookform/resolvers for PetForm (Task 2)
2026-06-06 10:07:04 +02:00
admin
e0214ea0f8
feat(01-01): wire Clerk proxy.ts, Prisma 7 schema + singleton, tRPC stack, R2 client, ActivePetContext
...
- Create proxy.ts at repo root with clerkMiddleware + createRouteMatcher (public allowlist: sign-up, log-in, forgot-password, reset-password, verify-email)
- Create prisma/schema.prisma: Owner, Species, Breed, Pet models with ownership index and FK constraints
- Create prisma.config.ts with Prisma 7 defineConfig (datasource url via env)
- Create src/lib/prisma.ts: Prisma 7 singleton using @prisma/adapter-pg for local dev, withAccelerate() for Prisma Accelerate on Vercel
- Create src/lib/r2.ts (server-only): S3Client + getPresignedUploadUrl with contentType enforcement and 300s expiry
- Create src/lib/auth.ts: getOwnerId() Clerk auth wrapper
- Create src/trpc/init.ts: createTRPCContext, router, publicProcedure, protectedProcedure (throws UNAUTHORIZED)
- Create src/trpc/routers/pets.ts: pets.create (upserts Owner, creates Pet with ownerId=ctx.userId), pets.list (scoped), pets.byId (ownership-asserted)
- Create src/trpc/routers/_app.ts: appRouter + AppRouter type export
- Create src/trpc/query-client.ts, server.ts (RSC caller), client.tsx (TRPCReactProvider + useTRPC)
- Create src/app/api/trpc/[trpc]/route.ts: tRPC fetch handler (GET + POST)
- Create src/context/ActivePetContext.tsx + src/hooks/useActivePet.ts: localStorage-backed active pet context
- Create prisma/seed.ts: seeds Dog/Cat/Bird species idempotently via upsert
- Update src/app/layout.tsx: wrap children in ClerkProvider (orange primary), TRPCReactProvider, ActivePetProvider, Toaster
- Install @prisma/adapter-pg + pg for Prisma 7 local dev (Rule 2: required for Prisma 7 engine compatibility)
- [Rule 1 - Bug] Prisma 7 no longer supports url/directUrl in schema.prisma; moved to prisma.config.ts with defineConfig
2026-06-05 19:18:16 +02:00