Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
30 changes: 28 additions & 2 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
# peanut-ui Development Rules

**Version:** 0.0.1 | **Updated:** October 17, 2025
**Version:** 0.0.2 | **Updated:** December 16, 2025

## 🚫 Random

- **Never open SVG files** - it crashes you. Only read jpeg, png, gif, or webp.
- **Never run jq command** - it crashes you.
- **Never run sleep** from command line - it hibernates pc.
- **Do not generate .md files** unless explicity told to do so.
- **Comments** should always be made in all lowercase and simple english
- **Error messages**, any error being shown in the ui should be user friendly and easy to understand, and any error being logged in consoles and sentry should be descriptive for developers to help with debugging

## 💻 Code Quality

- **Boy scout rule**: leave code better than you found it.
- **DRY** - do not repeat yourself. Reuse existing code and abstract shared functionality. Less code is better code.
- this also means to use shared consts (e.g. check src/constants)
- **Separate business logic from interface** - this is important for readability, debugging and testability.
- **Use explicit imports** where possible.
- **Reuse existing components and functions** - don't hardcode hacky solutions.
- **Warn about breaking changes** - when making changes, ensure you're not breaking existing functionality, and if there's a risk, explicitly WARN about it.
- **Mention refactor opportunities** - if you notice an opportunity to refactor or improve existing code, mention it. DO NOT make any changes you were not explicitly told to do. Only mention the potential change to the user.
- **Performance is important** - cache where possible, make sure to not make unnecessary re-renders or data fetching.
- **Flag breaking changes** - always flag if changes done in Frontend are breaking and require action on Backend (or viceversa)

## 🔗 URL as State (Critical for UX)

