From 8e396cd33193115e57ad306034a83973f1e8b52a Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 21 Apr 2026 21:29:59 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20nature=20des=20=C3=A9changes=20?= =?UTF-8?q?=E2=80=94=20cat=C3=A9gorisation=20et=20d=C3=A9tail=20des=20comm?= =?UTF-8?q?entaires=20de=20transactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau commentParser.ts : ~80 règles regex multilingues, 11 catégories - SubsquidAdapter : fetch du champ comment.remark depuis SubSquid - Transaction et TransactionArc : champs comment et category - StatsPanel : section Nature des échanges avec barres cliquables (détail inline) - FlowMap : tooltip au survol des arcs avec répartition catégories + commentaires - InfoPanel mis à jour Co-Authored-By: Claude Sonnet 4.6 --- releases/v1.6.0.md | 24 ++ src/App.tsx | 1 + src/components/FlowMap.tsx | 103 ++++++-- src/components/InfoPanel.tsx | 17 ++ src/components/StatsPanel.tsx | 75 +++++- src/data/arcData.ts | 33 ++- src/data/commentParser.ts | 300 +++++++++++++++++++++++ src/data/mockData.ts | 21 +- src/services/DataService.ts | 9 + src/services/adapters/SubsquidAdapter.ts | 6 + 10 files changed, 562 insertions(+), 27 deletions(-) create mode 100644 releases/v1.6.0.md create mode 100644 src/data/commentParser.ts diff --git a/releases/v1.6.0.md b/releases/v1.6.0.md new file mode 100644 index 0000000..fe6475f --- /dev/null +++ b/releases/v1.6.0.md @@ -0,0 +1,24 @@ +## What's Changed + +### Nouvelles fonctionnalités + +- **Nature des échanges** — les commentaires de transactions sont analysés et classés automatiquement en catégories : don & gratitude, alimentation, soin & bien-être, vêtements, culture & loisirs, événement, service & travaux, remboursement, migration, ticket, autre +- Distribution des catégories affichée dans le panneau latéral (barres proportionnelles sur les transactions commentées de la période) — chaque catégorie est cliquable pour dérouler la liste des transactions avec leur commentaire et montant +- Tooltip au survol des arcs en vue Flux : répartition des catégories + échantillon de commentaires bruts du corridor +- 76 % des transactions Ğ1v2 comportent un commentaire — le champ `remark` est désormais fetché depuis SubSquid + +### Améliorations + +- InfoPanel mis à jour : section *Nature des échanges* documentée + +### Détails techniques + +- Nouveau `src/data/commentParser.ts` — ~80 règles regex multilingues (FR/ES/CA/IT/EN/PT), 11 catégories, priorité ordonnée +- `SubsquidAdapter` : ajout de `comment { remark }` à la query GraphQL +- `Transaction` et `TransactionArc` : nouveaux champs `comment: string | null` et `category: TxCategory` +- `Corridor` : nouveaux champs `categories` (agrégées) et `comments` (échantillon jusqu'à 5) +- `PeriodStats` : nouveaux champs `categoryBreakdown` et `commentedCount` +- Zone de hit des arcs SVG élargie (+8 px) pour faciliter le survol +- Aucune nouvelle dépendance npm + +**Full Changelog**: https://git.syoul.fr/geoflux/compare/v1.5.0...v1.6.0 diff --git a/src/App.tsx b/src/App.tsx index 52a282d..efa340f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -152,6 +152,7 @@ export default function App() { focusCity, allTimestamps, onEndpointChange: () => setEndpointVersion((v) => v + 1), + transactions, }; return ( diff --git a/src/components/FlowMap.tsx b/src/components/FlowMap.tsx index 29e5289..37feaf5 100644 --- a/src/components/FlowMap.tsx +++ b/src/components/FlowMap.tsx @@ -24,6 +24,8 @@ const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux v import type { TransactionArc } from '../data/arcData'; import { buildCorridors } from '../data/arcData'; +import type { TxCategory } from '../data/commentParser'; +import { CATEGORY_LABELS, CATEGORY_COLORS, aggregateCategories } from '../data/commentParser'; // Leaflet default marker fix (Vite asset pipeline) import iconUrl from 'leaflet/dist/images/marker-icon.png'; @@ -169,6 +171,10 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { interface ClusterArc { fromIdx: number; toIdx: number; totalVolume: number; count: number; + categories: { category: TxCategory; count: number; volume: number }[]; + comments: string[]; + _catItems: { category: TxCategory; amount: number }[]; + _comments: string[]; } const clArcMap = new Map(); for (const c of corridors) { @@ -176,12 +182,18 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { const ti = cityClusterIdx.get(c.toCity); if (fi === undefined || ti === undefined || fi === ti) continue; // intra-cluster → ignoré const key = `${fi}||${ti}`; - if (!clArcMap.has(key)) clArcMap.set(key, { fromIdx: fi, toIdx: ti, totalVolume: 0, count: 0 }); + if (!clArcMap.has(key)) clArcMap.set(key, { fromIdx: fi, toIdx: ti, totalVolume: 0, count: 0, categories: [], comments: [], _catItems: [], _comments: [] }); const ca = clArcMap.get(key)!; ca.totalVolume += c.totalVolume; ca.count += c.count; + ca._catItems.push(...c.categories.map((cat) => ({ category: cat.category, amount: cat.volume }))); + ca._comments.push(...c.comments); } - const clusterArcs = [...clArcMap.values()].sort((a, b) => b.totalVolume - a.totalVolume); + const clusterArcs = [...clArcMap.values()].map((ca) => ({ + ...ca, + categories: aggregateCategories(ca._catItems), + comments: [...new Set(ca._comments)].filter(Boolean).slice(0, 4), + })).sort((a, b) => b.totalVolume - a.totalVolume); // --- 4. Couleur de balance par cluster --- const maxAbsNet = Math.max(...clusters.map(cl => Math.abs(cl.received - cl.emitted)), 1); @@ -239,9 +251,13 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { : isFocusTo ? '#00acc1' : '#2e2f3a'; + const arcKey = `${ca.fromIdx}||${ca.toIdx}`; + const midX = (1-0.5)*(1-0.5)*p1.x + 2*(1-0.5)*0.5*cx + 0.5*0.5*p2.x; + const midY = (1-0.5)*(1-0.5)*p1.y + 2*(1-0.5)*0.5*cy + 0.5*0.5*p2.y; return { idx, ca, p1, p2, cx, cy, arrowPts, strokeW, opacity, stroke, arrowFill, path: `M ${p1.x},${p1.y} Q ${cx},${cy} ${p2.x},${p2.y}`, + arcKey, midX, midY, }; }); @@ -263,9 +279,10 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { }, [corridors, cityNodes, focusCity, tick, mapReady, clustered]); const [popupIdx, setPopupIdx] = useState(null); + const [hoveredArc, setHoveredArc] = useState<{ key: string; x: number; y: number } | null>(null); - // Ferme le popup sur déplacement/zoom - useEffect(() => { setPopupIdx(null); }, [tick]); + // Ferme le popup et le tooltip sur déplacement/zoom + useEffect(() => { setPopupIdx(null); setHoveredArc(null); }, [tick]); // Handler de clic : ouvre/ferme le popup + focus const handleNodeClick = (nodeIdx: number) => { @@ -303,18 +320,22 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { {/* Arcs bezier */} - {svgElements.arcElems.map(a => ( - - - - - ))} + + {svgElements.arcElems.map(a => ( + setHoveredArc({ key: a.arcKey, x: a.midX, y: a.midY })} + onMouseLeave={() => setHoveredArc(null)} + style={{ cursor: 'default' }} + > + {/* Zone de hit invisible plus large */} + + + + + ))} + {/* Nœuds de clusters (pointer-events activés uniquement ici) */} @@ -347,6 +368,56 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { )} + {/* Tooltip arc — nature des échanges */} + {hoveredArc && svgElements && (() => { + const arcElem = svgElements.arcElems.find((a) => a.arcKey === hoveredArc.key); + if (!arcElem) return null; + const { ca } = arcElem; + const topCats = ca.categories.filter((c) => c.category !== 'migration' && c.category !== 'ticket').slice(0, 4); + const total = ca.categories.reduce((s, c) => s + c.count, 0); + const containerW = containerRef.current?.clientWidth ?? 600; + const containerH = containerRef.current?.clientHeight ?? 400; + const tipW = 200; + const tipH = 120; + const left = Math.min(hoveredArc.x + 12, containerW - tipW - 8); + const top = Math.min(Math.max(8, hoveredArc.y - tipH / 2), containerH - tipH - 8); + return ( +
+

