Datum (found.md, iOS-Feedback): neue DateField-Komponente mit Ziffern-
tastatur und automatisch eingefügten Punkten (TT.MM.JJJJ) für Geburtstag,
Adoptionstag, Gewicht, Tierarzt und Impfungen. Parser akzeptiert weiter
"-" und "/" sowie einstellige Tage/Monate. Wheel-Picker bleibt in
PENDING-NATIVE-BUILD.md (natives Modul).
Impf-Erinnerung (found.md, iOS-Feedback): täglicher Cron
/api/cron/vaccine-reminders erinnert einmalig 30 Tage vor
Vaccine.nextDueDate per Mitteilung (VACCINE_DUE) + Push mit Impfname und
Datum. Schema: NotificationType.VACCINE_DUE, Notification.vaccineId
(Cascade), Vaccine.reminderSentAt (Claim gegen Doppelversand, wird bei
Fehler wieder freigegeben). Web + App zeigen die Mitteilung an, Tippen
öffnet die Gesundheitsseite. App: Schnellauswahl +1/+3 Jahre für den
Folgetermin plus Hinweis auf die Erinnerung.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admin /p/[secret]/pets:
- Liste mit Suche (Name, Spitzname, Empfehlungscode, ID), Filtern und
Sortierung; Detail-Panel mit Kennzahlen, Owner, Profiltexten, Beiträgen
- Verifiziert / Gründungsmitglied pro Tier umschalten
- Empfehlungsränge des Owners: dauerhaft entziehen (bis zur Freigabe),
freigeben oder manuell vergeben (besondere Leistung)
- Inhalte leeren (Profilbild, Bio, Adoptionsgeschichte, Spitzname) und Tier
löschen — jeweils mit Begründung, Owner per Team-DM informiert bzw. per
E-Mail, wenn es sein einziges Tier war
Ränge: ReferralRankAchievement bekommt revokedAt/revokeReason/
grantedManually; entzogene Ränge werden weder neu vergeben noch im Cache
berücksichtigt (awardRanks, recomputeReferralRanks, referrals.me, Export).
Fix: deletePetCompletely (src/lib/delete-pet.ts) für Owner-Löschen,
Kontolöschung und Admin — vorher konnte das Löschen an StoryView/Report/
Repost-Fremdschlüsseln ohne Cascade scheitern, und die Zähler an fremden
Beiträgen/Storys wurden nicht verringert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pet.isFoundingMember: in pets.create per bewachter SQL-Anweisung für die
ersten 250 Tiere gesetzt, nie entzogen; Abzeichen (Spross) in Feed und
Profil, Web + App, unabhängig vom Empfehlungsprogramm
- Kurator "PawFeed Team": folgt jedem neuen Tier und schickt eine
Begrüßungs-Direktnachricht (DE/EN nach Sprache beim Anlegen), im
Hintergrund — blockiert das Onboarding nicht
- CuratorSettings + Admin-Seite /p/[secret]/curator: Schalter, Texte mit
Pflicht-Platzhalter {name} und Vorschau, Zähler der Gründungsmitglieder
- prisma/backfill_founding_members.sql, prisma/enable_rls_curator.sql
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Persönliche, unbegrenzt nutzbare Empfehlungscodes (z. B. BUBI-7K3M), die erst
nach der Registrierung im Onboarding eingelöst werden — keine Kopplung an den
Clerk-Sign-up-Ablauf mehr. Registrierung ohne Code bleibt möglich.
- Schema (rein additiv): Owner-Felder für Code, Einladungsstatus, Zähler und
Rang-Cache, ReferralRank / ReferralRankAchievement / ReferralSettings,
AcquisitionSource, NotificationType REFERRAL_JOINED / REFERRAL_RANK_UP
- Kernlogik src/lib/referrals.ts: Einlösen (7-Tage-Fenster, kein eigener Code,
keine Kreis-Einladungen, gesperrte Einladende ungültig), gegenseitiges
Folgen, Qualifizierung erst nach Tier + erstem Beitrag (Foto oder fertiges
Video), Tageslimit mit Prüf-Markierung, Ränge mit Bestandsschutz
- Web: /einladen, öffentliche Seite /r/[code], Onboarding-Schritt
"Wie hast du von PawFeed erfahren?", Rang-Abzeichen, Profilring,
"Eingeladen von …", neue Benachrichtigungen
- Admin /p/[secret]/referrals: Ränge anlegen/bearbeiten/deaktivieren mit
Live-Vorschau, Einstellungen, Übersicht mit Prüfung und Aberkennung;
bevorzugte Verifizierung als Vorteil
- prisma/scripts/referral-setup.ts: Start-Ränge, Settings, Code-Backfill,
optional --credit-existing für Alt-Einladungen
Noch offen: Mobile-App, Abbau des alten InviteCode-Ablaufs, Rechtstexte,
db push + Deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema groundwork for the Story music overlay: an admin-curated
MusicTrack catalog and three new optional Story fields (musicTrackId,
musicOffsetSecs, musicMuteOriginal). No user-upload path — licensing
liability stays with the admin who uploads a track.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend groundwork for a new pet-suggestion notification: a
PET_SUGGESTION NotificationType value and an Owner.petSuggestionsEnabled
opt-out (defaults to true), mirroring the existing hideSpiders pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Owner.expoPushToken + owner.registerPushToken/clearPushToken, plus a
best-effort Expo push send wired into notify() alongside the existing
in-app Notification row. Missing/expired token or unreachable Expo push
service silently no-ops — push delivery must never break the underlying
action it's attached to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a per-pet consent gate (Art. 9 Abs. 2 lit. a DSGVO) in front of the
whole Health-Tracking area (weight, vet visits, vaccines, emergency
vet, feeding, Health Card): an un-pre-checked checkbox must be
confirmed before any of it is used, enforced server-side in
health.ts (not just the UI) via a new assertHealthConsent check on
every data-bearing endpoint.
Revoking consent (health.revokeConsent) deletes every health record
for that pet in one transaction and is available at any time from the
Health page. Pet gains healthConsentGivenAt/healthConsentRevokedAt
(nullable, additive — db push already applied against the shared
Supabase instance).
Extends the Datenschutzerklärung (Abschnitt 3) with the Art. 9 legal
basis and the revoke/auto-delete mechanism, per counsel's follow-up.
Story reactions/comments mirror the existing Post/Ad engagement models
(StoryReaction/StoryComment tables, denormalized counters on Story) with
a comment overlay rendered on top of the still-playing story rather than
pausing it. No notification wiring yet, matching the Advertisement
engagement model's precedent.
pets.byId is owner-only (throws FORBIDDEN for pets you don't own), but
followers/page.tsx and following/page.tsx used it just to grab the
pet's name for the heading/empty-state — any visitor viewing another
pet's followers or following list got a 404 (Gitea #33). Swapped to
pets.getProfile, the same public-safe procedure the actual profile
page already uses.
Also adds the requested privacy control: Pet.followerVisibility
(EVERYONE/FOLLOWERS_ONLY/NOBODY, default EVERYONE) gates
follows.listFollowers/listFollowing via a new assertCanViewFollowerLists
helper — FOLLOWERS_ONLY checks whether any pet the caller owns follows
the target pet; the owner can always see their own. Both pages now
render a "private" message instead of erroring when blocked. Editable
from the pet edit page's Privacy section, next to the existing DM
policy control.
Dog breeds: added all 340 remaining FCI-recognized breeds (fetched from
fci.be's nomenclature, 2026-08-23) not already in the existing 20-entry
list, bringing the total to 360. 173 of the 340 additions get a German
BreedTranslation using standard German cynology naming; the rest keep
the same name in both locales (correct for many FCI breeds, which
German-language sources don't translate either), falling back via
pickLocalizedName as designed.
Cat breeds: added all 39 remaining FIFe-recognized breeds (fetched from
fifeweb.org/cats/breeds/, 2026-08-23), bringing the total to 54. 21 get
a German translation.
Existing entries were left untouched (upsert is keyed on
speciesId+name) so no Pet.breedId reference breaks. Seed already run
against the shared dev/prod DB (idempotent, additive only) — verified
360/54 breed counts and 193/36 DE-translated counts post-run.
Broadcasts had no delete path (Phase 1 gap) — had to remove a test
message via a one-off script yesterday. New admin.deleteBroadcast
mutation mirrors hidePost/banOwner: SUPER_ADMIN-gated (same bar as
createBroadcast), reason required (min 1 char), wraps delete + a
DELETE_BROADCAST ModerationLog entry in one transaction.
BroadcastReceipt rows cascade via the existing onDelete: Cascade.
Frontend: trash icon per history item opens a confirm dialog with a
required reason input, mirroring the posts admin page's delete-confirm
pattern.
Story gains mediaType (IMAGE/VIDEO) plus the same Mux fields as
VideoPost (muxUploadId/AssetId/PlaybackId, videoStatus, durationSecs,
aspectRatio); storageKey becomes optional since video stories don't
use Supabase Storage.
- 30s duration cap (deliberately shorter than the 180s normal-post
cap) enforced client-side before upload (reads the file's own
duration) and again server-side in the mux webhook and
stories.getVideoStatus's self-heal path, so a bypassed client check
or a direct API call can't produce an oversized story video.
- MAX_STORY_VIDEO_DURATION_SECS lives in a new client-safe
src/lib/story-video-limits.ts rather than stories.ts, which is
server-only — importing from a server-only module into StoryForm
(a client component) pulled the pg driver into the client bundle
and broke the build.
- Mux webhook now resolves an asset to either a VideoPost or a Story;
video stories never fan out to the feed (mirrors the normal-post
webhook branch, minus fanOutPost).
- StoryViewer: video stories autoplay muted (tap to unmute — browsers
block autoplay-with-sound past the story that was directly clicked
open) with a real playback-time progress bar (image stories keep
the fixed 5s bar), rendered via object-contain to correctly handle
the ~9:16 portrait ratio typical of phone-shot story videos instead
of a fixed 16:9 box.
Adds Pet.nickname (nullable, max 30 chars) and a shared formatPetName()
helper so the display format ("Rex (Rexi)") stays consistent across
feed, admin panel, messages, sidebar/nav, and mentions instead of
duplicating the formatting logic at each read site.
Chinchilla joins the small-pet species as its own Species (breeds are
color varieties: Standard Grey, Beige, Black Velvet, etc.).
Aquarium models the tank/setup as the pet identity rather than an
individual fish, matching how aquarists actually showcase their hobby —
Breed is the predominant fish species kept (Betta, Guppy, Discus, Koi,
etc.), same pattern as Bird covering many bird types via Breed. Pure
seed-data addition, no schema/code changes needed.
Add Horse, Rabbit, Guinea Pig, Hamster, Mouse, Rat, Snake, Lizard, Turtle,
Gecko, and Spider as Species (with breed taxonomies + DE translations),
alongside the existing Dog/Cat/Bird. The species/breed system was already
fully data-driven (pet creation, onboarding, ad targeting all read
pets.listSpecies/listBreeds dynamically), so this is mostly seed data.
Spiders get dedicated handling: an opt-in Owner.hideSpiders preference
(offered once during onboarding via ArachnophobiaOptIn, always
re-toggleable on /pets via HideSpidersToggle) fully excludes Species=Spider
pets/posts from feed, explore, and search when enabled.
Admins can send in-app announcements to all owners from /p/[secret]/broadcasts;
owners see them as a dismissible banner and confirm with "Gelesen". Tracks a
per-broadcast BroadcastReceipt (not a single rolling timestamp) so delivered/
read counts are visible per message and a later email-escalation phase can
target exactly the owners who haven't read a given broadcast.
Replaces the (never-launched) ad business direction with a freiwilliger
Beitrag/Community-Abo model: owners can support PawFeed via Stripe with
a one-time payment or a monthly SEPA/card subscription (2 EUR minimum),
declared as Sponsoring/Community-Abo, not a tax-deductible donation. The
Sponsor badge (only perk, no feature unlocks) is Owner-scoped and shows
on every post of every one of their pets, plus their profile.
- Schema: Owner.stripeCustomerId/sponsorSince/sponsorPeriodEnd/
sponsorSubscriptionId/sponsorSubscriptionStatus, new
SponsorContribution model (payment history)
- src/lib/stripe.ts (singleton client, mux.ts pattern) + src/lib/sponsor.ts
(isActiveSponsor: one-time givers keep the badge permanently, subscribers
through sponsorPeriodEnd even after cancelling -- no mid-period cliff)
- src/trpc/routers/sponsor.ts: getStatus, createCheckoutSession (SEPA +
card for subscriptions, card for one-time, price_data since amounts are
user-chosen), cancelSubscription (cancel_at_period_end, no hard stop)
- src/app/api/webhooks/stripe: checkout.session.completed, invoice.paid,
customer.subscription.updated/deleted (mux webhook route pattern)
- src/app/(app)/unterstuetzen: amount chips (3/5/10 EUR) prefilling an
editable field, one-time/subscription toggle, status + contribution
history, in-app cancel button (no Stripe-hosted portal, per requirement)
- Badge surfaced in feed/explore postInclude + PostCard, and on the pet
profile page next to the existing isVerified badge
- Nutzungsbedingungen Sec. 12 (draft, needs sign-off before launch) +
Stripe added to the Datenschutzerklaerung's Auftragsverarbeiter list
- src/__tests__/sponsor.test.ts (7 tests), Stripe client fully mocked
STRIPE_WEBHOOK_SECRET intentionally not yet configured -- the webhook
endpoint can only be registered in the Stripe dashboard once this is
deployed and reachable at https://pawfeed.org/api/webhooks/stripe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three of the five remaining Phase 4 admin-dashboard items from Gitea #10:
- Maintenance mode: Redis-backed global flag, gate in proxy.ts (exempts
/p/*, /api/*, legal pages, and sign-in/Clerk routes so a locked-out
admin can always get back in), SUPER_ADMIN-only dashboard toggle,
fails open on Redis errors.
- Registration IP: Owner.registrationIp captured defensively at every
owner.upsert create branch (consume-invite, legal.acknowledge,
legal.acknowledgeUploadConsent, pets.create, owner.setThemePreference)
since no single onboarding step is a code-guaranteed first touchpoint
across all invite/legal-doc states. Datenschutzerklärung updated and
user-approved.
- Trends & Promotion: Post.pinnedAt field, admin.pinPost/unpinPost
(mirrors the existing hidePost/unhidePost pattern), explore.getTrending
now surfaces pinned posts first regardless of the 48h window (with
explicit nulls:"last" ordering -- Postgres sorts NULL first on DESC by
default), trending-hashtags panel + pin/unpin button on the admin
posts page.
Password/2FA-reset admin trigger (item 5) was dropped -- Clerk's own
self-service reset already covers it. Broadcast/system messaging (item 2)
stays deferred pending a shared design session.
Reviewed via /code-review; findings addressed: the registration-IP capture
point, Postgres NULL ordering, and the maintenance-mode admin lockout gap
listed above were all fixes applied after that review, not part of the
original draft.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Owners can now optionally mark uploaded content as AI_MODIFIED or
AI_GENERATED (mutually exclusive, defaults to NONE) across every upload
surface — photo posts, video posts, milestones, stories, and pet avatars.
Once set, the choice is owner-immutable; only a moderator can correct it
via a new admin.setAiDisclosure mutation (Posts admin page), which
requires a reason and writes to ModerationLog like every other
moderation action.
The disclosure renders as a UI-only overlay badge (the two provided
label assets) wherever the content is shown — feed, post detail, story
viewer, milestone cards, and the pet profile avatar — never baked into
the image pixels.
New shared components: AiDisclosureSelect (the 3-way picker used in all
five upload forms) and AiDisclosureBadge (the overlay renderer).
Rate-limiting on messages.send, reports.create, reactions.toggle,
follows.create, reposts.create. Unique constraints on Report to stop
report-flooding. Mux webhook now fails hard in production without a
signing secret instead of skipping verification. Admin-panel outline
buttons fixed on 4 more pages (same white-on-dark bug as the Users page).
Timing-safe comparison for ADMIN_SECRET/CRON_SECRET. Fixed duplicate HSTS
header (Cloudflare's stronger one was being shadowed by the app's own).
tRPC now returns clean JSON 401s instead of HTML redirects when
unauthenticated, and no longer blocks public procedures for logged-out
visitors. 14 previously-silent catch blocks now log to GlitchTip.
invite/page.tsx and EmergencyVetSection.tsx fully localized (were 100%
hardcoded German). Stale TODO comments removed, auth.test.ts scaffold
rewritten to match the actual lazy-upsert pattern instead of a Clerk
webhook flow that was never built.
Wires up the already-present next-themes dependency and globals.css
:root/.dark token infrastructure. Persists the choice on Owner.themePreference
so it survives relogin on a new device, not just localStorage. Toggle lives
in the Sidebar (desktop) and the pet-switcher dropdown (mobile); Clerk's own
UI now follows the same theme via a small ClerkThemeProvider wrapper.
Adds a BlacklistEntry model (type WORD|LINK|HASHTAG, unique per
type+value) with a new admin module at /p/[secret]/blacklist —
add/list/delete entries, filterable by type.
Enforcement, per explicit spec from this session:
- New posts are checked at creation time (posts.create) against the
current blacklist (whole-word regex match for WORD, domain match
incl. subdomains for LINK, exact match for HASHTAG) and auto-hidden
immediately via the existing hiddenAt mechanism if matched — never
blocks creation, just hides right after, reversible via
admin.unhidePost. Logged as HIDE_POST with moderatorId "SYSTEM".
- Adding a new entry retroactively scans existing non-hidden posts
(capped at 5000 for safety) and hides matches too, logged per-post
under the admin who added the entry.
- Removing an entry does NOT auto-unhide already-hidden posts —
that stays a manual review step via the existing hide/unhide UI.
Matching lives in src/lib/blacklist.ts: word matching uses \b word
boundaries (a blacklisted "ass" won't match "assist"), link matching
extracts URLs and compares hostnames including subdomains.
Scope: post captions/hashtags only, not comment bodies — comments
don't have a hiddenAt field yet, so this is a natural follow-up once
they do, using the same pattern.
Gitea #10
Adds a Shadowban model (mirrors UserBan: reason, issuedBy, expiresAt =
null for permanent) plus MODERATOR+ shadowbanOwner/removeShadowban
mutations logging SHADOWBAN_USER/UNSHADOWBAN_USER to ModerationLog.
Unlike a ban, a shadowbanned owner is never told and keeps using the
app normally — their posts just quietly stop reaching anyone else.
Every reader-facing post query now excludes a shadowbanned owner's
pets for other viewers while still showing that owner their own
content, via a `pet: { OR: [{ ownerId: ctx.userId }, { owner: {
shadowban: null } }] }` filter: feed (both Redis-postId lookup and the
Postgres fallback), profile grid, hashtag search, and the milestones
tab. Explore trending has no reliable per-viewer identity (public
procedure) so it excludes shadowbanned owners unconditionally — a
low-stakes gap since a single post rarely lands there anyway.
Admin dashboard (Gitea #10's "Konten sperren ... Shadowban" spec item):
Users page now shows a violet SHADOWBANNED badge + reason next to the
existing ban badge, and a Ghost-icon shadowban/un-shadowban button
mirroring the ban/unban flow (reason required, unlike ban's optional
one, since the deceptive nature of the action warrants a mandatory
audit trail).
Gitea #10
Adds Post.hiddenAt (nullable, db push applied) plus MODERATOR+ admin
mutations hidePost/unhidePost, each logging HIDE_POST/UNHIDE_POST to
ModerationLog with the standard moderatorRole+ipAddress stamp.
hidePost requires a non-empty reason (visible to nobody but audit
trail); unhidePost is a plain reversal. The admin posts grid shows a
"Hidden" overlay badge and swaps the delete-adjacent action button
between hide/unhide based on hiddenAt.
Every reader-facing post query now filters hiddenAt: null so a hidden
post disappears everywhere without touching its row: feed (discovery
candidates, Postgres fallback, and the Redis-postId lookup — the same
place blocked-pet filtering already happens, so an already-fanned-out
hidden post silently drops out at read time), feed backfill on new
follows, explore trending, profile grid, hashtag search, and the
milestones tab.
Admin-facing queries (listPosts, analytics bucketing) are deliberately
NOT filtered — moderators need to see hidden posts to unhide them, and
volume analytics shouldn't be skewed by moderation actions.
Gitea #10
Gitea #10 Option B:
- new VerificationRequest model — owner submits a message via the pet
edit page, moderator reviews against a fixed 4-item checklist
(authenticity/impersonation risk, complete profile, good standing,
established presence). All four met -> approved, Pet.isVerified flips
on; any unmet -> auto-rejected with an itemized note of what's missing
- new admin queue /p/[secret]/verification (Moderator+ only, same gate
as the audit log), status filter, review dialog enforces the checklist
- owner is notified automatically either way via a "PawFeed Team" DM —
extracted the team-pet lookup/create logic (previously only inline in
startTeamConversation) into src/lib/team-pet.ts, adding sendTeamMessage
which reuses the normal Conversation/Message + MESSAGE notification
- review decisions log to ModerationLog with role+IP, same as every
other Gitea #10 action this session
Gitea #10 Phase 2:
- Warning model (append-only, no delete — Owner history), issueWarning +
listWarnings, surfaced on the Users page with a badge + dialog
- Pet.isVerified checkmark (lives on Pet, not Owner — pets are the social
identity per CLAUDE.md), toggleable from the Users page, shown on the
public profile page
- listReports now derives a HIGH/MEDIUM/LOW severity from ReportReason
and sorts severity-first; Reports page shows the derived badge
- Admin-triggered GDPR Art. 17 erasure: extracted the self-service
/api/account/delete logic into a shared deleteOwnerAccount() helper,
new admin.deleteUserAccount (SUPER_ADMIN only, requires typing the
exact ownerId to confirm, blocks self-deletion) reuses it — Delete
button only renders for SUPER_ADMIN/bootstrap callers
- all four new/changed mutations log to ModerationLog with role+IP
Gitea #10 follow-up:
- assertAdmin now takes a minRole (SUPPORT < MODERATOR < SUPER_ADMIN)
instead of a boolean requireSuperAdmin flag, and returns the caller's
resolved role so mutation sites can log it. Applied per the agreed
matrix: read-only/support tasks stay SUPPORT, destructive content/user
actions require MODERATOR, ads/roles/invites/legal-doc-publishing
require SUPER_ADMIN.
- ModerationLog gains moderatorRole and ipAddress (both nullable —
historical rows predate capture), populated on every write for
login-abuse forensics.
Gitea #10 follow-up, Phase 1:
- AdminRoleType gains SUPPORT (same access as MODERATOR for now, pending
a permission-matrix pass to scope it down)
- listUsers gains search (Clerk query + local pet-name match), ban-status,
and role filters
- ModerationLog now also captures ad create/update/delete and role
grant/revoke, which previously left no audit trail
- new getModeratorThroughput query surfaces per-moderator action counts
on the Moderators page
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.
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.
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 #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.
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.
- 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.
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>
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)
- 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>
- 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>
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>
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>