Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copilot Instructions for LineaPeersWatchersMap

## Project Architecture

- **Frontend (`ui/`)**: Vue 3 (Composition API, `<script setup>`) with Vite. Main features are interactive map (Leaflet), node clustering (leaflet.markercluster), heatmap (leaflet.heat), and charts (Chart.js via vue-chartjs). Key files: `src/components/MapView.vue`, `src/components/ClientStatsCharts.vue`, `src/views/Dashboard.vue`, `src/composables/useMap.js`, `src/composables/useNodes.js`.
- **Backend (`backend/`)**: Node.js + Express. Handles API endpoints and real-time data via WebSocket (Primus) to `ethstats.linea.build`. Key files: `index.js` (entry), `liveNodesState.js` (WebSocket logic), `controllers/`, `routes/`, `data/` (static/enriched node data).

## Data Flow

- **Static Nodes**: Periodically enriched from network, stored in `backend/data/peers_enriched.json`, served via `/static-nodes` endpoint.
- **Live Nodes**: Real-time updates from WebSocket, managed in `liveNodesState.js`, served via `/live-nodes` endpoint.
- **Frontend**: Fetches both static and live node data from backend, displays on map and charts.

## Developer Workflows

- **Backend**: Start with `node index.js` (or `npm run dev` if available). Dependencies in `backend/package.json`.
- **Frontend**: Start with `npm run dev` in `ui/`. Vite serves at `http://localhost:5173`. Dependencies in `ui/package.json`.
- **Data Update**: Static peer enrichment logic in `enrich-peers.js`. Temporary/intermediate data in `datatmp/`.

## Conventions & Patterns

- **Vue**: Use `<script setup>` and Composition API. Shared logic in `src/composables/`. Components are single-file Vue components.
- **Backend**: API routes in `routes/`, controllers in `controllers/`. Data files in `data/` (static) and `datatmp/` (intermediate).
- **Map Logic**: All map rendering and interaction logic is in `MapView.vue` and `useMap.js`.
- **Node Data**: Node fetching, enrichment, and geolocation logic is in backend, with frontend consuming via REST endpoints.

## Integration Points

- **WebSocket**: Backend connects to `wss://ethstats.linea.build/primus` for live node data.
- **Leaflet**: Frontend uses Leaflet and plugins for map visualization.
- **Chart.js**: Used for node statistics charts.

## External Dependencies

- **Frontend**: Vue 3, Vite, Leaflet, leaflet.markercluster, leaflet.heat, vue-chartjs, Axios.
- **Backend**: Express, Primus, CORS.

## Examples

- To add a new map feature, update `MapView.vue` and/or `useMap.js`.
- To enrich static node data, modify `enrich-peers.js` and update `data/peers_enriched.json`.
- To expose new backend data, add a route in `routes/`, a controller in `controllers/`, and update `index.js`.

---

If any section is unclear or missing, please provide feedback for further refinement.
76 changes: 27 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,40 @@

## Project Overview

This project provides a dynamic, real-time visualization of Linea mainnet peer nodes distributed across the globe. It features an interactive map displaying node locations, client types, and network statistics. The frontend is built with Vue 3 and Vite, utilizing Leaflet for map rendering, while the backend is a Node.js application using Express.
This project provides a dynamic visualization of Linea mainnet peer nodes distributed across the globe. It features an interactive map displaying node locations, client types, and network statistics. The app now runs frontend-only (Vue 3 + Vite); data is loaded from enriched JSON snapshots placed in `ui/public`.

## Key Features

* **Interactive Map:** Displays Linea peer nodes on a world map.
* **Node Information:** Provides details on individual nodes, including client type and location (derived from IP geolocation and AWS region heuristics).
* **Data Layers:**
* **Marker Clusters:** Groups nearby nodes for better visibility at wider zoom levels.
* **Heatmap:** Visualizes areas with high node density.
* **Network Statistics:**
* Total count of static peers.
* Distribution of nodes by client type (e.g., Geth, Besu, Erigon, Nethermind).
* Distribution of nodes by country.
* **Real-time Updates:** Live node data is sourced from `ethstats.linea.build` via WebSocket.
* **Static Peer Data:** Enriched static peer list is updated periodically.
* **Dark/Light Mode:** User-selectable theme for the dashboard.
* **Interactive Map:** Displays Linea peer nodes on a world map (Leaflet).
* **Node Information:** Client type and geo (from IP enrichment).
* **Data Layers:** Marker clusters + heatmap.
* **Network Statistics:** Totals, client distribution, country distribution.
* **Static Data:** Served from `ui/public/*.json` (no backend required).
* **Theme:** Dark/light toggle.

