Commit Graph
291 Commits
Author SHA1 Message Date
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
admin bf9bb34f96 perf(landing): replace low-res hero mockup PNG with a retina WebP
The old public/app-preview-mobile.png was exactly 327x714px -- 1:1 with
its CSS display size, so retina displays (2x/3x DPR) had no headroom and
Lighthouse flagged it as "serves images with low resolution"
(image-size-responsive, SpeedUp-Plan P2).

Replaced with a fresh feed screenshot supplied by the user, center-cropped
to the same 327:714 aspect ratio and delivered at 981x2142 (3x) as WebP
(~193 KB) so next/image's built-in optimizer has real headroom to generate
a proper srcset instead of upscaling a 1x source.
2026-08-20 20:33:33 +02:00
admin 556aac7e22 fix(theme): guard theme-icon rendering against SSR/client hydration mismatch
resolvedTheme from next-themes is undefined during SSR and on the client's
first paint (before its mount effect runs), so ThemeToggleButton and the
shared useThemeToggle hook (Sidebar, MobileNav) rendered a Sun/Moon icon
that could mismatch the server-rendered one on first hydration whenever
the system/stored theme was dark -- a React #418 hydration error.

Both now gate isDark on a mounted flag, matching next-themes' documented
hydration-safe pattern.

Found via a Chrome DevTools Lighthouse re-check on the SpeedUp-Plan work
(Gitea #28) -- pre-existing, unrelated to the Clerk-provider scoping.
2026-08-20 20:11:55 +02:00
admin 383817916d perf(landing): scope Clerk provider + fix contrast/landmark on marketing page
Moves ClerkThemeProvider from the root layout into (app)/layout.tsx and
(auth)/layout.tsx — it was previously mounted globally, shipping the
~240 KiB Clerk client SDK to every route including the public marketing
page. Only Sidebar's UserButton and the (auth) sign-in/sign-up forms
actually need client-side Clerk context; server-side auth() calls are
unaffected since they read from proxy.ts's clerkMiddleware, not React
context.

Also fixes two WCAG AA contrast failures on the hero/header CTAs
(orange-500/600 -> orange-700, ~2.8:1/3.6:1 -> ~5.5:1) and adds a
missing <main> landmark around the landing page content.

Source: PageSpeed Insights audit, mobile Performance 70 -> targets >90.
2026-08-20 19:00:35 +02:00
admin bc731ac434 fix(pets): stop active-pet selection reverting on every page reload
ActivePetProvider read localStorage in a useEffect, but
ActivePetInitializer (a descendant, so its effect fires first — React
runs effects child-before-parent) checked activePetId in its own
effect and overwrote localStorage with the owner's first pet whenever
it still saw the initial null — which was always, on every reload,
since the provider's own effect hadn't run yet. The switched-to pet
never had a chance to be read back.

Reads localStorage synchronously via a lazy useState initializer
(window-guarded for SSR) instead, so activePetId is already correct
on the client's first render and the race can't happen.
2026-08-16 17:17:25 +02:00
admin 27cb34df27 fix(stories): portal the story overlay to escape overflow-auto ancestor
Real root cause of the mobile rendering bug (confirmed via an actual
Android Chrome screenshot, not devtools emulation): StoryViewer's
fixed inset-0 div is a descendant of <main className="overflow-auto">
in the (app) layout. On Android Chrome, a position:fixed element
inside an overflow-auto ancestor can size itself against that
ancestor's full scrollable content instead of the real viewport —
the overlay rendered many times taller than the screen, so only a
small top slice was visible, looking like a heavily zoomed crop. The
previous two commits were chasing the video's object-fit/aspect-ratio,
which was never the actual problem.

Renders the overlay via createPortal to document.body instead, the
same pattern Radix/Base UI's Sheet and Dialog already use internally
for exactly this class of bug.
2026-08-16 17:09:50 +02:00
admin 661f36475a fix(stories): restore full-bleed mobile video rendering
The aspect-ratio-sized box from the previous commit caused a
mobile-only bug — video appeared cropped/zoomed instead of fitting.
Root cause not fully isolated (mux-player's object-fit default is
contain, confirmed in its source, so it wasn't that), but the sized
box is the only recent change in that path. Gates it to sm+ viewports
via a CSS custom property + Tailwind arbitrary aspect-ratio, and
reverts to full-bleed + explicit object-fit: contain below that —
the combination already verified working before the box-sizing
change.
2026-08-16 14:54:15 +02:00
admin 07c0f82293 fix(stories): size the video player box to the real aspect ratio
The player box was full-bleed (w-full h-full) with object-fit: contain
handling the letterboxing internally — visually correct but the box
itself still spanned the whole screen, so the black letterbox area
read as part of the player rather than just backdrop. Sizes the box
itself to the story's aspectRatio (defaulting to 9:16 before Mux
reports the real value), centered via flex — same visual result, but
now only the actual content area is the "box".
2026-08-16 14:47:34 +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 9c55e18d4e feat(sponsor): show Stripe wordmark above the one-time/monthly toggle
Adds a subtle "payments processed by Stripe" indicator (official
wordmark SVG from Simple Icons, muted/low-opacity) above the
contribution-type selector on the Unterstützen page, so visitors know
which payment provider handles the transaction before choosing an
option.
2026-08-16 13:57:27 +02:00
admin 824927cb77 fix(explore): support mouse-wheel scrolling on desktop
Hiding the scrollbar (previous commit) removed the only desktop
affordance for moving a horizontally-scrolling, scrollbar-hidden row
— mouse wheel scrolls vertically by default and there's no drag
handle anymore. Adds useHorizontalScroll, which redirects vertical
wheel delta to scrollLeft while leaving trackpad horizontal gestures
and touch swipe untouched. Applied to the explore species tabs and
StoryTray, which had the identical latent issue.
2026-08-16 13:45:07 +02:00
admin 0f3b672532 fix(explore): remove stray vertical scrollbar, hide horizontal scrollbar
overflow-x-auto with overflow-y left at its default resolves to auto
per the CSS overflow spec, so an irrelevant vertical scrollbar showed
up alongside the horizontal one. Adds overflow-y-hidden, plus a real
scrollbar-hide utility (@utility, Tailwind v4) for a cleaner look —
StoryTray.tsx already referenced this class name without it ever
being defined, so this fixes that dead reference too.
2026-08-16 13:39:32 +02:00
admin dfb01b2d49 fix(explore): make species tab bar horizontally scrollable
TabsList is inline-flex/w-fit with no overflow handling, so all 14+
species tabs squeezed into one line as the taxonomy grew instead of
scrolling. Wraps it in an overflow-x-auto container, scoped to the
Explore page only — the shared Tabs component is used elsewhere with
small, fixed tab counts that don't need this. Fixes Gitea #27.
2026-08-16 13:34:27 +02:00
admin 6a61bbcc81 feat(docker): zero-downtime rolling deploy for the 3 replicas
Adds a dependency-free liveness endpoint (GET /api/health), a Docker
healthcheck against it, and docker/rolling-deploy.sh which rebuilds
the shared image once and restarts pawfeed-1/2/3 sequentially, gated
on each becoming healthy before moving to the next. Verified
zero-downtime via continuous curl monitoring through a full run
against production.
2026-08-16 13:17:04 +02:00
admin faf8ee98b0 fix(videos): fan out posts that self-heal to READY via polling
getByPostId's self-heal path (client polls while PROCESSING, checks
Mux directly) set status/muxPlaybackId on READY but never called
fanOutPost, unlike the mux webhook handler's video.asset.ready case.
Videos that self-healed this way played fine but never entered any
pet's feed. Found via a live upload test after the replica rollout.
2026-08-16 13:07:31 +02:00
admin 1e060e9179 feat(docker): scale to 3 load-balanced replicas (Gitea #26)
Next.js standalone is single-process, so one container only used one
CPU core. A load test hit a hard ceiling at ~60-65 req/sec with one
core pegged at 133% while 3 cores sat idle. Now runs as pawfeed-1/2/3
behind an NPM least_conn upstream, verified via load test showing CPU
spread evenly across all three replicas.

Also caps the Prisma pg pool per-replica via DB_POOL_MAX so 3 replicas
stay well under the Supabase pooler's connection limit.
2026-08-16 11:38:05 +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
admin 4c1931fd64 fix(post-detail): fix carousel image centering at the actual root cause
Two prior attempts today (aabb7d3, a070b37) both failed in production
despite each being verified in isolation — confirmed via DevTools that
the deployed DOM/CSS exactly matched what had tested correctly, yet the
real page still rendered short images pinned to the top.

The real, unfixable-from-outside root cause: CarouselContent's own ref
div (src/components/ui/carousel.tsx) is hardcoded to className=
"overflow-hidden" with no way to reach it via props, so it never gets an
explicit height — every centering attempt built on top of it (grid,
flex, items-center at various levels) still bottomed out on that div
having only an indefinite, percentage-against-an-auto-parent height,
which is a genuinely ambiguous CSS case browsers can resolve
inconsistently.

Fix: give CarouselContent a new optional viewportClassName prop that
reaches that specific div, and pass h-full through it from
PostDetailDialog. Carousel's own root div already has a definite height
(traced up through the dialog's fixed dvh values), so h-full on the ref
div now resolves to a real pixel value instead of falling back to auto —
eliminating the ambiguity instead of working around it. Carousel/
CarouselItem revert to plain h-full + flex items-center/justify-center,
no grid trick needed anymore.

Re-verified via an isolated static HTML/CSS repro (embla-free, no auth
needed) before deploying this time.
2026-08-15 09:02:36 +02:00
admin a070b37e22 fix(post-detail): actually center carousel images (previous fix was a no-op)
The items-center/justify-center I added to CarouselItem in the last
commit had no effect — verified by building an isolated static HTML repro
of the exact carousel.tsx DOM structure (embla-free, no auth needed) and
testing it directly in a browser, since PostDetailDialog itself sits
behind Clerk auth and can't be exercised locally.

Root cause was one level higher than CarouselItem: CarouselContent's own
ref div (src/components/ui/carousel.tsx) is hardcoded to
"overflow-hidden" with no height/flex class ever reaching it — nothing
propagates the media column's definite height down to it, so it renders
at its own auto/content height and just sits at the top of the Carousel
element's box via normal block flow. CarouselItem's centering was
already correctly placed, but had no gap to center within, since the
box it was centering into never itself moved off the top.

Fix: attach centering to the Carousel component's own outermost element
instead — the deepest point reachable via props, since CarouselContent's
outer div doesn't forward a className. Used CSS Grid (grid + items-center)
rather than flex specifically because grid's default justify-items:stretch
preserves the ref div's full width (required for embla's horizontal
scroll-snap math); flex's shrink-to-content default for un-stretched
items would likely have broken horizontal scrolling between images.

Verified via the isolated repro: short/wide image now centers vertically
within the media column, and the "always full width" marker stayed
100% wide across all three test cases (single tall, single wide, mixed
pair in one row).
2026-08-14 20:31:21 +02:00
admin aabb7d3074 fix(post-detail): center carousel images of different sizes vertically
Regression from today's earlier "fill + object-contain" rework: fixing
that removed CarouselItem's flex items-center/justify-center, which is
what centered images in the carousel — because the shadcn Carousel
primitive's inner ref div (src/components/ui/carousel.tsx) never gets an
explicit height (its outer wrapper is hardcoded to just "overflow-hidden",
no className passthrough), the img's h-full never resolves to anything
definite there, so object-contain silently falls back to the image's
natural aspect-ratio sizing instead of fill+letterbox+center. Without
items-center, a shorter image among differently-sized carousel images
just sat flush at the top of the (taller, shared) row instead of centered.

The single-image path was unaffected — its container chain does resolve
to a definite height, so object-contain there works as intended.
2026-08-14 20:18:47 +02:00
admin d2bfe53baa copy(sidebar): update footer version tag from Alpha-Test to V1.0.0-rc 2026-08-14 20:10:48 +02:00
admin f29f2d3a18 tweak(about): scroll-synced opacity fade for paw trail, more random layout
Opacity now animates via --paw-opacity in the same scroll-driven keyframe
as the scale/rotate transform, so each print actually fades in as it
scrolls into view instead of appearing at full (low) opacity immediately
and only growing in size. Positions reworked: irregular top gaps and
non-alternating side assignment instead of a near-uniform ladder, offsets
moved from hugging the viewport edge to flanking the centered text column,
and count bumped 13 -> 15 (+15%).
2026-08-14 20:08:02 +02:00
admin d2056b8a8b tweak(about): stronger paw print trail, first one always visible, more of them
Opacity roughly doubled (0.06-0.08 -> 0.14-0.16) per user feedback that
the trail read as too faint. First print now skips the scroll-reveal
animation entirely and renders at rest immediately, signaling the trail
exists before the reader scrolls at all — the rest still stamp in on
scroll. Trail count bumped from 8 to 13 for denser coverage down the page.
2026-08-14 19:28:48 +02:00
admin 7496d900f3 feat(about): add motion/depth WOW effects to the "Über uns" page
- AnimatedNumber: stat tiles count up from 0 when scrolled into view
  instead of appearing static, so "live numbers" actually feels live.
- Pull quote: the story's strongest line is pulled out into its own large
  editorial-style callout between the hero paragraphs and the "Von Tier
  zu Tier" section, giving the wall of text a visual beat. paragraph2
  trimmed (DE+EN) so the line isn't read twice back to back.
- PawPrintTrail: a scattered trail of low-opacity paw prints behind the
  story content, each "stamping" into place via a pure-CSS scroll-driven
  animation (animation-timeline: view(), same technique as the existing
  hero-parallax-icon on the landing page) — no JS, respects
  prefers-reduced-motion.

Fixed a real bug while building the trail: -z-10 on its wrapper had no
local stacking context to be scoped to (the parent had `relative` but no
z-index), so it escaped past the page content and rendered behind the
document background — invisible regardless of opacity. Giving the page's
outer wrapper `z-0` fixes it.

Verified locally via dev server + Chrome DevTools MCP: light + dark theme,
375px mobile, quote/trail/stat-tile rendering all confirmed visually.
2026-08-14 19:22:14 +02:00
admin 9071bf3c9a feat(about): add "Über uns" page with founding story and live stats (Gitea #23)
New public /ueber-uns page — deliberately doesn't preview or link into the
platform itself (no phone mockup, no post previews), just the founding
story plus live member/pet/sponsor counts for transparency. Fully
bilingual via the existing LanguageSwitcher.

- src/trpc/routers/stats.ts: new public stats.getPublicStats procedure,
  Redis-cached (5min TTL), sponsor count includes lapsed subscriptions
  (not just currently-active badges) per discussion on the issue.
- src/proxy.ts: /ueber-uns registered as a public route, exempt from
  maintenance mode (doesn't touch the platform, so no reason to gate it).
- Extracted LandingHeader/LandingFooter from src/app/page.tsx into shared
  components under src/components/landing/ — this page reuses the same
  navbar, and duplicating it risked the responsive header fixes from
  earlier today drifting out of sync between two copies. The "Bald
  verfügbar" placeholder nav link now points at the real page.
- src/lib/stripe.ts: made the Stripe client a lazy singleton instead of
  constructing it at module scope. Eager construction crashed the entire
  tRPC appRouter (and anything importing it, e.g. createTRPCCaller() on
  any public page) whenever STRIPE_SECRET_KEY is unset — invisible until
  now because no public page had used createTRPCCaller() before.
- stats.getPublicStats wraps its Redis cache read/write in .catch() so a
  Redis outage degrades to uncached rather than breaking the page,
  matching the existing pattern in src/lib/maintenance.ts.

Verified locally via dev server + Chrome DevTools MCP emulation (DE/EN,
mobile 375px, desktop) — this is a public unauthenticated page so, unlike
most of today's other fixes, testable outside of Clerk auth.
2026-08-14 18:51:42 +02:00
admin 4ea8eaca04 fix(landing): fix mobile header overflow and a 768px tablet overflow
Mobile (<640px): the sign-up button was ~50% offscreen. The header
crammed logo + theme toggle + full LanguageSwitcher (globe icon +
"Sprache:" label + 2 flag buttons) + sign-up pill into a narrow
viewport. Collapsed LanguageSwitcher to icon-only flags below sm,
tightened header padding/gaps, and reduced the sign-up button's
mobile padding. Verified via Chrome DevTools viewport emulation at
320px and 375px — button fully visible at both.

While verifying, found a second, seemingly pre-existing overflow at
exactly 768px (the md: breakpoint): the desktop nav links (Home/Über
uns/Impressum) plus the full-size right-side group don't fit in that
width, cutting off the sign-up button again and causing horizontal
scroll. Moved the nav links from md:flex to lg:flex so they only
appear once there's actually room (confirmed clean at 768px and
1024px+).
2026-08-14 15:39:29 +02:00
admin a9a6a975a9 copy(sponsor): reframe subtitle as private financing, not ad replacement
The old copy positioned sponsoring as a substitute for ads ("instead
of ads"), but the platform still runs ads when advertising partners
are available — sponsoring and ads aren't mutually exclusive. Reframe
around the actual motivation: PawFeed is entirely privately financed,
and sponsoring helps cover ongoing costs (server hardware, software
licenses, tooling).
2026-08-14 15:23:08 +02:00
admin 23722f8b6d fix(media): rework portrait image sizing to fill+object-contain
Chasing this through width/height class combinations (w-fit/h-fit vs
w-auto/h-auto) kept trading one bug for another: centering broke height,
fixing height broke centering. Root cause: max-h-full/max-w-full on the
image are percentages, and every wrapper in the chain only ever had an
intrinsic (auto/fit-content) size — CSS treats percentages against an
intrinsically-sized parent as unresolvable, so which axis "won" depended
on incidental flex-stretch behavior rather than anything deliberate.

Replaced the shrink-wrap-to-image approach with fill-the-definite-parent:
wrapper and image now take w-full h-full of the already definitely-sized
media column (PostDetailDialog) / full-screen overlay (StoryViewer), and
object-contain (default centered) handles aspect-ratio scaling entirely
on its own — no percentage resolution against an ambiguous parent left
anywhere in the chain.

Not visually verified locally (auth-gated route) — needs a real check
against a portrait post/story after deploy.
2026-08-14 15:14:32 +02:00
admin 9efc207710 fix(media): restore height for portrait images, keep width centering fix
h-fit (from the previous fix) disabled the flex cross-axis stretch that
h-auto triggers. That stretch is what gave the image wrapper a definite
height in the first place — without it, the image's max-h-full (a
percentage) had no definite reference to resolve against, so browsers
ignored it and the image rendered at its full natural height, getting
clipped at the bottom of the dialog.

Width and height turned out to need opposite fixes: w-fit for width
(explicit shrink-to-fit avoids the flex-item main-axis stretch that
caused the left-alignment bug), h-auto for height (keeps the cross-axis
stretch that max-h-full depends on).
2026-08-14 15:07:40 +02:00
admin 98037d434e fix(media): center portrait images in post detail and story viewer
The image wrapper used w-auto inside a flex container, which is
ambiguous for flex items — it could stretch to fill the available
width instead of shrinking to the image's actual rendered size. Not
visible for landscape images (little leftover space either way), but
portrait images left a wide gap and sat flush left instead of centered.
Switch to w-fit/h-fit (fit-content), which is unambiguous regardless of
flex context. Same wrapperClassName pattern in both files, so applied
to both.
2026-08-14 15:02:17 +02:00
admin 375a32e69d feat(nav): add Sponsoring link to mobile pet-switcher dropdown
Desktop sidebar already lists /unterstuetzen as a primary nav item, but
mobile had no way to reach it at all. The 5-slot bottom tab bar has no
room for a 6th destination, so it goes in the pet-switcher dropdown
next to the existing secondary actions (add pet, theme toggle) instead
of a second floating header icon.
2026-08-14 15:02:12 +02:00
admin 5adbae5b8c fix(notifications): open the post dialog in-place instead of navigating away
router.push to the post owner's pet profile page (with a ?post= query
param picked up there) silently swapped out the notifications page in
the background. Closing the dialog then stranded the user on that
profile page instead of back on their notification list. Fetch the
post via posts.getById and open PostDetailDialog directly on the
notifications page instead — no navigation, no background page swap.
Drops the now-unused ?post= deep-link handling from ProfileTabs.
2026-08-14 14:47:00 +02:00
admin 25f27c5b35 fix(mobile): stop double bottom-padding hijacking scroll in message thread
MessageThread's wrapper reserved its own pb-32 for the fixed mobile nav
on top of the h-[100dvh] sizing, while the app shell's <main> already
reserves the same space via its own pb-32. The combined mismatch made
<main> itself scroll as a rigid block instead of just the message list.
Switch to h-full so the thread fills main's actual available height.

Also nudge the floating mail shortcut up (top-3 -> top-1.5) so it's
vertically centered against the transparent feed header instead of
sitting visibly lower than it.
2026-08-14 14:36:35 +02:00
admin b3d0551bd8 fix(notifications): deep-link like/comment/mention notifications to the actual post
Notification rows for REACTION/COMMENT/MENTION/PAW_BACK linked to the post
owner's profile page instead of the specific post — there was no way to
land on the post itself. Add posts.getById for direct post lookups and
have the pet profile page pick up a ?post= query param to auto-open the
post detail dialog, falling back to a fetch when the post isn't among the
most recently loaded ones. Follow notifications already linked correctly
to the actor's profile.
2026-08-14 14:36:25 +02:00
admin fbaceac497 fix(admin): make admin panel sidebar sticky, matching the feed page
aside now uses h-screen sticky top-0 overflow-y-auto -- same pattern as
the main app Sidebar.tsx -- so it stays in view while scrolling long
admin pages instead of scrolling away with the content.
2026-08-14 11:03:32 +02:00
admin 34854d6a34 chore(nav): remove deactivated invite links, add admin panel back button
- Sidebar: drop the /invite nav entry (invite-gate module is off,
  INVITE_REQUIRED=false since the 2026-08-10 public launch)
- Admin layout: drop the same for the /invites management page
- Admin layout: add a "Zurueck" footer link back to /feed

Routes themselves are untouched -- only the nav entries pointing at them.
2026-08-14 10:57:55 +02:00
adminandClaude Sonnet 5 91b7662419 fix(sponsor): show Verified badge in feed, add sidebar link, mark sponsors in admin
Post-launch fixes from live testing (Gitea #22):
- Feed/explore postInclude never selected Pet.isVerified, so PostCard only
  ever rendered the new Sponsor badge, silently dropping Verified next to
  it. Both now render side by side, matching the pet profile page.
- /unterstuetzen had no navigation entry anywhere -- added to the desktop
  Sidebar (mobile bottom nav is a fixed 5-tab bar, left alone per scope).
- Admin users list showed no sponsor status at all -- SPONSOR badge added
  next to the user_XXXX id, same pattern as the existing BANNED/
  SHADOWBANNED badges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:50:35 +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 2ec5b107ea chore(deploy): quiet down Sentry build logging after root-causing the sourcemap failure
Confirmed via extensive isolated reproduction (docker run, plain BuildKit
RUN step, and Node child_process.spawn -- all three succeed) that TLS and
the sentry-cli invocation itself are fine; GlitchTip's releases API
rejects the authenticated request with "CSRF check Failed" (403) even
with clean token auth and no cookies. That's a server-side GlitchTip
issue (separate Docker stack), not something fixable from this repo.
Keeping ca-certificates/SSL_CERT_FILE since they're correct regardless.
2026-08-14 07:44:31 +02:00
admin 467746ef03 debug(deploy): temporarily enable sentry-cli request/response logging
Diagnostic only -- narrowing down whether the sourcemap upload's SSL
error is real or a red herring next to the CSRF 403 seen in manual
reproduction. Will be removed once the root cause is confirmed.
2026-08-14 07:33:18 +02:00
admin ea59a26c85 fix(deploy): pin SSL_CERT_FILE/SSL_CERT_DIR for sentry-cli on Alpine
ca-certificates alone didn't fully resolve sentry-cli's TLS handshake
against GlitchTip in the Docker builder stage -- explicitly pointing its
vendored OpenSSL at the installed CA bundle location is the documented
workaround for this class of Rust-binary-on-Alpine cert issue.
2026-08-14 07:26:40 +02:00
admin 4fe5a1113e fix(deploy): install ca-certificates in Docker builder stage for sourcemap upload
sentry-cli (Rust binary) failed the sourcemap upload with "unable to get
local issuer certificate" when talking to GlitchTip over TLS -- the
node:22-alpine builder image ships without a CA bundle. Confirmed via
verbose build log after the SENTRY_RELEASE fix resolved the prior
--release undefined failure.
2026-08-14 07:21:24 +02:00
admin 6980b09e45 fix(deploy): resolve Sentry sourcemap upload release name via git SHA build arg
.git is excluded from the Docker build context (.dockerignore), so
@sentry/nextjs can't auto-detect a release via git and falls back to the
literal string "undefined" for --release, which made the sourcemaps
upload command fail with exit code 1 (confirmed via verbose build log).
start.sh now resolves the short SHA on the host and passes it through
as a SENTRY_RELEASE build arg, same pattern as the other Sentry vars.
2026-08-14 07:18:17 +02:00