Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dba133a
hotfix: grafana link broken in invite graph
Hugo0 Jan 20, 2026
d1a8f7c
feat(invite-graph): new users indicator and progressive inactive fade
Hugo0 Jan 20, 2026
b82a269
feat(invite-graph): add per-user external edge data and improve UI
Hugo0 Jan 20, 2026
24c1242
feat(invite-graph): enable search for external nodes with custom labels
Hugo0 Jan 20, 2026
986b45c
feat(invite-graph): activity filtering + logarithmic edge animations
Hugo0 Jan 20, 2026
16fdf6f
feat(graph): add payment-graph mode and top-nodes slider for full-graph
Hugo0 Jan 22, 2026
87b5d3b
graph fixes
Hugo0 Jan 22, 2026
8608676
chore: remove debug logging from InvitesGraph after fixing orphan mer…
Hugo0 Jan 22, 2026
780b64a
fix: apply prettier formatting to InvitesGraph
Hugo0 Jan 22, 2026
409e1a5
refactor: remove debug logs and add safety assertions
Hugo0 Jan 22, 2026
c0c25c6
feat: add payment mode support to types and preferences
Hugo0 Jan 22, 2026
faed87f
Merge pull request #1630 from peanutprotocol/feat/payment-graph-and-t…
Hugo0 Jan 22, 2026
2b87dd6
fix: update user details if missing during KYC process and reset erro…
kushagrasarathe Jan 22, 2026
16816bd
fix: prevent data corruption by updating user fields independently
kushagrasarathe Jan 22, 2026
b942bda
Merge pull request #1631 from peanutprotocol/hot-fix/kyc-cta
jjramirezn Jan 22, 2026
81c454f
feat: switch payment graph to password-based auth
Hugo0 Jan 22, 2026
b9f5a35
Merge pull request #1634 from peanutprotocol/feat/payment-graph-and-t…
Hugo0 Jan 22, 2026
c2888c5
fix: pass password to getExternalNodes for payment mode
Hugo0 Jan 22, 2026
7523703
fix: correct kkonrad username in full-graph whitelist
Hugo0 Jan 22, 2026
3fb95a9
feat: disable squid withdrawals
jjramirezn Jan 23, 2026
6bb7377
Merge pull request #1636 from peanutprotocol/feat/disable-squid-withdraw
Hugo0 Jan 23, 2026
358d731
Merge peanut-wallet (prod) into peanut-wallet-dev
Hugo0 Jan 27, 2026
1b5c491
style: run prettier on InvitesGraph files
Hugo0 Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

import { useState, useCallback } from 'react'
import { Button } from '@/components/0_Bruddle/Button'
import { useAuth } from '@/context/authContext'
import { IS_DEV } from '@/constants/general.consts'
import InvitesGraph from '@/components/Global/InvitesGraph'
import {
DEFAULT_FORCE_CONFIG,
DEFAULT_VISIBILITY_CONFIG,
DEFAULT_EXTERNAL_NODES_CONFIG,
} from '@/components/Global/InvitesGraph/types'

export default function InviteGraphPage() {
// Allowed users for full graph access (frontend check - backend also validates)
const ALLOWED_USERNAMES = ['squirrel', 'kkonrad', 'hugo']

export default function FullGraphPage() {
const { user, isFetchingUser } = useAuth()
const [apiKey, setApiKey] = useState('')
const [apiKeySubmitted, setApiKeySubmitted] = useState(false)
const [error, setError] = useState<string | null>(null)
Expand All @@ -27,14 +33,52 @@ export default function InviteGraphPage() {
window.location.href = '/dev'
}, [])

// Check if user is allowed (frontend defense - backend also validates)
// In dev mode, allow all users; in prod, restrict to allowed usernames
const isAllowedUser =
IS_DEV || (user?.user?.username && ALLOWED_USERNAMES.includes(user.user.username.toLowerCase()))

// Loading state
if (isFetchingUser) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900">
<div className="text-white">Loading...</div>
</div>
)
}

// Access denied screen
if (!isAllowedUser) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900">
<div className="w-full max-w-md space-y-6 rounded-2xl bg-white p-8 shadow-2xl">
<div className="text-center">
<div className="mb-4 text-6xl">🔒</div>
<h2 className="mb-2 text-2xl font-bold text-gray-900">Access Restricted</h2>
<p className="text-sm text-gray-600">This tool is only available to authorized users.</p>
{user?.user?.username && (
<p className="mt-2 text-xs text-gray-400">Logged in as: {user.user.username}</p>
)}
</div>
<button
onClick={() => (window.location.href = '/dev')}
className="w-full text-sm text-gray-500 hover:text-gray-700"
>
← Back to Dev Tools
</button>
</div>
</div>
)
}

