Skip to content

chore(marketing): consolidate the stray pages, and give /careers a real one - #2782

Closed
0xkkonrad wants to merge 22 commits into
mainfrom
chore/stray-pages-consolidation-v2
Closed

chore(marketing): consolidate the stray pages, and give /careers a real one#2782
0xkkonrad wants to merge 22 commits into
mainfrom
chore/stray-pages-consolidation-v2

Conversation

@0xkkonrad

Copy link
Copy Markdown
Contributor

Cleans up the stray marketing pages on peanut.me, found in a routing + copy audit. Rebased onto current dev; replaces #2767, which was opened before #2760 landed and went conflicting.

Two items from the audit are deliberately not here, per Konrad: the public /dev/* pages (filed as a Notion task for Jota instead) and the /pricing fee wording.

Already handled on dev — dropped from this PR

What changes

/careers becomes a real page. It was a 30-word interstitial — a mascot, one sentence, a link out to Notion. No <h1>, no footer, no Crisp, and metadata still selling "Instant Global P2P Payments". The footer's Jobs link bypassed it entirely for a second URL pointing at the same Notion page. It now has its own content (what Peanut is, how the work works, open roles), sourced from content/press/en.md so nothing is invented, and Notion is reached only by Apply on Notion. The footer link now points at /careers, and its label is Careers rather than "Career"/"Jobs" — it should name the page it opens.

/shhhhh q6 answers acceptance, not availability. It read "We're rolling out to select regions first… Eligibility is shown during signup." product/card.md requires public card copy to describe acceptance only, never naming countries or promising eligibility. It now answers where the card works. es-AR gains a voseo override, which stays a legal strict subset because the key exists in es-419.

Three routes retired, all permanent redirects:

Route Why
/exchange / Renders the same NoFees component already on the homepage; ships an empty client shell; title and H1 promised different things
/:locale/blog /:locale/content Byte-identical to /content with fixedType="blog". Index only — all 21 posts survive
/:locale/team /:locale/press /press already renders the same two members from the same source, with real photos

The content hub is crawlable again. /en/content and /en/blog emitted zero article links in server HTML while the sitemap listed 21 posts — ContentLanding is a client component read through nuqs, so Next prerenders the Suspense fallback, and the fallback was a pulsing skeleton. The list rendering is now an exported ContentLinkList that touches no URL state, and the fallback renders it.

Plus: /en/stories/index (a live duplicate of its own hub, linked from that hub) is gone; /press section headings are localized and it absorbs the team JSON-LD; three client routes stop declaring the homepage as their canonical; the root title and description drop the protocol-era framing; @PeanutProtocol@joinpeanut in all six places; the five card legal pages join the sitemap (English only — the localized copies canonicalize to /en, so listing all twenty would create fifteen self-competing duplicates).

Ragdoll. It had no prefers-reduced-motion handling at all — it now follows confetti.ts and degrades to a single settled frame rather than vanishing. Two new homes, both non-scrolling panes with a definite height: /maintenance (whose copy was the worst voice on the site) and the waitlist jail pane, gated on step === 'jail' so plain /invite doesn't get a draggable toy beside its CTA.

One real bug fixed on the way

The (mobile-ui) layout registers pull-to-refresh on document before the waitlist early-return, and its #scrollable-content guard is absent on that screen, so the callback fell through to true. Dragging the peanut down ~160px would have fired window.location.reload() and wiped the user's typed invite code. The ragdoll host now swallows touchstart — a native listener, because React hydrates on document and a synthetic stopPropagation wouldn't stop a same-target sibling.

Verification

  • npx jest273 suites, 3435 tests, 0 failures
  • npx tsc --noEmit0 errors
  • prettier --check on all changed files — clean
  • Redirects checked statically: no duplicate sources, every source route removed, every destination resolves. The two pre-existing /:path* catch-alls are host-scoped to docs.peanut.* and can't shadow these
  • ContentLinkList proven to server-render real anchors (/en/blog/rewards, /en/stories/lynn) with the | Peanut suffix stripped

Not verified locally: next build can't complete on this machine — earlyoom SIGTERMs the build worker at ~2.7 GB, with zero compile or type errors in the log before the kill. CI owns that gate. Runtime redirect behaviour also can't be exercised locally: next.config.js deliberately applies redirects.json only outside development.

Correction to a note in #2767

That PR claimed SEOFooter still links Jobs → Notion from the content manifest. That was wrong: RESOURCES_MOVED_ELSEWHERE in SEOFooter.tsx already suppresses the jobs and terms manifest entries (added in #2755), so they render nothing. No content-submodule change is needed. The manifest hrefs are stale data, but dead.

Follow-ups, deliberately not in this PR

  • The /shhhhh "20 a week" strings predate the ruling in product/card.md that the pace is now ~50/week — 7 catalog strings, 1 metadata line, and 3 hardcoded literals in ScarcityCounter.
  • /en/help looks to have the same client-component crawlability gap: its prod HTML carries only 3 /en/help/ links.
  • /maintenance and the 404 are still hardcoded English.

claude and others added 22 commits August 20, 2026 16:59
Before a request exists the QR already encodes the profile payment link
for the entered amount (/{username}/{amount}USDC), so there is no reason
to keep it blurred until the create button is clicked. It now unblurs on
a positive amount and re-blurs when the amount is cleared or zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQHgKufMZzqzwCviP4gXKs
On native the pull refetches via react-query instead of reloading the page,
so nothing on screen blinks and the gesture reads as having done nothing.
Give the indicator the full state sequence instead:

- pulling: the indicator scales in and the arrow rotates toward upright,
  flipping at the release threshold with a light haptic on the crossing
- refreshing: a readable spinner arc (was a thin quarter-circle path)
- done: green checkmark with a pop + success haptic, held briefly, and the
  content fades back in so the screen visibly re-renders

Also restyles the indicator to the app's brutalist look (black border + hard
shadow), guards Element.animate for WebViews that lack it, honours
prefers-reduced-motion for the content fade, and clears pending timeouts on
unmount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zVw46ZCvXYp7b4dpXvJiq
Going /profile → "Exchange rates and fees" → "Try it!" lands the user in the
add-money or withdraw root, depending on their balance and unlocked regions.
Both roots deliberately reset to /home on back rather than calling
router.back(), because their own sub-pages push back to the root and back()
ping-pongs there. That is right for a tab-bar entry, but it strands anyone who
arrived from another screen: back never returns to the widget they came from.

Add a `returnTo` query param the caller sets and the flow roots honour, so the
origin travels with the navigation instead of being guessed at the destination:

- new `withReturnTo` / `readReturnTo` helpers — same-origin only (reusing
  sanitizeRedirectURL), and a target pointing at the current page is dropped,
  since re-pushing the page you are on is a back button that does nothing.
- the exchange-rate CTA passes its own path *and* query string, so back
  restores the currency pair and amount the user was looking at.
- add-money and withdraw check it before falling back to /home. The send-flow
  /send branch and the in-page steps (country list → method selection, amount
  → method selection) keep priority, so back still unwinds one step at a time.

Tests: unit coverage for the helpers (including the off-origin and
self-referential rejections) plus back-navigation cases on both flow roots and
the exchange-rate CTA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…-app-fixes

Brings in the pull-to-refresh feedback work (arrow arming, spinner, success
checkmark + haptics, content settle) along with the current main base.
Reverses the iOS gate from 4564b40 and adds an iOS-only copy layer that
presents the referral programme as cashback. Web and Android are
unchanged: every pre-existing catalog string is byte-identical to dev, and
the two new UI elements are behind isIOSNative().

Hiding the programme while the backend kept accruing — and while
referral.reward.earned kept pushing "You earned $X! <name> joined Peanut
with your invite" to the same devices — was the worse position. Guideline
2.3.1 treats hidden-but-discoverable features as grounds for removal. The
programme will be disclosed in Notes for Review instead.

The old citation was also wrong: appStoreCompliance.ts cited 3.1.5(ii),
which is Mining. The clause about offering currency for encouraging
downloads is 3.1.5(v).

appStoreCompliance.ts is deleted along with its seven call sites — the
/rewards and /rewards/invites route guards, the home pill, the profile
row, both invite carousel CTAs, the surprise-claim treatment and the
receipt points row. Removing the route guard also fixes the
referral.reward.earned deep link, which pointed at a guarded route and
bounced iOS users to /home.

The cross-chain withdraw gate is untouched: it lives in
underMaintenance.config.ts, rests on 3.1.5(iii) (Exchanges, which needs
per-region licensing), and multi-chain swaps are the strongest crypto-app
signal we ship.

useAppTranslations wraps useTranslations and prefers an `iosCopy.<key>`
override when one exists, falling back to the base string otherwise.
Overrides sit inside the namespace they belong to, so call sites keep
their existing keys and only the hook name changes. The block is called
iosCopy, not ios, because profile.backup.steps.ios is already content — a
namespace whose own content sat under `ios` would have had every key
silently redirected. Platform is read at render time; the Capacitor bridge
is absent during prerender.

45 overrides per full locale plus voseo deltas for es-AR:

- rewards -> cashback. "Cashback" as a loanword is the standard term in
  both LatAm markets.
- "used Peanut" -> "paid with Peanut", "the more they use" -> "the more
  they pay". Ties the money to a transaction rather than a signup;
  downloading is what 3.1.5(v) names.
- A four-step "How cashback works" card on /rewards. The earn instruction
  used to be a half-sentence on the lifetime total; as its own card it
  reads better and states the process without pinning a dollar to a
  person.
- Dropped "friends & their friends" from qrPay.claim.inviteQrDescription
  and "contribute towards your points forever" from the sticker copy — the
  only two places the UI stated the transitive structure.

Push notification copy is deliberately unchanged. PushChannel targets by
userId and OneSignal fans one notification out to every subscription a
user has, so there is no per-platform copy path and a single notification
cannot say two different things to the same person's devices.

Tests cover both platforms, the fall-through, all four locales, and two
catalog invariants: every override shadows a real base key (a typo would
otherwise silently never resolve), and iosCopy stays distinct from the
existing ios content key.
…o-dev-20260820-2

chore: back-merge main → dev (2026-08-20 release + #2760)
Removing the route guard took the file's only useEffect with it, and
no-unused-vars is an error rather than a warning — the single eslint error
in the run, on top of the 65 pre-existing warnings.
feat(rewards): iOS-only cashback copy, referral surfaces restored
…cel drawer

Four unrelated mobile-app fixes.

**Home tab has no active state.** WalletNavigation compared `usePathname()`
to the nav href with `===`. The native build sets `trailingSlash: true`, so
the pathname is `/home/` there and the comparison never matched — every
active state in the app was silently lost, not just Home. Added
`isSameRoute()` next to the other route helpers and routed the mobile and
desktop nav through it.

**Withdraw's final screen loads twice.** `withdraw/page.tsx` called
`React.lazy()` inside the render body for its two native `?country=` views.
That hands back a fresh, unresolved lazy on every render, so each re-render
re-suspended: React hid the rendered view and swapped in the Suspense
fallback (null) until the import re-resolved a microtask later. The screen
blanked and loaded again — and the success view triggers a re-render itself
when it invalidates the transactions query. Hoisted both to module scope.
The regression test asserts the view isn't display:none straight after a
re-render; it fails against the old code.

**Stray rule under the receipt's last row.** The details card underlines
every row and drops the rule on the last one, but `shouldHideBorder` only
reaches rows the receipt renders itself. `BridgeDepositInstructions` expands
into rows of its own and doesn't take the flag, so the pending bank-deposit
receipt ends on a rule sitting directly on the card border. Rows with a
second runtime gate (a token icon still being fetched, a rate missing from
the payload) fail the same way — the config calls them last, the DOM never
gets them. Let the container decide with `[&>*:last-child]:border-b-0`.

**Cancel-link confirmation is a modal.** Replaced it with a bottom drawer.
It opens from two places — the transaction details drawer and the send-link
success page — so `Drawer` gained a `nested` prop that switches to vaul's
`NestedRoot`; a plain Root inside a Root double-applies the background scale
and fights over the scroll lock. That also retires the `!z-[10]` shuffle the
parent drawer needed to get out of the modal's way. Verified in Chromium
that the content fits without scrolling from 320x568 up, and that the
drawer's `max-h-[80vh] overflow-auto` takes over below that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…mobile-app-fixes

Brings in five mobile fixes:
- back navigation after the exchange-rate widget CTA (?returnTo)
- home tab active state on the trailing-slash native build
- withdraw's final screen re-suspending and loading twice
- stray dashed rule under the receipt's last row
- cancel-link confirmation converted from a modal to a drawer
The top strip was forced black in bd9a1b3 so it would stop flipping between
black and beige above the pink beta feedback ribbon. That ribbon is now hidden
on iOS (e56254e), and the underlying inconsistency was really a sizing/source
problem, fixed in 9b3384e by reading Capacitor's natively measured insets.

Recolor only — the safe zone keeps h-safe-top, so the natively measured inset on
Android 15+ and the env() fallback everywhere else are unchanged. Style.Light
pairs dark status-bar icons with the light strip.
fix(mobile): batch of native app fixes
… work

Two separate wrong answers on the send-link claim screen.

1. The guest-verification modal rendered one hardcoded line for every
trigger: "The sender isn't verified for this method." It fires on three
paths and is only right on one. A logged-out user tapping MercadoPago or
Pix is blocked by their own missing account — those methods never consult
the sender's rails at all. And GuestKycNeeded is also reached when the
link carries no senderUserId, or when the sender lookup 404s or throws,
where nothing whatsoever was established about the sender. In every case
it hands the claimer a fact they cannot act on, and usually an untrue one.

Callers now pass a reason. useDetermineBankClaimType exposes the sender's
capability as a tri-state, because "we asked and the answer was no" and
"we never got an answer" are different facts and only the first may be
shown as the sender's fault. Copy updated in en/es-419/pt-BR, with the
voseo delta in es-AR.

2. A claim that failed on ZeroDev's paymaster told the user to contact
support. The API sanitizes 5xx prose (rightly — this path has leaked the
vault address before), and postJson dropped the `code` from the body, so
friendlyError had nothing left to classify and fell through to the
support fallback. The link had already been rolled back and the next
attempt would most likely have worked. Six users hit this on 2026-08-19
(PEANUT-UI-SJ5).

postJson now carries the wire code through, and CHAIN_INFRA_UNAVAILABLE
maps to the existing retryable copy. It stays a plain Error rather than
ApiError on purpose: ApiError would also opt this endpoint into the
blanket "any 5xx is retryable" rule, and a claim can fail 5xx for reasons
retrying never fixes. Only codes the API states explicitly change advice
— an uncoded failure keeps exactly today's behavior, which is what makes
this safe to ship before the API side lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4LovewRn4hYZJrG4FLMLW
…cation-error-uzgmxr

fix(claim): stop blaming the sender, and offer a retry when one would work (dev)
…ions only

/careers was a 30-word interstitial: a mascot, one sentence, and a link out
to Notion. No <h1>, no footer, no Crisp, and metadata still selling 'Instant
Global P2P Payments' from the protocol era. The footer's Jobs link bypassed
it entirely, pointing at a second URL for the same Notion page.

The page now carries its own content — what Peanut is, how the work works,
and the open roles — sourced from content/press/en.md so nothing is invented.
Notion is reached only by 'Apply on Notion'. The footer Jobs link now points
at /careers instead of routing around it.

Uses the public *.notion.site URL; the old www.notion.so workspace URL
bounced logged-out visitors to a login.
shhhhh.faq.q6 read 'We're rolling out to select regions first and adding more
as our partners' coverage expands. Eligibility is shown during signup.' That is
issuance and availability framing; product/card.md requires public card copy to
describe acceptance only and never name countries or promise eligibility.

Now answers where the card can be used — accepted wherever Visa is accepted —
and sends people to the door for their own account status.

es-AR gains a q6 answer override for the voseo imperatives, since es-419 uses
tuteo here. The key exists in es-419, so the delta stays a strict subset.

The $10 reward claims this FAQ used to carry were already removed on dev.
Three marketing routes that carried no unique content, all now permanent
redirects. (/lp/card was retired separately on dev by #2760.)

- /exchange -> /. Rendered the same NoFees component already on the homepage
  plus a footer, and shipped an empty client shell: 'ZERO TRANSFER FEES',
  'no hidden fees' and 'See the markup on' all return zero matches in the live
  server HTML, while it sat in the sitemap at priority 0.7. Its metadata title
  promised 'Live Exchange Rates' while its H1 said 'ZERO TRANSFER FEES'.
  NoFees itself is untouched and still used by the homepage and /quests.
- /:locale/blog -> /:locale/content. Byte-identical to /content with
  fixedType='blog', which /content's own Blog chip already produces. Only the
  index is deleted; all 21 posts under blog/[slug] survive.
- /:locale/team -> /:locale/press. /press already renders the same two members
  from the same content file, with real photos.

Fallout cleaned up in the same pass: robots.ts no longer allows a route that
is now a 308, and the blog post breadcrumb — which feeds a BreadcrumbList
JSON-LD — points at /content?type=blog instead of naming a redirecting URL.
That form matches how /compare and /use-cases already link back, so the crumb
still reads 'Blog'.

Also drops the now-dead 'team' entry from ROUTE_SLUGS and refreshes three
comments that cited the retired routes as live.
…root identity

- /crisp-proxy, /kyc/success and /maintenance defined no metadata, so all
  three inherited the root layout's canonical: '/' and declared the homepage
  as their canonical while serving index, follow. Each is a client component,
  so metadata cannot live in the page — added sibling layouts carrying a
  self-canonical plus robots noindex, the pattern /exchange already used.
- Root title was 'Peanut - Instant Global P2P Payments in Digital Dollars',
  protocol-era framing inherited by every page without an override. The root
  description said the same thing. Both now describe the money app.
- The X handle @peanutprotocol appeared in six places while the footer links
  x.com/joinpeanut. All six now say @joinpeanut, including the Organization
  JSON-LD sameAs and llms-full.txt.
- Adds the five card legal pages to the sitemap. They return 200 with index,
  follow and were listed nowhere. English only: the localized copies serve
  English and already canonicalize to /en, so listing all twenty would create
  fifteen duplicates competing with their own canonical.
…ate, localize /press

- /en/content and /en/blog emitted zero article links in server HTML while the
  sitemap listed 21 posts. ContentLanding is a client component read through
  nuqs, so Next prerenders the Suspense fallback — and the fallback was a
  pulsing skeleton. The list rendering is now an exported ContentLinkList that
  touches no URL state, and the fallback renders it, so the links exist in the
  prerendered HTML. Interactive filtering is unchanged.
- Content cards rendered the raw frontmatter title, leaking the SEO suffix into
  the UI ('Lynn Pham: The Brand Girl | Peanut'). Stripped at render time only;
  sitemap and metadata still read the raw value.
- /en/stories linked to /en/stories/index, a live second copy of the hub with a
  self-referencing canonical and different hero copy. The page now applies the
  same slug === 'index' guard the sitemap already had.
- /press rendered all six section headings as hardcoded English on a [locale]
  route. They now come from the marketing catalogs, translated in all four.
- /press absorbs the team data: its Organization JSON-LD gains the member[]
  array that only /team used to emit, so deleting /team does not drop the
  site's only Person schema.

Drops the now-unused fixedType prop and the teamTitle/teamSubtitle keys, both
orphaned by the routes retired in this branch.
…nce and the waitlist

The ragdoll had no prefers-reduced-motion handling at all. It now follows the
same shape as confetti.ts — matchMedia read live per mount — and degrades
rather than disappearing: the world is stepped off-screen until the peanut
settles, then painted once, with no RAF loop and no pointer listeners.

Two new homes, both non-scrolling panes with a definite height:

- /maintenance replaces a static crying-peanut image. The copy was the worst
  voice on the site ('We are currently going through maintenance... Sorry for
  the inconvenience') and a button labelled 'Contact Support?'. Rewritten to
  match the 404's register.
- The waitlist jail pane, gated on step === 'jail' so plain /invite — a signup
  conversion surface — does not get a draggable toy next to its CTA. The
  illustration pane moves from min-h to a definite h-[55dvh]; min-h gives
  h-full nothing to resolve against, so the canvas would have silently
  collapsed to zero height while the loop kept running.

Fixes a real bug found while placing it: the (mobile-ui) layout registers
pull-to-refresh on document before the waitlist early-return, and its
#scrollable-content guard is absent on that screen, so the handler fell
through to true. Dragging the peanut down ~160px would have reloaded the page
and wiped the user's typed invite code. The host now swallows touchstart — a
native listener, because React hydrates on document and a synthetic
stopPropagation would not stop a same-target sibling.

startRagdoll now runs in a try/catch: it throws synchronously on a failed 2D
context, which would white-screen /maintenance, the one page whose job is to
work when nothing else does.

Also retires the stale 'triple-click Rewards title' comments — that easter egg
was deliberately cut in 347757d and never shipped.
The footer nav now points at /careers rather than the Notion board, so the
label should name the page it opens. Renames footerJobs to footerCareers and
sets the English value to 'Careers' (it read 'Career'). pt-BR moves from
'Vagas' — openings — to 'Carreiras'; es keeps 'Empleo', which already reads
correctly.

Note the SEO footer's own jobs entry, which still carries a Notion href in the
content manifest, is already suppressed by RESOURCES_MOVED_ELSEWHERE, so it
renders nothing and needs no change here.
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 21, 2026 9:01am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 883f3eef-9640-43d9-93bb-6e8a4457a956

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7313.46 → 7314.78 (+1.32)
Findings: -12 net (+47 new, -59 resolved)

🆕 New findings (47)

  • critical complexity — src/components/PeanutRagdoll/ragdoll.ts — CC 140, MI 51.34, SLOC 779
  • critical complexity — src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.03, SLOC 293
  • high complexity — src/app/[locale]/(marketing)/press/page.tsx — CC 42, MI 61.59, SLOC 97
  • high hotspot — src/components/Invites/JoinWaitlistPage.tsx — 30 commits, +463/-204 lines since 6 months ago
  • high complexity — src/app/sitemap.ts — CC 29, MI 47.39, SLOC 228
  • medium high-mdd — src/components/PeanutRagdoll/ragdoll.ts:113 — startRagdoll: MDD 198.0 (uses across many lines from declarations)
  • medium high-dlt — src/components/PeanutRagdoll/ragdoll.ts:113 — startRagdoll: DLT 85 (calls 85 distinct functions — high context load)
  • medium high-mdd — src/app/sitemap.ts:58 — generateSitemap: MDD 79.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/[locale]/(marketing)/press/page.tsx:102 — PressPage: MDD 63.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/LandingPage/Footer.tsx:57 — Footer: MDD 45.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/Invites/InvitesPageLayout.tsx:32 — InvitesPageLayout: MDD 41.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/PeanutRagdoll/ragdoll.ts:341 — buildWorld: MDD 38.6 (uses across many lines from declarations)
  • medium high-mdd — src/app/[locale]/(marketing)/blog/[slug]/page.tsx:52 — BlogPostPageLocalized: MDD 33.6 (uses across many lines from declarations)
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-privacy/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-prohibited-activities/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-international/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-us/page.tsx:24
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/privacy/page.tsx:25
  • medium structural-dup — app/[locale]/(marketing)/card-esign/page.tsx:24 — 30 duplicate lines / 172 tokens with app/[locale]/(marketing)/terms/page.tsx:25
  • medium complexity — src/components/Marketing/ContentLanding.tsx — CC 28, MI 68.09, SLOC 89

…and 27 more.

✅ Resolved (59)

  • src/components/PeanutRagdoll/ragdoll.ts — CC 131, MI 51.47, SLOC 753
  • src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.04, SLOC 293
  • src/app/[locale]/(marketing)/press/page.tsx — CC 40, MI 61.93, SLOC 87
  • src/context/kernelClient.context.tsx — 30 commits, +636/-203 lines since 6 months ago
  • src/app/sitemap.ts — CC 26, MI 43.05, SLOC 219
  • src/components/PeanutRagdoll/ragdoll.ts:95 — startRagdoll: MDD 190.1 (uses across many lines from declarations)
  • src/components/PeanutRagdoll/ragdoll.ts:95 — startRagdoll: DLT 83 (calls 83 distinct functions — high context load)
  • src/app/sitemap.ts:49 — generateSitemap: MDD 80.0 (uses across many lines from declarations)
  • src/app/[locale]/(marketing)/press/page.tsx:101 — PressPage: MDD 56.9 (uses across many lines from declarations)
  • src/components/LandingPage/Footer.tsx:57 — Footer: MDD 46.4 (uses across many lines from declarations)
  • src/components/Marketing/ContentLanding.tsx:32 — ContentLanding: MDD 41.0 (uses across many lines from declarations)
  • src/components/PeanutRagdoll/ragdoll.ts:321 — buildWorld: MDD 38.6 (uses across many lines from declarations)
  • src/app/[locale]/(marketing)/blog/[slug]/page.tsx:52 — BlogPostPageLocalized: MDD 33.0 (uses across many lines from declarations)
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-privacy/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-prohibited-activities/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-international/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/card-terms-us/page.tsx:24
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/privacy/page.tsx:25
  • app/[locale]/(marketing)/blog/page.tsx:17 — 32 duplicate lines / 172 tokens with app/[locale]/(marketing)/terms/page.tsx:25
  • src/components/Invites/InvitesPageLayout.tsx:21 — InvitesPageLayout: MDD 30.0 (uses across many lines from declarations)

…and 39 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Jobs/openRoles.ts 0.0 3.3 +3.3
src/app/crisp-proxy/layout.tsx 0.0 3.0 +3.0
src/app/kyc/success/layout.tsx 0.0 3.0 +3.0
src/app/maintenance/layout.tsx 0.0 3.0 +3.0
src/components/Jobs/Careers.tsx 0.0 2.4 +2.4
src/components/Jobs/RoleCard.tsx 0.0 1.8 +1.8
src/components/PeanutRagdoll/index.tsx 4.2 5.4 +1.2
src/app/sitemap.ts 11.7 10.8 -0.9
src/components/Jobs/index.tsx 1.7 0.0 -1.7
src/app/exchange/page.tsx 2.1 0.0 -2.1
src/app/exchange/layout.tsx 2.5 0.0 -2.5
src/app/[locale]/(marketing)/blog/page.tsx 5.5 0.0 -5.5
src/app/[locale]/(marketing)/team/page.tsx 7.4 0.0 -7.4

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3438 ran, 0 failed, 0 skipped, 59.4s

📊 Coverage (unit)

metric %
statements 68.8%
branches 54.0%
functions 59.8%
lines 69.7%
⏱ 10 slowest test cases
time test
4.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.8s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.4s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.4s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.3s src/utils/__tests__/crisp.test.ts › retries configuration on the next open after a failure
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Replaced by #2783, which targets main as requested.

This branch was based on dev. Retargeting its base to main alone would have made the PR carry 22 commits and 98 files — my 7 plus 15 unrelated ones sitting on dev but not yet in main (mobile fixes, rewards copy, claim errors). Rebasing the branch in place would have fixed that, but a repo ruleset blocks the non-fast-forward push, so #2783 is the same 7 commits rebased onto main on a fresh branch: 7 commits, 51 files, signatures intact.

@0xkkonrad 0xkkonrad closed this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants