dev #1
+3
-1
@@ -34,6 +34,7 @@ export default function App() {
|
|||||||
const [showMembers, setShowMembers] = useState(false);
|
const [showMembers, setShowMembers] = useState(false);
|
||||||
const [memberCities, setMemberCities] = useState<MemberCity[]>([]);
|
const [memberCities, setMemberCities] = useState<MemberCity[]>([]);
|
||||||
const [membersLoading, setMembersLoading] = useState(false);
|
const [membersLoading, setMembersLoading] = useState(false);
|
||||||
|
const [endpointVersion, setEndpointVersion] = useState(0);
|
||||||
const isMobile = useMediaQuery('(max-width: 639px)');
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
||||||
|
|
||||||
const toggleMembers = async () => {
|
const toggleMembers = async () => {
|
||||||
@@ -119,7 +120,7 @@ export default function App() {
|
|||||||
const interval = setInterval(() => load(false), 30_000);
|
const interval = setInterval(() => load(false), 30_000);
|
||||||
|
|
||||||
return () => { cancelled = true; clearInterval(interval); };
|
return () => { cancelled = true; clearInterval(interval); };
|
||||||
}, [periodDays]);
|
}, [periodDays, endpointVersion]);
|
||||||
|
|
||||||
// Stats heatmap sur la fenêtre courante en mode animation
|
// Stats heatmap sur la fenêtre courante en mode animation
|
||||||
const visibleStats: PeriodStats | null = animation.active
|
const visibleStats: PeriodStats | null = animation.active
|
||||||
@@ -150,6 +151,7 @@ export default function App() {
|
|||||||
flowStats,
|
flowStats,
|
||||||
focusCity,
|
focusCity,
|
||||||
allTimestamps,
|
allTimestamps,
|
||||||
|
onEndpointChange: () => setEndpointVersion((v) => v + 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import {
|
||||||
|
KNOWN_SUBSQUID_NODES,
|
||||||
|
KNOWN_CESIUM_NODES,
|
||||||
|
getSubsquidUrl,
|
||||||
|
getCesiumUrl,
|
||||||
|
setSubsquidUrl,
|
||||||
|
setCesiumUrl,
|
||||||
|
} from '../services/EndpointConfig';
|
||||||
|
import { testEndpoint } from '../hooks/useServiceStatus';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
service: 'subsquid' | 'cesium';
|
||||||
|
onClose: () => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestResult {
|
||||||
|
url: string;
|
||||||
|
state: 'testing' | 'ok' | 'slow' | 'error';
|
||||||
|
latencyMs: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LABELS = { subsquid: 'SubSquid', cesium: 'Cesium+' };
|
||||||
|
|
||||||
|
export function EndpointPopover({ service, onClose, onSaved }: Props) {
|
||||||
|
const currentUrl = service === 'subsquid' ? getSubsquidUrl() : getCesiumUrl();
|
||||||
|
const knownNodes = service === 'subsquid' ? KNOWN_SUBSQUID_NODES : KNOWN_CESIUM_NODES;
|
||||||
|
|
||||||
|
const [inputUrl, setInputUrl] = useState(currentUrl);
|
||||||
|
const [testResults, setTestResults] = useState<Map<string, TestResult>>(new Map());
|
||||||
|
|
||||||
|
const runTest = async (url: string) => {
|
||||||
|
setTestResults((prev) => new Map(prev).set(url, { url, state: 'testing', latencyMs: null }));
|
||||||
|
try {
|
||||||
|
const ms = await testEndpoint(service, url);
|
||||||
|
setTestResults((prev) =>
|
||||||
|
new Map(prev).set(url, { url, state: ms < 2000 ? 'ok' : 'slow', latencyMs: ms })
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
setTestResults((prev) => new Map(prev).set(url, { url, state: 'error', latencyMs: null }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const trimmed = inputUrl.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
if (service === 'subsquid') setSubsquidUrl(trimmed);
|
||||||
|
else setCesiumUrl(trimmed);
|
||||||
|
onSaved();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dot = (state: TestResult['state']) => {
|
||||||
|
if (state === 'testing') return <span className="text-[#6b7280] animate-pulse">●</span>;
|
||||||
|
if (state === 'ok') return <span className="text-emerald-400">●</span>;
|
||||||
|
if (state === 'slow') return <span className="text-amber-400">●</span>;
|
||||||
|
return <span className="text-red-500">●</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
>
|
||||||
|
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6 space-y-5">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-white font-bold text-base">
|
||||||
|
Configurer {LABELS[service]}
|
||||||
|
</h2>
|
||||||
|
<button onClick={onClose} className="text-[#4b5563] hover:text-white transition-colors text-xl leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nœuds connus */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">Nœuds connus</p>
|
||||||
|
{knownNodes.map((node) => {
|
||||||
|
const result = testResults.get(node.url);
|
||||||
|
const isActive = inputUrl === node.url;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={node.url}
|
||||||
|
className={`flex items-center justify-between rounded-xl border px-3 py-2.5 cursor-pointer transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'border-[#d4a843]/60 bg-[#d4a843]/5'
|
||||||
|
: 'border-[#1e1f2a] hover:border-[#2e2f3a]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setInputUrl(node.url)}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-white text-sm font-medium truncate">{node.label}</p>
|
||||||
|
<p className="text-[#4b5563] text-xs font-mono truncate">{node.url}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 ml-3 shrink-0">
|
||||||
|
{result && (
|
||||||
|
<span className="text-xs font-mono text-[#6b7280]">
|
||||||
|
{dot(result.state)}
|
||||||
|
{result.latencyMs !== null && ` ${result.latencyMs} ms`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); runTest(node.url); }}
|
||||||
|
className="text-xs text-[#4b5563] hover:text-[#d4a843] transition-colors px-2 py-1 border border-[#2e2f3a] rounded-lg"
|
||||||
|
>
|
||||||
|
Tester
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* URL personnalisée */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">URL personnalisée</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={inputUrl}
|
||||||
|
onChange={(e) => setInputUrl(e.target.value)}
|
||||||
|
placeholder={service === 'subsquid' ? 'https://…/v1/graphql' : 'https://…'}
|
||||||
|
className="flex-1 bg-[#0a0b0f] border border-[#2e2f3a] rounded-xl px-3 py-2 text-white text-sm font-mono placeholder-[#2e2f3a] focus:outline-none focus:border-[#d4a843]/60 transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => runTest(inputUrl.trim())}
|
||||||
|
disabled={!inputUrl.trim()}
|
||||||
|
className="text-xs text-[#4b5563] hover:text-[#d4a843] disabled:opacity-30 transition-colors px-3 py-2 border border-[#2e2f3a] rounded-xl"
|
||||||
|
>
|
||||||
|
Tester
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const result = testResults.get(inputUrl.trim());
|
||||||
|
if (!result || knownNodes.some((n) => n.url === inputUrl.trim())) return null;
|
||||||
|
return (
|
||||||
|
<p className="text-xs font-mono text-[#6b7280] pl-1">
|
||||||
|
{dot(result.state)}
|
||||||
|
{result.state === 'testing' && ' Test en cours…'}
|
||||||
|
{result.state === 'ok' && ` OK · ${result.latencyMs} ms`}
|
||||||
|
{result.state === 'slow' && ` Lent · ${result.latencyMs} ms`}
|
||||||
|
{result.state === 'error' && ' Inaccessible'}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm text-[#4b5563] hover:text-white border border-[#2e2f3a] rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!inputUrl.trim()}
|
||||||
|
className="px-4 py-2 text-sm font-medium bg-[#d4a843] text-[#0a0b0f] rounded-xl hover:bg-[#e0b84d] disabled:opacity-30 transition-colors"
|
||||||
|
>
|
||||||
|
Utiliser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -127,9 +127,25 @@ export function InfoPanel({ onClose }: InfoPanelProps) {
|
|||||||
</Feature>
|
</Feature>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Overlay Dividende Universel">
|
||||||
|
<Feature icon="DU" name="Membres actifs géolocalisés">
|
||||||
|
Le bouton <Kbd>DU</Kbd> (à gauche de la carte) affiche en overlay les membres Ğ1
|
||||||
|
actifs (WoT) ayant un profil Cesium+ géolocalisé.
|
||||||
|
Chaque point représente une ville avec des membres actifs.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title="Source de données">
|
<Section title="Source de données">
|
||||||
<Feature icon="●" name="Live Ğ1v2">
|
<Feature icon="●" name="Live Ğ1v2">
|
||||||
Données temps réel de la blockchain Ğ1v2, actualisées toutes les 30 secondes.
|
Données temps réel de la blockchain Ğ1v2 via SubSquid, actualisées toutes les 30 secondes.
|
||||||
|
Les profils de géolocalisation sont fournis par Cesium+.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="●" name="Statut des services">
|
||||||
|
Deux indicateurs en bas du panneau latéral affichent l'état de SubSquid et Cesium+ en temps réel
|
||||||
|
(<span className="text-emerald-400">vert</span> OK ·{' '}
|
||||||
|
<span className="text-amber-400">jaune</span> lent ·{' '}
|
||||||
|
<span className="text-red-400">rouge</span> inaccessible).
|
||||||
|
Un clic sur un indicateur permet de configurer ou changer l'endpoint.
|
||||||
</Feature>
|
</Feature>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { ServiceStatus, ServiceState } from '../hooks/useServiceStatus';
|
||||||
|
import { EndpointPopover } from './EndpointPopover';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
subsquid: ServiceStatus;
|
||||||
|
cesium: ServiceStatus;
|
||||||
|
onEndpointChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATE_COLOR: Record<ServiceState, string> = {
|
||||||
|
checking: 'text-[#4b5563] animate-pulse',
|
||||||
|
ok: 'text-emerald-400',
|
||||||
|
slow: 'text-amber-400',
|
||||||
|
error: 'text-red-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
function Dot({ status, label, onClick }: { status: ServiceStatus; label: string; onClick: () => void }) {
|
||||||
|
const latency = status.latencyMs !== null ? ` · ${status.latencyMs} ms` : '';
|
||||||
|
const title = `${label} — ${STATUS_LABEL_FULL[status.state]}${latency}\n${status.url}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
title={title}
|
||||||
|
className="flex items-center gap-1 text-[10px] font-mono text-[#4b5563] hover:text-white transition-colors group"
|
||||||
|
>
|
||||||
|
<span className={STATE_COLOR[status.state]}>●</span>
|
||||||
|
<span className="group-hover:text-[#6b7280]">
|
||||||
|
{label}
|
||||||
|
{status.latencyMs !== null && (
|
||||||
|
<span className="text-[#2e2f3a] ml-0.5">{status.latencyMs}ms</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABEL_FULL: Record<ServiceState, string> = {
|
||||||
|
checking: 'Vérification…',
|
||||||
|
ok: 'Accessible',
|
||||||
|
slow: 'Réponse lente',
|
||||||
|
error: 'Inaccessible',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ServiceStatusDots({ subsquid, cesium, onEndpointChange }: Props) {
|
||||||
|
const [popover, setPopover] = useState<'subsquid' | 'cesium' | null>(null);
|
||||||
|
|
||||||
|
const hasError = subsquid.state === 'error' || cesium.state === 'error';
|
||||||
|
const erroredService = subsquid.state === 'error' ? 'SubSquid' : 'Cesium+';
|
||||||
|
const erroredKey: 'subsquid' | 'cesium' = subsquid.state === 'error' ? 'subsquid' : 'cesium';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Bannière d'erreur */}
|
||||||
|
{hasError && (
|
||||||
|
<div className="flex items-center justify-between bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2 text-xs">
|
||||||
|
<span className="text-red-400">
|
||||||
|
<span className="text-red-500 mr-1">●</span>
|
||||||
|
{erroredService} inaccessible
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setPopover(erroredKey)}
|
||||||
|
className="text-[#d4a843] hover:text-[#e0b84d] transition-colors font-medium ml-2"
|
||||||
|
>
|
||||||
|
Configurer →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dots */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Dot status={subsquid} label="SubSquid" onClick={() => setPopover('subsquid')} />
|
||||||
|
<Dot status={cesium} label="Cesium+" onClick={() => setPopover('cesium')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Popover de configuration */}
|
||||||
|
{popover && (
|
||||||
|
<EndpointPopover
|
||||||
|
service={popover}
|
||||||
|
onClose={() => setPopover(null)}
|
||||||
|
onSaved={onEndpointChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { useRef } from 'react';
|
|||||||
import type { PeriodStats } from '../services/DataService';
|
import type { PeriodStats } from '../services/DataService';
|
||||||
import type { FlowStats } from '../data/arcData';
|
import type { FlowStats } from '../data/arcData';
|
||||||
import { Sparkline } from './Sparkline';
|
import { Sparkline } from './Sparkline';
|
||||||
|
import { ServiceStatusDots } from './ServiceStatusDots';
|
||||||
|
import { useServiceStatus } from '../hooks/useServiceStatus';
|
||||||
|
|
||||||
interface StatsPanelProps {
|
interface StatsPanelProps {
|
||||||
stats: PeriodStats | null;
|
stats: PeriodStats | null;
|
||||||
@@ -15,6 +17,7 @@ interface StatsPanelProps {
|
|||||||
flowStats?: FlowStats | null;
|
flowStats?: FlowStats | null;
|
||||||
focusCity?: string | null;
|
focusCity?: string | null;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
onEndpointChange?: () => void;
|
||||||
allTimestamps?: number[];
|
allTimestamps?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,8 @@ function CityRow({ city, volume, count, countryCode, accent }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose, className, allTimestamps = [] }: StatsPanelProps) {
|
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose, onEndpointChange, className, allTimestamps = [] }: StatsPanelProps) {
|
||||||
|
const { subsquid, cesium, recheck } = useServiceStatus();
|
||||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||||
const prevStats = useRef<PeriodStats | null>(null);
|
const prevStats = useRef<PeriodStats | null>(null);
|
||||||
|
|
||||||
@@ -97,7 +101,11 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim
|
|||||||
Ğ1Flux
|
Ğ1Flux
|
||||||
<span className="text-[#4b5563] text-xs font-normal ml-1.5">v{__APP_VERSION__}</span>
|
<span className="text-[#4b5563] text-xs font-normal ml-1.5">v{__APP_VERSION__}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[#4b5563] text-xs">Monnaie libre · Flux géo</p>
|
<ServiceStatusDots
|
||||||
|
subsquid={subsquid}
|
||||||
|
cesium={cesium}
|
||||||
|
onEndpointChange={() => { recheck(); onEndpointChange?.(); }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { getSubsquidUrl, getCesiumUrl } from '../services/EndpointConfig';
|
||||||
|
|
||||||
|
export type ServiceState = 'checking' | 'ok' | 'slow' | 'error';
|
||||||
|
|
||||||
|
export interface ServiceStatus {
|
||||||
|
state: ServiceState;
|
||||||
|
latencyMs: number | null;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServicesStatus {
|
||||||
|
subsquid: ServiceStatus;
|
||||||
|
cesium: ServiceStatus;
|
||||||
|
recheck: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 8_000;
|
||||||
|
const SLOW_THRESHOLD_MS = 2_000;
|
||||||
|
const POLL_INTERVAL_MS = 30_000;
|
||||||
|
|
||||||
|
async function pingSubsquid(url: string): Promise<number> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ query: '{ __typename }' }),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
await res.json();
|
||||||
|
return Date.now() - start;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pingCesium(url: string): Promise<number> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${url}/user/profile/_search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ size: 0, query: { match_all: {} } }),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return Date.now() - start;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function latencyToState(ms: number): ServiceState {
|
||||||
|
if (ms < SLOW_THRESHOLD_MS) return 'ok';
|
||||||
|
return 'slow';
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHECKING: ServiceStatus = { state: 'checking', latencyMs: null, url: '' };
|
||||||
|
|
||||||
|
export function useServiceStatus(): ServicesStatus {
|
||||||
|
const [subsquid, setSubsquid] = useState<ServiceStatus>(CHECKING);
|
||||||
|
const [cesium, setCesium] = useState<ServiceStatus>(CHECKING);
|
||||||
|
const [version, setVersion] = useState(0);
|
||||||
|
const cancelled = useRef(false);
|
||||||
|
|
||||||
|
const runChecks = useCallback(async () => {
|
||||||
|
const squidUrl = getSubsquidUrl();
|
||||||
|
const cesiumUrl = getCesiumUrl();
|
||||||
|
|
||||||
|
setSubsquid({ state: 'checking', latencyMs: null, url: squidUrl });
|
||||||
|
setCesium( { state: 'checking', latencyMs: null, url: cesiumUrl });
|
||||||
|
|
||||||
|
const [squidResult, cesiumResult] = await Promise.allSettled([
|
||||||
|
pingSubsquid(squidUrl),
|
||||||
|
pingCesium(cesiumUrl),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (cancelled.current) return;
|
||||||
|
|
||||||
|
if (squidResult.status === 'fulfilled') {
|
||||||
|
const ms = squidResult.value;
|
||||||
|
setSubsquid({ state: latencyToState(ms), latencyMs: ms, url: squidUrl });
|
||||||
|
} else {
|
||||||
|
setSubsquid({ state: 'error', latencyMs: null, url: squidUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cesiumResult.status === 'fulfilled') {
|
||||||
|
const ms = cesiumResult.value;
|
||||||
|
setCesium({ state: latencyToState(ms), latencyMs: ms, url: cesiumUrl });
|
||||||
|
} else {
|
||||||
|
setCesium({ state: 'error', latencyMs: null, url: cesiumUrl });
|
||||||
|
}
|
||||||
|
}, [version]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cancelled.current = false;
|
||||||
|
runChecks();
|
||||||
|
const interval = setInterval(runChecks, POLL_INTERVAL_MS);
|
||||||
|
return () => {
|
||||||
|
cancelled.current = true;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [runChecks]);
|
||||||
|
|
||||||
|
const recheck = useCallback(() => setVersion((v) => v + 1), []);
|
||||||
|
|
||||||
|
return { subsquid, cesium, recheck };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testEndpoint(type: 'subsquid' | 'cesium', url: string): Promise<number> {
|
||||||
|
return type === 'subsquid' ? pingSubsquid(url) : pingCesium(url);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const STORAGE_KEY = {
|
||||||
|
subsquid: 'geoflux-ep-subsquid',
|
||||||
|
cesium: 'geoflux-ep-cesium',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DEFAULT_ENDPOINTS = {
|
||||||
|
subsquid: 'https://squidv2s.syoul.fr/v1/graphql',
|
||||||
|
cesium: 'https://g1.data.e-is.pro',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const KNOWN_SUBSQUID_NODES: { label: string; url: string }[] = [
|
||||||
|
{ label: 'squidv2s.syoul.fr (défaut)', url: 'https://squidv2s.syoul.fr/v1/graphql' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const KNOWN_CESIUM_NODES: { label: string; url: string }[] = [
|
||||||
|
{ label: 'g1.data.e-is.pro (défaut)', url: 'https://g1.data.e-is.pro' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getSubsquidUrl(): string {
|
||||||
|
return localStorage.getItem(STORAGE_KEY.subsquid) ?? DEFAULT_ENDPOINTS.subsquid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCesiumUrl(): string {
|
||||||
|
return localStorage.getItem(STORAGE_KEY.cesium) ?? DEFAULT_ENDPOINTS.cesium;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSubsquidUrl(url: string): void {
|
||||||
|
if (url === DEFAULT_ENDPOINTS.subsquid) localStorage.removeItem(STORAGE_KEY.subsquid);
|
||||||
|
else localStorage.setItem(STORAGE_KEY.subsquid, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCesiumUrl(url: string): void {
|
||||||
|
if (url === DEFAULT_ENDPOINTS.cesium) localStorage.removeItem(STORAGE_KEY.cesium);
|
||||||
|
else localStorage.setItem(STORAGE_KEY.cesium, url);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getCesiumUrl } from '../EndpointConfig';
|
||||||
|
|
||||||
export const CESIUM_ENDPOINT = 'https://g1.data.e-is.pro';
|
export const CESIUM_ENDPOINT = 'https://g1.data.e-is.pro';
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ export async function resolveGeoByKeys(
|
|||||||
_source: ['title', 'city', 'geoPoint'],
|
_source: ['title', 'city', 'geoPoint'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${CESIUM_ENDPOINT}/user/profile/_search`, {
|
const response = await fetch(`${getCesiumUrl()}/user/profile/_search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(query),
|
body: JSON.stringify(query),
|
||||||
@@ -210,7 +211,7 @@ export async function resolveGeoByNames(
|
|||||||
_source: ['title', 'city', 'geoPoint'],
|
_source: ['title', 'city', 'geoPoint'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${CESIUM_ENDPOINT}/user/profile/_search`, {
|
const response = await fetch(`${getCesiumUrl()}/user/profile/_search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(query),
|
body: JSON.stringify(query),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getSubsquidUrl } from '../EndpointConfig';
|
||||||
|
|
||||||
export const SUBSQUID_ENDPOINT = 'https://squidv2s.syoul.fr/v1/graphql';
|
export const SUBSQUID_ENDPOINT = 'https://squidv2s.syoul.fr/v1/graphql';
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ const IDENTITY_KEY_MAP_QUERY = `
|
|||||||
* car previousId = clé génesis = clé Ed25519 v1 = _id dans Cesium+
|
* car previousId = clé génesis = clé Ed25519 v1 = _id dans Cesium+
|
||||||
*/
|
*/
|
||||||
export async function buildIdentityKeyMap(): Promise<Map<string, string>> {
|
export async function buildIdentityKeyMap(): Promise<Map<string, string>> {
|
||||||
const response = await fetch(SUBSQUID_ENDPOINT, {
|
const response = await fetch(getSubsquidUrl(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ query: IDENTITY_KEY_MAP_QUERY }),
|
body: JSON.stringify({ query: IDENTITY_KEY_MAP_QUERY }),
|
||||||
@@ -157,7 +158,7 @@ export async function buildIdentityKeyMap(): Promise<Map<string, string>> {
|
|||||||
export async function fetchCurrentUD(): Promise<number> {
|
export async function fetchCurrentUD(): Promise<number> {
|
||||||
const UD_FALLBACK = 11.78; // valeur au bloc 225874 — mis à jour si la requête échoue
|
const UD_FALLBACK = 11.78; // valeur au bloc 225874 — mis à jour si la requête échoue
|
||||||
try {
|
try {
|
||||||
const response = await fetch(SUBSQUID_ENDPOINT, {
|
const response = await fetch(getSubsquidUrl(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -193,7 +194,7 @@ const ACTIVE_MEMBERS_QUERY = `
|
|||||||
|
|
||||||
/** Retourne la liste des clés SS58 de tous les membres WoT actifs. */
|
/** Retourne la liste des clés SS58 de tous les membres WoT actifs. */
|
||||||
export async function fetchActiveMemberKeys(): Promise<string[]> {
|
export async function fetchActiveMemberKeys(): Promise<string[]> {
|
||||||
const res = await fetch(SUBSQUID_ENDPOINT, {
|
const res = await fetch(getSubsquidUrl(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ query: ACTIVE_MEMBERS_QUERY }),
|
body: JSON.stringify({ query: ACTIVE_MEMBERS_QUERY }),
|
||||||
@@ -222,7 +223,7 @@ export async function fetchTransfers(
|
|||||||
Date.now() - periodDays * 24 * 60 * 60 * 1000
|
Date.now() - periodDays * 24 * 60 * 60 * 1000
|
||||||
).toISOString();
|
).toISOString();
|
||||||
|
|
||||||
const response = await fetch(SUBSQUID_ENDPOINT, {
|
const response = await fetch(getSubsquidUrl(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user