Commit Graph
55 Commits
Author SHA1 Message Date
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 f48d98cf3a fix(security): lock Mux upload CORS origin to the real domain in prod
cors_origin was hardcoded to "*" for all environments. Now restricted
to NEXT_PUBLIC_APP_URL when NODE_ENV=production; stays "*" in dev so
localhost uploads keep working.
2026-08-10 12:50:40 +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 1461470246 refactor(cluster-c): extract shared notify() helper, align getActiveBan admin check
6 fire-and-forget notification.create() call sites (follows, reactions,
comments, reposts, messages, mention-helpers) duplicated the same
{recipientPetId, type, actorPetId?, postId?, commentId?} + .catch(() => {})
shape — consolidated into src/lib/notify.ts.

admin.ts's getActiveBan reimplemented the "is this user an admin" check
inline instead of reusing assertAdmin's logic; extracted a shared
isAdmin() helper so both paths stay in sync. assertAdmin itself is
untouched.
2026-07-21 20:50:33 +02:00
adminandClaude Sonnet 5 215fba80d4 fix(cluster-h): wrap admin delete+moderation-log writes in a transaction
deletePost, banUser, unbanUser, dismissReport, and deleteReportedPost each
ran the mutation and the moderationLog.create as two separate sequential
awaits. The audit flagged this as "should be Promise.all", but that would
have introduced a real bug: if the delete/ban call failed, a concurrently
fired log write could still succeed, recording an action that never
happened. Wrapped both in $transaction instead (matching the pattern
deleteComment already used) — atomic, and the log can only be written if
the mutation actually succeeded.

Reviewed the audit's other Cluster H finding (a few include-vs-select
over-fetches) and found nothing actionable — the flagged spots already
narrow relations with select where it matters; the unnarrowed ones are
small lookup/relation tables used in full by their call sites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 20:39:20 +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 6f671cfc63 fix(ads): render image in AdCard feed, add image upload to ad dialog 2026-06-21 16:21:38 +02:00
admin 0f34a2c9c6 feat(admin): show post preview in reports — images, caption, pet, counts 2026-06-21 16:10:01 +02:00
admin e2d14654c8 feat(admin): reports page with dismiss and delete-post actions 2026-06-21 16:00:33 +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
admin e407064866 feat(explore): add trending tab with hot posts and hashtags
- explore.getTrending: top-12 photo posts by reaction count (48h window)
  and top-10 hashtags by post count (7d window)
- Explore page opens on Trending tab by default; species/breed tabs
  preserved with breed filter hidden when not applicable
- Trending UI: hashtag chips linking to /hashtag/[tag] + 3-col post
  grid with paw-count overlay and PostCard detail dialog

feat(follows): auto-trigger follower milestone posts

- After each follow, count followers for followeePetId and check against
  all thresholds (500 / 1K / 2.5K / 5K / 10K / 25K / 50K / 100K /
  250K / 500K / 1M)
- Guard against duplicate awards via existing Milestone lookup
- Fire-and-forget: milestone post created + fanned out to follower feeds
  without blocking the follow response
