Commit Graph
256 Commits
Author SHA1 Message Date
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
admin 53789ca56e fix(security): close remaining CSP report-only gaps, make Sentry sourcemap upload verbose for debugging
- connect-src: allow *.fastly.mux.com (Mux HLS chunks/manifests via Fastly
  edge, dynamic regional subdomains) and *.litix.io (Mux Data beacon uses
  inferred.litix.io, not covered by the exact litix.io entry)
- media-src: allow image.mux.com (storyboard.vtt thumbnail track)
- next.config.ts: silent:true -> silent:false + debug:true so the next
  Docker build surfaces what the Sentry/GlitchTip sourcemap upload plugin
  actually does (still minified stacktraces in GlitchTip despite the
  build-arg fix in b469389)
2026-08-14 07:11:30 +02:00
admin e958f38575 docs: bring README up to date with current tech stack and architecture
Tech stack table was describing the original MVP plan (Vercel, Upstash
Redis, middleware.ts) rather than what's actually running: self-hosted
Docker deployment, self-hosted Redis, next-intl i18n, dark mode,
GlitchTip/Sentry monitoring, CSP, and the admin/moderation panel built
since. Also refreshes the project structure tree, Prisma schema
overview, and setup instructions to match the current codebase.
2026-08-13 21:17:42 +02:00
admin b469389654 fix(security): close CSP report-only gaps, wire Sentry sourcemap upload into Docker build
- connect-src: allow the GlitchTip domain itself so the SDK can report events
- media-src: allow Mux's edge CDN (*.edgemv.mux.com) for HLS manifests
- img-src: allow blob: for client-side upload previews
- pass SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN as Docker build args so
  withSentryConfig can upload readable source maps during `npm run build`