+ {ca.count} échanges · {ca.totalVolume.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} Ğ1 +

+ {topCats.length > 0 ? ( +
+ {topCats.map((c) => { + const pct = Math.round((c.count / total) * 100); + return ( +
+
+ {CATEGORY_LABELS[c.category]} + {pct}% +
+
+
+
+
+ ); + })} +
+ ) : null} + {ca.comments.length > 0 && ( +
+ {ca.comments.slice(0, 3).map((c, i) => ( +

"{c}"

+ ))} +
+ )} +
+ ); + })()} + {/* Bouton cluster / villes */} + + {isOpen && ( +
+ {detail.length === 0 ? ( +

Aucun commentaire disponible.

+ ) : ( +
+ {detail.slice(0, 30).map((t) => ( +
+

+ "{t.comment}" +

+ + {t.amount.toLocaleString('fr-FR', { maximumFractionDigits: 1 })} Ğ1 + +
+ ))} + {detail.length > 30 && ( +

+ +{detail.length - 30} autres +

+ )} +
+ )} +
+ )} +
+ ); + })} + + + )} + {/* Footer */}

diff --git a/src/data/arcData.ts b/src/data/arcData.ts index 589a2f1..693fe54 100644 --- a/src/data/arcData.ts +++ b/src/data/arcData.ts @@ -1,4 +1,6 @@ import type { Transaction } from './mockData'; +import type { TxCategory } from './commentParser'; +import { aggregateCategories } from './commentParser'; export interface TransactionArc { id: string; @@ -14,6 +16,8 @@ export interface TransactionArc { toCity: string; toCountry: string; toKey: string; + comment: string | null; + category: TxCategory; } /** Corridor agrégé par paire de villes (fromCity → toCity). */ @@ -28,6 +32,8 @@ export interface Corridor { toCountry: string; totalVolume: number; count: number; + categories: { category: TxCategory; count: number; volume: number }[]; + comments: string[]; // échantillon de commentaires bruts (max 5, non nuls) } export interface FlowStats { @@ -40,21 +46,30 @@ export interface FlowStats { /** Agrège les arcs individuels en corridors ville→ville, triés par volume. */ export function buildCorridors(arcs: TransactionArc[]): Corridor[] { - const map = new Map(); + const map = new Map; items: TransactionArc[] }>(); for (const arc of arcs) { const key = `${arc.fromCity}||${arc.toCity}`; if (!map.has(key)) { map.set(key, { - fromCity: arc.fromCity, fromLat: arc.fromLat, fromLng: arc.fromLng, fromCountry: arc.fromCountry, - toCity: arc.toCity, toLat: arc.toLat, toLng: arc.toLng, toCountry: arc.toCountry, - totalVolume: 0, count: 0, + corridor: { + fromCity: arc.fromCity, fromLat: arc.fromLat, fromLng: arc.fromLng, fromCountry: arc.fromCountry, + toCity: arc.toCity, toLat: arc.toLat, toLng: arc.toLng, toCountry: arc.toCountry, + totalVolume: 0, count: 0, + }, + items: [], }); } - const c = map.get(key)!; - c.totalVolume += arc.amount; - c.count++; + const entry = map.get(key)!; + entry.corridor.totalVolume += arc.amount; + entry.corridor.count++; + entry.items.push(arc); } - return [...map.values()].sort((a, b) => b.totalVolume - a.totalVolume); + + return [...map.values()].map(({ corridor, items }) => ({ + ...corridor, + categories: aggregateCategories(items.map((a) => ({ category: a.category, amount: a.amount }))), + comments: items.map((a) => a.comment).filter((c): c is string => !!c).slice(0, 5), + })).sort((a, b) => b.totalVolume - a.totalVolume); } export function computeFlowStats(arcs: TransactionArc[]): FlowStats { @@ -114,6 +129,8 @@ export function buildMockArcs(transactions: Transaction[]): TransactionArc[] { toLat: to.lat, toLng: to.lng, toCity: to.city, toCountry: to.countryCode, toKey: to.toKey, + comment: from.comment, + category: from.category, }); } return arcs; diff --git a/src/data/commentParser.ts b/src/data/commentParser.ts new file mode 100644 index 0000000..ec7a504 --- /dev/null +++ b/src/data/commentParser.ts @@ -0,0 +1,300 @@ +export type TxCategory = + | 'migration' + | 'ticket' + | 'remboursement' + | 'don' + | 'alimentation' + | 'soin' + | 'vetements' + | 'culture' + | 'evenement' + | 'service' + | 'autre'; + +export const CATEGORY_LABELS: Record = { + migration: 'Migration', + ticket: 'Ticket', + remboursement:'Remboursement', + don: 'Don / Gratitude', + alimentation: 'Alimentation', + soin: 'Soin & bien-être', + vetements: 'Vêtements', + culture: 'Culture & loisirs', + evenement: 'Événement', + service: 'Service & travaux', + autre: 'Autre', +}; + +export const CATEGORY_COLORS: Record = { + migration: '#4b5563', + ticket: '#6b7280', + remboursement:'#f59e0b', + don: '#ec4899', + alimentation: '#22c55e', + soin: '#06b6d4', + vetements: '#a78bfa', + culture: '#f97316', + evenement: '#eab308', + service: '#3b82f6', + autre: '#374151', +}; + +// --------------------------------------------------------------------------- +// Règles de détection — ordre = priorité, première règle qui matche gagne +// --------------------------------------------------------------------------- + +interface Rule { + category: TxCategory; + patterns: RegExp[]; +} + +const RULES: Rule[] = [ + { + category: 'migration', + patterns: [ + /ğecko:csmigration/i, + /csmigration/i, + /migration\s*v[12]/i, + /\bğecko\b/i, + ], + }, + { + category: 'ticket', + patterns: [ + /\bticket\s+\d{6,}/i, + ], + }, + { + category: 'remboursement', + patterns: [ + /\bretour\b/i, + /\brendu\b/i, + /\bremboursement\b/i, + /\bdevolución\b/i, + /\bdevolucio\b/i, + /\brimborso\b/i, + /\brégul\b/i, + /\bregulariz/i, + /double\s*paiement/i, + ], + }, + { + category: 'don', + patterns: [ + /\bdon\b/i, + /\bdonación\b/i, + /\bdonazione\b/i, + /\bdonacio\b/i, + /\bcadeau\b/i, + /\bgratitud/i, + /\bgratitude\b/i, + /\bmerci\b/i, + /\bgracias\b/i, + /\bgràcies\b/i, + /\bgracies\b/i, + /\bobrigad/i, + /\bthank/i, + /\bgràcia/i, + /\bgrazie\b/i, + /\bgrazie\b/i, + /\bbienvenu/i, + /\bwelcome\b/i, + /\bchukurei\b/i, + ], + }, + { + category: 'alimentation', + patterns: [ + /\brepas\b/i, + /\bpaella\b/i, + /\bcrêpe\b/i, + /\bcrepe\b/i, + /\bfalafel\b/i, + /\bpain\b/i, + /\bpan\b/i, + /\bgâteau/i, + /\bgateau\b/i, + /\bgalleta/i, + /\bpastis\b/i, + /\bpastel\b/i, + /\bburger\b/i, + /\bkombucha\b/i, + /\bœuf/i, + /\boeufs?\b/i, + /\bhuevo/i, + /\bfromage\b/i, + /\bflan\b/i, + /\balgue/i, + /\blegum/i, + /\bfruits?\b/i, + /\bpomme/i, + /\blimonad/i, + /\blimonada\b/i, + /\blégumin/i, + /\bporro\b/i, + /\bcarbassa\b/i, + /\bsobrasada\b/i, + /\biarmelada\b/i, + /\bnispero/i, + /\bbizcocho\b/i, + /\bchocolat/i, + /\balmendra/i, + /\bincienso/i, + /\bincens/i, + /alimentation/i, + /\bépice/i, + /\bcava\b/i, + /\bvin\b/i, + /\baceit/i, + /huile\s*d.?olive/i, + /\bgerminado/i, + ], + }, + { + category: 'soin', + patterns: [ + /\bsoin\b/i, + /\bmassage\b/i, + /\bbaume\b/i, + /\bhuile\s*essenti/i, + /\btisane\b/i, + /\bterapia\b/i, + /\bthérapie\b/i, + /\bherboristerie\b/i, + /\bplante/i, + /\bhomeopat/i, + /\baromath/i, + /\breiki\b/i, + /\bacupunct/i, + /\bostéo/i, + /\bkinesio/i, + /\btirage\b/i, + /\bcart(e|as)\b/i, + /\bmandalas?\b/i, + /\bconsoude\b/i, + /\bsauge\b/i, + /\bromarin\b/i, + /\bserum\b/i, + /\bsérum\b/i, + /\bpeeling\b/i, + /\bbifasico\b/i, + /\bormus\b/i, + /eau\s*de\s*mer/i, + ], + }, + { + category: 'vetements', + patterns: [ + /\bjupe\b/i, + /\bpantalon\b/i, + /\bblouson\b/i, + /\bchaussure/i, + /\bvêtement/i, + /\bropa\b/i, + /\bcardigan\b/i, + /\bmanteau\b/i, + /\bchemise\b/i, + /\btricot\b/i, + /\blaine\b/i, + /\btissus?\b/i, + ], + }, + { + category: 'culture', + patterns: [ + /\blivre\b/i, + /\blivres\b/i, + /\blibro\b/i, + /\bmusique\b/i, + /\bmusica\b/i, + /\bmúsica\b/i, + /\bconcierto\b/i, + /\bconcert\b/i, + /\bcd\b/i, + /\bvídeo\b/i, + /\bvideo\b/i, + /\bpelícula\b/i, + /\bfilm\b/i, + /\bpoème\b/i, + /\bpoema\b/i, + /\bbd\b/i, + /\bdessin\b/i, + /\bnexus\b/i, + /\bnaruto\b/i, + ], + }, + { + category: 'evenement', + patterns: [ + /\bg1ntada\b/i, + /\bğ1ntada\b/i, + /\bmercat\b/i, + /\bmarché\b/i, + /\bjornadas?\b/i, + /\bfestival\b/i, + /\bfête\b/i, + /\bfiesta\b/i, + /\brassemblement\b/i, + /\bcampillo\b/i, + /\beutopia\b/i, + /\brencontre\b/i, + ], + }, + { + category: 'service', + patterns: [ + /\bservice\b/i, + /\batelier\b/i, + /\baccompagnement\b/i, + /\btravaux\b/i, + /\bbarnum\b/i, + /\baccueil\b/i, + /\bhébergement\b/i, + /\blogement\b/i, + /\bnuit\b/i, + /\bnuits\b/i, + /\bnit\b/i, + /\bnits\b/i, + /\bvisita\b/i, + /\bamortigua/i, + /\bamortisseur/i, + /\bréparation\b/i, + ], + }, +]; + +export function parseComment(remark: string | null): TxCategory { + if (!remark) return 'autre'; + const text = remark.trim(); + if (!text) return 'autre'; + + for (const rule of RULES) { + if (rule.patterns.some((p) => p.test(text))) { + return rule.category; + } + } + return 'autre'; +} + +// --------------------------------------------------------------------------- +// Agrégation sur un tableau de catégories +// --------------------------------------------------------------------------- + +export interface CategoryCount { + category: TxCategory; + count: number; + volume: number; +} + +export function aggregateCategories( + items: { category: TxCategory; amount: number }[] +): CategoryCount[] { + const map = new Map(); + for (const { category, amount } of items) { + if (!map.has(category)) map.set(category, { category, count: 0, volume: 0 }); + const entry = map.get(category)!; + entry.count++; + entry.volume += amount; + } + return [...map.values()].sort((a, b) => b.count - a.count); +} diff --git a/src/data/mockData.ts b/src/data/mockData.ts index bfb92f9..c363782 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -1,3 +1,5 @@ +import type { TxCategory } from './commentParser'; + export interface Transaction { id: string; timestamp: number; // Unix ms (entier) @@ -8,6 +10,8 @@ export interface Transaction { countryCode: string; // ISO 3166-1 alpha-2, ex: "FR" fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars toKey: string; + comment: string | null; + category: TxCategory; } // French + European cities where Ğ1 is used @@ -79,6 +83,8 @@ function generateTransactions(count: number, maxAgeMs: number): Transaction[] { countryCode: 'FR', fromKey: generateKey(), toKey: generateKey(), + comment: null, + category: 'autre', }); } @@ -114,7 +120,20 @@ export function computeStats(transactions: Transaction[]) { .slice(0, 3) .map(([name, data]) => ({ name, ...data })); - return { totalVolume, transactionCount, topCities }; + const catMap = new Map(); + let commentedCount = 0; + for (const tx of transactions) { + if (tx.comment) commentedCount++; + const entry = catMap.get(tx.category) ?? { count: 0, volume: 0 }; + entry.count++; + entry.volume += tx.amount; + catMap.set(tx.category, entry); + } + const categoryBreakdown = [...catMap.entries()] + .map(([category, v]) => ({ category, ...v })) + .sort((a, b) => b.count - a.count); + + return { totalVolume, transactionCount, topCities, categoryBreakdown, commentedCount }; } export type { }; diff --git a/src/services/DataService.ts b/src/services/DataService.ts index 1055cd4..5a7fb5d 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -14,6 +14,7 @@ import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD, ss58ToDuniterKey, fetchActiveMemberKeys } from './adapters/SubsquidAdapter'; import { resolveGeoByKeys, resolveGeoByKeysBatched, cleanCityName } from './adapters/CesiumAdapter'; +import { parseComment } from '../data/commentParser'; import { getTransactionsForPeriod, computeStats, @@ -112,6 +113,8 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ countryCode: fromGeo.countryCode, fromKey: t.fromId, toKey: t.toId, + comment: t.comment, + category: parseComment(t.comment), }); // Arc : les deux extrémités géolocalisées + villes différentes @@ -133,6 +136,8 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ toLat: toGeo.lat, toLng: toGeo.lng, toCity, toCountry: toGeo.countryCode, toKey: t.toId, + comment: t.comment, + category: parseComment(t.comment), }); } @@ -192,6 +197,8 @@ export interface PeriodStats { transactionCount: number; // total blockchain (y compris non-géolocalisés) geoCount: number; // transactions visibles sur la carte topCities: { name: string; volume: number; count: number; countryCode: string }[]; + categoryBreakdown: { category: import('../data/commentParser').TxCategory; count: number; volume: number }[]; + commentedCount: number; // nb de transactions avec un commentaire } export interface DataResult { @@ -233,6 +240,8 @@ export async function fetchData(periodDays: number): Promise { transactionCount: totalCount, geoCount: geolocated.length, topCities: base.topCities, + categoryBreakdown: base.categoryBreakdown, + commentedCount: base.commentedCount, }, source: 'live', currentUD, diff --git a/src/services/adapters/SubsquidAdapter.ts b/src/services/adapters/SubsquidAdapter.ts index 19d2ff3..af70ca8 100644 --- a/src/services/adapters/SubsquidAdapter.ts +++ b/src/services/adapters/SubsquidAdapter.ts @@ -29,6 +29,7 @@ const SubsquidTransferNodeSchema = z.object({ from: z.object({ linkedIdentity: z.object({ name: z.string() }).nullable(), }).nullable(), + comment: z.object({ remark: z.string() }).nullable().optional(), }); const SubsquidResponseSchema = z.object({ @@ -52,6 +53,7 @@ export interface RawTransfer { fromId: string; toId: string; fromName: string; // nom d'identité Ğ1 de l'émetteur (peut être vide) + comment: string | null; } // --------------------------------------------------------------------------- @@ -77,6 +79,9 @@ const TRANSFERS_QUERY = ` name } } + comment { + remark + } } } } @@ -253,6 +258,7 @@ export async function fetchTransfers( fromId: node.fromId ?? '', toId: node.toId ?? '', fromName: node.from?.linkedIdentity?.name ?? '', + comment: node.comment?.remark ?? null, })), }; } -- 2.39.5 From 6b42a751406bf7e1c32e2bf9efa6bf849c17b5ce Mon Sep 17 00:00:00 2001 From: syoul Date: Wed, 22 Apr 2026 00:02:56 +0200 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20r=C3=A9duire=20la=20fr=C3=A9quence?= =?UTF-8?q?=20de=20polling=20=E2=80=94=20data=202=20min,=20statut=2060s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 2 +- src/hooks/useServiceStatus.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index efa340f..8fff58c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -117,7 +117,7 @@ export default function App() { }; load(true); - const interval = setInterval(() => load(false), 30_000); + const interval = setInterval(() => load(false), 120_000); return () => { cancelled = true; clearInterval(interval); }; }, [periodDays, endpointVersion]); diff --git a/src/hooks/useServiceStatus.ts b/src/hooks/useServiceStatus.ts index 49b614c..8bc6fdb 100644 --- a/src/hooks/useServiceStatus.ts +++ b/src/hooks/useServiceStatus.ts @@ -17,7 +17,7 @@ export interface ServicesStatus { const TIMEOUT_MS = 8_000; const SLOW_THRESHOLD_MS = 2_000; -const POLL_INTERVAL_MS = 30_000; +const POLL_INTERVAL_MS = 60_000; async function pingSubsquid(url: string): Promise { const controller = new AbortController(); -- 2.39.5