## Tech Stack

**Frontend (UI):**
* **Framework:** Vue 3 (Composition API with `<script setup>`)
* **Build Tool:** Vite
* **Mapping:**
* Leaflet.js
* `leaflet.markercluster` (for clustering nodes)
* `leaflet.heat` (for heatmap display)
* **Charting:** Chart.js (via `vue-chartjs`)
* **HTTP Client:** Axios
* **Styling:** Primarily custom CSS with global `box-sizing` and responsive design.

**Backend:**
* **Runtime:** Node.js
* **Framework:** Express.js
* **WebSocket Client:** Primus (to connect to `ethstats.linea.build`)
* **Middleware:** CORS

## Data Sources

* **Static Nodes:** Fetched from the backend API endpoint `/static-nodes`. This data is typically an enriched list of `admin_peers` from a random node on the network, refreshed periodically. Geolocation is derived from IP addresses and AWS region heuristics.
* **Live Nodes:** Sourced in real-time via a WebSocket connection to `wss://ethstats.linea.build/primus`.
* **Framework:** Vue 3 (Composition API with `<script setup>`)
* **Build Tool:** Vite
* **Mapping:** Leaflet.js, `leaflet.markercluster`, `leaflet.heat`
* **Charts:** Chart.js (via `vue-chartjs`)
* **Styling:** CSS + Tailwind v4 imports

**Data prep (CLI):**
* Node.js scripts to enrich raw `admin_peers` snapshots into JSON used by the UI.

## Data Flow / Enrichment

1) Get raw peers from your node (`admin_peers` RPC) or `admin_nodeInfo` for a single node.
2) Run the enrichment script to normalize and geo-tag:
* Script: `backend/process-peers.js`
* Usage: `node backend/process-peers.js <input_raw.json> <output_enriched.json>`
* Steps: filter private IPs, dedupe by enode, parse client name/version, batch geolocate via `ip-api.com`, emit `ip, client, clientName, clientVersion, enode, country, region, city, lat, lon`.
3) Optional: merge multiple enriched files (e.g., Besu + Erigon) into one combined snapshot placed in `ui/public/`.
4) The UI loads the chosen JSON from `ui/public` (see `ui/src/utils/constants.js` for endpoints).

Live WebSocket data from `ethstats.linea.build` is currently not required for the hosted build; the map runs from static snapshots.

## Setup and Running Locally

Expand All @@ -52,22 +46,6 @@ This project provides a dynamic, real-time visualization of Linea mainnet peer n
* Node.js (version 18.x or later recommended)
* npm (usually comes with Node.js)

### Backend

1. Navigate to the `backend` directory:
```bash
cd backend
```
2. Install dependencies:
```bash
npm install
```
3. Start the backend server (defaults to `http://localhost:3000` or as per your config):
```bash
node index.js
# or if you have a dev script: npm run dev
```

### Frontend (UI)

1. Navigate to the `ui` directory:
Expand Down
1 change: 1 addition & 0 deletions backend/data/peers.txt

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions backend/process-peers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// process-peers.js
// Reads a raw peers snapshot (admin_peers style) and emits an enriched peers file
// Usage: node backend/process-peers.js /path/to/peers.json /path/to/output.json

import fs from 'fs'
import fetch from 'node-fetch'

const [, , inputPath = '/Users/moris/Downloads/peers.json', outputPath = 'ui/public/peers_new.json'] = process.argv

const CLIENT_PARSE = (clientStr = '') => {
const lower = clientStr.toLowerCase()
const [nameRaw, versionRaw = 'Unknown'] = clientStr.split('/')

if (lower.includes('geth')) return { clientName: 'Geth', clientVersion: versionRaw }
if (lower.includes('besu')) return { clientName: 'Besu', clientVersion: versionRaw }
if (lower.includes('erigon')) return { clientName: 'Erigon', clientVersion: versionRaw }
if (lower.includes('nethermind')) return { clientName: 'Nethermind', clientVersion: versionRaw }
return { clientName: 'Other', clientVersion: versionRaw }
}

const isPrivateIp = (ip = '') => {
return (
ip.startsWith('10.') ||
ip.startsWith('192.168.') ||
ip.startsWith('172.') && (() => {
const second = parseInt(ip.split('.')[1], 10)
return second >= 16 && second <= 31
})() ||
ip.startsWith('127.') ||
ip.startsWith('169.254.')
)
}

const chunk = (arr, size) => {
const res = []
for (let i = 0; i < arr.length; i += size) res.push(arr.slice(i, i + size))
return res
}

