Commit Graph
337 Commits
Author SHA1 Message Date
admin 085d2abb53 feat(mobile): add post creation and comments
Post creation: gallery/camera picker (expo-image-picker) uploads each
image straight to Supabase Storage via a presigned PUT
(posts.getPresignedUrl + expo-file-system), then posts.create with the
resulting storage keys — reachable via a new "+" header button on the
Feed tab. No client-side blurhash (browser Canvas API, no RN
equivalent) or AI-disclosure/mention support yet, matching the rest of
mobile's plain-text captions.

Comments: a dedicated screen (posts/[postId]/comments) for reading,
posting, and deleting one's own comments, reachable by tapping the
comment icon on a PostCard. Ported from web's PostDetailDialog comment
panel, scoped down to just that panel since mobile's image full-view
already has its own route.
2026-09-05 15:35:32 +02:00
admin c1067b3245 feat(mobile): add header wordmark and full-screen image viewer
Header now shows the PawFeed paw+text logo on the Feed tab (matching
web's Sidebar wordmark). Tapping a post image opens a full-screen
viewer as a route-based fullScreenModal, with a blurred edge-to-edge
copy of the same photo behind the sharp foreground image instead of a
flat black backdrop, for a more dynamic Photos-app-style lightbox.
2026-09-05 14:53:13 +02:00
admin 72f09e9f12 docs(mobile): update README — pet profile is done, adjust remaining list 2026-09-05 13:48:09 +02:00
admin a737a17976 fix(mobile): feed separation, adaptive launcher icon; feat: full pet profile
Fixes from user testing on a real device:
- Feed: replaced an invisible spacing-only separator with an actual
  hairline divider between posts.
- Android launcher icon showed Expo's default placeholder (a blue arrow)
  because the app icon and the Android adaptive-icon layers
  (foreground/background/monochrome) are two separate asset sets — only
  icon.png/favicon.png had been replaced. Added scripts/derive-adaptive-
  icon.js to crop the paw glyph out of icon.png (detected by its marigold
  color) and generate all three adaptive-icon layers plus the splash icon;
  this is an explicit placeholder per user's own call, not final art.

New: full pet profile (app/(app)/pets/[petId]/), matching web's
src/app/(app)/pets/[petId]/page.tsx:
- View: identity, species, bio/adoption story, follower/following counts
  (tappable → list screens), the pet's own posts below.
- Follow/unfollow with real initial state (follows.isFollowing) and
  optimistic toggle — unlike feed reactions, this endpoint actually
  exists, so no need to fake an initial state.
- Edit screen (name/nickname/bio/adoption story via pets.update) for the
  owner's own pets.
- Tapping a pet (feed header, Explore card, Pets-tab row) now opens this
  profile instead of nowhere; the Pets tab's active-pet switch is now a
  separate checkmark control instead of overloading the whole row tap.

PostCard's post-shape prop is now structural rather than tied to
feed.getFeed's output type, since posts.byPetId selects `pet` via the
narrower petIdentitySelect (no isVerified/owner) — both need to satisfy
the same component.

Verified end-to-end on-device: feed separator visible, own-pet profile
shows a working Bearbeiten button whose form is correctly prefilled (a
stray edit was made to Bianca's nickname field by mistake during testing
and was discarded via back-navigation without saving — never sent to the
server), someone else's pet profile shows a working Follow/Unfollow
toggle, followers list navigates correctly, and the home-screen launcher
icon now shows the paw instead of the default Expo icon.
2026-09-05 13:47:20 +02:00
admin c35d4cda46 feat(mobile): Phase 1 — real Feed/Explore/Notifications/Pets tabs
Moves the mobile app from a connectivity scaffold to a real Phase 1 app:

- Migrate off Expo Go onto a local custom dev client (expo-dev-client,
  built via expo prebuild + expo run:android against the local Android
  SDK — no EAS account needed). Needed because react-native-mmkv requires
  native code Expo Go doesn't ship.
- Persisted, MMKV-backed TanStack Query cache (PersistQueryClientProvider)
  plus expo-image, so a relaunch renders the last-known feed/pets/
  notifications instantly instead of a blank screen while refetching.
- ActivePetContext: every pet-scoped procedure requires an explicit petId
  server-side (no server-side "current pet"), ported from web's
  ActivePetContext, MMKV-backed instead of localStorage.
- Bottom Tabs (Feed/Explore/Notifications/Pets) mirroring the web app's
  actual mobile nav, with Messages as a header button rather than a tab
  (matches web). Feed, PostCard, PawButton reactions, pet discovery grid,
  notifications list, and a conversations list are all wired to the real
  backend and verified against the live database.
- Design tokens/components extended with PostCard/PawButton/PetAvatar/
  PetIdentityBadges, all styled from the existing src/theme/tokens.ts.
- Fixed a react/react-dom version mismatch surfaced along the way, and
  handle Clerk's needs_client_trust (Device Trust) sign-in status with an
  email-code step.

Verified end-to-end in the Android emulator against pawfeed.org: feed
scrolls and paginates with real posts/images, paw reactions toggle
optimistically and persist, pet switching updates every screen, and
notifications/explore show live data.
2026-09-05 13:19:01 +02:00
admin c4aaa8b3d6 feat(mobile): update app icon and favicon to PawFeed branding 2026-09-05 11:58:17 +02:00
admin dc344f1188 feat(mobile): match PawFeed web design system, fix Clerk device trust
- Add design tokens (colors, radii, typography) mirroring DESIGN.md's
  "Spotlight Porch" system, plus Button/TextField/Card components built
  on them. Restyle the sign-in and placeholder home screens to match.
- Load Inter via @expo-google-fonts/inter, gate rendering on fonts +
  Clerk being ready instead of a blank screen.
- Pin react-dom to match react's version — Metro/Hermes hard-require
  exact equality, and react-dom wasn't pinned anywhere before.
- Handle Clerk's `needs_client_trust` status (Device Trust, tied to the
  dashboard's Native Applications API): send/verify an email code
  before finalizing sign-in.

Verified end-to-end in the Android emulator against the live backend:
sign-in, device-trust code verification, and the home screen showing
real stats.getPublicStats data.
2026-09-05 11:51:17 +02:00
admin 9eb31c3323 feat(mobile): add Expo/React Native scaffold for PawFeed Android app
Connectivity + auth scaffold only: Expo Router, Clerk session auth via
expo-secure-store, and a type-only tRPC AppRouter import from the web
app's router — no backend changes needed. See mobile/README.md.
2026-09-05 11:01:49 +02:00
admin c225c9aef7 fix(admin): mount Clerk client SDK on admin panel to stop UNAUTHORIZED
The admin panel (src/app/p/[secret]/layout.tsx) never wrapped its tree
in ClerkThemeProvider, unlike (app)/layout.tsx — a SpeedUp-Plan P0
optimization that deliberately kept Clerk's client bundle off routes
without a <UserButton/>. Consequence: no client-side session-token
refresh ran on admin pages. The initial page load stayed authenticated
(Clerk's server-side handshake reissues the token on full document
navigations), but subsequent tRPC XHRs — e.g. legal.publish — started
failing with UNAUTHORIZED/401 a few minutes in, since that handshake
only fires for navigations, not fetch calls. Confirmed live: a fresh
page reload always restored a working session, matching this exactly.

Mounting ClerkThemeProvider in the admin layout re-adds background
token refresh. Also gives the legal-publish error handler an
actionable message for UNAUTHORIZED specifically (distinct from other
failures), since a residual case can still occur after a real 7-day
session expiry.
2026-09-04 17:56:43 +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 122a5ba73b docs(legal): refine pet-data personal-data disclaimer per counsel wording
Narrow the Art. 4 No. 1 GDPR non-personal-data claim to data that
permits no inference about the owner as a natural person, and call
out the case where it can (e.g. pet photos alongside a profile
picture) as personal data after all.
2026-09-04 15:50:42 +02:00
admin ef2f67a353 docs(legal): address counsel review of Datenschutzerklärung
- clarify pet-only data (breed, weight, health records) is not
  personal data under Art. 4 No. 1 GDPR since GDPR protects only
  natural persons; still handled confidentially per policy
- switch Clerk/Cloudflare/Mux/Stripe from SCC framing to EU-U.S. DPF
  certification (Art. 45 adequacy decision), strengthen the residual
  SCC/Schrems-II fallback clause beyond TLS + access restrictions
- document 30-day retention and rationale for technical error reports
2026-09-04 15:40:20 +02:00
admin 922c2c1aec docs: correct script-src comment — strict-dynamic ignores the added host
Verified live that adding Clerk's frontend API host to script-src does
NOT resolve the CSP violation in modern browsers: with 'strict-dynamic'
present, CSP3 ignores host-based entries in that directive entirely
(Chrome's own violation message confirms this). The console noise is a
Clerk SDK-side loading quirk, not something fixable here without
dropping 'strict-dynamic'. Keeping the host entry only as the same
spec-mandated pre-CSP3 fallback the existing 'unsafe-inline' entry
already relies on, with the comment corrected to say so.
2026-09-01 20:58:07 +02:00
admin 99d9e5813e fix: allow Clerk's frontend API host in CSP script-src
Clerk's clerkMiddleware auto-adds its frontend API host to connect-src
but not script-src, so the initial (unversioned) clerk.browser.js/
ui.browser.js load was blocked by strict-dynamic before the 307 redirect
to the versioned URL — two CSP violations reported to /api/csp-report on
every page that renders Clerk UI, dragging the Best Practices Lighthouse
score down. Derives the host from the publishable key (same approach as
the existing Supabase/GlitchTip hostname helpers) instead of hardcoding it.
2026-09-01 20:23:24 +02:00
admin f56d1c7cb1 docs: document Clerk German localization, mark Capacitor mobile plan superseded
- README: note @clerk/localizations' deDE usage in the tech stack table
  and shipped-features list.
- ClerkThemeProvider: comment explaining why deDE is needed (Clerk's own
  UI defaults to English regardless of the app's next-intl locale).
- mobil.md: mark the Capacitor plan superseded by the React Native/Expo
  scaffold decided this session (see mobile/README.md).
2026-09-01 20:18:49 +02:00
admin 21e7f29acb feat: localize Clerk auth UI to German via @clerk/localizations
The sign-in/sign-up UI rendered in English despite the rest of the app
being German — @clerk/localizations ships an official deDE locale that
plugs directly into ClerkProvider's existing appearance config.
2026-09-01 19:53:55 +02:00
admin 1b83bb9665 docs: update README, add session handoff, fix deploy-script executable bit
README: document today's shipped work (story reactions/comments, per-pet
insights dashboard, DESIGN.md/PRODUCT.md design system, admin panel visual
pass) in "What's Shipped" and the admin panel section.

docker/rolling-deploy.sh and start.sh were tracked as 100644 (no executable
bit) since this repo is worked from Windows, which never records it — every
fresh git pull onto the Linux NAS made them non-executable again. Fixed via
git update-index --chmod=+x so this doesn't recur.
2026-08-25 19:24:52 +02:00
admin 641a58a1b5 feat(admin): add button legend to Posts page
The post-card action row (comments, hide/unhide, pin/unpin, AI-disclosure
correction, delete) had no explanation of what each icon does — add a help
popover next to the "Posts" heading listing icon + action name.
2026-08-25 19:17:39 +02:00
admin 06a25b4e28 feat(admin): link attention stat tiles to their queue pages
Open reports and Verifications pending summarized a number but gave no way to
act on it — link each tile straight to /reports and /verification, with
hover/focus states and a chevron affordance so it reads as navigation, not
just a number.
2026-08-25 19:14:43 +02:00
admin 2f7cacfc82 feat(admin): color-code dashboard Overview by category, unify depth system
The Overview card was a flat, uncategorized list of 10 stats with no visual
grouping. Split it into three labeled, color-identified groups so a moderator
can scan by kind of number instead of reading every label:

- Activity (emerald): owners, pets, posts, DAU, MAU
- Violations (rose): banned users, shadowbanned, blacklist entries — values
  only pick up the color when actually > 0, same "act on this" convention as
  the existing AttentionStat tiles
- Info (sky): active ads, verifications closed

Also switched the maintenance banner and AttentionStat tiles from border to
ring-1, matching the shared Card component's depth convention so the whole
dashboard uses one visual vocabulary instead of two.
2026-08-25 19:09:34 +02:00
admin f12f4edc69 feat(admin): fix admin shell theme tokens, declutter dashboard, add active-page nav highlight
Admin shell used hardcoded zinc-* classes instead of semantic design tokens and
a redundant "PawFeed Admin" kicker label. Dashboard was a wall of 12 identically
weighted stat cards with no hierarchy. Sidebar/mobile nav gave no indication of
the current page.

- layout.tsx/AdminMobileNav.tsx/GranularityToggle.tsx/TimeSeriesChart.tsx: replace
  zinc-* with bg-background/text-foreground/border-border etc., force dark via
  the `dark` class so the admin surface stays permanently dark regardless of
  site-wide theme toggle
- New AdminSidebarNav.tsx: highlights the current page (desktop sidebar)
- AdminMobileNav.tsx: same active-state logic for the mobile drawer, topbar
  shows the current page label instead of a static title
- page.tsx: replace the 12-card stat grid with 2 AttentionStat tiles (reports,
  pending verifications — color means "act on this") plus a flat Overview list,
  and convert the 5 chart containers to the shared Card component
2026-08-25 18:52:08 +02:00
admin 2cb54d4ed4 feat(design): rebuild landing page, header, and legal pages against new DESIGN.md
Introduces PRODUCT.md/DESIGN.md (Impeccable skill) capturing PawFeed's
existing brand identity ("The Spotlight Porch") as durable reference, then
applies it:

- Landing hero: restrained "spotlight" glow instead of a full orange
  gradient panel, tagline as the actual headline with a marigold accent on
  "Stars"/"stars", real PawPrint mark fixed (was a mismatched hand-drawn
  shape, also fixed a viewBox overflow clipping bug).
- Features section: one dominant mechanism (pet-as-identity) plus a quiet
  supporting list, replacing the four-identical-cards grid craft-floor
  refuses.
- Header: dropped the generic glass-blur sticky nav for a flat header with
  a soft marigold gradient hairline; CTA button now matches DESIGN.md
  (rounded-lg, no shadow, same orange as the hero CTA); logo gets a
  paw-spring hover as a small on-brand moment; new active-page indicator
  (LandingNavLinks) under the current nav link.
- Legal pages (Impressum/Datenschutz/Nutzungsbedingungen): replaced the
  stacked numbered-card-per-section layout with a flowing typographic
  document (hairline dividers, no boxes, no arbitrary section-number
  badges) via the shared LegalLayout/LegalSection components.
- Nutzungsbedingungen: moved Sponsoring earlier (now 3a, right after
  Nutzerinhalte & Urheberrecht) while keeping Salvatorische Klausel/Kontakt
  last; existing section numbers left untouched to avoid breaking the
  "gemäß Abschnitt 5" cross-reference in Haftungsausschluss.
2026-08-25 18:22:58 +02:00
admin 6e9e08be70 feat(insights): make spotlight post/story cards clickable
Top-post and top-story cards in the pet insights dashboard now open the
real post (PostDetailDialog, same fetch-by-id pattern as the notifications
page) or story (StoryViewer, new initialStoryId prop to jump straight to
the right story instead of always starting at index 0).
2026-08-24 18:57:33 +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 59cb72b57e docs: add session handoff for admin mobile fix, breed taxonomy, Gitea #33 2026-08-23 17:16:05 +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 8b74ca5763 fix(admin): make users page responsive on mobile
Search input (w-64) + two status/role selects (w-36) sat in one
unwrappable row, and each user row's shrink-0 action-button cluster
(up to 6 buttons) never yielded width to the info block — both
squeezed everything together below md:. Filter bar now stacks
(search full-width, filters side by side below it), and user rows
stack info above actions on mobile, row layout from sm: up.
2026-08-23 16:05:50 +02:00
admin f7049569e1 fix(admin): make admin panel layout responsive on mobile
The permanent 224px sidebar in the admin layout had no responsive
handling at all, leaving almost no width for content on a phone
(Gitea #10). Sidebar is now hidden below md: and replaced by a
topbar + slide-in Sheet drawer (AdminMobileNav) with the same nav
links; content padding and the outer flex direction adapt per
breakpoint.
2026-08-23 16:00:31 +02:00
admin 707b0b6797 docs: add session handoff for security review + full server sync 2026-08-23 15:31:56 +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 ddf3c2c4e6 fix(security): close GDPR-export/health-card token guessing and video IDOR
- GDPR export token (admin.exportUserData) and health-card token
  (health.getOrCreateCard) now get an explicit crypto-random token
  instead of falling back to Prisma's cuid() default, which is
  timestamp/counter-derived and not meant for unguessable secrets.
- Add IP-based rate limiting to both public token endpoints
  (/api/gdpr-export/[token], /health-card/[token]) to close the
  brute-force window.
- videos.getByPostId now applies the same hiddenAt/shadowban
  visibility filter posts.getById already uses, closing an IDOR that
  let any authenticated user read Mux data for hidden/shadowbanned
  video posts.

Found via a security-reviewer pass over the tRPC/API layer.
2026-08-23 14:25:25 +02:00
admin 4414ef12ed docs: update README to current state, add domain glossary
- CSP: Report-Only -> enforced (nonce + strict-dynamic, Mozilla A+)
- Add Stripe/SponsorContribution, Broadcast/BroadcastReceipt (missing
  from tech stack, model overview, admin panel description, shipped
  features) — both real, deployed features the README never mentioned
- Species list: 3 examples -> the actual 16-category taxonomy
- Known Issues: NPM origin-cert quirk, 401-vs-403 admin debugging note
- New Glossary section: Owner/Pet/Post/Milestone/... relationships
2026-08-22 15:59:29 +02:00
admin d829a755f6 docs: commit session handoffs and local settings
Handoff docs accumulated as untracked files across several sessions —
catching git up per the existing "docs: add session handoff" precedent.
2026-08-22 15:54:34 +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 1965179d36 fix(landing): update securityheaders.com footer badge to A+
Rescan came back A+ (up from A) after the strict-CSP change.
2026-08-22 15:32:28 +02:00
admin 39a72ae45a fix(landing): use official Cloudflare badge artwork instead of custom text
Cloudflare's brand guidelines flag a hand-written "Protected by
Cloudflare" text label as misleading — replace with their official
badge PNG (public/badges/cloudflare-protected.png), same fixed-size
<img> pattern already used for AiDisclosureBadge.
2026-08-22 15:22:41 +02:00
admin 8d058b00a1 feat(landing): add Cloudflare + security-headers trust badges to footer
Was implemented and locally verified earlier this session but never
committed/deployed before moving on to the CSP work — closing that gap.
2026-08-22 15:12:45 +02:00
admin 9265aa4dc1 fix(security): switch CSP to nonce + strict-dynamic (drop unsafe script-src sources)
Mozilla Observatory's rescan flagged the enforced CSP as "implemented
unsafely" (-20) for unsafe-inline/https:/http: in script-src. Enable
Clerk's strict mode, which drops http:/https: and adds a per-request
nonce + 'strict-dynamic' instead. Clerk forwards the resulting CSP
header (with nonce) on both the response and the downstream request,
which is exactly what Next.js's automatic nonce application reads
during SSR, so framework scripts and page bundles are covered for
free. next-themes' hand-written FOUC-prevention <script> is not
covered by that automatic mechanism, so RootLayout now reads the
x-nonce header and passes it to ThemeProvider explicitly.

'unsafe-inline' stays in the header text as a spec-mandated fallback
for pre-CSP3 browsers; strict-dynamic-aware browsers ignore it.
'unsafe-eval' is kept for now (see prior commit ddbeee7) — untouched
by this change, separate follow-up once confirmed unnecessary.
2026-08-22 14:39:53 +02:00
admin ab1b58d644 feat(security): capture line/column/sample in CSP violation reports
Add 'report-sample' to script-src and capture lineNumber/columnNumber/
sample in /api/csp-report for both report shapes (Reporting API and
legacy report-uri). Previously only sourceFile was captured, which
wasn't enough to pin down the 2026-08-15 eval violations — live repro
via the avatar-crop flow (react-easy-crop) came back clean, so the
next real occurrence needs better diagnostics to actually find it.
2026-08-22 14:14:27 +02:00
admin ddbeee759b fix(security): enforce Content-Security-Policy instead of report-only
Mozilla Observatory docked 25 points for CSP being report-only. The
report-only period since 2026-08-12 came back clean in GlitchTip except
6 isolated script-src eval hits on 2026-08-15 (likely react-easy-crop
on the avatar-crop flow) that never recurred. Keep 'unsafe-eval' in
script-src defensively so an enforced policy can't repeat the earlier
incident where a blocked required resource crashed ClerkProvider
app-wide.
2026-08-22 13:51:55 +02:00
admin e89ddc4d32 fix(feed): add reaction/comment/repost actions to milestone posts, fix dark-mode contrast
MilestoneCard never rendered PostCard's action row (PawButton,
comment button, PawBackButton) -- reacting/commenting was already
fully supported server-side (reactions.ts/comments.ts don't restrict
by post type), the UI for it was just missing since MILE-02 shipped.

Also widens the prop type from a narrow local MilestoneCardPost to
the existing PostCardPost (PostCard.tsx already passes the full post
object in, the local type just didn't reflect it) -- this both
unlocks reactionCount/commentCount and lets MilestoneCard open the
same PostDetailDialog PostCard uses for commenting, instead of
needing a parallel implementation.

Separately: the card's bg-orange-50/border-orange-200 were hardcoded
light-only Tailwind classes with no dark: variant, so in Dark Mode the
pet name (text-foreground, which flips to a light color) sat on a
background that stayed pale and unreadable -- same failure mode as
the Clerk auth pages fixed earlier. Switched to the opacity-based
bg-orange-500/5 + border-orange-500/20 pattern already used elsewhere
in the app (unterstuetzen page, notifications list), which adapts to
either theme automatically instead of needing a manual dark: override.
2026-08-21 22:44:26 +02:00
admin fa3f5faa58 fix(i18n): translate milestone type labels
MILESTONE_META in src/lib/milestone-meta.ts held hardcoded English
labels ("Birthday", "Vet Visit", ...) consumed directly by
MilestoneCard and MilestoneForm -- bypassing next-intl entirely, so no
de.json entry could ever have translated them (there was no key to
translate, not a missing one).

Adds a MilestoneTypes namespace to both message catalogs and switches
the two user-facing consumers to useTranslations("MilestoneTypes")
for the label, keeping MILESTONE_META only for the icon lookup. The
admin posts page (p/[secret]/posts) still reads MILESTONE_META.label
directly and is left as-is -- the admin panel is English-only by
existing project convention, not part of the localized surface.
2026-08-21 22:33:03 +02:00
admin c1951b29b9 fix(auth): remove hardcoded light-mode Clerk appearance override on log-in/sign-up
SignIn/SignUp passed their own appearance.variables.colorBackground
("#FAFAFA", always light) and colorPrimary, which Clerk merges on top
of ClerkThemeProvider's appearance -- so in Dark Mode the provider's
baseTheme: dark (light-on-dark default text colors) stayed active
while colorBackground got forced back to light by the page-level
override. Light text on a light background made the whole form
unreadable.

Both page-level appearance props now only set elements.formButtonPrimary
(the orange CTA, theme-independent) -- colorPrimary/colorBackground are
already handled correctly, theme-aware, by ClerkThemeProvider.
2026-08-21 22:24:09 +02:00
admin 3d8c473116 refactor(ui): extract shared PetIdentityBadges component
The verified-checkmark + sponsor-sparkle JSX block was copy-pasted
across 6 near-identical spots (PostCard, pets/[petId] profile,
Sidebar x2, MobileNav x2, pets/[petId]/edit) with inconsistent icon
sizing (h-3 / h-3.5 / h-4) -- part of the same Gitea #31 DRY audit as
the pet-repository select consolidation.

Adds src/components/ui/pet-identity-badges.tsx (same role as the
existing pet-avatar.tsx: a small, domain-specific, reusable
presentational component) and migrates all 5 display sites to it. The
sponsor prop is optional -- Sidebar/MobileNav/edit page simply omit it
rather than faking a sponsor variant. isActiveSponsor() from
lib/sponsor.ts is unchanged, only reused instead of duplicated.

Deliberately NOT touched: the admin verify-toggle button in
p/[secret]/users/page.tsx -- an interactive control with its own
click/fill logic, not a passive display badge; forcing it into this
component would be a forced abstraction, not real DRY.

Pure refactor, no visual change -- verified via tsc, vitest, and a
production build.
2026-08-21 22:07:53 +02:00
admin e0c887c71c refactor(trpc): centralize pet-identity select fragments in a repository layer
The Prisma select fragment { id, name, nickname, avatarKey } (and its
isVerified + owner-sponsor extension) was copy-pasted ~38x across 13
router files -- no repository/query layer existed to consolidate it
(Gitea #31 DRY audit).

Adds src/repositories/pet-repository.ts exporting petIdentitySelect and
petCardSelect (composed via spread) plus their inferred Prisma payload
types, and migrates every call site to import and reuse them -- either
directly or composed with the few extra fields a given query needs
(e.g. ownerId, bio, isVerified where the base fragment doesn't cover
it). A handful of structurally different, smaller field selections
(e.g. name+nickname only, no id) were deliberately left as-is rather
than force-fit into the shared shape.

Pure refactor, no behavior change -- verified via tsc, vitest, and a
production build.
2026-08-21 22:01:57 +02:00
admin 1f177c5e88 perf(landing): inline render-blocking CSS, fix LCP image fetchpriority
Two follow-ups from a fresh PageSpeed Insights run (mobile 87/100/100/100):

1. next.config.ts: enable experimental.inlineCss. Removes a ~342ms
   render-blocking CSS request (globals.css, ~21.6 KiB gzip -- shared by
   every route since Tailwind bundles the whole app's utilities into one
   file). Officially documented, dependency-free, and explicitly
   recommended for small atomic-CSS bundles like ours. Applies globally
   (can't be scoped per route), so hard/full page loads lose cross-page
   CSS caching -- soft client-side navigations are unaffected.

2. page.tsx hero image: `priority` was deprecated in Next.js 16 in favor
   of `fetchPriority`/`loading` as separate props -- it still emitted a
   <link rel=preload> but no longer set fetchpriority="high" on the <img>
   itself. Replaced with loading="eager" fetchPriority="high" per the
   bundled Next.js 16 docs.

SpeedUp-Plan, Gitea #28.
2026-08-21 06:43:22 +02:00
admin a9e0636fe1 fix(llms.txt): remove stale invite-required note
Invite gating was already turned off a while back; sign-up is open to
everyone now.
2026-08-20 20:56:44 +02:00
admin 167b9e1a80 feat(seo): add llms.txt, fix middleware blocking .txt static files
Adds public/llms.txt (llms.txt spec: H1 + summary + linked sections)
describing the product for AI agents/crawlers -- usage-focused only, no
internal vendor/infra details.

Also fixes proxy.ts's middleware matcher, which excludes common static
file extensions from Clerk's auth gate but was missing .txt -- every
request to /llms.txt (and any future .txt file, e.g. robots.txt) was
being redirected to the Clerk sign-in page instead of served. No app
route under src/app uses a .txt path, so this only affects genuinely
static files in public/.

SpeedUp-Plan (Gitea #28) -- last remaining Lighthouse finding.
2026-08-20 20:43:25 +02:00