- feed.ts: Postgres fallback path (used when Redis is down) never
filtered blocked pets, unlike the primary Redis path — blocked pets'
posts could reappear during a Redis hiccup.
- feed-helpers.ts/feed.ts: getFeedPage always paginated by a hardcoded
PAGE_SIZE=20, silently ignoring the client's requested (zod-validated
1-50) limit whenever Redis was up.
- milestones.ts: create accepted any MilestoneType including the
follower-threshold values (FOLLOWERS_500..FOLLOWERS_1M), which are
meant to be exclusively auto-awarded — restriction existed only in
the MilestoneForm picker, not server-side. Now validated against the
existing USER_INITIATED_MILESTONE_TYPES constant.
- admin.ts: deletePost/deleteReportedPost didn't decrement the original
post's repostCount when the deleted post was itself a repost, unlike
reposts.ts's user-facing delete — counter drifted upward permanently.
- PawButton.tsx: count was optimistically guessed off a `reacted` flag
that can be stale (callers pass initialReacted={false} unconditionally),
permanently corrupting the displayed count on click. Count is now
computed from the server's actual toggle result instead of guessed.
- PostDetailDialog.tsx: Report/Block dropdown items had no onClick and
neither ReportSheet nor BlockDialog were rendered — wired up to match
PostCard.tsx's existing pattern.
- VideoCard.tsx: invalidated the entire query cache on every video-ready
transition; scoped to feed/posts/explore router queries instead.
- health.ts: deleteWeightLog/deleteVetVisit/deleteVaccine checked pet
ownership but deleted the target row by id alone, never verifying it
belonged to that pet — any owner could delete another pet's health
records. Fixed with fetch-then-check (same pattern as comments.ts).
- notifications.ts: list/unreadCount/markAllRead/markRead had zero
assertPetOwnership calls — any owner could read or clear another
pet's notification inbox by supplying its (publicly-visible) petId.
- ads.ts: toggleReaction/addComment/toggleRepost never verified petId
ownership, unlike the near-identical post reactions/comments/reposts
routers — allowed attributing ad interactions to arbitrary pets.
- FollowButton.tsx: early return before 3 hooks violated Rules of
Hooks, crashing React when the active pet switched to one already
rendered in the same list/grid (explore, followers list).
Added regression tests for all 4 (health.ts and notifications.ts had
zero test coverage before this — not a coincidence, per the audit).
.env.example was missing MUX_TOKEN_ID/MUX_TOKEN_SECRET/MUX_WEBHOOK_SECRET/
NEXT_PUBLIC_APP_URL even though they're required fields per docker/.env.
AvatarUpload's docblock still described the pre-migration R2 upload path;
storage has been Supabase-only since the R2 removal.
Local .env/.env.local consolidation (not tracked by git) done alongside
this: merged the split DB/Supabase/Clerk/Mux config into .env.local and
removed the dead, never-filled R2 placeholder vars from .env.
getAvatarUrl and getMediaUrl were byte-identical bucket URL builders in
two separate files. getAvatarUrl is now a re-export of getMediaUrl —
all ~34 call sites keep working unchanged.
AvatarUpload, MultiImageUpload, and VideoUploadForm each duplicated the
same XHR PUT-with-progress Promise wrapper. The three flows differ too
much (single vs. multi-file, with/without a confirm step) for a single
useFileUpload hook to be a clean fit, so only the truly identical piece
— the XHR PUT itself — was extracted into uploadFileWithProgress().
- search/page.tsx: debounce timer lived in the change handler, whose
cleanup return value is discarded (only useEffect cleanup runs) — every
keystroke fired an uncancelled query. Extracted shared useDebounce hook,
applied to both search and mention-textarea (previously undebounced).
- VideoCard: MuxPlayer (pulls in the HLS.js runtime) is now next/dynamic
instead of a static import, keeping it out of the main feed bundle.
- FeedList: post/ad interleaving array was rebuilt on every render
(e.g. every scroll-sentinel inView toggle) — now memoized.
- PostCard: memoized now that FeedList hands it referentially stable
post objects.
- AdCard: replaced a setState-in-effect (server->local sync of
optimistic reaction/repost state) with the React-recommended
render-time adjustment pattern; fixed a real react-hooks/set-state-in-effect
lint error. Two pre-existing, legitimate instances of the same pattern
(FeedList's hydration-safe localStorage read, PostCard's embla carousel
subscription) got scoped eslint-disable comments with rationale instead
of a risky rewrite.
- Reviewed MultiImageUpload.tsx's flagged effect: notifies the parent
deliberately outside the render phase (already commented, no lint
violation) — left unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a pull-based legal-update flow instead of fanning out through the
petId-scoped Notification model (ToS/Datenschutz acceptance is an
Owner-level concern, not a per-pet one):
- Prisma: LegalDocType enum, LegalDocumentVersion model,
Owner.legalAcknowledgedAt.
- trpc/routers/legal.ts: admin publish/listVersions, owner-facing
getPendingUpdate/acknowledge. assertAdmin exported from admin.ts
instead of duplicated.
- Admin panel /legal: publish form (doc type + change summary) + history.
Publishing IS sending — every owner sees it on next load until
acknowledged; republishing re-surfaces it for everyone.
- LegalUpdateBanner mounted in (app)/layout.tsx: non-dismissable sheet
with the stored change summary, "Verstanden" (acknowledge) and
"Konto löschen" (routes into DeleteAccountDialog via new
?deleteAccount=1 auto-open support) actions.
Closes the last DEBUG-List.md item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds DateWheelPicker (src/components/ui/date-wheel-picker.tsx) — an
iOS-style bottom-sheet with scroll-snap day/month/year columns — and
swaps out the 6 remaining <input type="date"> usages in PetForm
(birthday, adoptedAt) and HealthDashboard (weight log, vet visit,
vaccine given/due dates). Closes the last open DEBUG-List.md item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bundles the 2026-07-12 code-audit session (Clusters A/B/C/D partial/E/F):
denormalized Post/Advertisement reaction/comment/repost counters synced
transactionally instead of live _count queries; real cursor-based pagination
for followers/following/blocks lists; assertPetOwnership + formatRelativeTime
centralized; dead r2.ts + AWS SDK deps removed; missing DB indexes added;
account-deletion flow, mention notifications, pull-to-refresh feed, and
mobile UI/i18n fixes from the surrounding sessions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Translate ProfileTabs, ExploreCard, PostDetailDialog, HealthDashboard (EN + DE)
- PostDetailDialog: Instagram-style two-column layout (image left, comments right)
- Fix sm:max-w-[960px] override for dialog width on all breakpoints
- ActivePetInitializer: auto-set first pet on login if activePetId is null
- Onboarding: set activePetId immediately after first pet creation
- Add PetFeedingInfo model (foodType, feedingTimes, dailyAmountG, foodBrand, specialDiet)
- FeedingSection component with view/edit/delete, shown in HealthDashboard
- Health Card public page includes feeding info block
- Delete CommentSheet.tsx (dead code)
- NotificationBell: refetchInterval 30000->10000, refetchOnWindowFocus, i18n label
- ProfileTabs: Mux thumbnail for VIDEO posts in grid, play icon overlay
- parse-caption: add mention segment type for @word parsing
- MentionTextarea: new component with @-autocomplete via search.pets
- PostCard: render @mentions as blue links to /search
- CommentSheet: parse comment bodies for mentions, use MentionTextarea
- PhotoPostForm: use MentionTextarea for caption input
- Sidebar + MobileNav: auto-call setActivePet(pets[0]) when activePetId
is null so Follow/reaction buttons work immediately after onboarding
- PostCard detail mode: reduce image max-h from 70dvh to 50dvh so the
footer (paw + comment buttons) stays visible within the dialog
- ProfileTabs dialog: reduce max-h from 90dvh to 85dvh
- Auth pages: remove layout.logoPlacement and colorText (not in Clerk
Variables type, caused Docker build failure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EmptyFeed is rendered inside FeedList ("use client"), so getTranslations
(server-only) caused a white screen. Switched to useTranslations hook.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PostCard, Comments, Follow, Messages, FeedList namespaces to en/de
messages. Moves formatRelativeTime/formatDay into components so they can
use the t() hook. All hardcoded strings replaced with next-intl calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install next-intl@4 with cookie-based locale switching (NEXT_LOCALE)
- Add LanguageSwitcher component in Sidebar footer
- Translate Nav, PostTypeSheet, Feed, Explore, WelcomeCard, PetForm,
AvatarUpload, and Onboarding pages (EN + DE)
- Add SpeciesTranslation and BreedTranslation DB models for locale-aware
taxonomy; listSpecies and listBreeds accept optional locale param
- Seed German translations for all 3 species and 45 breeds
- Add SQL migration script for Supabase (add_i18n_translations.sql)
- fix(invite): secure cookie flag and set-invite route for HTTPS
- EmergencyVet model: name, address, phone, website, lat, lng (1:1 to Pet)
- health router: getEmergencyVet, setEmergencyVet, deleteEmergencyVet
- EmergencyVetSection: Nominatim search (debounced 500ms, OSM attribution),
result dropdown auto-fills address + coordinates, manual override for all fields
- OSM iframe map (no API key) with "In Karte öffnen" link; shown in edit
mode as coordinate preview and in view mode after save
- Health card (public): red-accented emergency vet block with phone link,
website link and OSM map; map hidden on print (print:hidden)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sheet side=bottom was anchored to screen bottom edge
- Default SheetContent X button overlapped PostCard 3-dot menu
- Dialog renders centered, showCloseButton=false removes the X
(backdrop click / Escape still closes; 3-dots remain the only top-right control)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Renders a smooth bezier line chart above the weight log list when
2+ entries exist. No external library — pure SVG with:
- Orange-500 line + gradient area fill (PawFeed accent)
- Hover tooltip with dark bg, orange weight value, muted date
- Glow ring + dot state change on hover
- Bezier curve interpolation for smooth line
- Responsive via viewBox (maxHeight 190px)
- Y-axis: auto-scaled with 4 grid ticks
- X-axis: up to 5 date labels (DD.MM), always shows first + last
- Trend delta label (neutral, context-dependent for pets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>