async function geolocateBatch(ips) {
const batches = chunk(ips, 100)
const results = {}

for (const group of batches) {
try {
const resp = await fetch('http://ip-api.com/batch?fields=status,country,regionName,city,lat,lon,query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(group)
})
const data = await resp.json()
data.forEach(entry => {
if (entry && entry.status === 'success') {
results[entry.query] = {
country: entry.country || 'Unknown',
region: entry.regionName || '',
city: entry.city || '',
lat: entry.lat || 0,
lon: entry.lon || 0
}
}
})
} catch (err) {
console.warn('Geo batch failed, continuing:', err?.message || err)
}
// polite delay to avoid hammering the free endpoint
await new Promise(r => setTimeout(r, 750))
}

return results
}

async function main() {
console.log(`Reading input: ${inputPath}`)
const raw = JSON.parse(fs.readFileSync(inputPath, 'utf-8'))
const peers = raw.result || []

// Extract entries, dedupe by enode
const seenEnode = new Set()
const entries = []
for (const p of peers) {
const enode = p.enode
const remote = p.network?.remoteAddress || ''
const ip = remote.split(':')[0]
if (!enode || !ip) continue
if (isPrivateIp(ip)) continue
if (seenEnode.has(enode)) continue
seenEnode.add(enode)

const { clientName, clientVersion } = CLIENT_PARSE(p.name || '')
entries.push({
ip,
client: p.name || '',
clientName,
clientVersion,
enode,
})
}

const uniqueIps = [...new Set(entries.map(e => e.ip))]
console.log(`Entries after filtering: ${entries.length} (unique IPs: ${uniqueIps.length})`)

console.log('Geolocating via ip-api.com (batch)...')
const geoMap = await geolocateBatch(uniqueIps)

const enriched = entries.map(e => {
const g = geoMap[e.ip] || {}
return {
...e,
country: g.country || 'Unknown',
region: g.region || '',
city: g.city || '',
lat: g.lat || 0,
lon: g.lon || 0,
}
})

fs.writeFileSync(outputPath, JSON.stringify(enriched, null, 2))
console.log(`✅ Wrote ${enriched.length} records to ${outputPath}`)
}

main().catch(err => {
console.error('Failed to process peers:', err)
process.exit(1)
})


18 changes: 9 additions & 9 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@
<meta name="robots" content="index, follow" />

<!-- SEO -->
<meta name="description" content="Track Linea mainnet peers in real-time. Visualize Ethereum clients, node locations, and network stats on an interactive map." />
<meta name="keywords" content="Linea, zkEVM, Ethereum, peer map, node stats, Besu, Erigon, Geth, Nethermind, blockchain infra" />
<meta name="description" content="Track Linea mainnet nodes and peers in real time. Visualize Ethereum clients, node locations, and network stats on an interactive map." />
<meta name="keywords" content="Linea, zkEVM, Ethereum, node map, nodes, peers, client stats, Besu, Erigon, Geth, Nethermind, blockchain infra" />
<link rel="canonical" href="https://lineapeerswatchersmap.netlify.app/" />
<meta name="application-name" content="Linea Nodes & Peers Map" />

<!-- Open Graph / Social -->
<meta property="og:title" content="Linea Mainnet Peers Watcher Map" />
<meta property="og:description" content="Visualize Linea mainnet peers across the world. Live client, location, and node count insights." />
<meta property="og:title" content="Linea Nodes & Peers Map" />
<meta property="og:description" content="Visualize Linea mainnet nodes and peers across the world. Live client, location, and node count insights." />
<meta property="og:image" content="https://lineapeerswatchersmap.netlify.app/og-image.png" />
<meta property="og:url" content="https://lineapeerswatchersmap.netlify.app" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Linea Peers Watcher" />
<meta property="og:site_name" content="Linea Nodes & Peers Map" />

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Linea Mainnet Peers Watcher Map" />
<meta name="twitter:description" content="Live Linea network stats: peers, clients, and geolocation heatmap." />
<meta name="twitter:title" content="Linea Nodes & Peers Map" />
<meta name="twitter:description" content="Live Linea network stats: nodes, peers, clients, and geolocation heatmap." />
<meta name="twitter:image" content="https://lineapeerswatchersmap.netlify.app/og-image.png" />

<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/besu.png" />

<title>Linea Mainnet Peers Watcher Map</title>
<title>Linea Nodes & Peers Map | Linea Mainnet Tracker</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand All @@ -46,6 +47,5 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
</body>
</html>
Loading