Commit Graph
68 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 81e617ece5 fix(admin): Melder-Nachricht grammatisch korrekt (zum Beitrag/Profil)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:16:48 +02:00
adminandClaude Opus 5.5 f51b158989 feat(admin): Team-Nachricht an Melder bei abgewiesenem/bestaetigtem Report
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>
2026-10-04 13:09:39 +02:00
adminandClaude Opus 5.5 7ab74f8077 fix: Datums-Test nach mobile/ verschieben (Docker-Build ohne mobile/)
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>
2026-10-04 12:50:35 +02:00
adminandClaude Opus 5.5 c74bbffccb feat: Datumseingabe TT.MM.JJJJ (App) + Erinnerung an Folgeimpfung
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>
2026-10-04 12:42:20 +02:00
adminandClaude Opus 5.5 f112d36f4a feat: neues Tier folgt dem Kurator-Tier zurück
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>
2026-10-03 19:49:46 +02:00
adminandClaude Opus 5.5 9adba560a7 feat: Admin-Dashboard — Verteilung der Tiere nach Tierart und Rasse
- 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>
2026-10-02 21:17:21 +02:00
adminandClaude Opus 5.5 566a2eedd0 feat: Admin-Tierverwaltung + vollständiges Löschen von Tieren
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>
2026-10-02 21:03:05 +02:00
adminandClaude Opus 5.5 94c143e17d feat: Download-Weiterleitung /app für Flyer und QR-Codes (D3)
- 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>
2026-10-02 20:39:40 +02:00
adminandClaude Opus 5.5 307b76e8fd feat: Gründungsmitglieder (erste 250 Tiere) und Kurator-Begrüßung
- 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>
2026-10-02 20:26:43 +02:00
adminandClaude Opus 5.5 2c0680cef9 feat: 7-Tage-Ansicht der eigenen Storys für den Ersteller (Web + App)
- 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>
2026-10-02 19:30:22 +02:00
adminandClaude Opus 5.5 a01c58e5f0 feat: Aufräum-Job für Storys älter als 7 Tage
- /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>
2026-10-02 16:43:07 +02:00
adminandClaude Opus 5.5 81d9c6da30 feat: eigene Story löschen (Web + App)
- 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>
2026-10-01 21:17:16 +02:00
adminandClaude Opus 5.5 ccebfa46a6 feat: Story-Kommentare, Pfoten und Zuschauerliste in der App
- 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>
2026-10-01 20:59:02 +02:00
adminandClaude Opus 5.5 1bf0f9282a fix: HEIF/HEIC-Fotos (Apple) für Beiträge und Storys annehmen
- 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>
2026-10-01 20:26:24 +02:00
adminandClaude Opus 5.5 18f25ce316 fix: DSGVO-Lücken des Empfehlungsprogramms schließen (Auskunft + Löschung)
- 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>
2026-10-01 18:57:59 +02:00
adminandClaude Opus 5.5 32ba01f7cf refactor: altes Einladungs-Gate (InviteCode/pf_invite) durch Empfehlungsprogramm ersetzen
- 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>
2026-10-01 06:35:22 +02:00
adminandClaude Opus 5.5 4d0aa7ab9e feat: Clerk-unabhängiges Empfehlungsprogramm mit einstellbaren Rängen (Web + Admin)
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>
2026-10-01 06:25:13 +02:00
adminandClaude Sonnet 5 e9bad092f2 feat: pet-suggestion notifications, story music overlay, auth UX fixes
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>
2026-09-14 21:02:49 +02:00
adminandClaude Sonnet 5 0323e8d449 feat: add Expo push notification infrastructure
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>
2026-09-06 09:49:02 +02:00
admin e9378d9d7a feat(health): require explicit, revocable consent for Health-Tracking
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.
2026-09-04 16:10:14 +02:00
admin 45448573ea feat(insights): add owner-facing post/story analytics (Gitea #30)
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.
2026-08-24 18:39:03 +02:00
admin 05c9abfb7a feat(stories): add paw reactions and comments (Gitea #29)
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.
2026-08-24 18:28:19 +02:00
admin 75377c4711 fix(follows): fix 404 on followers/following pages, add visibility setting
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.
2026-08-23 17:10:06 +02:00
admin d9930cba60 fix(security): enforce blocks on interaction, close remaining review findings
- New assertNotBlocked() helper (src/lib/assert-not-blocked.ts), mirroring
  assertPetOwnership. comments.create, reactions.toggle (create path), and
  reposts.create now reject direct interaction between blocked pets, not
  just feed visibility (product decision: enforce, matching messages.ts's
  existing DM block check).
- videos.updateCaption and stories.getViewers now route through the shared
  assertPetOwnership helper instead of duplicating the inline ownership
  check.
- ads.ts mutations (recordView, toggleReaction, addComment, toggleRepost)
  gain the same rate limiting their post-side equivalents already have.
- follows.listFollowers/listFollowing/getPetsWithActiveStories now apply
  getSpiderExclusionId(), matching the established per-query pattern in
  feed.ts/explore.ts/search.ts.

Remaining findings from the review are addressed:
- timingSafeStringEqual's length short-circuit needs no fix (matches
  Node's own crypto.timingSafeEqual behavior).
2026-08-23 14:50:20 +02:00
admin 5870d8448a feat(admin): add deleteBroadcast with required reason for the audit trail
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.
2026-08-22 15:48:39 +02:00
admin 1ee2b53a57 feat(species): expand taxonomy to 14 species, add Arachnophobie-Helfer
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.
2026-08-15 11:57:24 +02:00
admin 5660a3a09d feat(admin): system broadcast banner, Phase 1 (Gitea #10)
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.
2026-08-15 11:27:11 +02:00
admin b3d0551bd8 fix(notifications): deep-link like/comment/mention notifications to the actual post
Notification rows for REACTION/COMMENT/MENTION/PAW_BACK linked to the post
owner's profile page instead of the specific post — there was no way to
land on the post itself. Add posts.getById for direct post lookups and
have the pet profile page pick up a ?post= query param to auto-open the
post detail dialog, falling back to a fetch when the post isn't among the
most recently loaded ones. Follow notifications already linked correctly
to the actor's profile.
2026-08-14 14:36:25 +02:00
adminandClaude Sonnet 5 c6ffc1336e feat(sponsor): Stripe-based voluntary sponsoring with Sponsor badge (Gitea #22)
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>
2026-08-14 10:24:08 +02:00
adminandClaude Sonnet 5 f5f788b38c feat(admin): maintenance mode, registration IP capture, post pinning & trending hashtags (Gitea #10 Phase 4)
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>
2026-08-14 08:45:44 +02:00
admin 50e2848312 feat(legal): AI-content disclosure for user uploads (Gitea #18)
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).
2026-08-13 13:44:16 +02:00
admin b4a49f8ed3 fix: address findings from full security/functionality audit (Gitea #17)
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.
2026-08-13 12:42:49 +02:00
admin d670cf9173 feat(theme): account-bound Dark Mode toggle (Gitea #8)
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.
2026-08-13 10:54:23 +02:00
admin 7395d83194 feat(admin): word/link/hashtag blacklist module (Gitea #10 Phase 3, part 3/4)
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
2026-08-12 16:06:37 +02:00
admin efdf407642 feat(admin): add species/breed ad targeting (Gitea #9)
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.
2026-08-11 15:09:56 +02:00
admin 5e2a11e44f feat(admin): route GDPR export downloads through pawfeed.org instead of Supabase (Gitea #6 follow-up)
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.
2026-08-11 14:21:39 +02:00
admin f12dec05bd feat(admin): add GDPR Art. 15 data-export feature (Gitea #6, Plan B)
Aggregates everything held about an Owner (gdpr-export.ts: pets, posts,
stories, comments, reactions, reposts, follows/blocks, health records,
messages, notifications, reports, ban/consent history, Clerk profile)
into a single JSON bundle, uploads it to a private Supabase Storage
bucket (data-export-storage.ts — separate from the public pet-avatars
bucket used for media), and emails the owner a 7-day signed download
link via the new SMTP module. Triggered from a new export button per
user in /p/[secret]/users, logged to ModerationLog.

Deliberately not a zip with embedded media bytes: avoids new archiving
dependencies, the risk of a slow synchronous request for owners with
many posts, and the fact that Mux video assets aren't trivially
re-downloadable — media is referenced by its existing CDN URL instead.
Ad interactions and issued invite codes are not yet included, flagged
in the export's own scope note rather than silently omitted.

Verified end-to-end against the live shared DB before committing:
built a real export, uploaded it, generated a signed URL, and sent a
real test email — all succeeded.
2026-08-11 11:53:27 +02:00
admin 328e54c68e feat(admin): show Clerk profile data and add a Team-messaging channel to the users panel
Gitea #6 (parts 1+2): admin.listUsers now batch-fetches email/name from
Clerk (users only stored the Clerk userId locally, everything else
lives in Clerk) instead of showing a bare userId. Adds
admin.startTeamConversation, which lazily creates a "PawFeed Team"
system Pet under ADMIN_OWNER_ID and reuses the existing pet-to-pet
Conversation/Message model instead of a parallel admin-inbox system —
the admin panel switches ActivePet to Team and opens the ordinary
/messages/[conversationId] thread, so no new UI is needed on the
recipient's side.

Known limitation: only the literal ADMIN_OWNER_ID account can act as
Team, since assertPetOwnership requires an exact owner match.
Extending this to every AdminRole holder is left for a follow-up.

Part 3 (GDPR Art. 15 data export) is deliberately not included —
research only per the issue's own "erst prüfen dann bauen" note.
2026-08-11 11:06:42 +02:00
admin b6610ca7a7 feat(media): add Blurhash placeholders for images across feed, stories, and admin views
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.
2026-08-11 10:03:08 +02:00
admin 17f560eb46 feat(legal): add mandatory upload-rights and data-processing consent to onboarding
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.
2026-08-11 09:33:39 +02:00
admin f43541a4cc feat(security): add Redis-backed rate limiting and CSP/security headers
Public go-live hardening: Redis-backed rate limiting on the previously
invite-gated public surface (explore browsing, post/comment creation,
invite-cookie routes) plus CSP, HSTS, and standard security headers on
every response, now that the invite gate no longer filters traffic.
2026-08-10 07:57:56 +02:00
admin b4ef5bc195 feat(ads): track and display per-ad view counts for advertisers
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.
2026-07-23 20:22:42 +02:00
admin 0e02d3da71 feat(admin): add posts-over-time and owner-growth charts to the dashboard
Two new chart panels on the admin dashboard, each with a day/week/month
granularity toggle:
- "Posts over time" — bar chart of new posts per bucket (trailing 30
  days / 12 weeks / 12 months, zero-filled so gaps show as zero, not
  missing points).
- "Owner growth" — line+area chart of the cumulative total registered
  owners, seeded from a pre-window baseline count so the curve starts
  at the correct absolute total instead of zero.

Backend: admin.getPostsOverTime/getOwnerGrowth bucket timestamps in
plain JS (UTC-consistent day/ISO-week-Monday/month-1st truncation) —
deliberately not raw SQL date_trunc, to avoid DB-session-timezone
ambiguity and keep this testable without hitting Postgres. Data volume
is small enough (Alpha-stage app) that fetching bare createdAt columns
and bucketing in memory is the simpler, lower-risk choice over adding
the codebase's first raw-SQL query.

Frontend: new src/components/admin/ (TimeSeriesChart, GranularityToggle)
following the dataviz skill — thin 2px marks, rounded bar corners,
recessive gridlines, hover crosshair+tooltip on both variants, single
brand-orange hue since each chart is one series (no legend needed).
Admin surface is fixed dark (zinc-950), so colors are hardcoded rather
than light/dark-aware, unlike the app-wide WeightChart this borrows its
SVG approach from.

Added 2 regression tests covering zero-fill bucketing and the
cumulative-with-baseline math. prisma-mock.ts gained owner.findMany/
owner.count (previously missing).
2026-07-23 19:59:36 +02:00
admin bb1515cb93 feat(onboarding): add community-guidelines acknowledgment step before pet creation
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.
2026-07-23 19:37:40 +02:00
admin ff607a604d feat(feed): cold-start discovery feed for new pets with no follows
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.
2026-07-23 18:21:03 +02:00
admin 25fa2a3bec feat(i18n): resolve Species/Breed display names via SpeciesTranslation/BreedTranslation
User found "Cat" showing English on a German-locale profile page. Root
cause: Species.name/Breed.name are the canonical English values; a
SpeciesTranslation/BreedTranslation table + seed data (Hund/Katze/Vogel
etc.) already existed in the schema, and PetForm's species/breed picker
already used it correctly via an explicit locale input param — but every
*display* surface (pet profile, PetCard, Sidebar/MobileNav, search,
explore, followers/following lists, mention @-search, health-card) just
read the raw untranslated name.

Adds `locale` to the tRPC context (createTRPCContext reads the
NEXT_LOCALE cookie the same way i18n/request.ts does), so read
procedures can resolve translations without every caller needing to
pass a locale param. New src/lib/localized-name.ts::pickLocalizedName()
does the lookup with a same-shape fallback to the raw name.

Updated: pets.ts (list/byId/getProfile/update), search.ts (pets/byOwner),
explore.ts (listPets), follows.ts (listFollowers/listFollowing).
health-card/[token]/page.tsx (public, unauthenticated, intentionally
all-German) resolves with a fixed "de" locale instead of ctx.locale,
consistent with its existing hardcoded WeightChart locale.

Verified the seed data is actually present in the production DB
(SpeciesTranslation has Dog→Hund, Cat→Katze, Bird→Vogel) — this fix is
immediately effective on deploy, not blocked on a missing seed run.
2026-07-23 15:19:55 +02:00
admin 4fcde7eb2a fix: remaining MEDIUM/LOW findings from 2026-07-21 audit rerun
- 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.
2026-07-22 19:34:18 +02:00
admin bf49024ace fix: 6 HIGH-severity findings from 2026-07-21 audit rerun
- feed.ts: Postgres fallback path (used when Redis is down) never
  filtered blocked pets, unlike the primary Redis path — blocked pets'
  posts could reappear during a Redis hiccup.
- feed-helpers.ts/feed.ts: getFeedPage always paginated by a hardcoded
  PAGE_SIZE=20, silently ignoring the client's requested (zod-validated
  1-50) limit whenever Redis was up.
- milestones.ts: create accepted any MilestoneType including the
  follower-threshold values (FOLLOWERS_500..FOLLOWERS_1M), which are
  meant to be exclusively auto-awarded — restriction existed only in
  the MilestoneForm picker, not server-side. Now validated against the
  existing USER_INITIATED_MILESTONE_TYPES constant.
- admin.ts: deletePost/deleteReportedPost didn't decrement the original
  post's repostCount when the deleted post was itself a repost, unlike
  reposts.ts's user-facing delete — counter drifted upward permanently.
- PawButton.tsx: count was optimistically guessed off a `reacted` flag
  that can be stale (callers pass initialReacted={false} unconditionally),
  permanently corrupting the displayed count on click. Count is now
  computed from the server's actual toggle result instead of guessed.
- PostDetailDialog.tsx: Report/Block dropdown items had no onClick and
  neither ReportSheet nor BlockDialog were rendered — wired up to match
  PostCard.tsx's existing pattern.
- VideoCard.tsx: invalidated the entire query cache on every video-ready
  transition; scoped to feed/posts/explore router queries instead.
2026-07-22 19:24:21 +02:00
admin 03abe472da fix(security): 4 CRITICAL authorization bugs from 2026-07-21 audit rerun
- health.ts: deleteWeightLog/deleteVetVisit/deleteVaccine checked pet
  ownership but deleted the target row by id alone, never verifying it
  belonged to that pet — any owner could delete another pet's health
  records. Fixed with fetch-then-check (same pattern as comments.ts).
- notifications.ts: list/unreadCount/markAllRead/markRead had zero
  assertPetOwnership calls — any owner could read or clear another
  pet's notification inbox by supplying its (publicly-visible) petId.
- ads.ts: toggleReaction/addComment/toggleRepost never verified petId
  ownership, unlike the near-identical post reactions/comments/reposts
  routers — allowed attributing ad interactions to arbitrary pets.
- FollowButton.tsx: early return before 3 hooks violated Rules of
  Hooks, crashing React when the active pet switched to one already
  rendered in the same list/grid (explore, followers list).

Added regression tests for all 4 (health.ts and notifications.ts had
zero test coverage before this — not a coincidence, per the audit).
2026-07-22 18:21:39 +02:00
admin 2648869f53 fix(tests): resolve 6 pre-existing test failures in pet-profile and stories suites
Prisma mock was missing the inviteCode model entirely, so pets.create's
starter-invite-code check crashed on undefined.count(). STORY-02 tests
mocked story.count, but hasActiveStory actually calls story.findMany
(needs the ids for the follow-up storyView.count query) — updated the
tests to match the real implementation.
2026-07-21 20:46:22 +02:00