- **URL is source of truth** - use query parameters for user-facing state that should survive navigation, refresh, or sharing (step indicators, amounts, filters, view modes, selected items)
- **Use nuqs library** - always use `useQueryStates` from [nuqs](https://nuqs.dev) for type-safe URL state management. never manually parse/set query params with router.push or URLSearchParams
- **Enable deep-linking** - users should be able to share or bookmark URLs mid-flow (e.g. `?step=inputAmount&amount=500&currency=ARS`)
- **Proper navigation** - URL state enables correct back/forward browser button behavior
- **Multi-step flows** - the URL should always reflect current step and relevant data, making the app behave like a proper web app, not a trapped SPA
- **Reserve useState for ephemeral UI** - only use React useState for truly transient state:
- loading spinners and skeleton states
- modal open/close state
- form validation errors (unless they should persist)
- hover/focus states
- temporary UI animations
- **Don't URL-ify everything** - API responses, user authentication state, and internal component state generally shouldn't be in the URL unless they're user-facing and shareable
- **Type safety** - define parsers for query params (e.g. `parseAsInteger`, `parseAsStringEnum`) to ensure type safety and validation

## 🚫 Import Rules (critical for build performance)

- **No barrel imports** - never use `import * as X from '@/constants'` or create index.ts barrel files. always import from specific files (e.g. `import { PEANUT_API_URL } from '@/constants/general.consts'`). barrel imports slow down builds and cause bundling issues.
- **No circular dependencies** - before adding imports, check if the target file imports from the current file. circular deps cause `Cannot access X before initialization` errors. move shared types to `interfaces.ts` if needed.
- **No node.js packages in client components** - packages like `web-push`, `fs`, `crypto` (node) can't be used in `'use client'` files. use server actions or api routes instead.
- **Check for legacy code** - before importing from a file, check if it has TODO comments marking it as legacy/deprecated. prefer newer implementations.

## 🚫 Export Rules (critical for build performance)

- **Do not export multiple stuff from same component**:
- never export types or other utility methods from a component or a hook
- for types always use a separate file if they need to be reused
- and for utility/helper functions use a separate utils file to export them and use if they need to be reused
- same for files with multiple components exported, do not export multiple components from same file and if you see this done anywhere in the code, abstract it to other file

## 🧪 Testing

- **Test new code** - where tests make sense, test new code. Especially with fast unit tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

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, {
DEFAULT_FORCE_CONFIG,
DEFAULT_VISIBILITY_CONFIG,
DEFAULT_EXTERNAL_NODES_CONFIG,
} from '@/components/Global/InvitesGraph'

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 @@ -26,14 +32,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 @@ -76,8 +120,8 @@ export default function InviteGraphPage() {
renderOverlays={({
showUsernames,
setShowUsernames,
showAllNodes,
setShowAllNodes,
topNodes,
setTopNodes,
activityFilter,
setActivityFilter,
forceConfig,
Expand Down Expand Up @@ -433,27 +477,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 @@ -613,7 +661,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 @@ -623,15 +671,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 @@ -680,6 +743,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 @@ -728,7 +795,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()
}
Comment on lines +1 to +19
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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Next.js notFound() client component 'use client' error behavior

💡 Result:

notFound() is not a “client-side navigation” API in the App Router. It’s implemented by throwing a special error (NEXT_HTTP_ERROR_FALLBACK;404) to terminate rendering of the current route segment, so Next.js can render that segment’s not-found.js UI. This mechanism is designed for server-side rendering paths (e.g., Server Components during render, and other server-controlled flows), not for Client Component event handlers. [1]

Because "use client" makes the file part of the client module graph (rendered on the client after navigation/hydration), calling notFound() there typically results in a runtime error / red error overlay, rather than showing not-found.js, since there’s no server render to “bail out of” and swap in the segment’s Not Found boundary. [1], [2]

What to do in a Client Component instead

  • Render your own “not found” UI conditionally, or
  • Navigate with the client router (e.g., router.replace(...)) to a dedicated route, rather than calling notFound(). (The not-found.js convention is specifically for when notFound() is thrown during segment rendering.) [3]

Note: Tutorials showing notFound() inside a "use client" component are misleading for real App Router behavior. [1], [4]

Sources:
[1] Next.js API Reference: notFound() (throws NEXT_HTTP_ERROR_FALLBACK;404)
[2] Next.js Docs: Server vs Client Components ("use client" boundary; Client Components render on the client)
[3] Next.js API Reference: not-found.js convention (rendered when notFound() is thrown in a segment)
[4] Example article demonstrating notFound() inside a Client Component (non-official; conflicts with intended mechanism)


🏁 Script executed:

# First, let's examine the actual file to understand the context
cat -n src/app/\(mobile-ui\)/dev/layout.tsx 2>/dev/null || find . -name "layout.tsx" -path "*dev*" | head -5

Repository: peanutprotocol/peanut-ui

Length of output: 1028


notFound() throws a runtime error in client components and won't render the not-found page

notFound() is implemented by throwing a special error during server-side rendering to bail out of a route segment. Since this file is marked with 'use client', the component renders on the client after navigation, so calling notFound() will result in a runtime error instead of showing not-found.js. Either move this guard to a Server Component or middleware, or replace with a client-side redirect using the router.

🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/layout.tsx around lines 1 - 19, The DevLayout client
component currently calls notFound(), which throws in client-side code; either
move the guard out of the client module (e.g., implement the check in a Server
Component or middleware using IS_DEV and PRODUCTION_ALLOWED_ROUTES) or replace
the client-side behavior: remove notFound(), import useRouter from
'next/navigation', call const router = useRouter(), and inside a useEffect check
the pathname against PRODUCTION_ALLOWED_ROUTES and call router.replace('/404')
(or another safe route) when not allowed, returning null while redirecting;
reference DevLayout, PRODUCTION_ALLOWED_ROUTES, IS_DEV, usePathname, and
notFound to locate the change.

}

return <>{children}</>
}
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>
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 | 🟡 Minor

Inconsistent info text with actual access control.

This text states tools are "only available in development mode", but according to src/constants/routes.ts, /dev/payment-graph is a public route accessible via API key. Consider updating the text to accurately reflect access policies, or clarifying which tools have different access levels.

🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/page.tsx at line 81, The text in the dev page ("•
These tools are only available in development mode") is inconsistent with route
access rules defined in src/constants/routes.ts (notably the /dev/payment-graph
public API-key route); update the copy in src/app/(mobile-ui)/dev/page.tsx to
accurately reflect access control (either change the line to note that some
tools require development mode while /dev/payment-graph is public via API key,
or indicate per-tool access like "Development-only; some tools (e.g.
/dev/payment-graph) are public via API key"). Locate the string in the Dev page
component and adjust the wording to match the actual policies or add a short
per-tool note linking to the routes/constants for clarity.

<li>• Perfect for testing on multiple devices</li>
<li>• Share the URL with team members for testing</li>
</ul>
Expand Down
Loading
Loading