2026-08-13 20:13:00 +02:00
adminandClaude Sonnet 5 fd6f1f0564 fix(mail): use inline paw SVG instead of emoji in branded header (Gitea #21)
The 🐾 emoji rendered inconsistently across mail clients/OS fonts and
looked off-brand. Replaces it with the exact lucide-react "paw-print"
mark (same path data as the landing page's filled paw icon) inlined
as raw SVG in the email header.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 15:27:04 +02:00
adminandClaude Sonnet 5 19f22102df feat(mail): use branded HTML template with personalized greeting (Gitea #21)
GDPR export emails were unstyled HTML with a bare "Hallo," greeting.
Adds a landing-page-branded email shell (orange gradient header, paw
wordmark) and pulls the owner's first name from their Clerk profile
for the salutation, falling back to "Hallo," when unavailable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 15:13:08 +02:00
admin f0a878c432 fix(mail): show "PawFeed Team" as sender name instead of "team" (Gitea #20)
MAIL_FROM is a bare address with no display name, so mail clients fell
back to showing the local-part as the sender. Wrap it in a proper
display name in code instead of depending on env var formatting.
2026-08-13 14:15:37 +02:00
admin 092268b8e7 fix(content): center letterboxed images and enlarge the AI-disclosure badge
BlurImage's wrapper defaults to w-full/h-full for fill-container usages
(feed thumbnails with object-cover) — but in object-contain/letterboxed
contexts (story viewer, post detail single-image and carousel) that same
wrapper stretched to the full viewing area, defeating the parent's
flex-centering and leaving the actual rendered image pinned to the
top-left. Fixed by wrapping those three usages in an inline-flex
container sized to the actual image (wrapperClassName override), which
also gives the AI-disclosure badge a real image-relative anchor instead
of floating in the surrounding letterbox space. Bumped the badge's base
size (h-5 → h-7, h-8/h-10 in the larger detail/story contexts) per
live-test feedback that it was hard to notice.
2026-08-13 14:01:17 +02:00
admin 50e2848312 feat(legal): AI-content disclosure for user uploads (Gitea #18)
Owners can now optionally mark uploaded content as AI_MODIFIED or
AI_GENERATED (mutually exclusive, defaults to NONE) across every upload
surface — photo posts, video posts, milestones, stories, and pet avatars.
Once set, the choice is owner-immutable; only a moderator can correct it
via a new admin.setAiDisclosure mutation (Posts admin page), which
requires a reason and writes to ModerationLog like every other
moderation action.

The disclosure renders as a UI-only overlay badge (the two provided
label assets) wherever the content is shown — feed, post detail, story
viewer, milestone cards, and the pet profile avatar — never baked into
the image pixels.

New shared components: AiDisclosureSelect (the 3-way picker used in all
five upload forms) and AiDisclosureBadge (the overlay renderer).
2026-08-13 13:44:16 +02:00
admin 2d0e390c76 fix(infra): remove the direct host port exposure on 4563
Step 2/2 (Audit Gitea #17). NPM's proxy host now forwards to pawfeed:3000
over the shared nginx_default Docker network (verified working in the
previous deploy), so the app no longer needs a published host port at
all — it was reachable unencrypted on the LAN, bypassing Cloudflare's
TLS termination and protections entirely.
2026-08-13 13:01:58 +02:00
admin 9885fc9006 fix(infra): join the shared nginx_default network (transition step)
Step 1/2 of removing the direct, unencrypted LAN exposure on port 4563
(Audit Gitea #17). Attaches pawfeed to Nginx Proxy Manager's Docker
network so NPM can reach it via the container DNS name (pawfeed:3000)
instead of the published host port. Port mapping stays for now — removed
in a follow-up commit once NPM's proxy host forward target is switched
over, to avoid a downtime gap between the two changes.
2026-08-13 12:58:46 +02:00
admin ebc5267f35 fix(infra): require a password on the Redis container
Redis had no auth, defended only by not being port-mapped to the host.
Defense-in-depth per the audit — REDIS_PASSWORD is generated server-side
in docker/.env (not committed) and threaded through to both the redis
service's --requirepass and the app's REDIS_URL.
2026-08-13 12:47:51 +02:00
admin b4a49f8ed3 fix: address findings from full security/functionality audit (Gitea #17)
Rate-limiting on messages.send, reports.create, reactions.toggle,
follows.create, reposts.create. Unique constraints on Report to stop
report-flooding. Mux webhook now fails hard in production without a
signing secret instead of skipping verification. Admin-panel outline
buttons fixed on 4 more pages (same white-on-dark bug as the Users page).
Timing-safe comparison for ADMIN_SECRET/CRON_SECRET. Fixed duplicate HSTS
header (Cloudflare's stronger one was being shadowed by the app's own).
tRPC now returns clean JSON 401s instead of HTML redirects when
unauthenticated, and no longer blocks public procedures for logged-out
visitors. 14 previously-silent catch blocks now log to GlitchTip.
invite/page.tsx and EmergencyVetSection.tsx fully localized (were 100%
hardcoded German). Stale TODO comments removed, auth.test.ts scaffold
rewritten to match the actual lazy-upsert pattern instead of a Clerk
webhook flow that was never built.
2026-08-13 12:42:49 +02:00
admin 2b52345c66 feat(landing): add guest-facing Dark Mode toggle to the navbar
Visitors without an account had no way to switch themes — the toggle so
far only existed in the authenticated Sidebar/MobileNav. This variant
skips the account-bound persistence (no Owner row for guests) and just
flips next-themes' own localStorage-backed state.
2026-08-13 11:11:53 +02:00
admin d21f4708dc fix(landing): keep hero floating-badge text dark in Dark Mode
The badges (Neuer Beitrag von Bubi / Likes-Follows-Reaktionen / Story ist
aktiv) intentionally keep a hardcoded white chip background, but their text
followed the text-foreground token, which flips to near-white in Dark Mode —
nearly invisible on the still-white chip.
2026-08-13 11:06:39 +02:00
admin d670cf9173 feat(theme): account-bound Dark Mode toggle (Gitea #8)
Wires up the already-present next-themes dependency and globals.css
:root/.dark token infrastructure. Persists the choice on Owner.themePreference
so it survives relogin on a new device, not just localStorage. Toggle lives
in the Sidebar (desktop) and the pet-switcher dropdown (mobile); Clerk's own
UI now follows the same theme via a small ClerkThemeProvider wrapper.
2026-08-13 10:54:23 +02:00
admin 2085336748 feat(admin): comma-separated bulk add on the Blacklist page
The add-entry input took the whole field as one literal value, so
typing "word1, word2" created a single entry matching that exact
phrase instead of two separate words. Splits on comma client-side and
calls the existing single-value createBlacklistEntry mutation once
per value (dedup'd), with one consolidated toast instead of one per
word. No backend change — reuses the existing retroactive-scan
mutation as-is.
2026-08-12 20:48:41 +02:00
admin ee04bf5432 fix(legal): refine Drittlandübermittlungen wording per second lawyer pass
Follow-up on the Schrems II addition: the lawyer's note (Gitea #3
comment 258) requested more precise phrasing — "Auftragsverarbeiter"
instead of "Dienstleister", a risk-based framing ("dem Risiko
angemessenes Schutzniveau"), and the review commitment now targets
the effectiveness of the safeguards themselves rather than ongoing
legal-landscape monitoring. Kept the concrete TLS/access-control
specifics since those are still factually accurate.
2026-08-12 19:50:15 +02:00
admin c84a2359b5 fix(admin): dark background on outline buttons (Users page)
Root cause: the shadcn outline Button variant uses bg-background,
which resolves to the light-theme CSS variable since the admin panel
doesn't wrap itself in a .dark class — so every outline button here
had a white background regardless of border/text color overrides,
making the colored text glare instead of read cleanly. Explicit
bg-zinc-900/hover:bg-zinc-800 on all five outline buttons on this
page (Message, Export, Warn, Un-shadowban, Unban) fixes it.
2026-08-12 19:44:04 +02:00
admin cad7b8c5fc fix(admin): label Message/Export/Warn as text buttons, not icon-only
Icon-only buttons needed a hover or click before their function was
clear, unlike Ban/Shadowban which already show text. Adds visible
labels to match, and wraps the action row so the wider buttons don't
get clipped on narrower admin-panel windows.
2026-08-12 19:37:25 +02:00
admin 72ae462513 feat(admin): shared PawFeed Team inbox in the admin panel (Gitea #10)
The existing "Message" button on the Users page routed into the
normal /messages UI via assertPetOwnership, which only ever worked
for the literal ADMIN_OWNER_ID account — every other admin/moderator
got a silent FORBIDDEN, since the Team pet has one fixed owner. This
was already flagged as a known limitation in a code comment.

Adds three admin-gated procedures (assertAdmin instead of
assertPetOwnership) — listTeamConversations, getTeamConversationThread,
sendTeamReply — reusing the existing Conversation/Message model and
the sendTeamMessage helper. New /p/[secret]/messages page: a two-pane
inbox (conversation list + thread + reply box) any SUPPORT+ admin can
use, so the whole team sees the same conversation history instead of
only the bootstrap account. The Users-page Message button now deep-links
here instead of the broken consumer route.
2026-08-12 19:28:07 +02:00
admin 765a6fa486 fix(legal): add Schrems II third-country transfer safeguards to privacy policy
Lawyer review flagged that section 4 (third-party processors) named
SCC as the sole basis for transfers to US-based processors (Clerk,
Mux) without mentioning that SCC alone doesn't always guarantee an
adequate protection level per Schrems II — additional technical/
organizational safeguards need to be disclosed too.

Renames the section to explicitly cover Drittlandübermittlungen and
adds a paragraph on the actual safeguards in place (TLS in transit,
role-based admin access controls, ongoing legal-landscape review).
Also fixes a stale "Stand: Juni 2026" footer date that didn't match
the page header's "August 2026".

Note: publishing a new version via /p/[secret]/legal (docType
DATENSCHUTZ) is still needed to trigger the re-acknowledgment banner
for existing owners — that's a deliberate SUPER_ADMIN action, not
done here.
2026-08-12 19:12:54 +02:00
admin 4000a1eed9 feat(admin): DAU/MAU tracking and expanded dashboard stats (Gitea #10 Phase 3, part 4/4)
Redis-backed visit tracking (src/lib/visit-tracking.ts): daily SADD
sets keyed "visits:YYYY-MM-DD", same fail-open idiom as the existing
rate-limit.ts. Recorded once per authenticated (app) layout render —
SADD is idempotent so no client-side dedup is needed. DAU = today's
SCARD, MAU = SUNION across the trailing 30 daily sets.

Dashboard additions:
- 6 new stat cards: DAU, MAU, shadowban count, blacklist entry count,
  verifications open/closed.
- 3 new time-series charts (reusing the existing TimeSeriesChart/
  GranularityToggle components): visits, comments, and reactions
  over time. Comments/reactions are plain Postgres bucket queries
  cloned from the existing getPostsOverTime pattern; visits reroutes
  through the new Redis helpers per bucket.

This closes out Gitea #10 Phase 3 (hide-without-delete, shadowban,
blacklist, and now analytics — all four parts shipped).
2026-08-12 19:03:40 +02:00
admin a35cb6640f feat(admin): clickable reported posts and color-coded user actions (Gitea #10)
Two follow-up requests on the admin dashboard:

- Reports page: the post preview is now clickable, opening a detail
  dialog with full-size images, the complete comment list (reusing
  the existing admin.listComments query), and a "liked by" list of
  pets — previously moderators only saw a caption snippet and raw
  counts, with no way to inspect what was actually reported. Backed
  by a new admin.getReportedPostDetail query (images + reactions;
  comments intentionally reuse the existing procedure instead of
  duplicating that fetch).
- Users page: Warning/Export/Message buttons get distinct colors
  (amber/blue/sky) matching the existing Ban (red) / Shadowban
  (violet) pattern, instead of all three sharing the same neutral
  gray outline.
2026-08-12 18:49:44 +02:00
admin 95df270821 feat(landing): wrap hero preview in an iPhone-style device frame
Gitea #11 follow-up: replaces the plain rounded rectangle around the
mobile screenshot with an actual iPhone mockup — dark bezel, Dynamic
Island, side buttons, and a home indicator bar in its own strip below
the screenshot (kept separate from the image content so it doesn't
overlap the app's own tab bar).
2026-08-12 18:25:20 +02:00
admin 76deab796c feat(landing): use real mobile screenshot in hero phone mockup
Gitea #11 follow-up comment: the phone mockup previously showed the
desktop feed layout squeezed into a phone bezel. Swaps in an actual
mobile-view screenshot (portrait, bottom tab bar) so the mockup reads
as a real app screen. Repositions the three floating badges to clear
the screenshot's own header and tab bar, since the new portrait image
is much taller than the old near-square desktop capture.
2026-08-12 18:11:37 +02:00
admin 6b79dfb570 fix(landing): correct German copy and restore feature-card hover animation
Finishes the manual edits from Gitea #16: fixes capitalization on
"Jetzt starten" and grammar on the hero badge text, updates the
banned-page contact address to team@pawfeed.org, and adds the
missing `group` class so the feature-card icon's `group-hover:scale-105`
actually fires. Keeps the RelativeTime strings in their original
compact form ({count}m/h/d) since that format is documented as
intentional for tight UI spots (comment lists, notifications, ad cards).
2026-08-12 17:27:59 +02:00
admin 7395d83194 feat(admin): word/link/hashtag blacklist module (Gitea #10 Phase 3, part 3/4)
Adds a BlacklistEntry model (type WORD|LINK|HASHTAG, unique per
type+value) with a new admin module at /p/[secret]/blacklist —
add/list/delete entries, filterable by type.

Enforcement, per explicit spec from this session:
- New posts are checked at creation time (posts.create) against the
  current blacklist (whole-word regex match for WORD, domain match
  incl. subdomains for LINK, exact match for HASHTAG) and auto-hidden
  immediately via the existing hiddenAt mechanism if matched — never
  blocks creation, just hides right after, reversible via
  admin.unhidePost. Logged as HIDE_POST with moderatorId "SYSTEM".
- Adding a new entry retroactively scans existing non-hidden posts
  (capped at 5000 for safety) and hides matches too, logged per-post
  under the admin who added the entry.
- Removing an entry does NOT auto-unhide already-hidden posts —
  that stays a manual review step via the existing hide/unhide UI.

Matching lives in src/lib/blacklist.ts: word matching uses \b word
boundaries (a blacklisted "ass" won't match "assist"), link matching
extracts URLs and compares hostnames including subdomains.

Scope: post captions/hashtags only, not comment bodies — comments
don't have a hiddenAt field yet, so this is a natural follow-up once
they do, using the same pattern.

Gitea #10
2026-08-12 16:06:37 +02:00
admin eb26cb5fbe feat(admin): shadowban owners (Gitea #10 Phase 3, part 2/4)
Adds a Shadowban model (mirrors UserBan: reason, issuedBy, expiresAt =
null for permanent) plus MODERATOR+ shadowbanOwner/removeShadowban
mutations logging SHADOWBAN_USER/UNSHADOWBAN_USER to ModerationLog.

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

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

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

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

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

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

Gitea #10
2026-08-12 14:23:00 +02:00
admin 9f9db9e742 feat(landing): interactive redesign with scroll reveals and parallax
Rebuilds the public landing page per the structural reference in
Gitea #11 (navbar layout, rounded hero card with phone mockup +
floating badges, catchphrase section, second detail-feature card,
footer repeating the navbar) — colors/typography adapted to PawFeed's
existing orange brand instead of the generic SaaS reference palette.

New:
- Sticky navbar (logo, Home/About-coming-soon/Impressum, language
  switcher, Login + Sign-up pill)
- Hero: rounded-4xl gradient card, phone-framed app preview with
  three floating badge cards (idle bob animation), scroll-linked
  parallax on the background paw watermark via native CSS
  animation-timeline: view() (progressive enhancement, no JS)
- Catchphrase section reusing the existing 4 feature translations,
  each card staggered into view via a new ScrollReveal client
  component (IntersectionObserver, no scroll-handler churn)
- Second rounded detail-feature card with two checklist points
- Footer repeats the navbar links + tagline + "designed by" line

All reveal/parallax/float animations respect prefers-reduced-motion
and are pure CSS — no new animation dependency. Verified in a real
browser at 390px and ~1568px (mobile nav collapse, hero stacking,
floating badges hidden on mobile, scroll reveals firing correctly).

Gitea #11
2026-08-12 14:04:57 +02:00
admin 13513583bc feat(security): ship CSP in Report-Only mode via Clerk's middleware option
Replaces the dead hand-rolled buildCsp() draft in next.config.ts (never
wired in — its script-src had no 'unsafe-inline'/nonce, which blocked
Next.js's own hydration script and blanked the whole app when tested
live on 2026-08-10) with Clerk's official contentSecurityPolicy option
on clerkMiddleware(). This is Clerk's documented workaround: its default
directives already include the permissive script-src Next.js App Router
needs, merged with our app-specific img-src/connect-src/media-src/
font-src/frame-ancestors/object-src/base-uri additions (Supabase, Mux,
litix).

Ships as Content-Security-Policy-Report-Only (reportOnly: true) with
both reporting mechanisms wired to a new /api/csp-report route handler:
Clerk's reportTo (Reporting API) and a classic report-uri (for browsers
without Reporting API support). The route normalizes both report shapes,
rate-limits by IP, and forwards violations to Sentry/GlitchTip as
warnings — never blocks the request.

Deliberately NOT enforced yet — move to reportOnly: false only after a
clean observation period with zero unexpected violations.

Gitea #5
2026-08-12 13:42:08 +02:00
admin d086fc67f0 fix(deps): upgrade Next.js to 16.3.0, closing remaining audit findings
npm audit fix --force bumps next 16.2.7 -> 16.3.0 (postcss/sharp come
along as transitive deps), closing the last 3 high-severity findings:
middleware/proxy bypass in Turbopack single-locale apps, SSRF in
Server Actions and rewrites, cache response confusion, unauthenticated
internal Server Function endpoint disclosure, and SVG image-optimization
DoS. npm audit now reports 0 vulnerabilities.

tsc/vitest/build all verified green before deploying.
2026-08-12 13:24:29 +02:00
admin 8d20b489d3 fix(deps): apply non-breaking npm audit fixes
Resolves 11 of 16 flagged vulnerabilities via npm audit fix (undici,
hono/@hono/node-server, brace-expansion, fast-uri, ip-address, esbuild,
body-parser, valibot, nanoid) — all transitive dev/build tooling deps,
no direct dependency ranges changed. tsc/vitest/build all verified green.

Remaining 3 (next, postcss, sharp) need npm audit fix --force, which
would bump next past the stated package.json range — deferred pending
a dedicated test pass.
2026-08-12 13:15:08 +02:00
admin 6fd5f2dff8 feat(pet): circular avatar cropping with zoom before upload
Adds an AvatarCropDialog step (react-easy-crop, cropShape="round")
between file selection and the presigned-URL upload in AvatarUpload:
the user crops/zooms a circular region, which is re-encoded via canvas
to the original file's mime type before the existing upload flow runs.
File-size/type validation moved from uploadFile into the new
selectFile gate that opens the dialog.

Gitea #13
2026-08-12 12:52:32 +02:00
admin 5fbf39593d feat(ui): initials-color avatars and collapsible desktop sidebar
Adds a PetAvatar wrapper (hash(name) mod 360 -> HSL background) and
migrates the four most-visible avatar call sites (sidebar, mobile nav,
post card, post detail incl. comments) to use it instead of duplicated
inline initials logic.

Desktop sidebar can now collapse to an icon-only 76px rail, persisted
in localStorage (same client-init pattern as ActivePetContext). All
nav rows, the new-post button, and the owner row adapt; secondary
sections (language switcher, legal footer) hide when collapsed.

Gitea #12, #14
2026-08-12 12:52:21 +02:00