Commit Graph
56 Commits
Author SHA1 Message Date
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 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 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 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 af0cea6d3f feat: add MusicTrack model and Story music-overlay fields
Schema groundwork for the Story music overlay: an admin-curated
MusicTrack catalog and three new optional Story fields (musicTrackId,
musicOffsetSecs, musicMuteOriginal). No user-upload path — licensing
liability stays with the admin who uploads a track.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 19:59:42 +02:00
adminandClaude Sonnet 5 0a9038a9a5 feat: add PET_SUGGESTION notification type and owner opt-out
Backend groundwork for a new pet-suggestion notification: a
PET_SUGGESTION NotificationType value and an Owner.petSuggestionsEnabled
opt-out (defaults to true), mirroring the existing hideSpiders pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 18:51:44 +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 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 b0b83703cd feat(taxonomy): expand dog/cat breeds to full FCI/FIFe nomenclature
Dog breeds: added all 340 remaining FCI-recognized breeds (fetched from
fci.be's nomenclature, 2026-08-23) not already in the existing 20-entry
list, bringing the total to 360. 173 of the 340 additions get a German
BreedTranslation using standard German cynology naming; the rest keep
the same name in both locales (correct for many FCI breeds, which
German-language sources don't translate either), falling back via
pickLocalizedName as designed.

Cat breeds: added all 39 remaining FIFe-recognized breeds (fetched from
fifeweb.org/cats/breeds/, 2026-08-23), bringing the total to 54. 21 get
a German translation.

Existing entries were left untouched (upsert is keyed on
speciesId+name) so no Pet.breedId reference breaks. Seed already run
against the shared dev/prod DB (idempotent, additive only) — verified
360/54 breed counts and 193/36 DE-translated counts post-run.
2026-08-23 16:35:46 +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 8186c36b9a feat(stories): support 30s video uploads, mirroring normal video posts
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.
2026-08-16 14:36:44 +02:00
admin 1293f7c879 feat(pets): add optional nickname/Rufname shown alongside the pet's real name
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.
2026-08-15 13:25:54 +02:00
admin 4daac56bfe feat(species): add Chinchilla and Aquarium (Gitea #25)
Chinchilla joins the small-pet species as its own Species (breeds are
color varieties: Standard Grey, Beige, Black Velvet, etc.).

Aquarium models the tank/setup as the pet identity rather than an
individual fish, matching how aquarists actually showcase their hobby —
Breed is the predominant fish species kept (Betta, Guppy, Discus, Koi,
etc.), same pattern as Bird covering many bird types via Breed. Pure
seed-data addition, no schema/code changes needed.
2026-08-15 12:13:48 +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
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 eb26cb5fbe feat(admin): shadowban owners (Gitea #10 Phase 3, part 2/4)
Adds a Shadowban model (mirrors UserBan: reason, issuedBy, expiresAt =
null for permanent) plus MODERATOR+ shadowbanOwner/removeShadowban
mutations logging SHADOWBAN_USER/UNSHADOWBAN_USER to ModerationLog.

Unlike a ban, a shadowbanned owner is never told and keeps using the
app normally — their posts just quietly stop reaching anyone else.
Every reader-facing post query now excludes a shadowbanned owner's
pets for other viewers while still showing that owner their own
content, via a `pet: { OR: [{ ownerId: ctx.userId }, { owner: {
shadowban: null } }] }` filter: feed (both Redis-postId lookup and the
Postgres fallback), profile grid, hashtag search, and the milestones
tab. Explore trending has no reliable per-viewer identity (public
procedure) so it excludes shadowbanned owners unconditionally — a
low-stakes gap since a single post rarely lands there anyway.

Admin dashboard (Gitea #10's "Konten sperren ... Shadowban" spec item):
Users page now shows a violet SHADOWBANNED badge + reason next to the
existing ban badge, and a Ghost-icon shadowban/un-shadowban button
mirroring the ban/unban flow (reason required, unlike ban's optional
one, since the deceptive nature of the action warrants a mandatory
audit trail).

Gitea #10
2026-08-12 15:49:45 +02:00
admin a29c57e37a feat(admin): hide posts without deleting (Gitea #10 Phase 3, part 1/4)
Adds Post.hiddenAt (nullable, db push applied) plus MODERATOR+ admin
mutations hidePost/unhidePost, each logging HIDE_POST/UNHIDE_POST to
ModerationLog with the standard moderatorRole+ipAddress stamp.

hidePost requires a non-empty reason (visible to nobody but audit
trail); unhidePost is a plain reversal. The admin posts grid shows a
"Hidden" overlay badge and swaps the delete-adjacent action button
between hide/unhide based on hiddenAt.

Every reader-facing post query now filters hiddenAt: null so a hidden
post disappears everywhere without touching its row: feed (discovery
candidates, Postgres fallback, and the Redis-postId lookup — the same
place blocked-pet filtering already happens, so an already-fanned-out
hidden post silently drops out at read time), feed backfill on new
follows, explore trending, profile grid, hashtag search, and the
milestones tab.

Admin-facing queries (listPosts, analytics bucketing) are deliberately
NOT filtered — moderators need to see hidden posts to unhide them, and
volume analytics shouldn't be skewed by moderation actions.

Gitea #10
2026-08-12 14:23:00 +02:00
admin a4c4568065 feat(verification): self-service request flow with mandatory review checklist
Gitea #10 Option B:
- new VerificationRequest model — owner submits a message via the pet
  edit page, moderator reviews against a fixed 4-item checklist
  (authenticity/impersonation risk, complete profile, good standing,
  established presence). All four met -> approved, Pet.isVerified flips
  on; any unmet -> auto-rejected with an itemized note of what's missing
- new admin queue /p/[secret]/verification (Moderator+ only, same gate
  as the audit log), status filter, review dialog enforces the checklist
- owner is notified automatically either way via a "PawFeed Team" DM —
  extracted the team-pet lookup/create logic (previously only inline in
  startTeamConversation) into src/lib/team-pet.ts, adding sendTeamMessage
  which reuses the normal Conversation/Message + MESSAGE notification
- review decisions log to ModerationLog with role+IP, same as every
  other Gitea #10 action this session
2026-08-12 10:03:59 +02:00
admin 54fb0d55bc feat(admin): warning system, pet verification badges, report severity, admin GDPR erasure
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
2026-08-12 09:32:33 +02:00
admin c94e60ab82 feat(admin): enforce 3-tier permission matrix, stamp role+IP on every log entry
Gitea #10 follow-up:
- assertAdmin now takes a minRole (SUPPORT < MODERATOR < SUPER_ADMIN)
  instead of a boolean requireSuperAdmin flag, and returns the caller's
  resolved role so mutation sites can log it. Applied per the agreed
  matrix: read-only/support tasks stay SUPPORT, destructive content/user
  actions require MODERATOR, ads/roles/invites/legal-doc-publishing
  require SUPER_ADMIN.
- ModerationLog gains moderatorRole and ipAddress (both nullable —
  historical rows predate capture), populated on every write for
  login-abuse forensics.
2026-08-12 08:53:32 +02:00
admin 2d656bd779 feat(admin): add SUPPORT role, user search/filter, and moderation audit gaps
Gitea #10 follow-up, Phase 1:
- AdminRoleType gains SUPPORT (same access as MODERATOR for now, pending
  a permission-matrix pass to scope it down)
- listUsers gains search (Clerk query + local pet-name match), ban-status,
  and role filters
- ModerationLog now also captures ad create/update/delete and role
  grant/revoke, which previously left no audit trail
- new getModeratorThroughput query surfaces per-moderator action counts
  on the Moderators page
2026-08-12 08:38:25 +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 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 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 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
adminandClaude Sonnet 5 70115de483 feat(legal): admin-published legal updates + owner acknowledgement banner
Adds a pull-based legal-update flow instead of fanning out through the
petId-scoped Notification model (ToS/Datenschutz acceptance is an
Owner-level concern, not a per-pet one):

- Prisma: LegalDocType enum, LegalDocumentVersion model,
  Owner.legalAcknowledgedAt.
- trpc/routers/legal.ts: admin publish/listVersions, owner-facing
  getPendingUpdate/acknowledge. assertAdmin exported from admin.ts
  instead of duplicated.
- Admin panel /legal: publish form (doc type + change summary) + history.
  Publishing IS sending — every owner sees it on next load until
  acknowledged; republishing re-surfaces it for everyone.
- LegalUpdateBanner mounted in (app)/layout.tsx: non-dismissable sheet
  with the stored change summary, "Verstanden" (acknowledge) and
  "Konto löschen" (routes into DeleteAccountDialog via new
  ?deleteAccount=1 auto-open support) actions.

Closes the last DEBUG-List.md item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 20:08:49 +02:00
adminandClaude Sonnet 5 f14be58c06 feat: engagement-counter denormalization, pagination fixes, dead-code cleanup
Bundles the 2026-07-12 code-audit session (Clusters A/B/C/D partial/E/F):
denormalized Post/Advertisement reaction/comment/repost counters synced
transactionally instead of live _count queries; real cursor-based pagination
for followers/following/blocks lists; assertPetOwnership + formatRelativeTime
centralized; dead r2.ts + AWS SDK deps removed; missing DB indexes added;
account-deletion flow, mention notifications, pull-to-refresh feed, and
mobile UI/i18n fixes from the surrounding sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 19:22:23 +02:00
admin dc4c196721 feat: i18n translations, PostDetailDialog 2-col layout, feeding info health module
- Translate ProfileTabs, ExploreCard, PostDetailDialog, HealthDashboard (EN + DE)
- PostDetailDialog: Instagram-style two-column layout (image left, comments right)
- Fix sm:max-w-[960px] override for dialog width on all breakpoints
- ActivePetInitializer: auto-set first pet on login if activePetId is null
- Onboarding: set activePetId immediately after first pet creation
- Add PetFeedingInfo model (foodType, feedingTimes, dailyAmountG, foodBrand, specialDiet)
- FeedingSection component with view/edit/delete, shown in HealthDashboard
- Health Card public page includes feeding info block
- Delete CommentSheet.tsx (dead code)
2026-06-23 14:39:21 +02:00
admin 4b17537a9a feat(i18n): add EN/DE language support with next-intl
- Install next-intl@4 with cookie-based locale switching (NEXT_LOCALE)
- Add LanguageSwitcher component in Sidebar footer
- Translate Nav, PostTypeSheet, Feed, Explore, WelcomeCard, PetForm,
  AvatarUpload, and Onboarding pages (EN + DE)
- Add SpeciesTranslation and BreedTranslation DB models for locale-aware
  taxonomy; listSpecies and listBreeds accept optional locale param
- Seed German translations for all 3 species and 45 breeds
- Add SQL migration script for Supabase (add_i18n_translations.sql)
- fix(invite): secure cookie flag and set-invite route for HTTPS
2026-06-22 14:01:16 +02:00
admin 5800e06cd9 feat: ad engagement, invite gate, sidebar footer, docker setup
Ad interactions (reactions/comments/reposts on ads):
- Schema: AdReaction, AdComment, AdRepost models with RLS enabled
- tRPC: new ads router (getFeedData, toggleReaction, addComment, listComments, toggleRepost)
- AdCard: fully interactive card with paw, comment sheet, repost — mirrors PostCard style
- getActiveAds includes _count for advertiser engagement metrics

Invite system enforcement:
- proxy.ts: /sign-up blocked without pf_invite cookie
- INVITE_REQUIRED=false env var disables gate for post-beta live launch
- Full invite flow: /join → cookie → /sign-up → consume-invite → 3 codes issued to new user
- Admin: listInviteCodes, createRootInvite, revokeInvite procedures

Sidebar mini-footer:
- Impressum + Datenschutz links always visible at sidebar bottom
- PawFeed Alpha-Test copyright line

Docker self-hosting:
- docker/Dockerfile: 3-stage build using Next.js standalone output
- docker/docker-compose.yml: env_file + build-args for NEXT_PUBLIC_ vars
- .dockerignore at project root
- next.config.ts: output standalone for minimal image
- .env.example: documented INVITE_REQUIRED
2026-06-21 18:12:13 +02:00
admin ee658b4788 fix(middleware): migrate to proxy.ts for Next.js 16, enable Supabase RLS on all tables 2026-06-21 15:56:32 +02:00
admin b5f02fe7a4 feat(admin): add hidden admin panel, ban system, ads, and legal pages
- Admin panel at /p/[ADMIN_SECRET]/* — middleware returns 404 for wrong/missing secret
- tRPC adminRouter: getStats, listPosts, deletePost, listComments, deleteComment,
  listUsers, banUser/unbanUser, listAds, createAd/updateAd/deleteAd, getActiveAds,
  listModerators, grantRole/revokeRole, getModerationLog, getActiveBan
- Prisma schema: AdminRole, UserBan, Advertisement, ModerationLog models added
- Ban enforcement: (app)/layout.tsx checks active ban on every request → /banned
- Feed ad injection: active ads shown every 7th post in FeedList (AdCard component)
- Clerk middleware.ts: protects all routes; /p/[secret] validated before auth check
- Impressum + Datenschutz pages: contact data base64-obfuscated, decoded client-side only
- shadcn Switch component added
- ADMIN_SECRET + ADMIN_OWNER_ID documented in .env.example
2026-06-21 15:21:07 +02:00
adminandClaude Sonnet 4.6 d17d71e55a feat(health): emergency vet with OSM search and map
- EmergencyVet model: name, address, phone, website, lat, lng (1:1 to Pet)
- health router: getEmergencyVet, setEmergencyVet, deleteEmergencyVet
- EmergencyVetSection: Nominatim search (debounced 500ms, OSM attribution),
  result dropdown auto-fills address + coordinates, manual override for all fields
- OSM iframe map (no API key) with "In Karte öffnen" link; shown in edit
  mode as coordinate preview and in view mode after save
- Health card (public): red-accented emergency vet block with phone link,
  website link and OSM map; map hidden on print (print:hidden)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:39:46 +02:00
adminandClaude Sonnet 4.6 f6aa825ad1 feat(v2): algorithmic feed toggle, anniversary notifications, welcome card
Algorithmic Feed:
- feed.getFeed gains `mode: chrono | algo` param (default: chrono)
- Algo scoring: reactions*3 + comments*5 + reposts*4 + recency bonus
- FeedList: pill toggle Fuer dich (algo) / Aktuell (chrono), saved to localStorage

Anniversary Notifications:
- Pet schema: birthday Date?, adoptedAt Date?
- NotificationType enum: +BIRTHDAY, +ADOPTION_DAY
- /api/cron/anniversaries: daily cron (08:00 UTC via vercel.json)
- Pet edit form: birthday + adoptedAt date pickers
- Notifications page: Cake/Home icons, German copy, links to health page

New-User Welcome Card:
- WelcomeCard shown at top of feed while localStorage flag unset
- 5 feature tiles: Posts/Videos, Stories, Milestones, DMs, Health
- Dismissable via X or Verstanden; per-pet flag in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:10:50 +02:00
adminandClaude Sonnet 4.6 80559c3b3f feat(health): health card share link, PDF export, weight chart in card
- ShareDialog: expiry selector (7/14/30/60/90 days, default 14), shows
  expiry date after generation, "Als PDF öffnen / drucken" button
- WeightChart extracted to standalone component (WeightChart.tsx) so it
  can be shared between HealthDashboard and the public health card page
- Health card page: renders WeightChart when 2+ weight entries exist
- AutoPrint client component: auto-triggers window.print() when card is
  opened with ?print=1 (600ms delay for render to settle)
- README: Phase 6 + Phase 7 marked complete with full feature summaries;
  project structure and router list updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 06:06:28 +02:00
adminandClaude Sonnet 4.6 f1c798a4b0 feat(07): Phase 7 — Health & Vet Tracking
Schema: WeightLog, VetVisit, Vaccine, HealthCard models
Router: health.ts — CRUD for weight/vet/vaccines + health card token management
Pages:
  - /pets/[petId]/health — owner-only dashboard (weight, vet visits, vaccines)
  - /health-card/[token] — public shareable health card (no auth required)
Profile: Health button added for own pets; Edit + Health side by side

Health data is owner-private; the Health Card link is the only public
access point, shareable via token URL (vet, friends, sitters).
Token can be regenerated to revoke access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 05:07:56 +02:00
adminandClaude Sonnet 4.6 e2e932804b feat(06): Phase 6 — Direct Messages
- Schema: DmPolicy enum (EVERYONE/FOLLOWERS_ONLY), dmPolicy on Pet, Conversation + Message models
- conversationsRouter: list (inbox), getOrCreate (with DmPolicy + block check), unreadCount
- messagesRouter: list (3s poll, auto mark-read), send (+ MESSAGE notification), markRead
- /messages inbox page with unread badges + last message preview
- /messages/[conversationId] thread with bubble UI, day dividers, Enter to send
- Message button on pet profiles (ProfileActions) → getOrCreate → navigate to thread
- Sidebar: Messages link activated
- Pet edit page: DM Privacy setting (Everyone / Only pets I follow)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:45:45 +02:00
adminandClaude Sonnet 4.6 60366691e8 feat(05): Phase 5 — short-form video via Mux
Schema:
- Add VideoPost model (muxUploadId, muxAssetId, muxPlaybackId,
  status, durationSecs, aspectRatio) linked 1-to-1 with Post
- Add VideoStatus enum (PROCESSING/READY/ERROR)
- Add PostType.VIDEO

Backend:
- src/lib/mux.ts — singleton Mux client + webhook secret
- videos router: createUpload (Mux direct upload URL + PROCESSING post),
  getByPostId (status polling), updateCaption
- POST /api/webhooks/mux: verifies signature; handles
  video.upload.asset_created (store muxAssetId),
  video.asset.ready (set READY + playbackId + fan-out to feed),
  video.asset.errored (set ERROR)
- feed.ts + posts.byPetId: include videoPost in all post queries

UI:
- VideoUploadForm: drop zone → XHR PUT to Mux upload URL with
  progress bar; caption field; transitions to "Processing…" on success
- VideoCard: PROCESSING shows spinner; READY renders MuxPlayer
  (HLS, orange accent); polls every 5s until status changes
- PostTypeSheet: "Video Post" option added (between Photo and Milestone)
- PostCard: VIDEO type renders VideoCard instead of image carousel

Env vars: MUX_TOKEN_ID, MUX_TOKEN_SECRET, MUX_WEBHOOK_SECRET,
NEXT_PUBLIC_APP_URL — placeholders added to .env.local

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:27:43 +02:00
adminandClaude Sonnet 4.6 08fd0dfb71 feat(04): Phase 4 — in-app notifications
Schema:
- Add NotificationType enum (FOLLOW, REACTION, COMMENT, MESSAGE)
- Add Notification model with recipientPet, actorPet, post, comment
  relations; index on [recipientPetId, read, createdAt desc]

Backend:
- notifications router: list (cursor-paginated), unreadCount,
  markRead, markAllRead
- follows.create: fire-and-forget FOLLOW notification to followee
- reactions.toggle: REACTION notification on create (skip self)
- comments.create: COMMENT notification with commentId (skip self)
  All triggers use .catch(()=>{}) to never fail the main action

UI:
- NotificationBell component with orange badge, polls every 30s
- /notifications page: list with avatar, type icon, text, thumbnail,
  read/unread dot; auto-marks all read on visit
- Sidebar: Notifications link activated with NotificationBell
- MobileNav: Bell replaces Search tab (Search still at /search)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:59:23 +02:00
adminandClaude Sonnet 4.6 73666a3916 feat(03-01): Phase 3 schema foundation — Reaction/Comment/Repost/Hashtag models + router stubs RED
- Prisma: add Reaction, Comment, Repost, Hashtag, PostHashtag models; PostType.REPOST enum
- Back-relations on Post and Pet for all Phase 3 models (named to avoid ambiguous-relation errors)
- 5 tRPC router stubs: reactions, comments, reposts, explore, search — all registered in _app.ts
- Extend prisma-mock.ts with Phase 3 model mocks
- 5 test scaffold files: 14 failing RED + 3 FORBIDDEN passing; 53 Phase 2 tests untouched
- Phase 3 planning: CONTEXT, RESEARCH, UI-SPEC, VALIDATION, 6 PLANs, ROADMAP + STATE updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:31:48 +02:00