From 782a1f466f03842f76e773f8f6ded64528d42827 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sat, 23 May 2026 18:33:00 +0700 Subject: [PATCH] feat(p2p stats): redesign connected peers with world map and country flags Restyle the connected-peers list in the classic square, flat imageboard style, move it to the end of the panel, and open it by default. Add an offline approximate world map and per-peer country flags (sprite order taken from the original 4chan flags.css), plus codify the old-school design rules in DESIGN.md and point agents to them from AGENTS.md. --- AGENTS.md | 9 + DESIGN.md | 6 + public/assets/icons/flags-1.png | Bin 0 -> 81015 bytes public/llms-full.txt | 15 + .../__tests__/p2p-stats-settings.test.tsx | 7 +- .../p2p-stats-settings.module.css | 117 ++++++- .../p2p-stats-settings/p2p-stats-settings.tsx | 85 +++-- .../p2p-stats-settings/peer-world-map.tsx | 262 +++++++++++++++ src/lib/__tests__/country-flags.test.ts | 40 +++ src/lib/__tests__/peer-geo.test.ts | 96 ++++++ src/lib/country-flags.ts | 300 ++++++++++++++++++ src/lib/peer-geo.ts | 167 ++++++++++ 12 files changed, 1065 insertions(+), 39 deletions(-) create mode 100644 public/assets/icons/flags-1.png create mode 100644 src/components/settings-modal/p2p-stats-settings/peer-world-map.tsx create mode 100644 src/lib/__tests__/country-flags.test.ts create mode 100644 src/lib/__tests__/peer-geo.test.ts create mode 100644 src/lib/country-flags.ts create mode 100644 src/lib/peer-geo.ts diff --git a/AGENTS.md b/AGENTS.md index 2eb79eb9..1e7cb1bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ Agents may use compiled context to navigate quickly, but must verify against sou | Situation | Required action | |---|---| +| New UI designed or styled, or any visual/component/theme work (`src/components`, `src/views`, CSS modules, `src/themes.css`) | Read `DESIGN.md` first and follow it; self-review the diff against its Do/Don't list before verifying. 5chan is a classic imageboard: square edges, 1px theme borders, flat surfaces, dense text, theme palette. No rounded corners, pill/badge shapes, hover background fills, CSS transitions, or soft shadows on product UI | | React UI logic changed (`src/components`, `src/views`, `src/hooks`, UI stores) | Follow React architecture rules below, review the diff with `vercel-react-best-practices` and `vercel:react-best-practices` when available, fix valid findings, then run `yarn doctor` | | `package.json` changed | Run `corepack yarn install` to keep `yarn.lock` in sync | | Dependencies or import graph changed | Run `yarn knip` as an advisory manifest/import audit | @@ -108,6 +109,14 @@ src/ - Avoid boolean flag soup for complex flows; model state clearly in Zustand. - Use React Router for navigation; no manual history manipulation. +### Design Rules + +- Read `DESIGN.md` before designing, styling, or adding any UI, and self-review the diff against its Do/Don't list before verification. `DESIGN.md` is the source of truth for 5chan's classic-imageboard look. +- Preserve the old-school imageboard aesthetic: square edges, 1px theme borders, flat surfaces, dense compact text, and the existing theme palette (CSS variables in `src/themes.css` or `currentColor`). +- Do not introduce modern UI patterns on product surfaces: no rounded corners/`border-radius`, no pill/capsule/badge shapes, no hover background-color changes or CSS transitions/animations on rows or cards, and no soft shadows, glass, gradients, or new accent/marketing colors. +- Functional per-entity colors (e.g. a small hashed swatch identifying a peer) are allowed, but render them flat and square as small indicators, not as decorative gradients. +- New decentralized/technical surfaces (P2P stats, addresses, tables) still follow the same language: compact, square, theme-aware, terse labels. + ### Code Organization Rules - Keep components focused; split large components. diff --git a/DESIGN.md b/DESIGN.md index 213a46a7..82efd9ee 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -129,6 +129,8 @@ The palette is inherited from classic imageboard themes and should remain recogn **The No Modern Accent Rule.** Do not introduce a new brand accent, gradient palette, neon crypto palette, or marketing color system for core product UI. +**The Functional Color Exception.** Small, deterministic per-entity colors used as data — for example a hashed swatch that gives each connected peer a stable identity — are allowed. Render them as flat, square, small indicators (not gradients, glows, or large fills). This exception is for conveying data, not for decoration or branding. + ## 3. Typography **Display Font:** Tahoma, sans-serif. @@ -178,6 +180,8 @@ The palette is inherited from classic imageboard themes and should remain recogn - **Shadow Strategy:** none except the small legacy menu shadow. - **Border:** 1px theme borders. - **Internal Padding:** compact, commonly `2px`, `5px`, or `0.5em`. +- **Hover:** do not change a row's or card's background on hover, and do not add CSS transitions or animations to them. Reserve hover feedback for text-link color changes. +- **Inline labels:** plain text, bold for emphasis. Do not wrap labels (tags, statuses, transports, counts) in pill, capsule, chip, or rounded badge shapes; a label is text, not a button. ### Inputs / Fields @@ -218,6 +222,8 @@ The palette is inherited from classic imageboard themes and should remain recogn - Do not modernize core imageboard UI for taste. - Do not add rounded cards, gradient hero areas, glass panels, soft shadows, dark-mode defaults, or large marketing sections to product surfaces. +- Do not use pill, capsule, chip, or rounded badge shapes for labels, tags, statuses, transports, or counts. +- Do not add hover background fills, color fades, or CSS transitions/animations to rows, cards, or list items. - Do not replace compact text links with large icon-button toolbars unless the existing surface already uses that pattern. - Do not introduce a new design system that competes with Yotsuba, Yotsuba B, Futaba, or the existing theme variable model. - Do not make Bitsocial, crypto, or decentralization visuals dominate routine browsing. diff --git a/public/assets/icons/flags-1.png b/public/assets/icons/flags-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2f5c8bc081f9a2a45157913b6e81353eb0c0c260 GIT binary patch literal 81015 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 literal 0 HcmV?d00001 diff --git a/public/llms-full.txt b/public/llms-full.txt index 2ae6b701..98ee452f 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -220,6 +220,7 @@ Agents may use compiled context to navigate quickly, but must verify against sou | Situation | Required action | |---|---| +| New UI designed or styled, or any visual/component/theme work (`src/components`, `src/views`, CSS modules, `src/themes.css`) | Read `DESIGN.md` first and follow it; self-review the diff against its Do/Don't list before verifying. 5chan is a classic imageboard: square edges, 1px theme borders, flat surfaces, dense text, theme palette. No rounded corners, pill/badge shapes, hover background fills, CSS transitions, or soft shadows on product UI | | React UI logic changed (`src/components`, `src/views`, `src/hooks`, UI stores) | Follow React architecture rules below, review the diff with `vercel-react-best-practices` and `vercel:react-best-practices` when available, fix valid findings, then run `yarn doctor` | | `package.json` changed | Run `corepack yarn install` to keep `yarn.lock` in sync | | Dependencies or import graph changed | Run `yarn knip` as an advisory manifest/import audit | @@ -279,6 +280,14 @@ src/ - Avoid boolean flag soup for complex flows; model state clearly in Zustand. - Use React Router for navigation; no manual history manipulation. +### Design Rules + +- Read `DESIGN.md` before designing, styling, or adding any UI, and self-review the diff against its Do/Don't list before verification. `DESIGN.md` is the source of truth for 5chan's classic-imageboard look. +- Preserve the old-school imageboard aesthetic: square edges, 1px theme borders, flat surfaces, dense compact text, and the existing theme palette (CSS variables in `src/themes.css` or `currentColor`). +- Do not introduce modern UI patterns on product surfaces: no rounded corners/`border-radius`, no pill/capsule/badge shapes, no hover background-color changes or CSS transitions/animations on rows or cards, and no soft shadows, glass, gradients, or new accent/marketing colors. +- Functional per-entity colors (e.g. a small hashed swatch identifying a peer) are allowed, but render them flat and square as small indicators, not as decorative gradients. +- New decentralized/technical surfaces (P2P stats, addresses, tables) still follow the same language: compact, square, theme-aware, terse labels. + ### Code Organization Rules - Keep components focused; split large components. @@ -609,6 +618,8 @@ The palette is inherited from classic imageboard themes and should remain recogn **The No Modern Accent Rule.** Do not introduce a new brand accent, gradient palette, neon crypto palette, or marketing color system for core product UI. +**The Functional Color Exception.** Small, deterministic per-entity colors used as data — for example a hashed swatch that gives each connected peer a stable identity — are allowed. Render them as flat, square, small indicators (not gradients, glows, or large fills). This exception is for conveying data, not for decoration or branding. + ## 3. Typography **Display Font:** Tahoma, sans-serif. @@ -658,6 +669,8 @@ The palette is inherited from classic imageboard themes and should remain recogn - **Shadow Strategy:** none except the small legacy menu shadow. - **Border:** 1px theme borders. - **Internal Padding:** compact, commonly `2px`, `5px`, or `0.5em`. +- **Hover:** do not change a row's or card's background on hover, and do not add CSS transitions or animations to them. Reserve hover feedback for text-link color changes. +- **Inline labels:** plain text, bold for emphasis. Do not wrap labels (tags, statuses, transports, counts) in pill, capsule, chip, or rounded badge shapes; a label is text, not a button. ### Inputs / Fields @@ -698,6 +711,8 @@ The palette is inherited from classic imageboard themes and should remain recogn - Do not modernize core imageboard UI for taste. - Do not add rounded cards, gradient hero areas, glass panels, soft shadows, dark-mode defaults, or large marketing sections to product surfaces. +- Do not use pill, capsule, chip, or rounded badge shapes for labels, tags, statuses, transports, or counts. +- Do not add hover background fills, color fades, or CSS transitions/animations to rows, cards, or list items. - Do not replace compact text links with large icon-button toolbars unless the existing surface already uses that pattern. - Do not introduce a new design system that competes with Yotsuba, Yotsuba B, Futaba, or the existing theme variable model. - Do not make Bitsocial, crypto, or decentralization visuals dominate routine browsing. diff --git a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx index 21a36566..e83f2f1a 100644 --- a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx +++ b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx @@ -132,13 +132,14 @@ describe('P2PStatsSettings', () => { await act(async () => Promise.resolve()); const rows = getStatRows(); + const connectedPeers = container.querySelector('[data-testid="connected-peers"]'); expect(container.textContent).toContain('Leeching'); expect(container.textContent).not.toContain('browser Helia'); expect(container.textContent).not.toContain('seed mode'); expect(container.textContent).not.toContain('status'); - expect(rows.get('Connected peers')).toContain('2 peers, 1 connection'); - expect(rows.get('Connected peers')).toContain('WebSocket'); - expect(rows.get('Connected peers')).toContain('/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1'); + expect(connectedPeers?.textContent).toContain('2 peers, 1 connection'); + expect(connectedPeers?.textContent).toContain('WebSocket'); + expect(connectedPeers?.textContent).toContain('/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1'); expect(rows.has('connections')).toBe(false); expect(rows.has('Listen addresses')).toBe(false); expect(rows.has('p2p_stats_updated')).toBe(true); diff --git a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css index 21847e5b..01fc8dca 100644 --- a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css +++ b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css @@ -38,44 +38,139 @@ word-break: break-word; } -.connectedPeers summary { +/* Connected peers span the full panel width instead of the narrow value column. */ +.stats td.connectedPeersCell { + padding: 4px 4px 5px 0; +} + +.connectedPeersSummary { cursor: pointer; + padding: 1px 0; } .connectedPeerList { display: grid; - gap: 6px; - margin-top: 5px; + gap: 5px; + margin-top: 6px; } .connectedPeer { - border-top: 1px solid var(--border-color, #d9d9d9); - padding-top: 5px; + display: grid; + gap: 3px; + padding: 4px 6px; + border: 1px solid rgba(127, 127, 127, 0.28); + border: 1px solid color-mix(in srgb, currentColor 22%, transparent); } .connectedPeerMeta { display: flex; flex-wrap: wrap; + align-items: center; gap: 5px; - font-size: 0.9em; + font-size: 0.85em; } .connectionTransport { font-weight: 700; } -.peerId, -.connectionAddress { - word-break: break-all; +.connectionDirection, +.connectionStatus { + display: inline-flex; + align-items: center; + gap: 4px; + text-transform: capitalize; + opacity: 0.85; +} + +.connectionDirection::before { + font-weight: 700; +} + +.connectionDirection[data-direction*='out' i]::before { + content: '↑'; +} + +.connectionDirection[data-direction*='in' i]::before { + content: '↓'; +} + +.connectionStatus::before { + content: ''; + width: 7px; + height: 7px; + background: #9aa0a6; +} + +.connectionStatus[data-status*='open' i]::before { + background: #3fb950; +} + +.peerId { + overflow: hidden; + font-family: monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.connectionAddressRow { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.peerFlag { + flex-shrink: 0; + width: 16px; + height: 11px; + background-image: url("/assets/icons/flags-1.png"); + background-repeat: no-repeat; + image-rendering: pixelated; } .connectionAddress { - display: block; - margin-top: 2px; + flex: 1; + min-width: 0; + overflow: hidden; + font-size: 0.85em; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.7; } .connectedPeerEmpty { + padding: 6px 8px; font-style: italic; + opacity: 0.7; +} + +.peerWorldMap { + margin-top: 6px; + border: 1px solid rgba(127, 127, 127, 0.28); + border: 1px solid color-mix(in srgb, currentColor 22%, transparent); +} + +.peerWorldMapSvg { + display: block; + width: 100%; + height: auto; +} + +.landDot { + fill: currentColor; + opacity: 0.18; +} + +.peerMarker { + fill: #3fb950; +} + +.peerWorldMapCaption { + padding: 1px 4px 2px; + font-size: 0.8em; + font-style: italic; + opacity: 0.6; } .statsMeta { diff --git a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx index c854bb86..96e558fe 100644 --- a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx +++ b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx @@ -1,7 +1,10 @@ import { memo, useEffect, useReducer } from 'react'; import { useAccount, usePkcRpcSettings } from '@bitsocial/bitsocial-react-hooks'; import { useTranslation } from 'react-i18next'; +import { getCountryFlagPosition, getCountryLabel } from '../../../lib/country-flags'; +import { getApproximateCountryCode } from '../../../lib/peer-geo'; import { getP2PRuntimeMode, type P2PRuntimeMode } from '../../../lib/p2p-runtime'; +import PeerWorldMap from './peer-world-map'; import styles from './p2p-stats-settings.module.css'; type AccountShape = Record; @@ -512,9 +515,9 @@ const getBrowserLibp2pStats = async (account?: AccountShape): Promise return [ { name: 'Mode', value: getBrowserMode(client) }, { name: 'Peer ID', value: libp2p?.peerId?.toString() ?? 'unknown' }, - getBrowserConnectedPeersRow(peers, connections), { name: 'Data received', value: transferStats.downloadedBytes === undefined ? 'unknown' : formatBytes(transferStats.downloadedBytes) }, { name: 'Data sent', value: transferStats.uploadedBytes === undefined ? 'unknown' : formatBytes(transferStats.uploadedBytes) }, + getBrowserConnectedPeersRow(peers, connections), ]; }; @@ -547,13 +550,13 @@ const getElectronKuboStats = async (rpcState?: string, signal?: AbortSignal): Pr { name: 'PKC RPC', value: rpcState ?? 'unknown' }, { name: 'Peer ID', value: identity.ID ?? 'unknown' }, { name: 'Agent', value: identity.AgentVersion ?? version.Version ?? 'unknown' }, - getElectronConnectedPeersRow(peers), { name: 'Bandwidth in', value: `${formatBytes(bandwidth.TotalIn)} total, ${formatRate(bandwidth.RateIn)}` }, { name: 'Bandwidth out', value: `${formatBytes(bandwidth.TotalOut)} total, ${formatRate(bandwidth.RateOut)}` }, { name: 'Repo size', value: formatBytes(repo.RepoSize) }, { name: 'Repo objects', value: String(repo.NumObjects ?? 'unknown') }, { name: 'Bitswap peers', value: formatCount(Array.isArray(bitswap.Peers) ? bitswap.Peers.length : 0, 'peer') }, { name: 'Bitswap wantlist', value: formatCount(Array.isArray(bitswap.Wantlist) ? bitswap.Wantlist.length : 0, 'item') }, + getElectronConnectedPeersRow(peers), ]; }; @@ -563,23 +566,51 @@ const getP2PStats = async (mode: P2PRuntimeMode, account?: AccountShape, rpcStat }; const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => ( -
- - {formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')} +
+ + {row.name}: {formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')} +
{row.entries.length ? ( - row.entries.map((entry) => ( -
-
- {entry.transport} - {entry.direction && {entry.direction}} - {entry.status && {entry.status}} + row.entries.map((entry) => { + const countryCode = getApproximateCountryCode(entry.address); + const flagPosition = getCountryFlagPosition(countryCode); + return ( +
+
+ {entry.transport} + {entry.direction && ( + + {entry.direction} + + )} + {entry.status && ( + + {entry.status} + + )} +
+
+ {entry.peerId} +
+
+ {flagPosition && ( + + )} + + {entry.address} + +
-
{entry.peerId}
- {entry.address} -
- )) + ); + }) ) : (
No active peer addresses
)} @@ -587,8 +618,6 @@ const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => (
); -const StatValue = ({ row }: { row: StatRow }) => (row.type === 'connectedPeers' ? : row.value); - const P2PStatsSettings = () => { const { t } = useTranslation(); const account = useAccount() as AccountShape | undefined; @@ -635,14 +664,20 @@ const P2PStatsSettings = () => { <> - {statsState.rows.map((row) => ( - - - - - ))} + {statsState.rows.map((row) => + row.type === 'connectedPeers' ? ( + + + + ) : ( + + + + + ), + )} {updatedAtLabel && ( diff --git a/src/components/settings-modal/p2p-stats-settings/peer-world-map.tsx b/src/components/settings-modal/p2p-stats-settings/peer-world-map.tsx new file mode 100644 index 00000000..5c0769cc --- /dev/null +++ b/src/components/settings-modal/p2p-stats-settings/peer-world-map.tsx @@ -0,0 +1,262 @@ +import { getApproximateLatLon } from '../../../lib/peer-geo'; +import styles from './p2p-stats-settings.module.css'; + +type MapPeer = { + address: string; + id: string; + peerId: string; +}; + +// Rough continent outlines as [lon, lat] vertices. Deliberately coarse: they are +// only rasterized into a faint dotted backdrop, so approximate shapes are fine. +const CONTINENTS: [number, number][][] = [ + // North America + [ + [-168, 65], + [-156, 71], + [-128, 70], + [-110, 68], + [-95, 69], + [-81, 73], + [-78, 67], + [-64, 60], + [-56, 52], + [-66, 49], + [-67, 44], + [-70, 41], + [-75, 35], + [-81, 25], + [-90, 29], + [-97, 26], + [-97, 21], + [-105, 20], + [-112, 24], + [-117, 32], + [-122, 37], + [-125, 43], + [-130, 51], + [-141, 59], + [-155, 58], + ], + // Greenland + [ + [-45, 60], + [-30, 60], + [-18, 66], + [-20, 73], + [-25, 80], + [-40, 83], + [-58, 82], + [-55, 76], + [-50, 68], + ], + // South America + [ + [-81, 8], + [-72, 11], + [-62, 10], + [-50, 0], + [-35, -6], + [-39, -14], + [-48, -25], + [-58, -34], + [-65, -41], + [-69, -50], + [-74, -53], + [-72, -45], + [-73, -37], + [-71, -28], + [-71, -18], + [-78, -8], + [-81, -4], + [-80, 2], + ], + // Europe + [ + [-10, 36], + [-9, 44], + [-2, 43], + [-2, 49], + [-5, 54], + [-6, 58], + [5, 61], + [8, 58], + [12, 59], + [16, 66], + [24, 71], + [30, 70], + [40, 67], + [46, 60], + [42, 52], + [36, 46], + [28, 45], + [24, 40], + [18, 40], + [12, 38], + [2, 42], + [-4, 37], + ], + // Africa + [ + [-17, 15], + [-16, 21], + [-10, 27], + [-6, 32], + [1, 37], + [10, 37], + [11, 33], + [20, 32], + [25, 32], + [32, 31], + [35, 24], + [37, 18], + [43, 12], + [51, 12], + [44, 5], + [48, -3], + [40, -15], + [35, -21], + [31, -26], + [26, -34], + [20, -35], + [16, -29], + [12, -17], + [9, -1], + [8, 4], + [-4, 5], + [-9, 5], + [-13, 9], + ], + // Asia + [ + [42, 48], + [45, 55], + [55, 62], + [68, 68], + [80, 73], + [100, 77], + [115, 74], + [140, 73], + [160, 70], + [170, 66], + [178, 65], + [170, 60], + [158, 53], + [150, 46], + [143, 44], + [140, 50], + [135, 44], + [131, 43], + [127, 37], + [122, 40], + [120, 34], + [122, 30], + [115, 22], + [108, 21], + [106, 11], + [100, 14], + [98, 8], + [100, 2], + [95, 7], + [90, 22], + [88, 22], + [80, 13], + [77, 8], + [73, 17], + [68, 24], + [61, 25], + [57, 37], + [52, 41], + [47, 43], + ], + // Australia + [ + [113, -22], + [121, -19], + [129, -15], + [136, -12], + [142, -11], + [145, -15], + [147, -19], + [153, -25], + [153, -31], + [150, -37], + [143, -39], + [138, -35], + [131, -32], + [123, -34], + [115, -34], + [114, -29], + ], + // Indonesia + [ + [96, 5], + [120, 7], + [128, 8], + [131, 1], + [122, -4], + [114, -8], + [103, -8], + [98, -1], + ], + // Japan + [ + [130, 31], + [136, 35], + [141, 41], + [142, 45], + [140, 38], + [135, 34], + [131, 31], + ], +]; + +const pointInPolygon = (lon: number, lat: number, polygon: [number, number][]) => { + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const [xi, yi] = polygon[i]; + const [xj, yj] = polygon[j]; + if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) inside = !inside; + } + return inside; +}; + +// Equirectangular projection into the SVG viewBox below: x = lon + 180, y = 90 - lat. +const LAND_STEP = 3.5; +const LAND_DOTS: { x: number; y: number }[] = (() => { + const dots: { x: number; y: number }[] = []; + for (let lat = 80; lat >= -56; lat -= LAND_STEP) { + for (let lon = -178; lon <= 180; lon += LAND_STEP) { + if (CONTINENTS.some((continent) => pointInPolygon(lon, lat, continent))) dots.push({ x: lon + 180, y: 90 - lat }); + } + } + return dots; +})(); + +const PeerWorldMap = ({ peers }: { peers: MapPeer[] }) => { + const plotted: { id: string; peerId: string; x: number; y: number }[] = []; + for (const peer of peers) { + const location = getApproximateLatLon(peer.address); + if (location) plotted.push({ id: peer.id, peerId: peer.peerId, x: location.lon + 180, y: 90 - location.lat }); + } + + if (!plotted.length) return null; + + return ( +
+ + {LAND_DOTS.map((dot) => ( + + ))} + {plotted.map((peer) => ( + + {peer.peerId} + + ))} + +
approximate locations
+
+ ); +}; + +export default PeerWorldMap; diff --git a/src/lib/__tests__/country-flags.test.ts b/src/lib/__tests__/country-flags.test.ts new file mode 100644 index 00000000..d98feb47 --- /dev/null +++ b/src/lib/__tests__/country-flags.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { COUNTRY_FLAG_HEIGHT, COUNTRY_FLAG_WIDTH, getCountryFlagPosition, getCountryLabel, normalizeCountryCode } from '../country-flags'; + +describe('country-flags', () => { + it('places the first sprite cell at the origin', () => { + expect(getCountryFlagPosition('ad')).toEqual({ x: 0, y: 0 }); + }); + + it('maps a code to a grid-aligned sprite position', () => { + const position = getCountryFlagPosition('us'); + expect(position).toBeDefined(); + expect(position!.x % COUNTRY_FLAG_WIDTH).toBe(0); + expect(position!.y % COUNTRY_FLAG_HEIGHT).toBe(0); + expect(position!.x).toBeGreaterThanOrEqual(0); + expect(position!.x).toBeLessThan(16 * COUNTRY_FLAG_WIDTH); + }); + + // Exact positions from the original 4chan flags.css (the source of flags-1.png). + it('matches the known sprite positions from flags.css', () => { + expect(getCountryFlagPosition('us')).toEqual({ x: 240, y: 154 }); + expect(getCountryFlagPosition('ru')).toEqual({ x: 64, y: 132 }); + expect(getCountryFlagPosition('pl')).toEqual({ x: 128, y: 121 }); + expect(getCountryFlagPosition('jp')).toEqual({ x: 112, y: 77 }); + expect(getCountryFlagPosition('de')).toEqual({ x: 176, y: 33 }); + expect(getCountryFlagPosition('gb')).toEqual({ x: 32, y: 55 }); + expect(getCountryFlagPosition('br')).toEqual({ x: 240, y: 11 }); + expect(getCountryFlagPosition('za')).toEqual({ x: 0, y: 176 }); + }); + + it('normalizes uk to gb and rejects unknown codes', () => { + expect(normalizeCountryCode('UK')).toBe('gb'); + expect(normalizeCountryCode('zz')).toBeUndefined(); + expect(getCountryFlagPosition('zz')).toBeUndefined(); + }); + + it('returns a human-readable label for ISO codes', () => { + expect(getCountryLabel('de')).toBeTruthy(); + expect(getCountryLabel('zz')).toBeUndefined(); + }); +}); diff --git a/src/lib/__tests__/peer-geo.test.ts b/src/lib/__tests__/peer-geo.test.ts new file mode 100644 index 00000000..9fb53a06 --- /dev/null +++ b/src/lib/__tests__/peer-geo.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { extractIpv4FromAddress, getApproximateCountryCode, getApproximateLatLon, isPrivateOrReservedIpv4 } from '../peer-geo'; + +const addr = (ip: string) => `/ip4/${ip}/tcp/4001/ws/p2p/12D3KooWExample`; + +describe('extractIpv4FromAddress', () => { + it('extracts the IPv4 from a multiaddr', () => { + expect(extractIpv4FromAddress('/ip4/147.75.84.175/tcp/4001/ws')).toBe('147.75.84.175'); + }); + + it('returns null for IPv6 and plain DNS multiaddrs', () => { + expect(extractIpv4FromAddress('/ip6/2606:4700::1111/tcp/4001')).toBeNull(); + expect(extractIpv4FromAddress('/dns4/relay.example.org/tcp/443/wss')).toBeNull(); + }); + + it('extracts an IPv4 embedded with dashes in a DNS hostname', () => { + expect(extractIpv4FromAddress('/dns4/91-234-56-78.host.example/tcp/443/wss')).toBe('91.234.56.78'); + expect(extractIpv4FromAddress('/dns4/ip-203-0-113-9.provider.net/tcp/443/wss')).toBe('203.0.113.9'); + }); +}); + +describe('isPrivateOrReservedIpv4', () => { + it('flags private and reserved ranges', () => { + for (const ip of ['10.0.0.1', '172.16.5.4', '192.168.1.10', '127.0.0.1', '169.254.1.1', '100.64.0.1', '0.0.0.0', '239.255.0.1']) { + expect(isPrivateOrReservedIpv4(ip)).toBe(true); + } + }); + + it('treats public addresses as routable', () => { + for (const ip of ['8.8.8.8', '147.75.84.175', '1.1.1.1', '80.80.80.80']) { + expect(isPrivateOrReservedIpv4(ip)).toBe(false); + } + }); +}); + +describe('getApproximateLatLon', () => { + it('returns null when the peer cannot be placed offline', () => { + expect(getApproximateLatLon('/ip4/192.168.1.5/tcp/4001')).toBeNull(); + expect(getApproximateLatLon('/dns4/relay.example.org/tcp/443/wss')).toBeNull(); + expect(getApproximateLatLon('/ip6/2606:4700::1111/tcp/4001')).toBeNull(); + }); + + it('is deterministic for the same address', () => { + expect(getApproximateLatLon(addr('8.8.8.8'))).toEqual(getApproximateLatLon(addr('8.8.8.8'))); + }); + + it('places addresses in the expected continental region', () => { + const na = getApproximateLatLon(addr('8.8.8.8')); + expect(na?.lon).toBeLessThan(-80); // North America + expect(na?.lat).toBeGreaterThan(30); + + const eu = getApproximateLatLon(addr('80.80.80.80')); + expect(eu?.lon).toBeGreaterThan(0); + expect(eu?.lon).toBeLessThan(30); + expect(eu?.lat).toBeGreaterThan(40); + + const as = getApproximateLatLon(addr('1.1.1.1')); + expect(as?.lon).toBeGreaterThan(90); + + const af = getApproximateLatLon(addr('41.0.0.1')); + expect(af?.lon).toBeGreaterThan(8); + expect(af?.lon).toBeLessThan(34); + expect(af?.lat).toBeLessThan(12); + + const sa = getApproximateLatLon(addr('200.0.0.1')); + expect(sa?.lon).toBeLessThan(-45); + expect(sa?.lat).toBeLessThan(-5); + }); + + it('stays within valid coordinate bounds', () => { + const loc = getApproximateLatLon(addr('203.0.113.7')); + expect(loc).not.toBeNull(); + expect(loc!.lat).toBeGreaterThanOrEqual(-85); + expect(loc!.lat).toBeLessThanOrEqual(85); + expect(loc!.lon).toBeGreaterThanOrEqual(-180); + expect(loc!.lon).toBeLessThanOrEqual(180); + }); +}); + +describe('getApproximateCountryCode', () => { + it('returns undefined when the peer cannot be placed offline', () => { + expect(getApproximateCountryCode('/ip4/10.0.0.1/tcp/4001')).toBeUndefined(); + expect(getApproximateCountryCode('/dns4/relay.example.org/tcp/443/wss')).toBeUndefined(); + expect(getApproximateCountryCode('/ip6/2606:4700::1111/tcp/4001')).toBeUndefined(); + }); + + it('is deterministic and returns a known 2-letter code for public peers', () => { + const code = getApproximateCountryCode(addr('8.8.8.8')); + expect(code).toBe(getApproximateCountryCode(addr('8.8.8.8'))); + expect(code).toMatch(/^[a-z]{2}$/); + }); + + it('derives a code from an IPv4 embedded in a DNS hostname', () => { + expect(getApproximateCountryCode('/dns4/91-234-56-78.host.example/tcp/443/wss')).toMatch(/^[a-z]{2}$/); + }); +}); diff --git a/src/lib/country-flags.ts b/src/lib/country-flags.ts new file mode 100644 index 00000000..a2c2cd9d --- /dev/null +++ b/src/lib/country-flags.ts @@ -0,0 +1,300 @@ +// Country flag sprite mapping for /assets/icons/flags-1.png. +// +// The sprite packs 16x11px flags in a 16-column, row-major grid; a flag's sprite +// position is its index in COUNTRY_FLAG_CODES, so this order must match the PNG +// exactly. This order is taken verbatim from the original 4chan flags.css sprite +// (flags.8.png), which is the source of this PNG. + +export const COUNTRY_FLAG_WIDTH = 16; +export const COUNTRY_FLAG_HEIGHT = 11; +const COUNTRY_FLAG_COLUMNS = 16; + +const COUNTRY_FLAG_CODES = [ + 'ad', + 'ae', + 'af', + 'ag', + 'ai', + 'al', + 'am', + 'an', + 'ao', + 'aq', + 'ar', + 'as', + 'at', + 'au', + 'aw', + 'ax', + 'az', + 'ba', + 'bb', + 'bd', + 'be', + 'bf', + 'bg', + 'bh', + 'bi', + 'bj', + 'bl', + 'bm', + 'bn', + 'bo', + 'bq', + 'br', + 'bs', + 'bt', + 'bv', + 'bw', + 'by', + 'bz', + 'ca', + 'catalonia', + 'cc', + 'cd', + 'cf', + 'cg', + 'ch', + 'ci', + 'ck', + 'cl', + 'cm', + 'cn', + 'co', + 'cr', + 'cs', + 'cu', + 'cv', + 'cw', + 'cx', + 'cy', + 'cz', + 'de', + 'dj', + 'dk', + 'dm', + 'do', + 'dz', + 'ec', + 'ee', + 'eg', + 'eh', + 'xe', + 'er', + 'es', + 'et', + 'eu', + 'fam', + 'fi', + 'fj', + 'fk', + 'fm', + 'fo', + 'fr', + 'ga', + 'gb', + 'gd', + 'ge', + 'gf', + 'gg', + 'gh', + 'gi', + 'gl', + 'gm', + 'gn', + 'gp', + 'gq', + 'gr', + 'gs', + 'gt', + 'gu', + 'gw', + 'gy', + 'hk', + 'hm', + 'hn', + 'hr', + 'ht', + 'hu', + 'id', + 'ie', + 'il', + 'im', + 'in', + 'io', + 'iq', + 'ir', + 'is', + 'it', + 'je', + 'jm', + 'jo', + 'jp', + 'ke', + 'kg', + 'kh', + 'ki', + 'km', + 'kn', + 'kp', + 'kr', + 'kw', + 'ky', + 'kz', + 'la', + 'lb', + 'lc', + 'li', + 'lk', + 'lr', + 'ls', + 'lt', + 'lu', + 'lv', + 'ly', + 'ma', + 'mc', + 'md', + 'me', + 'mf', + 'mg', + 'mh', + 'mk', + 'ml', + 'mm', + 'mn', + 'mo', + 'mp', + 'mq', + 'mr', + 'ms', + 'mt', + 'mu', + 'mv', + 'mw', + 'mx', + 'my', + 'mz', + 'na', + 'nc', + 'ne', + 'nf', + 'ng', + 'ni', + 'nl', + 'no', + 'np', + 'nr', + 'nu', + 'nz', + 'om', + 'pa', + 'pe', + 'pf', + 'pg', + 'ph', + 'pk', + 'pl', + 'pm', + 'pn', + 'pr', + 'ps', + 'pt', + 'pw', + 'py', + 'qa', + 're', + 'ro', + 'rs', + 'ru', + 'rw', + 'sa', + 'sb', + 'sc', + 'xs', + 'sd', + 'se', + 'sg', + 'sh', + 'si', + 'sj', + 'sk', + 'sl', + 'sm', + 'sn', + 'so', + 'sr', + 'ss', + 'st', + 'sv', + 'sx', + 'sy', + 'sz', + 'tc', + 'td', + 'tf', + 'tg', + 'th', + 'tj', + 'tk', + 'tl', + 'tm', + 'tn', + 'to', + 'tr', + 'tt', + 'tv', + 'tw', + 'tz', + 'ua', + 'ug', + 'um', + 'us', + 'uy', + 'uz', + 'va', + 'vc', + 've', + 'vg', + 'vi', + 'vn', + 'vu', + 'xw', + 'wf', + 'ws', + 'xk', + 'xx', + 'ye', + 'yt', + 'za', + 'zm', + 'zw', +] as const; + +const COUNTRY_FLAG_INDEX_BY_CODE: ReadonlyMap = new Map(COUNTRY_FLAG_CODES.map((code, index) => [code, index])); + +const countryDisplayNames = typeof Intl.DisplayNames === 'function' ? new Intl.DisplayNames(undefined, { type: 'region' }) : undefined; + +export const normalizeCountryCode = (value: string | undefined) => { + const code = value?.trim().toLowerCase(); + if (!code) return undefined; + if (code === 'uk') return 'gb'; + return COUNTRY_FLAG_INDEX_BY_CODE.has(code) ? code : undefined; +}; + +export const getCountryFlagPosition = (value: string | undefined): { x: number; y: number } | undefined => { + const code = normalizeCountryCode(value); + const index = code === undefined ? undefined : COUNTRY_FLAG_INDEX_BY_CODE.get(code); + if (index === undefined) return undefined; + return { + x: (index % COUNTRY_FLAG_COLUMNS) * COUNTRY_FLAG_WIDTH, + y: Math.floor(index / COUNTRY_FLAG_COLUMNS) * COUNTRY_FLAG_HEIGHT, + }; +}; + +export const getCountryLabel = (value: string | undefined) => { + const code = normalizeCountryCode(value); + if (!code) return undefined; + const regionName = code.length === 2 ? countryDisplayNames?.of(code.toUpperCase()) : undefined; + return regionName ?? code.toUpperCase(); +}; diff --git a/src/lib/peer-geo.ts b/src/lib/peer-geo.ts new file mode 100644 index 00000000..29a903eb --- /dev/null +++ b/src/lib/peer-geo.ts @@ -0,0 +1,167 @@ +// Offline, approximate peer geolocation for the P2P stats world map. +// +// This intentionally avoids any external geolocation API: 5chan is serverless and +// privacy-focused, so we must not leak the set of peers a user is connected to. +// Instead we map an IPv4 address to its Regional Internet Registry (RIR) region at +// continent resolution, using the coarse IANA /8 allocation table below. Positions +// are therefore approximate ("roughly which continent"), not precise coordinates. + +export type LatLon = { lat: number; lon: number }; + +type Region = 'AF' | 'AS' | 'EU' | 'NA' | 'SA'; + +const REGION_CENTROIDS: Record = { + AF: { lat: 4, lon: 21 }, + AS: { lat: 30, lon: 105 }, + EU: { lat: 50, lon: 15 }, + NA: { lat: 39, lon: -97 }, + SA: { lat: -15, lon: -58 }, +}; + +// Approximate first-octet (/8) -> RIR region, applied in order (later wins). +// Coarse and not authoritative; uncovered octets fall back to NA (ARIN-heavy +// legacy space). Good enough for a continent-level dot on a map. +const REGION_RANGES: [number, number, Region][] = [ + // APNIC (Asia / Pacific) + [1, 1, 'AS'], + [14, 14, 'AS'], + [27, 27, 'AS'], + [36, 36, 'AS'], + [39, 39, 'AS'], + [42, 43, 'AS'], + [49, 49, 'AS'], + [58, 61, 'AS'], + [101, 103, 'AS'], + [106, 106, 'AS'], + [110, 126, 'AS'], + [133, 133, 'AS'], + [150, 153, 'AS'], + [163, 163, 'AS'], + [171, 171, 'AS'], + [175, 175, 'AS'], + [180, 183, 'AS'], + [202, 203, 'AS'], + [210, 211, 'AS'], + [218, 223, 'AS'], + // RIPE NCC (Europe / Middle East) + [2, 2, 'EU'], + [5, 5, 'EU'], + [25, 25, 'EU'], + [31, 31, 'EU'], + [37, 37, 'EU'], + [46, 46, 'EU'], + [51, 51, 'EU'], + [53, 53, 'EU'], + [57, 57, 'EU'], + [62, 62, 'EU'], + [77, 95, 'EU'], + [109, 109, 'EU'], + [141, 141, 'EU'], + [145, 145, 'EU'], + [151, 151, 'EU'], + [176, 176, 'EU'], + [178, 178, 'EU'], + [185, 185, 'EU'], + [188, 188, 'EU'], + [193, 195, 'EU'], + [212, 213, 'EU'], + [217, 217, 'EU'], + // AFRINIC (Africa) + [41, 41, 'AF'], + [102, 102, 'AF'], + [105, 105, 'AF'], + [154, 156, 'AF'], + [196, 197, 'AF'], + // LACNIC (Latin America / Caribbean) + [177, 177, 'SA'], + [179, 179, 'SA'], + [181, 181, 'SA'], + [186, 187, 'SA'], + [189, 191, 'SA'], + [200, 201, 'SA'], +]; + +const REGION_BY_OCTET: Region[] = (() => { + const table: Region[] = Array.from({ length: 256 }, () => 'NA' as Region); + for (const [start, end, region] of REGION_RANGES) { + for (let octet = start; octet <= end; octet++) table[octet] = region; + } + return table; +})(); + +export const extractIpv4FromAddress = (address: string): string | null => { + const direct = /\/ip4\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/.exec(address); + if (direct) return direct[1]; + // Some peers are reached via a DNS name that embeds the IPv4 with dashes, e.g. + // /dns4/91-234-56-78.host.example -> 91.234.56.78 + const dashed = /\/dns[46]?\/[^/]*?(? { + const parts = ip.split('.').map(Number); + if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; + return parts; +}; + +export const isPrivateOrReservedIpv4 = (ip: string): boolean => { + const parts = parseOctets(ip); + if (!parts) return true; + const [a, b] = parts; + if (a === 0 || a === 10 || a === 127) return true; // this-network, private, loopback + if (a === 169 && b === 254) return true; // link-local + if (a === 172 && b >= 16 && b <= 31) return true; // private + if (a === 192 && b === 168) return true; // private + if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT + if (a >= 224) return true; // multicast / reserved + return false; +}; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +const hashOctets = (parts: number[]) => { + let hash = 0; + for (const part of parts) hash = (Math.imul(hash, 131) + part) | 0; + return Math.abs(hash); +}; + +// Maps a peer's multiaddr to an approximate lat/lon, or null when it cannot be +// placed offline (private/reserved IPv4, IPv6, or a DNS address). +export const getApproximateLatLon = (address: string): LatLon | null => { + const ip = extractIpv4FromAddress(address); + if (!ip || isPrivateOrReservedIpv4(ip)) return null; + const parts = parseOctets(ip); + if (!parts) return null; + + const centroid = REGION_CENTROIDS[REGION_BY_OCTET[parts[0]]]; + const hash = hashOctets(parts); + // Deterministic intra-region spread so peers don't stack on one point. The + // region is the meaningful signal; the offset is cosmetic. + const lonOffset = ((hash % 1000) / 1000 - 0.5) * 24; // +/- 12 deg + const latOffset = ((Math.floor(hash / 1000) % 1000) / 1000 - 0.5) * 14; // +/- 7 deg + return { lat: clamp(centroid.lat + latOffset, -85, 85), lon: clamp(centroid.lon + lonOffset, -180, 180) }; +}; + +// Representative countries per region. The RIR table only resolves to a continent, +// so the flag is a deterministic, approximate pick from the region's common +// countries — consistent with the map's "approximate locations" framing, not real +// per-peer country geolocation. +const REGION_COUNTRIES: Record = { + AF: ['za', 'ng', 'eg', 'ke', 'ma', 'dz', 'tn'], + AS: ['jp', 'cn', 'sg', 'in', 'kr', 'hk', 'id', 'vn', 'th', 'tw'], + EU: ['de', 'nl', 'fr', 'gb', 'ru', 'se', 'fi', 'pl', 'it', 'es', 'ua', 'ro'], + NA: ['us', 'us', 'ca', 'us', 'mx'], + SA: ['br', 'ar', 'cl', 'co', 'pe'], +}; + +// Approximate 2-letter country code for a peer, or undefined when it cannot be +// placed offline. See REGION_COUNTRIES: this is region-level, not precise. +export const getApproximateCountryCode = (address: string): string | undefined => { + const ip = extractIpv4FromAddress(address); + if (!ip || isPrivateOrReservedIpv4(ip)) return undefined; + const parts = parseOctets(ip); + if (!parts) return undefined; + const pool = REGION_COUNTRIES[REGION_BY_OCTET[parts[0]]]; + return pool[hashOctets(parts) % pool.length]; +};
{row.name} - -
+ +
{row.name}{row.value}
{t('p2p_stats_updated')}