User found "Cat" showing English on a German-locale profile page. Root
cause: Species.name/Breed.name are the canonical English values; a
SpeciesTranslation/BreedTranslation table + seed data (Hund/Katze/Vogel
etc.) already existed in the schema, and PetForm's species/breed picker
already used it correctly via an explicit locale input param — but every
*display* surface (pet profile, PetCard, Sidebar/MobileNav, search,
explore, followers/following lists, mention @-search, health-card) just
read the raw untranslated name.
Adds `locale` to the tRPC context (createTRPCContext reads the
NEXT_LOCALE cookie the same way i18n/request.ts does), so read
procedures can resolve translations without every caller needing to
pass a locale param. New src/lib/localized-name.ts::pickLocalizedName()
does the lookup with a same-shape fallback to the raw name.
Updated: pets.ts (list/byId/getProfile/update), search.ts (pets/byOwner),
explore.ts (listPets), follows.ts (listFollowers/listFollowing).
health-card/[token]/page.tsx (public, unauthenticated, intentionally
all-German) resolves with a fixed "de" locale instead of ctx.locale,
consistent with its existing hardcoded WeightChart locale.
Verified the seed data is actually present in the production DB
(SpeciesTranslation has Dog→Hund, Cat→Katze, Bird→Vogel) — this fix is
immediately effective on deploy, not blocked on a missing seed run.
The entire /pets area (My Pets grid, pet profile page, followers/following
lists, the profile options dropdown, ReportSheet, BlockDialog) had zero
i18n despite ProfileTabs right next to it being fully translated — user
reported still seeing English after switching the app to German.
Adds 7 new message namespaces (PetsPage, PetProfile, FollowersPage,
FollowingPage, ProfileActions, ReportSheet, BlockDialog) with full EN/DE
parity, plus bio/adoptionStory placeholder keys to the existing PetForm
namespace (two placeholders were hardcoded English template literals
despite the rest of that form already being translated).
Server Components (pets/page.tsx, pets/[petId]/page.tsx,
followers/following pages) use getTranslations from next-intl/server
instead of the useTranslations client hook.
Adds 6 new message namespaces (DeleteAccount, NotificationsPage,
PhotoPostForm, StoryForm, MilestoneForm, VideoUploadForm, StoryViewer,
StoryTray, RepostCard) plus a Health.share sub-namespace, with full
EN/DE parity (verified programmatically, 0 keys missing on either side):
- DeleteAccountDialog.tsx: was 100% hardcoded German for a destructive,
irreversible action — now fully translated.
- notifications/page.tsx: mixed hardcoded English (FOLLOW/REACTION/...)
with hardcoded German (BIRTHDAY/ADOPTION_DAY) in the same list.
- HealthDashboard.tsx + WeightChart.tsx: date formatting was hardcoded
to toLocaleDateString("de-DE", ...) regardless of locale, unlike the
DateWheelPicker in the same forms which correctly used useLocale().
ShareDialog was entirely untranslated German alongside fully-translated
sibling tabs.
- PhotoPostForm/MilestoneForm/StoryForm/VideoUploadForm: the entire
post-creation flow had zero i18n despite the parent PostTypeSheet
being fully translated.
- StoryViewer/StoryTray/RepostCard: no i18n at all.
health-card/[token]/page.tsx passes a fixed "de-DE" locale to the now
locale-aware WeightChart, since that whole page is a separate,
out-of-audit-scope public page that's intentionally all-German.
- consume-invite: check-then-act race on single-use invite redemption —
two concurrent redemptions could both pass the pre-check. Now guarded
with an atomic updateMany(where: {code, usedById: null, revoked: false}).
- schema.prisma: added missing index on Report.targetPostId (used by
admin.ts's groupBy/filter).
- RepostCard.tsx: reposter link used a raw <a> (full page reload) instead
of next/link; the original poster's header had no link at all.
- Sidebar.tsx/MobileNav.tsx: removed the duplicated "set active pet on
first load" effect — ActivePetInitializer already does this centrally
and is mounted alongside both in the app layout.
- prisma.ts: removed the dead Vercel/Prisma Accelerate code path
(PRISMA_ACCELERATE_URL is never set — this is a self-hosted Docker
deployment) and the now-unused @prisma/extension-accelerate dependency.
- .env.example: corrected the NEXT_PUBLIC_APP_URL comment (Mux cors_origin
is intentionally "*", not wired to it) and documented CRON_SECRET,
which was missing despite both cron routes requiring it.
- reactions.ts: removed hasReacted/getCount — dead code with no frontend
callers; getCount also duplicated the denormalized Post.reactionCount.
- 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.
trim-feeds and anniversaries validate their own CRON_SECRET bearer
token, but Clerk's middleware ran auth.protect() first and 307-redirected
every request (including the cron caller) to sign-in before the route
handler's own check ever ran. Same pattern as /api/webhooks/(.*), which
also authenticates itself independently of Clerk.
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().
6 fire-and-forget notification.create() call sites (follows, reactions,
comments, reposts, messages, mention-helpers) duplicated the same
{recipientPetId, type, actorPetId?, postId?, commentId?} + .catch(() => {})
shape — consolidated into src/lib/notify.ts.
admin.ts's getActiveBan reimplemented the "is this user an admin" check
inline instead of reusing assertAdmin's logic; extracted a shared
isAdmin() helper so both paths stay in sync. assertAdmin itself is
untouched.
Prisma mock was missing the inviteCode model entirely, so pets.create's
starter-invite-code check crashed on undefined.count(). STORY-02 tests
mocked story.count, but hasActiveStory actually calls story.findMany
(needs the ids for the follow-up storyView.count query) — updated the
tests to match the real implementation.
deletePost, banUser, unbanUser, dismissReport, and deleteReportedPost each
ran the mutation and the moderationLog.create as two separate sequential
awaits. The audit flagged this as "should be Promise.all", but that would
have introduced a real bug: if the delete/ban call failed, a concurrently
fired log write could still succeed, recording an action that never
happened. Wrapped both in $transaction instead (matching the pattern
deleteComment already used) — atomic, and the log can only be written if
the mutation actually succeeded.
Reviewed the audit's other Cluster H finding (a few include-vs-select
over-fetches) and found nothing actionable — the flagged spots already
narrow relations with select where it matters; the unnarrowed ones are
small lookup/relation tables used in full by their call sites.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
Section 5 now explicitly names pornographic/sexually explicit content
(with a zero-tolerance note on depictions of minors) and a broader
catch-all for other content illegal under German law (violence
glorification, Volksverhetzung, unconstitutional symbols, terrorism,
illegal weapons/drug trade) instead of the vague "illegal content of
any kind" line. Bumps the "Stand" date to reflect the content change.
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>
colorText is not part of Clerk Variables type in current version,
causing TypeScript build failure in Docker.
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
- explore.getTrending: top-12 photo posts by reaction count (48h window)
and top-10 hashtags by post count (7d window)
- Explore page opens on Trending tab by default; species/breed tabs
preserved with breed filter hidden when not applicable
- Trending UI: hashtag chips linking to /hashtag/[tag] + 3-col post
grid with paw-count overlay and PostCard detail dialog
feat(follows): auto-trigger follower milestone posts
- After each follow, count followers for followeePetId and check against
all thresholds (500 / 1K / 2.5K / 5K / 10K / 25K / 50K / 100K /
250K / 500K / 1M)
- Guard against duplicate awards via existing Milestone lookup
- Fire-and-forget: milestone post created + fanned out to follower feeds
without blocking the follow response