2026-06-21 14:41:20 +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 5d396a7bb4 fix: make pet profiles clickable from feed, search, and notifications
- Add pets.getProfile procedure (no ownership check) so any auth'd user can view any pet profile
- Profile page now uses getProfile; Edit link and owner attribution only shown for own pets
- PostCard header: wrap PawRing + pet name in Link to /pets/[petId]
- Search Pets tab + People tab pet rows: wrap avatar/name in Link to /pets/[petId]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 04:52:45 +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 da0f9f2bb7 fix: resolve 5 bugs from dev testing
- PostCard: add variant="detail" (object-contain + black bg) for profile sheet — full image in original format
- VideoCard: getByPostId polls Mux API directly when PROCESSING, self-heals without webhooks in dev
- PawRing: gray ring for all-seen stories, orange for unseen; reads viewerPetId from ActivePetContext
- Search: add People tab via new search.byOwner (Clerk username search → owner's pets)
- Notifications: rows now navigate to actor profile (FOLLOW) or pet profile (REACTION/COMMENT)
- Notifications router: include post.petId for navigation target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 08:41:46 +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 a9886948b8 fix: empty feed + unclickable profile grid
- feed.getFeed: when Redis is unavailable, fall back to a direct
  Postgres query (followed pets + own posts, cursor-based) instead of
  returning an empty array — feed now works in dev without Upstash
- ProfileTabs: wrap grid thumbnails in <button> + open a bottom Sheet
  with the full PostCard on tap; previously tapping a photo did nothing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:38:53 +02:00
adminandClaude Sonnet 4.6 5af2a40bb3 fix: resolve three runtime errors found in dev smoke test
- MultiImageUpload: move onKeysChange out of setImages updater into
  useEffect to fix React "Cannot update a component while rendering"
  warning; same fix applied to handleRemove
- feed.getFeed: wrap getFeedPage in try/catch so Redis failure
  (Upstash not configured) returns empty feed instead of 500
- posts.create + reposts.create: wrap fanOutPost with .catch(() => {})
  so Redis failure never rolls back an already-committed post

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:33:14 +02:00
adminandClaude Sonnet 4.6 847ac97bde feat(03-06): Hashtag search end-to-end — Phase 3 complete
- hashtag-helpers.ts: extractHashtags (regex+dedup) + saveHashtags (upsert)
- parse-caption.ts: parseCaption → CaptionSegment[] for client rendering
- search.byHashtag GREEN: hashtag.findUnique → postHashtag.findMany + pagination
- posts.create + reposts.create: extract+save hashtags after post creation
- /hashtag/[tag]: infinite scroll post grid + useInView sentinel
- /search Tags tab: byHashtag query wired, 3-col post grid
- PostCard: parseCaption replaces raw caption — hashtags are orange Link to /hashtag/[tag]
- STATE.md: Phase 3 complete, 3/7 phases done

Tests: 67 passing, 0 failing (5 todo), tsc clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:16:03 +02:00
adminandClaude Sonnet 4.6 59a3be27b0 feat(03-05): Explore + pet search — routers GREEN + pages + nav
- explore.listPets: publicProcedure, speciesId/breedId filter, cursor pagination
- search.pets: protectedProcedure, ILIKE name+breed OR, cursor pagination
- ExploreCard: avatar, breed badge, follower count, FollowButton
- /explore: species tabs + breed filter Select + infinite pet grid
- /search: debounced input, Pets tab with FollowButton rows, Tags stub
- Sidebar: Explore activated, Search added
- MobileNav: Explore activated, Notifications placeholder → Search

Tests: explore 3/3 GREEN, search.pets 2/2 GREEN; 65 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:09:24 +02:00
adminandClaude Sonnet 4.6 8507fe9489 feat(03-04): Paw-Back/Repost — reposts router GREEN + RepostCard + PawBackButton
- reposts.create: ownership + repost-of-repost guard + idempotency + fanOutPost
- reposts.delete: ownership + cascade via post.delete
- reposts.isReposted: compound-key lookup, returns boolean
- PawBackButton: optimistic toggle, orange when reposted, invalidates feed
- RepostCard: attribution header + original pet/images/caption, no action row
- PostCard: REPOST type dispatches to RepostCard; stub replaced with PawBackButton
- feed.ts + posts.ts: repost include added to all post hydration queries
- PostCardPost type: OriginalPostContent + repost field

Tests: 3/3 reposts GREEN, 60 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:01:58 +02:00
adminandClaude Sonnet 4.6 6fbb99e7c9 feat(03-02): Paw reactions GREEN — toggle/count router + PawButton on PostCard
- reactions.toggle: assertPetOwnership → findUnique(postId_petId) → create or delete, return {reacted}
- reactions.hasReacted, getCount implemented
- feed.ts + posts.byPetId: add _count{reactions,comments} to hydration include
- PawButton: optimistic toggle, filled/outline PawPrint icon, count badge
- PostCard: replace "0 likes/0 comments" placeholder with action row (PawButton + comment chip + Paw-Back stub)
- PostCardPost type: add optional _count field
- 4/4 reactions tests GREEN; 56 passing total (+3 from 53)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:43:31 +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
admin 554ca1deae test(02-06): add milestones tests RED → implement milestonesRouter GREEN — MILE-01 2026-06-13 10:21:40 +02:00
admin e30531f834 feat(02-05): storiesRouter GREEN + PawRing + StoryViewer + StoryTray + StoryForm 2026-06-13 10:08:23 +02:00
admin 99bff88545 test(02-05): add stories tests RED — STORY-01-03 2026-06-13 10:08:16 +02:00
admin fa9b33ad7e refactor(feed): use PrismaClient type directly in assertPetOwnership 2026-06-13 10:02:17 +02:00
adminandClaude Sonnet 4.6 75d1e80665 feat(02-04): implement postsRouter GREEN — PHOTO-01–04
- getPresignedUrl: scopes key to posts/{ownerId}/{petId}/, ownership check (T-02-30)
- create: ownership assertion + Post+PostImage nested write + fanOutPost (PHOTO-01)
- create: imageKeys.max(10) enforces carousel limit (PHOTO-02, T-02-32)
- imageKeys.startsWith('posts/') prevents path traversal (T-02-31)
- updateCaption: assertPostOwnership via post→pet→ownerId (PHOTO-03, T-02-34)
- delete: assertPostOwnership + prisma.post.delete, PostImages cascade (PHOTO-04)
- byPetId: cursor-paginated query with images+pet+milestone include

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:43:35 +02:00
admin f85230a074 feat(02-02): implement blocksRouter + reportsRouter (GREEN)
- blocks.create: assertPetOwnership + self-block rejection + prisma.block.create (SAFE-01)
- blocks.list: assertPetOwnership + prisma.block.findMany scoped by blockerPetId (T-02-12)
- reports.create: assertPetOwnership + both-null target rejection + prisma.report.create (SAFE-02)
- ReportReason enum: SPAM, HARASSMENT, INAPPROPRIATE, MISINFORMATION, OTHER (T-02-13)
- Blocks authoritative in Postgres only; never Redis (RESEARCH anti-pattern avoided)
2026-06-07 12:33:42 +02:00
admin 7bdb2989ca feat(02-02): implement followsRouter (GREEN)
- follows.create: assertPetOwnership + self-follow rejection + prisma.follow.create + backfillOnFollow
- follows.delete: assertPetOwnership + prisma.follow.delete by composite key
- follows.isFollowing: prisma.follow.findUnique → boolean
- follows.listFollowers: scoped by followeePetId, includes follower display fields
- follows.listFollowing: scoped by followerPetId, includes followee display fields
- All mutations use protectedProcedure; social graph is pet-to-pet never ownerId (SOCL-01/02/03)
2026-06-07 12:31:50 +02:00
admin 56f976e3c4 feat(02-03): implement feed.getFeed router with Redis + Prisma hydration
- assertPetOwnership enforces petId ownership (T-02-20)
- getFeedPage reads Redis sorted set with cursor pagination
- Block filter applied at Prisma hydration layer (T-02-21)
- Post order restored to Redis score order after Prisma findMany (Pitfall 7)
- FEED-01 tests green
2026-06-07 12:31:36 +02:00