// API key input screen
if (!apiKeySubmitted) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900">
<div className="w-full max-w-md space-y-6 rounded-2xl bg-white p-8 shadow-2xl">
<div className="text-center">
<div className="mb-4 text-6xl">🕸️</div>
<h2 className="mb-2 text-2xl font-bold text-gray-900">Invite Graph</h2>
<h2 className="mb-2 text-2xl font-bold text-gray-900">Full Graph</h2>
<p className="text-sm text-gray-600">
Admin tool - Enter your API key to visualize the network
</p>
Expand Down Expand Up @@ -77,8 +121,8 @@ export default function InviteGraphPage() {
renderOverlays={({
showUsernames,
setShowUsernames,
showAllNodes,
setShowAllNodes,
topNodes,
setTopNodes,
activityFilter,
setActivityFilter,
forceConfig,
Expand Down Expand Up @@ -434,27 +478,31 @@ export default function InviteGraphPage() {
)}
{!externalNodesError && externalNodesConfig.enabled && (
<div className="space-y-1.5 pl-4">
{/* Min connections slider - show only external addresses used by N+ users */}
{/* Min connections - discrete options */}
<div className="space-y-0.5">
<div className="flex items-center justify-between">
<span className="text-[9px] text-gray-500">
Show if ≥{externalNodesConfig.minConnections} users
</span>
<span className="text-[9px] text-gray-500">Min users:</span>
<div className="flex flex-wrap gap-1">
{[1, 2, 3, 5, 10, 20, 50].map((val) => (
<button
key={val}
onClick={() =>
setExternalNodesConfig({
...externalNodesConfig,
minConnections: val,
})
}
className={`rounded px-1.5 py-0.5 text-[9px] transition-colors ${
externalNodesConfig.minConnections === val
? 'bg-orange-600 text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
}`}
>
{val}
</button>
))}
</div>
</div>
<input
type="range"
min="2"
max="20"
step="1"
value={externalNodesConfig.minConnections}
onChange={(e) =>
setExternalNodesConfig({
...externalNodesConfig,
minConnections: parseInt(e.target.value),
})
}
className="h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-orange-600"
/>
</div>
{/* Type filters */}
<div className="flex gap-2 text-[9px]">
Expand Down Expand Up @@ -614,7 +662,7 @@ export default function InviteGraphPage() {
</div>

{/* Other options */}
<div className="flex gap-3">
<div className="flex items-center gap-3">
<label className="flex cursor-pointer items-center gap-1">
<input
type="checkbox"
Expand All @@ -624,15 +672,30 @@ export default function InviteGraphPage() {
/>
<span className="text-gray-600">Names</span>
</label>
<label className="flex cursor-pointer items-center gap-1">
<input
type="checkbox"
checked={showAllNodes}
onChange={(e) => setShowAllNodes(e.target.checked)}
className="text-red-600 h-3 w-3 rounded border-gray-300"
/>
<span className="text-gray-600">All nodes</span>
</label>
</div>

{/* Top nodes slider */}
<div className="space-y-0.5">
<div className="flex items-center justify-between">
<span className="text-[10px] text-gray-500">Top nodes:</span>
<span className="text-[9px] text-gray-500">
{topNodes === 0 ? 'All' : topNodes.toLocaleString()}
</span>
</div>
<input
type="range"
min="0"
max="10000"
step="500"
value={topNodes}
onChange={(e) => setTopNodes(parseInt(e.target.value))}
className="h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-purple-600"
/>
<div className="flex justify-between text-[8px] text-gray-400">
<span>All</span>
<span>5k</span>
<span>10k</span>
</div>
</div>

{/* Activity window */}
Expand Down Expand Up @@ -681,6 +744,10 @@ export default function InviteGraphPage() {
<div className="space-y-1 text-[9px]">
{/* Nodes */}
<div className="flex flex-wrap gap-x-2 gap-y-0.5 text-gray-500">
<span className="flex items-center gap-0.5">
<span className="inline-block h-2 w-2 rounded-full bg-green-500"></span>
New
</span>
<span className="flex items-center gap-0.5">
<span className="inline-block h-2 w-2 rounded-full bg-purple-500"></span>
Active
Expand Down Expand Up @@ -729,7 +796,9 @@ export default function InviteGraphPage() {
</span>
</div>
<p className="text-gray-400">Click → Grafana | Right-click → Focus</p>
{!showAllNodes && <p className="text-gray-400">Showing top 5000 nodes</p>}
{topNodes > 0 && (
<p className="text-gray-400">Showing top {topNodes.toLocaleString()} nodes</p>
)}
</div>
</div>
</div>
Expand Down
23 changes: 23 additions & 0 deletions src/app/(mobile-ui)/dev/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'

import { usePathname } from 'next/navigation'
import { notFound } from 'next/navigation'
import { IS_DEV } from '@/constants/general.consts'

// Routes that are allowed in production (protected by API key / user check)
const PRODUCTION_ALLOWED_ROUTES = ['/dev/full-graph', '/dev/payment-graph']

export default function DevLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()

// In production, only allow specific routes (full-graph, payment-graph)
// Other dev tools (leaderboard, shake-test, dev index) are dev-only
if (!IS_DEV) {
const isAllowedInProd = PRODUCTION_ALLOWED_ROUTES.some((route) => pathname?.startsWith(route))
if (!isAllowedInProd) {
notFound()
}
}

return <>{children}</>
}
Comment on lines +1 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Next.js notFound() function server-only client component behavior

💡 Result:

What notFound() does (App Router)

  • notFound() is control flow, not a render helper: calling it throws a special internal error (NEXT_HTTP_ERROR_FALLBACK;404) that immediately stops rendering for the current route segment and causes Next.js to render that segment’s not-found.(js|tsx) UI (and injects noindex). [1]
  • not-found.(js|tsx) is the file that renders when notFound() is thrown in that segment (and the root app/not-found also handles unmatched URLs). By default it’s a Server Component; you can make it a Client Component if you need client hooks, but then you must do client-side data work yourself. [2]

Where you can (and can’t) use it

  • Supported locations: Server Components, Route Handlers, and Server Actions. [3]
  • Not supported in Client Components ('use client'): calling notFound() there is not a supported way to show a 404 boundary.

Practical patterns when you “need 404” from client code

  1. Move the existence check to a Server Component (page/layout) and call notFound() there. [1]
  2. If the decision only happens after a client interaction, call a Server Action that can notFound() (or return a flag), then refresh/navigate accordingly. [3]
  3. If you just want a client-side UX, navigate (e.g., router.replace('/somewhere')) or render your own “not found” UI—don’t rely on notFound() from the client.

Sources: Next.js notFound() docs [1], Next.js not-found file docs [2], Next.js source comment describing supported environments [3].


🏁 Script executed:

# Search for how notFound() is used elsewhere in the codebase
rg 'notFound' --type typescript --type tsx -B 2 -A 2

Repository: peanutprotocol/peanut-ui

Length of output: 102


🏁 Script executed:

# Search for notFound usage with correct syntax
rg 'notFound' -A 3 -B 3

Repository: peanutprotocol/peanut-ui

Length of output: 4655


🏁 Script executed:

# Check if notFound can be imported in 'use client' files - search for such patterns
rg "'use client'" -A 10 | rg -A 5 'notFound'

Repository: peanutprotocol/peanut-ui

Length of output: 527


🏁 Script executed:

# Check the actual file to see what would happen
cat -n src/app/\(mobile-ui\)/dev/layout.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 1028


🏁 Script executed:

# Check if there are other client components with access guards or similar patterns
rg "'use client'" -A 15 | rg -B 2 -A 2 'useRouter\|router\.replace\|router\.push'

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

# Check what IS_DEV is and when notFound would be called
rg 'IS_DEV' src/constants/general.consts -A 2 -B 2

Repository: peanutprotocol/peanut-ui

Length of output: 136


🏁 Script executed:

# Find where IS_DEV is defined
fd -e ts -e tsx -e js | xargs rg 'export.*IS_DEV' | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 365


🏁 Script executed:

# Check if there are server components used as wrappers in the dev route
find src/app/\(mobile-ui\)/dev -type f -name '*.tsx' -o -name '*.ts'

Repository: peanutprotocol/peanut-ui

Length of output: 315


🏁 Script executed:

# Check if there are middleware or other guards in place for dev routes
rg 'middleware' src/ -l

Repository: peanutprotocol/peanut-ui

Length of output: 123


🏁 Script executed:

# Check parent layouts to see if there's already a dev guard
fd -path '*/dev*' -name 'layout.tsx' -o -path '*/dev*' -name 'page.tsx' | xargs grep -l "'use client'" 2>/dev/null

Repository: peanutprotocol/peanut-ui

Length of output: 238


🏁 Script executed:

# Check if there are Server Components wrapping the dev route
cat src/app/\(mobile-ui\)/page.tsx | head -50

Repository: peanutprotocol/peanut-ui

Length of output: 130


🏁 Script executed:

# Check if dev layout could be a Server Component instead
grep -r "export default.*function\|export default async function" src/app/\(mobile-ui\)/ | head -5

Repository: peanutprotocol/peanut-ui

Length of output: 590


🏁 Script executed:

# Verify what the actual error would be - search Next.js issues or similar patterns
rg "notFound.*use client" --type-list

Repository: peanutprotocol/peanut-ui

Length of output: 457


🏁 Script executed:

# Look at the structure - is there a parent layout that could handle this?
cat -n "src/app/(mobile-ui)/layout.tsx" 2>/dev/null | head -30 || echo "File not found"

Repository: peanutprotocol/peanut-ui

Length of output: 1713


🏁 Script executed:

# Check if '/404' is a valid fallback or if there's a custom 404 page
fd -e tsx -e ts -path '*/404*'

Repository: peanutprotocol/peanut-ui

Length of output: 236


🏁 Script executed:

# Verify that the proposed pattern (useEffect + router.replace) is used elsewhere
rg "useEffect.*router\.replace\|router\.replace.*useEffect" -B 3 -A 3

Repository: peanutprotocol/peanut-ui

Length of output: 50


Remove notFound() from this Client Component; use server-side guard instead.

notFound() is not supported in Client Components. It will not set a 404 status and may fail silently. Since the guard requires usePathname() (a client hook), the cleanest fix is to move the permission check to a parent Server Component or wrap this layout in a server-side guard.

Alternatively, if keeping it as a Client Component, use client-side redirect on mount:

Client-side redirect pattern
 'use client'

 import { usePathname, useRouter } from 'next/navigation'
-import { notFound } from 'next/navigation'
+import { useEffect } from 'react'
 import { IS_DEV } from '@/constants/general.consts'

 const PRODUCTION_ALLOWED_ROUTES = ['/dev/full-graph', '/dev/payment-graph']

 export default function DevLayout({ children }: { children: React.ReactNode }) {
     const pathname = usePathname()
+    const router = useRouter()
+
+    useEffect(() => {
+        if (!IS_DEV && pathname) {
+            const allowed = PRODUCTION_ALLOWED_ROUTES.some(
+                (route) => pathname === route || pathname.startsWith(`${route}/`)
+            )
+            if (!allowed) {
+                router.replace('/')
+            }
+        }
+    }, [IS_DEV, pathname, router])

     // In production, only allow specific routes (full-graph, payment-graph)
     // Other dev tools (leaderboard, shake-test, dev index) are dev-only
-    if (!IS_DEV) {
-        const isAllowedInProd = PRODUCTION_ALLOWED_ROUTES.some((route) => pathname?.startsWith(route))
-        if (!isAllowedInProd) {
-            notFound()
-        }
-    }

     return <>{children}</>
 }

Note: startsWith alone allows unintended prefixes (e.g., /dev/full-graphx); the fix above uses exact-or-subpath matching.

🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/layout.tsx around lines 1 - 23, DevLayout currently
calls notFound() from a Client Component (using usePathname), which is
unsupported; remove the notFound() call and move the production-route guard into
a parent Server Component (or create a server-side wrapper) that checks IS_DEV
and PRODUCTION_ALLOWED_ROUTES and returns a proper 404 there, or if you must
stay client-side implement a client-side redirect on mount instead of notFound;
also tighten the route check in PRODUCTION_ALLOWED_ROUTES logic (use exact match
or ensure the path segment boundary when checking startsWith to avoid matching
prefixes like "/dev/full-graphx"). Reference: DevLayout, usePathname, notFound,
IS_DEV, PRODUCTION_ALLOWED_ROUTES.

16 changes: 12 additions & 4 deletions src/app/(mobile-ui)/dev/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ export default function DevToolsPage() {
status: 'active',
},
{
name: 'Invite Graph',
description: 'Interactive force-directed graph visualization of all user invites (admin only)',
path: '/dev/invite-graph',
name: 'Full Graph',
description:
'Interactive force-directed graph visualization of all users, invites, and P2P activity (admin only)',
path: '/dev/full-graph',
icon: '🕸️',
status: 'active',
},
{
name: 'Payment Graph',
description: 'P2P payment flow visualization',
path: '/dev/payment-graph',
icon: '💸',
status: 'active',
},
{
name: 'Shake Test',
description: 'Test progressive shake animation and confetti for perk claiming',
Expand Down Expand Up @@ -70,7 +78,7 @@ export default function DevToolsPage() {
<Card className="space-y-2 bg-blue-50 p-4">
<h3 className="font-bold text-blue-900">ℹ️ Info</h3>
<ul className="space-y-1 text-sm text-blue-800">
<li>• These tools are publicly accessible (no login required)</li>
<li>• These tools are only available in development mode</li>
<li>• Perfect for testing on multiple devices</li>
<li>• Share the URL with team members for testing</li>
</ul>
Expand Down
Loading
Loading