feat: vue dividende universel — overlay membres actifs géolocalisés
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Bouton DU (gauche carte) : affiche en overlay des cercles verts proportionnels au nombre de membres WoT actifs géolocalisés par ville. Chargement à la demande, mis en cache 1h. Pipeline : SubsquidAdapter.fetchActiveMemberKeys() → isMember:true (~7000) CesiumAdapter.resolveGeoByKeysBatched() → lots de 500 clés DataService.fetchMemberCities() → agrégation + cache 1h HeatMap → CircleMarkers Leaflet en overlay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
||||
*/
|
||||
|
||||
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD, ss58ToDuniterKey } from './adapters/SubsquidAdapter';
|
||||
import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter';
|
||||
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD, ss58ToDuniterKey, fetchActiveMemberKeys } from './adapters/SubsquidAdapter';
|
||||
import { resolveGeoByKeys, resolveGeoByKeysBatched, cleanCityName } from './adapters/CesiumAdapter';
|
||||
import {
|
||||
getTransactionsForPeriod,
|
||||
computeStats,
|
||||
@@ -139,6 +139,51 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
||||
return { geolocated, arcs, totalCount, totalVolume, allTimestamps: rawTransfers.map((t) => t.timestamp) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vue dividende universel : membres actifs géolocalisés par ville
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface MemberCity {
|
||||
city: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
count: number;
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
let memberCitiesCache: { data: MemberCity[]; expiresAt: number } | null = null;
|
||||
|
||||
/**
|
||||
* Retourne la liste des villes avec le nombre de membres WoT actifs géolocalisés.
|
||||
* Résultat mis en cache 1 heure (le nombre de membres évolue lentement).
|
||||
* Traite les ~7000 clés en lots de 500 pour ne pas surcharger Cesium+.
|
||||
*/
|
||||
export async function fetchMemberCities(): Promise<MemberCity[]> {
|
||||
if (memberCitiesCache && Date.now() < memberCitiesCache.expiresAt) return memberCitiesCache.data;
|
||||
|
||||
const duniterKeys = await fetchActiveMemberKeys();
|
||||
const unique = [...new Set(duniterKeys)];
|
||||
const geoMap = await resolveGeoByKeysBatched(unique);
|
||||
|
||||
const cityMap = new Map<string, { lat: number; lng: number; count: number; countryCode: string }>();
|
||||
for (const geo of geoMap.values()) {
|
||||
const city = cleanCityName(geo.city);
|
||||
const existing = cityMap.get(city);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
cityMap.set(city, { lat: geo.lat, lng: geo.lng, count: 1, countryCode: geo.countryCode });
|
||||
}
|
||||
}
|
||||
|
||||
const data: MemberCity[] = [...cityMap.entries()]
|
||||
.map(([city, v]) => ({ city, ...v }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
memberCitiesCache = { data, expiresAt: Date.now() + 60 * 60 * 1000 };
|
||||
return data;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -163,6 +163,23 @@ export async function resolveGeoByKeys(
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Même que resolveGeoByKeys mais traite les grands tableaux par lots.
|
||||
* Nécessaire pour les 6000+ membres actifs (évite des requêtes ES trop grandes).
|
||||
*/
|
||||
export async function resolveGeoByKeysBatched(
|
||||
duniterKeys: string[],
|
||||
batchSize = 500,
|
||||
): Promise<Map<string, GeoProfile>> {
|
||||
const result = new Map<string, GeoProfile>();
|
||||
for (let i = 0; i < duniterKeys.length; i += batchSize) {
|
||||
const batch = duniterKeys.slice(i, i + batchSize);
|
||||
const partial = await resolveGeoByKeys(batch);
|
||||
for (const [k, v] of partial) result.set(k, v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout les coordonnées de plusieurs membres Ğ1 par leur nom d'identité.
|
||||
* Envoie une requête Elasticsearch multi-terms en un seul appel.
|
||||
|
||||
@@ -174,6 +174,41 @@ export async function fetchCurrentUD(): Promise<number> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Membres actifs WoT (isMember = true)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ACTIVE_MEMBERS_QUERY = `
|
||||
query {
|
||||
identities(filter: { isMember: { equalTo: true } }, first: 20000) {
|
||||
nodes {
|
||||
accountId
|
||||
ownerKeyChange(orderBy: BLOCK_NUMBER_ASC, first: 1) {
|
||||
nodes { previousId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/** Retourne la liste des clés SS58 de tous les membres WoT actifs. */
|
||||
export async function fetchActiveMemberKeys(): Promise<string[]> {
|
||||
const res = await fetch(SUBSQUID_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: ACTIVE_MEMBERS_QUERY }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Subsquid HTTP ${res.status}`);
|
||||
const raw = await res.json();
|
||||
if (raw.errors?.length) throw new Error(raw.errors[0].message);
|
||||
|
||||
return (raw.data.identities.nodes as { accountId: string; ownerKeyChange: { nodes: { previousId: string }[] } }[])
|
||||
.map((node) => {
|
||||
const genesisKey: string = node.ownerKeyChange.nodes[0]?.previousId ?? node.accountId;
|
||||
return ss58ToDuniterKey(genesisKey);
|
||||
});
|
||||
}
|
||||
|
||||
export interface FetchTransfersResult {
|
||||
transfers: RawTransfer[];
|
||||
totalCount: number;
|
||||
|
||||
Reference in New Issue
Block a user