„Ü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-Wunsch 04.10.: „Benachrichtigung bringt dich auf kuerzestem Weg
dorthin, wo es wichtig ist“.
- Nachrichten: notifications.list liefert die conversationId (ueber das
sortierte Tier-Paar), App und Web oeffnen direkt das Gespraech.
- Push-Mitteilungen: Antippen fuehrte bisher nur in die App. Jetzt
useNotificationTapRouting (useLastNotificationResponse, auch Kaltstart),
wechselt bei Bedarf aufs betroffene eigene Tier. Push-Daten tragen dafuer
petId, actorPetId, storyId, conversationId.
- Eine gemeinsame Zielregel fuer Liste und Push (mobile/src/lib/
notification-route.ts, getestet): Nachricht → Gespraech, Kommentar/Pfote/
Erwaehnung/Paw-Back → Beitrag, Story → Story, Follow/Vorschlag → Profil,
Geburtstag/Adoption/Impfung → Gesundheit, Rang → Einladen.
- Bei Nachrichten und Storys fuehrt auch der Name zum Ziel; Story-Viewer
laedt Name/Bild nach, wenn er per Push geoeffnet wird.
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>
- mobile/src/lib/story-overlays.ts + story-stickers.ts (identisch zu src/lib,
plus Gesten-Geometrie fuer den Editor); Paritaetstest liegt in mobile/
(Docker-Build kennt mobile/ nicht)
- Rueckgaben von create/createText/getVideoStatus ohne overlays-JSON und
flacher Eingabetyp: Prismas rekursiver JsonValue- bzw. der tiefe Zod-Typ
sprengten sonst den App-Compiler (TS2589)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
found.md: Testnutzer mit "Goldene Pfote" (PROFILE_RING) sah keinen Ring —
der Ring war bewusst nur im Profilkopf. Auf Wunsch jetzt ueberall:
- petIdentitySelect liefert owner.referralRankId/referralPerks mit
(referralOwnerSelect), ebenso search, explore.listPets und pets.list.
- Web: RankRing (box-shadow, keine Layoutverschiebung) um Avatare in Feed,
Kommentaren, Suche, Explore, Nachrichten, Mitteilungen, Follower-Listen,
Meilenstein-/Repost-Karten, Story-Zuschauern/-Kommentaren; PetAvatar
nimmt owner.
- App: PetAvatar mit owner zeichnet den Ring als Overlay; an allen
Listen/Karten uebergeben.
- PawRing (Web + App): ungesehene Story-Pfote in Rangfarbe statt Orange,
ohne Story schlichter Ring in Rangfarbe; gesehene Storys bleiben grau.
Sponsor-Felder im Kartentyp optional (Identity-owner traegt nur den Rang).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
found.md: Admin -> Tiere zeigte nur den hoechsten Rang (Cache referralRankId).
Die Liste laedt jetzt alle ReferralRankAchievements des Owners (nach Schwelle)
und zeigt sie als Symbole, unabhaengig vom Vorteil NAME_BADGE; entzogene
Raenge ausgegraut, Tooltip mit 'manuell vergeben' / 'entzogen'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
found.md: Abweisen schickt dem Melder eine Team-Nachricht mit optionaler
Begruendung (auch im ModerationLog), Loeschen eines gemeldeten Beitrags
dankt jedem Melder einmal. Beides per Schalter im Admin-Dialog abschaltbar,
best-effort nach der Moderation. Die interne Loesch-Begruendung geht nicht
an den Melder.
Fix nebenbei: deleteReportedPost loescht jetzt die Reports des Beitrags
(targetPostId ist keine Relation, sie blieben bisher verwaist in der Liste).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Test in src/__tests__ importierte aus mobile/, das .dockerignore und
die Web-tsconfig ausschliessen -> next build brach beim Typecheck ab.
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>
Bisher folgte nur "PawFeed Team" dem neuen Tier, der Feed des neuen
Nutzers startete leer. welcomeNewPet legt jetzt auch den Follow in
Gegenrichtung an und befüllt den Redis-Feed per backfillOnFollow mit den
letzten Team-Posts. Gleicher Admin-Schalter (autoFollowEnabled),
Beschriftung im Kurator-Panel angepasst.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- petAdmin.speciesDistribution: Anzahl pro Tierart (auch Arten ohne Tiere)
und pro Rasse, deutsche Namen, ohne das Kurator-Tier „PawFeed Team“
- Karte "Tierarten" auf dem Dashboard: sortiertes Balkendiagramm (eine
Reihe, Markenorange), Anzahl + Anteil als Text, Tooltip bei Hover/Fokus,
Klick klappt die Rassen der Art auf
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>
- Android → Google Play, UTM-Werte als Play-referrer (Play Console)
- iPhone → App Store, sobald APP_STORE_URL gesetzt ist (D5); bis dahin Web
- sonst → Web-Startseite; ?ref=<Code> führt auf die Einladungsseite /r/<Code>
- nur sichere UTM-Werte werden übernommen, nie gecacht (302, no-store)
- Zählung pro Tag/Quelle/Gerät in Redis, Anzeige im Admin unter
Empfehlungen → Übersicht (30 Tage)
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>
- /api/cron/cleanup-stories (CRON_SECRET): löscht Storys älter als 7 Tage
samt Aufrufen, Pfoten, Kommentaren, Supabase-Bild und Mux-Asset; max. 200
pro Lauf, Fehler einzelner Storys brechen den Lauf nicht ab
- gemeinsame Funktion deleteStoryWithMedia (src/lib/story-cleanup.ts), auch
von stories.delete genutzt
- Datenschutzerklärung Abschnitt 6: Storys spätestens nach 7 Tagen gelöscht
- Datenexport: Story-Block heißt "stories" statt "activeStories"
- CLAUDE.md: Cron-Jobs laufen über die NAS-Crontab + docker/run-cron.sh
(der alte Hinweis "nichts ruft trim-feeds auf" war veraltet)
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>
- App-Viewer: Pfote, Kommentar-Sheet (schreiben, eigene löschen) und für die
eigene Story die Zuschauerliste; Timer pausiert, solange ein Sheet offen ist
- stories.listActive: optionales viewerPetId → reactedByViewer, damit die
Pfote gefüllt startet (abwärtskompatibel für alte App-Builds)
- Web-StoryPawButton nutzt reactedByViewer ebenfalls und wird pro Story
neu initialisiert
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>
Die Avatar-Variante "lg" setzt per data-Attribut fest size-10 (40 px) und
überstimmte die eigene Größe — jetzt Standardvariante mit size-28 / sm:size-36
und dezentem Ring in Markenfarbe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Datenexport (Art. 15): neuer Block referralProgram mit eigenem Code,
Herkunftsangabe, "eingeladen von", gemachten Einladungen, erreichten Rängen,
aktuellen Vorteilen und den früheren Einmal-Codes. Andere Personen nur über
den Tiernamen — invitedById (Clerk-ID eines Dritten) fliegt aus dem Export.
- Kontolöschung: invitedById bei Eingeladenen und usedById bei Beta-Codes
werden geleert, damit keine Clerk-ID der gelöschten Person zurückbleibt.
Gezählte Einladungen bleiben gezählt.
- Einlösen sperrt jetzt auch über invitedAt, damit nach dem Leeren von
invitedById kein zweiter Code eingelöst werden kann.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Proxy-Sperre für /sign-up, INVITE_REQUIRED, set-invite/consume-invite,
invites-Router, Admin-Invite-Seite und -Prozeduren, Startcodes in
pets.create entfernt — Registrierung ist offen
- /join?code=… leitet auf /r/<code> um, /invite auf /einladen; alte geteilte
Links laufen nicht ins Leere
- InviteCode-Tabelle bleibt nur als Historie (Kontolöschung räumt sie weiter auf)
- Datenschutzerklärung 4b (Empfehlungsprogramm, Herkunftsangabe, pf_ref-Cookie)
und Nutzungsbedingungen 3b (Programmbedingungen, Bestandsschutz, Missbrauch)
- CLAUDE.md: Abschnitt "Referral program" statt "Invite system"
Build, Typecheck und alle Tests grün.
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>
The story upload input's accept attribute listed explicit image+video
MIME subtypes (image/jpeg,...,video/mp4,...). Several mobile
browsers'/OS's native 'Photos & Videos' pickers only reliably offer
both media kinds when given the broad image/*,video/* wildcard
categories - the explicit mixed list was silently narrowing the picker
to photos only, matching the user's report ('nur Zugriff auf Fotos,
Videos werden gar nicht angezeigt').
The actual accept/reject decision is unchanged (still enforced in
handleFile() against the specific ALLOWED_IMAGE_TYPES/
ALLOWED_VIDEO_TYPES lists) - this only relaxes what the OS offers in
the picker dialog.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed the one
finding that became newly reachable by this fix: videos were
previously never selectable at all, so a parameterized/codec-suffixed
MIME string (e.g. from some Android camera-roll
pickers) could never surface as a rejection; now strips codec
parameters before the ALLOWED_VIDEO_TYPES comparison. Documented two
other findings as accepted, not-fixed-here trade-offs in code comments
(iOS HEIC auto-transcode interaction, broader picker surfacing formats
that still get rejected post-selection) rather than silently ignoring
them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two independent root causes behind 'admin music upload fails':
1. Supabase bucket name mismatch — code expects 'music-tracks' (plural,
MUSIC_STORAGE_BUCKET), but the bucket was created as 'music-track'
(singular). Every upload failed with a bucket-not-found error from
Supabase Storage, surfaced only as a generic toast. Fixed by creating
the correctly-named bucket (public, 5MB limit, audio/mpeg|mp4|aac
allowed_mime_types matching the app's own constants) via the Supabase
Storage API; verified end-to-end with a signed-upload-url + PUT probe.
The old empty 'music-track' bucket is unused but still exists -
deleting it needs the user's own action (blocked here by the
auto-mode classifier's cloud-storage-delete guard).
2. Client-side validation trusted the browser-reported File.type, which
is sniffed from the OS's MIME registry and is inconsistent for
.m4a/.aac (e.g. Windows Chrome reports .m4a as audio/x-m4a, not
audio/mp4) - a perfectly valid file could get rejected as 'wrong
type'. Now derives the content-type from the file extension instead.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed two of four
findings: the extension lookup now uses a Map instead of a plain object
(prototype-shadowing via a file named e.g. 'x.constructor' previously
bypassed the rejection), and requires an actual '.' before the
extension (a bare filename like 'mp3' no longer matches). Extracted the
allowed-types/size constants into a new client-safe src/lib/music-
content-types.ts (supabase-storage.ts re-exports them) so the client
form and server-side Zod schema share one source of truth instead of
two independently maintained copies. Left the trust-the-extension
trade-off as-is (documented in code): this panel sits behind
ADMIN_SECRET + a role check, not the public upload surface, and the
old File.type check was never real content-based validation either.
Full test suite green (one unrelated legal.test.ts timeout in the
full-suite run was confirmed flaky - passes cleanly in isolation).
Co-Authored-By: Claude Sonnet 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>
New public page at /kinderschutz — required by Google Play's Child
Safety Standards policy for apps with user-generated content/social
features (https://support.google.com/googleplay/android-developer/answer/14747720).
Registered as public + maintenance-exempt in proxy.ts, matching the
existing /impressum, /datenschutz, /nutzungsbedingungen pattern.
Content covers only mechanisms that actually exist in this codebase:
in-app reporting (ReportSheet + admin moderation review), a named
responsible contact (same identity as /impressum), and the German/EU
legal framework (StGB §184b/c, DSA, JuSchG) for law-enforcement
cooperation on confirmed CSAE content — no invented automated-
detection claims.
Cross-linked from the other legal pages' footers, the web /pets
footer, and the mobile app's My Pets legal-links section (new
pets.legalChildSafety i18n key, DE/EN).
- i18next + react-i18next + expo-localization: device-locale detection,
an MMKV-persisted DE/EN/Automatisch override (mirrors web's cookie-only
locale, no backend field needed), and a language switcher on the Pets
tab next to the theme toggle
- every screen and shared component converted from hardcoded German
strings to translation keys; de.json/en.json kept in exact key-parity
(218 keys each, verified programmatically)
- backend: src/trpc/init.ts now accepts an x-locale header as a fallback
locale source (cookie still wins, web unaffected) since mobile has no
cookie jar to set NEXT_LOCALE in; mobile's tRPC client sends the
current language on every request
- fixes a latent bug: species/breed pickers were hardcoded to locale
"de" while every other pet query resolved names via ctx.locale's
default (English, since mobile never sent a locale cookie) — both now
use the same current-language value
- expo-localization is a new native module (config plugin) and needs a
native rebuild to actually link; deviceLocale() falls back to English
instead of crashing when it isn't linked yet, so the app stays usable
before that rebuild happens
Needs a NAS deploy (src/trpc/init.ts is shared backend code) before the
x-locale fallback and the species/breed locale fix take effect for mobile.
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>
The admin panel (src/app/p/[secret]/layout.tsx) never wrapped its tree
in ClerkThemeProvider, unlike (app)/layout.tsx — a SpeedUp-Plan P0
optimization that deliberately kept Clerk's client bundle off routes
without a <UserButton/>. Consequence: no client-side session-token
refresh ran on admin pages. The initial page load stayed authenticated
(Clerk's server-side handshake reissues the token on full document
navigations), but subsequent tRPC XHRs — e.g. legal.publish — started
failing with UNAUTHORIZED/401 a few minutes in, since that handshake
only fires for navigations, not fetch calls. Confirmed live: a fresh
page reload always restored a working session, matching this exactly.
Mounting ClerkThemeProvider in the admin layout re-adds background
token refresh. Also gives the legal-publish error handler an
actionable message for UNAUTHORIZED specifically (distinct from other
failures), since a residual case can still occur after a real 7-day
session expiry.
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.
Narrow the Art. 4 No. 1 GDPR non-personal-data claim to data that
permits no inference about the owner as a natural person, and call
out the case where it can (e.g. pet photos alongside a profile
picture) as personal data after all.
- clarify pet-only data (breed, weight, health records) is not
personal data under Art. 4 No. 1 GDPR since GDPR protects only
natural persons; still handled confidentially per policy
- switch Clerk/Cloudflare/Mux/Stripe from SCC framing to EU-U.S. DPF
certification (Art. 45 adequacy decision), strengthen the residual
SCC/Schrems-II fallback clause beyond TLS + access restrictions
- document 30-day retention and rationale for technical error reports
Verified live that adding Clerk's frontend API host to script-src does
NOT resolve the CSP violation in modern browsers: with 'strict-dynamic'
present, CSP3 ignores host-based entries in that directive entirely
(Chrome's own violation message confirms this). The console noise is a
Clerk SDK-side loading quirk, not something fixable here without
dropping 'strict-dynamic'. Keeping the host entry only as the same
spec-mandated pre-CSP3 fallback the existing 'unsafe-inline' entry
already relies on, with the comment corrected to say so.
Clerk's clerkMiddleware auto-adds its frontend API host to connect-src
but not script-src, so the initial (unversioned) clerk.browser.js/
ui.browser.js load was blocked by strict-dynamic before the 307 redirect
to the versioned URL — two CSP violations reported to /api/csp-report on
every page that renders Clerk UI, dragging the Best Practices Lighthouse
score down. Derives the host from the publishable key (same approach as
the existing Supabase/GlitchTip hostname helpers) instead of hardcoding it.
- README: note @clerk/localizations' deDE usage in the tech stack table
and shipped-features list.
- ClerkThemeProvider: comment explaining why deDE is needed (Clerk's own
UI defaults to English regardless of the app's next-intl locale).
- mobil.md: mark the Capacitor plan superseded by the React Native/Expo
scaffold decided this session (see mobile/README.md).
The sign-in/sign-up UI rendered in English despite the rest of the app
being German — @clerk/localizations ships an official deDE locale that
plugs directly into ClerkProvider's existing appearance config.
The post-card action row (comments, hide/unhide, pin/unpin, AI-disclosure
correction, delete) had no explanation of what each icon does — add a help
popover next to the "Posts" heading listing icon + action name.
Open reports and Verifications pending summarized a number but gave no way to
act on it — link each tile straight to /reports and /verification, with
hover/focus states and a chevron affordance so it reads as navigation, not
just a number.
The Overview card was a flat, uncategorized list of 10 stats with no visual
grouping. Split it into three labeled, color-identified groups so a moderator
can scan by kind of number instead of reading every label:
- Activity (emerald): owners, pets, posts, DAU, MAU
- Violations (rose): banned users, shadowbanned, blacklist entries — values
only pick up the color when actually > 0, same "act on this" convention as
the existing AttentionStat tiles
- Info (sky): active ads, verifications closed
Also switched the maintenance banner and AttentionStat tiles from border to
ring-1, matching the shared Card component's depth convention so the whole
dashboard uses one visual vocabulary instead of two.
Admin shell used hardcoded zinc-* classes instead of semantic design tokens and
a redundant "PawFeed Admin" kicker label. Dashboard was a wall of 12 identically
weighted stat cards with no hierarchy. Sidebar/mobile nav gave no indication of
the current page.
- layout.tsx/AdminMobileNav.tsx/GranularityToggle.tsx/TimeSeriesChart.tsx: replace
zinc-* with bg-background/text-foreground/border-border etc., force dark via
the `dark` class so the admin surface stays permanently dark regardless of
site-wide theme toggle
- New AdminSidebarNav.tsx: highlights the current page (desktop sidebar)
- AdminMobileNav.tsx: same active-state logic for the mobile drawer, topbar
shows the current page label instead of a static title
- page.tsx: replace the 12-card stat grid with 2 AttentionStat tiles (reports,
pending verifications — color means "act on this") plus a flat Overview list,
and convert the 5 chart containers to the shared Card component
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.
Top-post and top-story cards in the pet insights dashboard now open the
real post (PostDetailDialog, same fetch-by-id pattern as the notifications
page) or story (StoryViewer, new initialStoryId prop to jump straight to
the right story instead of always starting at index 0).