Merge branch 'codex/feature/prettier-peer-list'

This commit is contained in:
Tommaso Casaburi
2026-05-23 18:34:05 +07:00
12 changed files with 1065 additions and 39 deletions
@@ -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);
@@ -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 {
@@ -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<string, any>;
@@ -512,9 +515,9 @@ const getBrowserLibp2pStats = async (account?: AccountShape): Promise<StatRow[]>
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 }) => (
<details className={styles.connectedPeers} data-testid='connected-peers'>
<summary>
{formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')}
<details data-testid='connected-peers' open>
<summary className={styles.connectedPeersSummary}>
{row.name}: {formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')}
</summary>
<PeerWorldMap peers={row.entries} />
<div className={styles.connectedPeerList}>
{row.entries.length ? (
row.entries.map((entry) => (
<div className={styles.connectedPeer} key={entry.id}>
<div className={styles.connectedPeerMeta}>
<span className={styles.connectionTransport}>{entry.transport}</span>
{entry.direction && <span>{entry.direction}</span>}
{entry.status && <span>{entry.status}</span>}
row.entries.map((entry) => {
const countryCode = getApproximateCountryCode(entry.address);
const flagPosition = getCountryFlagPosition(countryCode);
return (
<div className={styles.connectedPeer} key={entry.id}>
<div className={styles.connectedPeerMeta}>
<span className={styles.connectionTransport}>{entry.transport}</span>
{entry.direction && (
<span className={styles.connectionDirection} data-direction={entry.direction}>
{entry.direction}
</span>
)}
{entry.status && (
<span className={styles.connectionStatus} data-status={entry.status}>
{entry.status}
</span>
)}
</div>
<div className={styles.peerId} title={entry.peerId}>
{entry.peerId}
</div>
<div className={styles.connectionAddressRow}>
{flagPosition && (
<span
aria-label={getCountryLabel(countryCode)}
className={styles.peerFlag}
role='img'
style={{ backgroundPosition: `-${flagPosition.x}px -${flagPosition.y}px` }}
title={getCountryLabel(countryCode)}
/>
)}
<code className={styles.connectionAddress} title={entry.address}>
{entry.address}
</code>
</div>
</div>
<div className={styles.peerId}>{entry.peerId}</div>
<code className={styles.connectionAddress}>{entry.address}</code>
</div>
))
);
})
) : (
<div className={styles.connectedPeerEmpty}>No active peer addresses</div>
)}
@@ -587,8 +618,6 @@ const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => (
</details>
);
const StatValue = ({ row }: { row: StatRow }) => (row.type === 'connectedPeers' ? <ConnectedPeersValue row={row} /> : row.value);
const P2PStatsSettings = () => {
const { t } = useTranslation();
const account = useAccount() as AccountShape | undefined;
@@ -635,14 +664,20 @@ const P2PStatsSettings = () => {
<>
<table className={styles.stats}>
<tbody>
{statsState.rows.map((row) => (
<tr key={row.name}>
<td className={styles.statName}>{row.name}</td>
<td className={styles.statValue}>
<StatValue row={row} />
</td>
</tr>
))}
{statsState.rows.map((row) =>
row.type === 'connectedPeers' ? (
<tr key={row.name}>
<td className={styles.connectedPeersCell} colSpan={2}>
<ConnectedPeersValue row={row} />
</td>
</tr>
) : (
<tr key={row.name}>
<td className={styles.statName}>{row.name}</td>
<td className={styles.statValue}>{row.value}</td>
</tr>
),
)}
{updatedAtLabel && (
<tr>
<td className={styles.statName}>{t('p2p_stats_updated')}</td>
@@ -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 (
<div className={styles.peerWorldMap}>
<svg className={styles.peerWorldMapSvg} viewBox='0 8 360 140' shapeRendering='crispEdges' role='img' aria-label='Approximate peer locations'>
{LAND_DOTS.map((dot) => (
<rect className={styles.landDot} height={1.6} key={`${dot.x}-${dot.y}`} width={1.6} x={dot.x - 0.8} y={dot.y - 0.8} />
))}
{plotted.map((peer) => (
<rect className={styles.peerMarker} height={4.5} key={peer.id} width={4.5} x={peer.x - 2.25} y={peer.y - 2.25}>
<title>{peer.peerId}</title>
</rect>
))}
</svg>
<div className={styles.peerWorldMapCaption}>approximate locations</div>
</div>
);
};
export default PeerWorldMap;