The landing page was hardcoded English, so switching languages had no
visible effect on it — added a "Landing" namespace to both message
catalogs and wired src/app/page.tsx up via getTranslations, matching
the pattern already used by other server-rendered pages.
The new public landing page (/) was hero-only — missing a language
switcher, legal links, and anything showing what the product actually
looks like. Adds:
- A slim header with the wordmark and a flag-based language switcher
(LanguageSwitcher now shows 🇬🇧/🇩🇪 instead of English/Deutsch text,
used here and everywhere else it's already mounted)
- An app-preview section: a real screenshot of the live feed inside a
browser-chrome mockup card
- A 4-item feature overview (pet-first profiles, posts/stories/video,
health tracking, pet-to-pet messaging)
- A footer with Impressum/Datenschutz/AGB links, matching what the
authenticated sidebar already links to
/ unconditionally redirected to /feed, gated entirely by proxy.ts's
auth.protect() — so logged-out visitors AND social-media crawlers
scraping og:image for link previews got bounced to Clerk's hosted
sign-in before Next.js ever rendered this route's metadata (surfaced
as Facebook Sharing Debugger's "og:image should be explicit" warning,
since the crawler never saw our actual tags).
Adds "/" to proxy.ts's public matcher and moves the
authenticated-redirect into the page itself via a plain auth() read
(not .protect()), so it can render a real hero page — matching the OG
image's warm gradient + paw-mark visual language — for logged-out
visitors and crawlers alike, while still bouncing authenticated users
straight to /feed as before.
Replaces the code-generated opengraph-image.tsx (placeholder circles,
default font) with a static opengraph-image.png exported from the
designer's refined SVG — a properly-shaped paw mark and a different
typeface. The source SVG traces letterforms as vector paths and embeds
a raster background, which isn't practical to reproduce in a
next/og ImageResponse component, so it's shipped as a static asset
via Next.js's file-convention instead (same og:image/twitter:image
wiring, no proxy.ts change needed — same route path as before).
Social-sharing previews had no image — layout.tsx's OG/Twitter tags
existed but pointed at nothing. Adds src/app/opengraph-image.tsx using
next/og's ImageResponse to render a branded card (paw mark, wordmark,
tagline) at request time instead of shipping a static asset, and
switches twitter.card to summary_large_image now that there's an
image for it to show.
Advertisement.species existed in the schema but was never wired up —
the admin form never let you set it and the feed's ad query ignored it
entirely, showing every active ad to every pet regardless of species.
Adds a new Advertisement.breeds field alongside it and wires both
through the whole path: a targeting UI in the ad create/edit dialog
(species checkboxes, each expandable into its own breed checklist),
and a filter in admin.getActiveAds (now scoped to the viewing pet) so
cat food ads stop reaching dog owners. Empty species/breeds = shown to
everyone, unchanged from today's behavior.
The aspect-square container had no overflow-hidden, so with overflow
default-visible its own box height wasn't enforced against a
percentage-sized child image — portrait photos fell back to their
intrinsic (taller) aspect ratio instead of getting cropped square,
found while visually verifying the deployed grid. Landscape images
were unaffected, which masked the bug in earlier local testing.
The list view made post content hard to scan — thumbnails rendered at
inconsistent sizes and captions were easy to miss. Switches to a
responsive grid (3 cols on desktop) with a fixed-size thumbnail per
card, a same-size placeholder for posts without media, a caption
below each card, and a play-icon overlay + Mux thumbnail for video
posts (listPosts now also selects videoPost.muxPlaybackId).
Replaces the emailed Supabase signed URL with a bespoke DataExportToken
DB record, delivered via a new /api/gdpr-export/[token] route that
streams the file server-side. The storage backend is now an internal
implementation detail; every link the user sees reads pawfeed.org.
Aggregates everything held about an Owner (gdpr-export.ts: pets, posts,
stories, comments, reactions, reposts, follows/blocks, health records,
messages, notifications, reports, ban/consent history, Clerk profile)
into a single JSON bundle, uploads it to a private Supabase Storage
bucket (data-export-storage.ts — separate from the public pet-avatars
bucket used for media), and emails the owner a 7-day signed download
link via the new SMTP module. Triggered from a new export button per
user in /p/[secret]/users, logged to ModerationLog.
Deliberately not a zip with embedded media bytes: avoids new archiving
dependencies, the risk of a slow synchronous request for owners with
many posts, and the fact that Mux video assets aren't trivially
re-downloadable — media is referenced by its existing CDN URL instead.
Ad interactions and issued invite codes are not yet included, flagged
in the export's own scope note rather than silently omitted.
Verified end-to-end against the live shared DB before committing:
built a real export, uploaded it, generated a signed URL, and sent a
real test email — all succeeded.
Gitea #6 (parts 1+2): admin.listUsers now batch-fetches email/name from
Clerk (users only stored the Clerk userId locally, everything else
lives in Clerk) instead of showing a bare userId. Adds
admin.startTeamConversation, which lazily creates a "PawFeed Team"
system Pet under ADMIN_OWNER_ID and reuses the existing pet-to-pet
Conversation/Message model instead of a parallel admin-inbox system —
the admin panel switches ActivePet to Team and opens the ordinary
/messages/[conversationId] thread, so no new UI is needed on the
recipient's side.
Known limitation: only the literal ADMIN_OWNER_ID account can act as
Team, since assertPetOwnership requires an exact owner match.
Extending this to every AdminRole holder is left for a follow-up.
Part 3 (GDPR Art. 15 data export) is deliberately not included —
research only per the issue's own "erst prüfen dann bauen" note.
Absolutely positioning it at top-4 right-4 over the centered content
overlapped the step headings on narrow viewports (reported live).
Gives it its own row in a flex-col layout instead, so it never
competes for space with the card content below it.
New visitors always got the English default because locale resolution
only ever read the NEXT_LOCALE cookie — never set until a user visits
a page with the LanguageSwitcher, which only lives in the authenticated
app Sidebar. /onboarding/pet runs before that, so a German browser
landed in English with no way to change it (reported live after
deploying the upload-consent step from Gitea #3).
Adds an Accept-Language fallback in src/i18n/request.ts (only consulted
when no cookie is set) and renders the existing LanguageSwitcher on the
onboarding page so users can override the guess.
Gitea #1: prevents blank image flashes while photos load. Blurhash is
computed client-side at upload time (canvas downsampling + the
blurhash package) for posts, milestones, and stories, stored alongside
each PostImage/Story row, and decoded into a smooth color placeholder
via the new BlurImage component that cross-fades to the loaded photo.
Wired into every content-photo surface: feed cards, post detail zoom,
reposts, milestone cards, story viewer, explore/search grids,
notification thumbnails, profile grid, and the admin posts/reports
panels. Pet avatars and ad creatives intentionally excluded — separate
content pipelines with disproportionate effort for the payoff.
Gitea #2: the site is now public (INVITE_REQUIRED=false), so the
alpha-status clause and invite-code registration requirement no longer
apply and are removed. Hardens the remaining terms: age/capacity
requirement, DSA host-provider privilege with a notice-and-action
section referencing the existing report feature, a proportionate
suspension clause, a limited-liability clause based on the
unentgeltliche-Nutzung privilege (§§ 521, 599, 690 BGB), a
§308-Nr.5-BGB-compliant change clause with objection period, an EU
consumer-protection carve-out, and a severability clause.
Gitea #3: harden Datenschutz for the now-public site. Adds a blocking
first-pet-onboarding step (two checkboxes) confirming the owner holds
rights to uploaded photos/videos and consents to data transfer to
Clerk/Cloudflare/Mux/Supabase. Consent timestamps are stored separately
on Owner so either declaration can be re-requested independently later.
Also documents GlitchTip as a processor and adds an explicit consent
section to the Datenschutzerklärung.
Sentry's Next.js App Router setup requires this file to capture React
rendering errors at the root layout level — GlitchTip's own setup
checklist flagged it as missing.
Public launch prep — link previews on social/messaging apps were
falling back to bare URLs with no title/description. No branded OG
image exists yet (public/ only has create-next-app placeholders), so
this ships text-only; a real 1200x630 image is a follow-up.
Public go-live hardening: Redis-backed rate limiting on the previously
invite-gated public surface (explore browsing, post/comment creation,
invite-cookie routes) plus CSP, HSTS, and standard security headers on
every response, now that the invite gate no longer filters traffic.
User feedback: pet profile pictures couldn't be enlarged. New
PetAvatarZoom client component wraps the profile avatar in a button —
clicking opens a lightbox Dialog showing the full photo, reusing
DialogContent's existing zoom-in-95 + fade entrance animation rather
than hand-rolling a new transition. A subtle hover scale + cursor-zoom-in
signals it's interactive. No-op when the pet has no avatar set (falls
back to initials, nothing to zoom into).
Advertisers/admins had no way to see how many times an ad was actually
shown — only engagement counts (reactions/comments/reposts) existed.
Adds Advertisement.adViewCount, a denormalized counter matching the
existing engagement-counter pattern.
AdCard.tsx fires ads.recordView once per card, the first time it
scrolls ≥50% into view (react-intersection-observer's triggerOnce,
already a project dependency via FeedList's pagination sentinel) —
matches standard "viewable impression" semantics rather than counting
on mount (which would also count ads that render off-screen and are
never actually seen). Not deduplicated per pet, unlike reactions/reposts
— an impression counts every time an ad becomes visible, matching how
ad platforms report view counts.
Admin ads panel (/p/[secret]/ads) now shows view/reaction/comment/
repost counts inline on every ad row — the actual "results" advertisers
want from a placement, previously not surfaced anywhere despite already
existing on the Advertisement row for the other three counters.
Two new chart panels on the admin dashboard, each with a day/week/month
granularity toggle:
- "Posts over time" — bar chart of new posts per bucket (trailing 30
days / 12 weeks / 12 months, zero-filled so gaps show as zero, not
missing points).
- "Owner growth" — line+area chart of the cumulative total registered
owners, seeded from a pre-window baseline count so the curve starts
at the correct absolute total instead of zero.
Backend: admin.getPostsOverTime/getOwnerGrowth bucket timestamps in
plain JS (UTC-consistent day/ISO-week-Monday/month-1st truncation) —
deliberately not raw SQL date_trunc, to avoid DB-session-timezone
ambiguity and keep this testable without hitting Postgres. Data volume
is small enough (Alpha-stage app) that fetching bare createdAt columns
and bucketing in memory is the simpler, lower-risk choice over adding
the codebase's first raw-SQL query.
Frontend: new src/components/admin/ (TimeSeriesChart, GranularityToggle)
following the dataviz skill — thin 2px marks, rounded bar corners,
recessive gridlines, hover crosshair+tooltip on both variants, single
brand-orange hue since each chart is one series (no legend needed).
Admin surface is fixed dark (zinc-950), so colors are hardcoded rather
than light/dark-aware, unlike the app-wide WeightChart this borrows its
SVG approach from.
Added 2 regression tests covering zero-fill bucketing and the
cumulative-with-baseline math. prisma-mock.ts gained owner.findMany/
owner.count (previously missing).
New users landed directly on pet creation with no prompt to acknowledge
community guidelines — the existing LegalUpdateBanner/LegalDocumentVersion
mechanism only ever ran *after* the first pet existed (since the Owner
row wasn't created until pets.create's owner.upsert), and turned out to
be entirely dormant (zero LegalDocumentVersion rows had ever been
published in prod).
Adds a step 0 to /onboarding/pet: OnboardingGuidelines queries the same
legal.getPendingUpdate the in-app update banner uses and shows the
latest published summary before letting the user continue to pet
creation. Reuses the existing LegalDocumentVersion/admin-publish
infrastructure (/p/[secret]/legal) instead of hardcoding copy, so
content stays editable without a deploy. If nothing has been published
yet, the step is skipped automatically — no dead-end for a fresh
deployment.
legal.acknowledge switched from owner.update to owner.upsert since it
can now be called before the Owner row exists (pre-first-pet).
Deliberately did NOT add a cookie-consent checkbox: Datenschutz §5
already discloses that only strictly-necessary session cookies are used
and no tracking/ad cookies are set without explicit consent — under
GDPR/ePrivacy, strictly-necessary cookies don't require active consent,
only disclosure. The guidelines step links to both Nutzungsbedingungen
and Datenschutz instead of a redundant separate consent mechanism.
Side effect (intentional): existing owners who never explicitly
acknowledged anything will see the same content once via the in-app
LegalUpdateBanner the next time a version is published, since it's the
same underlying data model.
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.
- 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>
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)
- 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>
- 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
Emergency vet contact data (phone, address, map) was visible on the
token-shared health card URL. Health data is owner-private; the health
card is the single sharing exception but should not include internal
emergency contact details.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>