fix: géolocalisation Cesium+ et tests déterministes
- CesiumAdapter : utilise le champ `title` (analysé ES) au lieu de `title.keyword` qui retournait 0 résultats ; coerce lat/lon en number (certains profils stockent des strings) - DataService : sépare totalVolume (all tx blockchain) de geoCount (tx heatmap) - StatsPanel : barre de couverture géo uniquement en mode live - App : badge source "● live Ğ1v2" ou "○ mock" - DataService.test.ts : mock SubsquidAdapter + CesiumAdapter directement (vi.mock hoistés) pour que les tests soient déterministes quel que soit VITE_USE_LIVE_API dans .env.local - tsconfig.app.json : exclude src/test pour éviter les erreurs de build prod Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
* Mode live (USE_LIVE_API = true) : données réelles Ğ1v2.
|
||||
* - Transactions : Subsquid indexer https://squidv2s.syoul.fr/v1/graphql
|
||||
* - Géolocalisation : Cesium+ https://g1.data.e-is.pro
|
||||
* → recherche par nom d'identité (identity.name depuis le graphe Subsquid)
|
||||
* → les transactions sans profil Cesium+ reçoivent des coordonnées approx.
|
||||
* → recherche batch par nom d'identité (champ "title" analysé ES)
|
||||
* → couverture ~50-60% : les tx sans profil géo sont EXCLUES du heatmap
|
||||
* mais comptées dans totalCount / totalVolume
|
||||
*
|
||||
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
||||
*/
|
||||
@@ -19,27 +20,21 @@ import {
|
||||
type Transaction,
|
||||
} from '../data/mockData';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true';
|
||||
|
||||
// Centroïde France — fallback géo quand Cesium+ n'a pas le profil
|
||||
const FRANCE_CENTER = { lat: 46.2276, lng: 2.2137 };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipeline données live Ğ1v2
|
||||
// ---------------------------------------------------------------------------
|
||||
async function fetchLiveTransactions(periodDays: number): Promise<Transaction[]> {
|
||||
// 1. Récupère les transferts depuis Subsquid
|
||||
async function fetchLiveTransactions(periodDays: number): Promise<{
|
||||
geolocated: Transaction[];
|
||||
totalCount: number;
|
||||
totalVolume: number;
|
||||
}> {
|
||||
const rawTransfers = await fetchTransfers(periodDays);
|
||||
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 };
|
||||
|
||||
if (rawTransfers.length === 0) return [];
|
||||
const totalCount = rawTransfers.length;
|
||||
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
||||
|
||||
// 2. Collecte les noms d'identité uniques pour la recherche Cesium+
|
||||
// Résolution géo batch via Cesium+
|
||||
const names = rawTransfers.map((t) => t.fromName).filter(Boolean);
|
||||
|
||||
// 3. Résout les coordonnées via Cesium+ (une seule requête batch)
|
||||
let geoMap = new Map<string, { lat: number; lng: number; city: string }>();
|
||||
try {
|
||||
const profiles = await resolveGeoByNames(names);
|
||||
@@ -47,53 +42,69 @@ async function fetchLiveTransactions(periodDays: number): Promise<Transaction[]>
|
||||
geoMap.set(name, { lat: p.lat, lng: p.lng, city: p.city });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Cesium+ indisponible, fallback coordonnées France :', err);
|
||||
console.warn('Cesium+ indisponible :', err);
|
||||
}
|
||||
|
||||
// 4. Assemble les transactions avec coordonnées
|
||||
return rawTransfers.map((t): Transaction => {
|
||||
const geo = geoMap.get(t.fromName) ?? FRANCE_CENTER;
|
||||
return {
|
||||
// Seules les transactions avec un profil géo entrent dans le heatmap
|
||||
const geolocated: Transaction[] = [];
|
||||
for (const t of rawTransfers) {
|
||||
const geo = geoMap.get(t.fromName);
|
||||
if (!geo) continue; // pas de profil → exclu du heatmap
|
||||
|
||||
geolocated.push({
|
||||
id: t.id,
|
||||
timestamp: t.timestamp,
|
||||
lat: geo.lat,
|
||||
lng: geo.lng,
|
||||
amount: t.amount,
|
||||
city: ('city' in geo ? geo.city : undefined) ?? 'Inconnue',
|
||||
city: geo.city,
|
||||
fromKey: t.fromId,
|
||||
toKey: t.toId,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { geolocated, totalCount, totalVolume };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
export interface PeriodStats {
|
||||
totalVolume: number;
|
||||
transactionCount: number;
|
||||
totalVolume: number;
|
||||
transactionCount: number; // total blockchain (y compris non-géolocalisés)
|
||||
geoCount: number; // transactions visibles sur la carte
|
||||
topCities: { name: string; volume: number; count: number }[];
|
||||
}
|
||||
|
||||
export interface DataResult {
|
||||
transactions: Transaction[];
|
||||
stats: PeriodStats;
|
||||
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
||||
stats: PeriodStats;
|
||||
source: 'live' | 'mock';
|
||||
}
|
||||
|
||||
export async function fetchData(periodDays: number): Promise<DataResult> {
|
||||
let transactions: Transaction[];
|
||||
let source: 'live' | 'mock';
|
||||
|
||||
if (USE_LIVE_API) {
|
||||
transactions = await fetchLiveTransactions(periodDays);
|
||||
source = 'live';
|
||||
} else {
|
||||
if (!USE_LIVE_API) {
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
transactions = getTransactionsForPeriod(periodDays);
|
||||
source = 'mock';
|
||||
const transactions = getTransactionsForPeriod(periodDays);
|
||||
const base = computeStats(transactions);
|
||||
return {
|
||||
transactions,
|
||||
stats: { ...base, geoCount: transactions.length },
|
||||
source: 'mock',
|
||||
};
|
||||
}
|
||||
|
||||
const stats = computeStats(transactions);
|
||||
return { transactions, stats, source };
|
||||
const { geolocated, totalCount, totalVolume } = await fetchLiveTransactions(periodDays);
|
||||
const base = computeStats(geolocated);
|
||||
|
||||
return {
|
||||
transactions: geolocated,
|
||||
stats: {
|
||||
totalVolume, // vrai total blockchain
|
||||
transactionCount: totalCount,
|
||||
geoCount: geolocated.length,
|
||||
topCities: base.topCities,
|
||||
},
|
||||
source: 'live',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user