diff --git a/src/App.tsx b/src/App.tsx index 87db7d3..4bb5df8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ export default function App() { const [refreshing, setRefreshing] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [source, setSource] = useState<'live' | 'mock'>('mock'); + const [currentUD, setCurrentUD] = useState(11.78); const animation = useAnimation(transactions, periodDays); @@ -32,11 +33,12 @@ export default function App() { if (showLoading) setLoading(true); else setRefreshing(true); fetchData(periodDays) - .then(({ transactions, stats, source }) => { + .then(({ transactions, stats, source, currentUD }) => { if (!cancelled) { setTransactions(transactions); setStats(stats); setSource(source); + setCurrentUD(currentUD); setLastUpdate(new Date()); } }) @@ -68,6 +70,7 @@ export default function App() { loading={loading} periodDays={periodDays} source={source} + currentUD={currentUD} animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined} /> diff --git a/src/components/StatsPanel.tsx b/src/components/StatsPanel.tsx index b5fb36c..bebedaa 100644 --- a/src/components/StatsPanel.tsx +++ b/src/components/StatsPanel.tsx @@ -6,6 +6,7 @@ interface StatsPanelProps { loading: boolean; periodDays: number; source: 'live' | 'mock'; + currentUD: number; animationLabel?: string; } @@ -25,7 +26,14 @@ function StatCard({ label, value, sub, delta }: { label: string; value: string; ); } -export function StatsPanel({ stats, loading, periodDays, source, animationLabel }: StatsPanelProps) { +function formatDU(g1: number, ud: number): string { + const du = g1 / ud; + if (du < 10) return `≈ ${du.toFixed(2)} DU`; + if (du < 100) return `≈ ${du.toFixed(1)} DU`; + return `≈ ${Math.round(du).toLocaleString('fr-FR')} DU`; +} + +export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel }: StatsPanelProps) { const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`; const prevStats = useRef(null); @@ -89,12 +97,16 @@ export function StatsPanel({ stats, loading, periodDays, source, animationLabel prevVolume ? 'up' : stats.totalVolume < prevVolume ? 'down' : null) : null} /> { + const avg = stats.totalVolume / (stats.transactionCount || 1); + return `≈ ${avg.toFixed(2)} Ğ1 / tx · ${formatDU(avg, currentUD)} / tx`; + })()} delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null} /> {/* Couverture géo — uniquement en mode live */} diff --git a/src/services/DataService.ts b/src/services/DataService.ts index c0c1dec..3c14530 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -12,7 +12,7 @@ * Pour activer : définir VITE_USE_LIVE_API=true dans .env.local */ -import { fetchTransfers, buildIdentityKeyMap } from './adapters/SubsquidAdapter'; +import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD } from './adapters/SubsquidAdapter'; import { resolveGeoByKeys } from './adapters/CesiumAdapter'; import { getTransactionsForPeriod, @@ -22,6 +22,16 @@ import { const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true'; +// Cache du DU courant, valide 1 heure (le DU change tous les ~6 mois) +let udCache: { value: number; expiresAt: number } | null = null; + +async function getCurrentUD(): Promise { + if (udCache && Date.now() < udCache.expiresAt) return udCache.value; + const value = await fetchCurrentUD(); + udCache = { value, expiresAt: Date.now() + 60 * 60 * 1000 }; + return value; +} + // Cache de la carte identité SS58→DuniterKey, valide 10 minutes let keyMapCache: { map: Map; expiresAt: number } | null = null; @@ -106,6 +116,7 @@ export interface DataResult { transactions: Transaction[]; // uniquement géolocalisées → heatmap stats: PeriodStats; source: 'live' | 'mock'; + currentUD: number; // valeur du DU courant en Ğ1 } export async function fetchData(periodDays: number): Promise { @@ -117,10 +128,14 @@ export async function fetchData(periodDays: number): Promise { transactions, stats: { ...base, geoCount: transactions.length }, source: 'mock', + currentUD: 11.78, }; } - const { geolocated, totalCount, totalVolume } = await fetchLiveTransactions(periodDays); + const [{ geolocated, totalCount, totalVolume }, currentUD] = await Promise.all([ + fetchLiveTransactions(periodDays), + getCurrentUD(), + ]); const base = computeStats(geolocated); return { @@ -132,5 +147,6 @@ export async function fetchData(periodDays: number): Promise { topCities: base.topCities, }, source: 'live', + currentUD, }; } diff --git a/src/services/adapters/SubsquidAdapter.ts b/src/services/adapters/SubsquidAdapter.ts index 2b76998..ecc9edb 100644 --- a/src/services/adapters/SubsquidAdapter.ts +++ b/src/services/adapters/SubsquidAdapter.ts @@ -153,6 +153,27 @@ export async function buildIdentityKeyMap(): Promise> { return result; } +/** Retourne la valeur du DU courant en Ğ1 (ex : 11.78). Fallback hardcodé si indisponible. */ +export async function fetchCurrentUD(): Promise { + const UD_FALLBACK = 11.78; // valeur au bloc 225874 — mis à jour si la requête échoue + try { + const response = await fetch(SUBSQUID_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: `{ universalDividends(orderBy: BLOCK_NUMBER_DESC, first: 1) { nodes { amount } } }`, + }), + }); + if (!response.ok) return UD_FALLBACK; + const raw = await response.json(); + const amountStr: string | undefined = raw?.data?.universalDividends?.nodes?.[0]?.amount; + if (!amountStr) return UD_FALLBACK; + return parseInt(amountStr, 10) / 100; + } catch { + return UD_FALLBACK; + } +} + export interface FetchTransfersResult { transfers: RawTransfer[]; totalCount: number;