APP_STORE_URL gesetzt (/app leitet iPhones jetzt in den App Store),
offizielles Apple-Badge neben Google Play auf Landingpage, /r/[code]
und im Footer; Texte nennen iPhone und Android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Neue Tabelle PetCaretaker (max. 1 pro Tier): Code einladen (7 Tage) ->
einlösen (PENDING) -> Freigabe durch den Haupt-Besitzer (ACTIVE)
- assertPetOwnership erlaubt aktiven Mitbetreuer; assertPetMainOwner für
Löschen, Privatsphäre, Gedenkmodus, Gesundheits-Einwilligung
- pets.list/byId inkl. mitbetreuter Tiere mit Rolle; Push auch an die Geräte
des Mitbetreuers; Konto-Löschung übergibt das Tier an ihn
- Web: Verwaltung in "Profil bearbeiten", Code einlösen unter "Meine Tiere"
und im Onboarding, Link /mitbetreuen/<code>, Mitteilung CARETAKER_REQUEST
- App: dieselben Abläufe (OTA-fähig, Teilen statt Kopieren)
- Datenschutz: Absatz zum Mitbetreuer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Neue Abschnitte 4c (Push-Mitteilungen: Push-Token pro Gerät, Versand über
Expo/Apple/Google, Einwilligung im Systemdialog, Löschung) und 4d
(Vermisst-Meldung, öffentliche Seite, Suchmaschinen nur nach Einwilligung,
Kontaktdaten freiwillig und nur eigene, Löschung bei Ende der Meldung)
- Abschnitt 2, 4 (Expo, Apple, Google mit DPF) und 6 ergänzt
- Formular-Hinweis: nur eigene Kontaktdaten eintragen (Web + App)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LostReport (eine aktive Meldung je Tier) + Pet.lostSince als Cache fürs
Abzeichen, NotificationType PET_LOST/PET_FOUND
- lostReports.create/update/markFound/mine: nur Besitzer, nicht im
Gedenkmodus, höchstens eine Meldung pro 24 h, Zeitpunkt nicht in der Zukunft
- Mitteilung + Push an Follower und gefolgte Tiere, je Besitzer einmal, eigene
Tiere nie; optionale Entwarnung bei „Wieder da!“
- Öffentliche Seite /vermisst/<code> ohne Login: Foto, Merkmale, zuletzt
gesehen, Kontakt nach Wahl des Besitzers (Telefon/E-Mail/PawFeed),
Fotos aus dem Feed, Vorschaubild; Suchmaschinen nur wenn erlaubt;
nach dem Finden „Wieder sicher zu Hause“ ohne Kontaktdaten
- Web + App: Melden ganz oben in „Profil bearbeiten“, Vermisst-Banner im
Profil, Abzeichen neben dem Namen, Mitteilungen führen zum Profil
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
„Über die Regenbogenbrücke gegangen“ statt „verstorben“, Anteilnahme im
Bestätigungsdialog, „Tag des Abschieds“, „Profil als Erinnerung bewahren“ —
Web, App, Mitteilung und Push-Text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pet.deceasedAt + NotificationType.PET_DECEASED (additiv)
- pets.setDeceased: nur Besitzer, kein Datum in der Zukunft, rückgängig
machbar; beim ersten Setzen einmalig stille Mitteilung (ohne Push) an
Follower anderer Besitzer
- Verstorbene Tiere bekommen keine Geburtstags-/Adoptionstag-Mitteilungen,
Impf-Erinnerungen und Tier-Vorschläge mehr und werden nicht vorgeschlagen
- Abzeichen „In Erinnerung“ (Ribbon) überall neben dem Namen, Gedenk-Banner
im Profil, Bereich „Gedenkmodus“ in „Profil bearbeiten“ (Web + App)
- Mitteilung führt zum Profil des Tiers (Liste + Push-Tap)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tester-Feedback 04.10. (STORY_MENTION):
- Web: Klick oeffnet die Story als Overlay an der richtigen Stelle
(initialStoryId) statt ueber das Tierprofil; abgelaufen → Profil.
- App: ganzer Eintrag inkl. Name oeffnet die Story, Viewer springt per
storyId direkt zur markierten Story.
- Unbekannte Mitteilungstypen zeigen einen neutralen Text statt des
Code-Namens (der rohe Text kam von einem Geraet mit aelterem App-Stand).
- Keine Markier-Mitteilung an eigene Tiere des Erstellers (petsOfOtherOwners).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Web zeigt Story-Editor-Inhalte nur an (Entscheidung: kein Web-Editor):
StoryOverlayLayer mit gleicher Geometrie/Groessen wie die App, Text-Storys
mit Farbverlauf, Markierungen fuehren ins Tierprofil, Notschalter beachtet.
Mitteilungen STORY_MENTION/COMMENT/REACTION, Hinweistexte: Follower-
Sichtbarkeit regelt jetzt auch das Markieren.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- 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>
- stories.listActive: der Owner bekommt seine Storys der letzten 7 Tage mit
isExpired, alle anderen weiterhin nur die aktiven 24 Stunden
- stories.hasActiveStory: optional includeOwnRecent → grauer Ring
(expiredOnly) nur in der Story-Leiste, nicht an Profilbildern im Feed
- Viewer: Hinweis "Abgelaufen – nur für dich sichtbar", Kommentare bei
abgelaufenen Storys nur lesbar, Pfote nur als Zähler; Löschen und
Zuschauerliste bleiben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stories.delete: nur Owner; Aufrufe, Story (Pfoten/Kommentare per Cascade),
danach Bild in Supabase Storage bzw. Mux-Asset best-effort
- Web: Papierkorb neben der Zuschauerzahl + Rückfrage, Timer pausiert,
gelöscht wird die beim Antippen gemerkte Story
- App: gleicher Papierkorb mit nativer Rückfrage, Timer pausiert
- found.md: Nutzer-Feedback "Story löschbar" erledigt, 7-Tage-Wunsch mit
Befund als offener Punkt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Leiste zeigte nur Storys gefolgter Tiere — das eigene aktive Tier war
nie enthalten, man konnte die eigene Story nach dem Posten nicht ansehen.
Neuer Ring direkt nach der "+"-Kachel, solange stories.hasActiveStory für
das aktive Tier true ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App: post-new.tsx sortierte HEIC/HEIF still aus, obwohl resizeForUpload()
ohnehin in JPEG umwandelt — jetzt erlaubt, plus Hinweis bei übersprungenen
Fotos
- Web: neues src/lib/heic-to-jpeg.ts wandelt HEIC vor dem Upload im Browser
in JPEG um (Safari kann HEIC nativ dekodieren); eingebaut in Beiträge,
Storys, Meilensteine und Tierbild, accept um HEIC ergänzt. Browser ohne
HEIC-Dekodierung bekommen eine klare Meldung statt "falsches Format".
- Server-Whitelist (JPEG/PNG/WebP) bleibt unverändert
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>
- Replace next/font/google with next/font/local + vendored Inter variable
woff2 (latin, @fontsource-variable/inter@5.3.0), so neither visitors nor
the Docker build contact Google's servers.
- Add PlayStoreBadge (official Google Play artwork, DE/EN by locale, served
locally from /public/badges) to the hero, a new "PawFeed in your pocket"
section, and the shared landing footer (also covers /ueber-uns).
- Centralize the Play Store URL in src/lib/app-links.ts (derived from the
android.package id org.pawfeed.app).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three pieces of work from this session:
- Pet-suggestion notifications: a weekly cron (species/breed affinity
heuristic) suggests pets to follow, with an owner opt-out and web/mobile
notification UI wiring.
- Story music overlay: admin-curated royalty-free track catalog
(/p/[secret]/music), attachable to stories on web (StoryForm/StoryViewer),
with mute-original support for video stories. Mobile UI is deferred to
the next native build (needs an audio-playback module) — tracked in
mobile/PENDING-NATIVE-BUILD.md.
- Auth UX fixes: sign-in/sign-up now use KeyboardAvoidingView + ScrollView
and a shared PasswordField with a show/hide toggle (mobile). Web's
ActivePetContext gained clearGactivePetId(), wired to sign-out via a new
SignOutCleanup component, fixing a false "you do not have permission to
access this pet" error after switching accounts on the same browser.
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.
Introduces PRODUCT.md/DESIGN.md (Impeccable skill) capturing PawFeed's
existing brand identity ("The Spotlight Porch") as durable reference, then
applies it:
- Landing hero: restrained "spotlight" glow instead of a full orange
gradient panel, tagline as the actual headline with a marigold accent on
"Stars"/"stars", real PawPrint mark fixed (was a mismatched hand-drawn
shape, also fixed a viewBox overflow clipping bug).
- Features section: one dominant mechanism (pet-as-identity) plus a quiet
supporting list, replacing the four-identical-cards grid craft-floor
refuses.
- Header: dropped the generic glass-blur sticky nav for a flat header with
a soft marigold gradient hairline; CTA button now matches DESIGN.md
(rounded-lg, no shadow, same orange as the hero CTA); logo gets a
paw-spring hover as a small on-brand moment; new active-page indicator
(LandingNavLinks) under the current nav link.
- Legal pages (Impressum/Datenschutz/Nutzungsbedingungen): replaced the
stacked numbered-card-per-section layout with a flowing typographic
document (hairline dividers, no boxes, no arbitrary section-number
badges) via the shared LegalLayout/LegalSection components.
- Nutzungsbedingungen: moved Sponsoring earlier (now 3a, right after
Nutzerinhalte & Urheberrecht) while keeping Salvatorische Klausel/Kontakt
last; existing section numbers left untouched to avoid breaking the
"gemäß Abschnitt 5" cross-reference in Haftungsausschluss.
New insights router aggregates the reactionCount/commentCount counters
(Post pre-existing, Story added in #29) into a small per-pet dashboard —
total paws/comments received and which post/story is resonating most.
Reachable from the owner's own pet profile ("Meine Tiere"), owner-only.
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.
Cloudflare's brand guidelines flag a hand-written "Protected by
Cloudflare" text label as misleading — replace with their official
badge PNG (public/badges/cloudflare-protected.png), same fixed-size
<img> pattern already used for AiDisclosureBadge.
MILESTONE_META in src/lib/milestone-meta.ts held hardcoded English
labels ("Birthday", "Vet Visit", ...) consumed directly by
MilestoneCard and MilestoneForm -- bypassing next-intl entirely, so no
de.json entry could ever have translated them (there was no key to
translate, not a missing one).
Adds a MilestoneTypes namespace to both message catalogs and switches
the two user-facing consumers to useTranslations("MilestoneTypes")
for the label, keeping MILESTONE_META only for the icon lookup. The
admin posts page (p/[secret]/posts) still reads MILESTONE_META.label
directly and is left as-is -- the admin panel is English-only by
existing project convention, not part of the localized surface.
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 a subtle "payments processed by Stripe" indicator (official
wordmark SVG from Simple Icons, muted/low-opacity) above the
contribution-type selector on the Unterstützen page, so visitors know
which payment provider handles the transaction before choosing an
option.
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.
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.
- AnimatedNumber: stat tiles count up from 0 when scrolled into view
instead of appearing static, so "live numbers" actually feels live.
- Pull quote: the story's strongest line is pulled out into its own large
editorial-style callout between the hero paragraphs and the "Von Tier
zu Tier" section, giving the wall of text a visual beat. paragraph2
trimmed (DE+EN) so the line isn't read twice back to back.
- PawPrintTrail: a scattered trail of low-opacity paw prints behind the
story content, each "stamping" into place via a pure-CSS scroll-driven
animation (animation-timeline: view(), same technique as the existing
hero-parallax-icon on the landing page) — no JS, respects
prefers-reduced-motion.
Fixed a real bug while building the trail: -z-10 on its wrapper had no
local stacking context to be scoped to (the parent had `relative` but no
z-index), so it escaped past the page content and rendered behind the
document background — invisible regardless of opacity. Giving the page's
outer wrapper `z-0` fixes it.
Verified locally via dev server + Chrome DevTools MCP: light + dark theme,
375px mobile, quote/trail/stat-tile rendering all confirmed visually.
New public /ueber-uns page — deliberately doesn't preview or link into the
platform itself (no phone mockup, no post previews), just the founding
story plus live member/pet/sponsor counts for transparency. Fully
bilingual via the existing LanguageSwitcher.
- src/trpc/routers/stats.ts: new public stats.getPublicStats procedure,
Redis-cached (5min TTL), sponsor count includes lapsed subscriptions
(not just currently-active badges) per discussion on the issue.
- src/proxy.ts: /ueber-uns registered as a public route, exempt from
maintenance mode (doesn't touch the platform, so no reason to gate it).
- Extracted LandingHeader/LandingFooter from src/app/page.tsx into shared
components under src/components/landing/ — this page reuses the same
navbar, and duplicating it risked the responsive header fixes from
earlier today drifting out of sync between two copies. The "Bald
verfügbar" placeholder nav link now points at the real page.
- src/lib/stripe.ts: made the Stripe client a lazy singleton instead of
constructing it at module scope. Eager construction crashed the entire
tRPC appRouter (and anything importing it, e.g. createTRPCCaller() on
any public page) whenever STRIPE_SECRET_KEY is unset — invisible until
now because no public page had used createTRPCCaller() before.
- stats.getPublicStats wraps its Redis cache read/write in .catch() so a
Redis outage degrades to uncached rather than breaking the page,
matching the existing pattern in src/lib/maintenance.ts.
Verified locally via dev server + Chrome DevTools MCP emulation (DE/EN,
mobile 375px, desktop) — this is a public unauthenticated page so, unlike
most of today's other fixes, testable outside of Clerk auth.
The old copy positioned sponsoring as a substitute for ads ("instead
of ads"), but the platform still runs ads when advertising partners
are available — sponsoring and ads aren't mutually exclusive. Reframe
around the actual motivation: PawFeed is entirely privately financed,
and sponsoring helps cover ongoing costs (server hardware, software
licenses, tooling).
Post-launch fixes from live testing (Gitea #22):
- Feed/explore postInclude never selected Pet.isVerified, so PostCard only
ever rendered the new Sponsor badge, silently dropping Verified next to
it. Both now render side by side, matching the pet profile page.
- /unterstuetzen had no navigation entry anywhere -- added to the desktop
Sidebar (mobile bottom nav is a fixed 5-tab bar, left alone per scope).
- Admin users list showed no sponsor status at all -- SPONSOR badge added
next to the user_XXXX id, same pattern as the existing BANNED/
SHADOWBANNED badges.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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.
Finishes the manual edits from Gitea #16: fixes capitalization on
"Jetzt starten" and grammar on the hero badge text, updates the
banned-page contact address to team@pawfeed.org, and adds the
missing `group` class so the feature-card icon's `group-hover:scale-105`
actually fires. Keeps the RelativeTime strings in their original
compact form ({count}m/h/d) since that format is documented as
intentional for tight UI spots (comment lists, notifications, ad cards).
Rebuilds the public landing page per the structural reference in
Gitea #11 (navbar layout, rounded hero card with phone mockup +
floating badges, catchphrase section, second detail-feature card,
footer repeating the navbar) — colors/typography adapted to PawFeed's
existing orange brand instead of the generic SaaS reference palette.
New:
- Sticky navbar (logo, Home/About-coming-soon/Impressum, language
switcher, Login + Sign-up pill)
- Hero: rounded-4xl gradient card, phone-framed app preview with
three floating badge cards (idle bob animation), scroll-linked
parallax on the background paw watermark via native CSS
animation-timeline: view() (progressive enhancement, no JS)
- Catchphrase section reusing the existing 4 feature translations,
each card staggered into view via a new ScrollReveal client
component (IntersectionObserver, no scroll-handler churn)
- Second rounded detail-feature card with two checklist points
- Footer repeats the navbar links + tagline + "designed by" line
All reveal/parallax/float animations respect prefers-reduced-motion
and are pure CSS — no new animation dependency. Verified in a real
browser at 390px and ~1568px (mobile nav collapse, hero stacking,
floating badges hidden on mobile, scroll reveals firing correctly).
Gitea #11
Adds a PetAvatar wrapper (hash(name) mod 360 -> HSL background) and
migrates the four most-visible avatar call sites (sidebar, mobile nav,
post card, post detail incl. comments) to use it instead of duplicated
inline initials logic.
Desktop sidebar can now collapse to an icon-only 76px rail, persisted
in localStorage (same client-init pattern as ActivePetContext). All
nav rows, the new-post button, and the owner row adapt; secondary
sections (language switcher, legal footer) hide when collapsed.
Gitea #12, #14
Three follow-ups from testing the verification flow:
- listVerificationRequests now pulls bio, join date, post/follower
counts, and the owner's ban+warning status onto the review card, so a
moderator can check the checklist without leaving the page
- Edit-pet page was entirely hardcoded English regardless of locale —
converted to next-intl (new EditPet namespace, en+de)
- the pet switcher (Sidebar + MobileNav) didn't show the verified badge,
only the full profile page did — added it to both
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
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.
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.
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.
A brand-new pet with zero posts of its own and no followees with posts
saw a completely empty feed on first visit. Instead of that dead-feeling
first impression, getFeed now falls back to recent posts from other pets
of the same species, ranked by the existing algoScore (engagement +
recency) — same idea as a "For You" feed, scoped by species instead of
follows.
Only kicks in on the first page (no cursor) of the primary Redis-backed
path, so paginating an already-empty follow feed doesn't keep serving
discovery pages. Response carries a `source: "following" | "discovery"`
flag; FeedList shows a small dismissless banner explaining the posts
aren't from follows yet, otherwise the feed renders identically
(WelcomeCard, feed-mode toggle, PostCard list all unchanged).
Added 2 regression tests covering the discovery-fallback trigger and
the "don't discovery-fallback while paginating" edge case.
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.