10 Commits

Author SHA1 Message Date
syoul ac168c3689 Merge branch 'dev'
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-28 11:52:27 +01:00
syoul 8f9a11c4e8 chore: bump version to 1.4.1
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:50:31 +01:00
syoul 63f50d5762 feat: géolocaliser les comptes non-membres via Cesium+
ci/woodpecker/push/woodpecker Pipeline was successful
Pour les fromId/toId absents du keyMap WoT, applique ss58ToDuniterKey
directement pour tenter un lookup Cesium+. Les non-membres ayant un
profil géolocalisé (ex: comptes portefeuille avec ville renseignée)
apparaissent désormais dans le flux animé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:38:31 +01:00
syoul 9f3752b621 chore: merge dev → main v1.4.0
ci/woodpecker/push/woodpecker Pipeline was successful
- feat: bouton ℹ flottant isolé sous ☰ (mobile) / top-left (desktop)
- fix: supprimer label 'Vitesse' + bouton ✕ AnimationPlayer
- fix: couleur émetteurs rouge #e53935 (meilleur contraste vs or)
- fix: InfoPanel — dégradés or→rouge / or→vert documentés
- docs: features-roadmap.md (14 features planifiées)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:31:49 +01:00
syoul 6fc1705f6d chore: bump version to 1.4.0
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:31:36 +01:00
syoul 15807c7bcb fix: InfoPanel — couleur émetteurs rouge (dégradé or→rouge) + description dégradés
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:27:16 +01:00
syoul bac113e51b fix: couleur émetteurs #e53935 (rouge) au lieu de #ff6d00 (orange)
ci/woodpecker/push/woodpecker Pipeline was successful
Meilleur contraste avec la couleur neutre or #d4a843.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:22:45 +01:00
syoul 6d01c8d29e fix: supprimer bouton ✕ de l'AnimationPlayer (fermeture via ▶ Animer)
ci/woodpecker/push/woodpecker Pipeline was successful
Économise une ligne sur mobile. onClose retiré de l'interface et du JSX.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:19:52 +01:00
syoul 46b11710cc fix: supprimer label 'Vitesse' dans AnimationPlayer (gain de place mobile)
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:15:41 +01:00
syoul 78ede01d11 feat: déplacer bouton ℹ hors du PeriodSelector → bouton flottant isolé
ci/woodpecker/push/woodpecker Pipeline was successful
- Sous ☰ sur mobile (top-16 left-4), top-4 left-4 sur desktop
- PeriodSelector : suppression prop onInfo + bouton ℹ intégré

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:12:24 +01:00
7 changed files with 25 additions and 34 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "g1flux", "name": "g1flux",
"private": true, "private": true,
"version": "1.3.2", "version": "1.4.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+9 -2
View File
@@ -131,6 +131,15 @@ export default function App() {
</button> </button>
)} )}
{/* Bouton info — sous ☰ sur mobile, top-left sur desktop */}
<button
onClick={() => setInfoOpen(true)}
className={`absolute ${isMobile ? 'top-16' : 'top-4'} left-4 z-[1001] w-10 h-10 bg-[#0a0b0f]/90 backdrop-blur-sm border border-[#2e2f3a] rounded-xl flex items-center justify-center text-[#6b7280] hover:text-[#d4a843] transition-colors text-base`}
aria-label="Aide"
>
</button>
{/* Period selector — floating over map */} {/* Period selector — floating over map */}
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}> <div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
<PeriodSelector <PeriodSelector
@@ -143,7 +152,6 @@ export default function App() {
geoPercent={visibleStats && visibleStats.transactionCount > 0 geoPercent={visibleStats && visibleStats.transactionCount > 0
? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100) ? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100)
: null} : null}
onInfo={() => setInfoOpen(true)}
/> />
</div> </div>
@@ -184,7 +192,6 @@ export default function App() {
onPlay={animation.play} onPlay={animation.play}
onPause={animation.pause} onPause={animation.pause}
onSpeedChange={animation.setSpeed} onSpeedChange={animation.setSpeed}
onClose={animation.deactivate}
/> />
)} )}
+1 -12
View File
@@ -9,7 +9,6 @@ interface AnimationPlayerProps {
onPlay: () => void; onPlay: () => void;
onPause: () => void; onPause: () => void;
onSpeedChange: (s: 1 | 2 | 4) => void; onSpeedChange: (s: 1 | 2 | 4) => void;
onClose: () => void;
} }
export function AnimationPlayer({ export function AnimationPlayer({
@@ -21,7 +20,6 @@ export function AnimationPlayer({
onPlay, onPlay,
onPause, onPause,
onSpeedChange, onSpeedChange,
onClose,
}: AnimationPlayerProps) { }: AnimationPlayerProps) {
const frame = frames[currentIndex]; const frame = frames[currentIndex];
@@ -78,8 +76,7 @@ export function AnimationPlayer({
{/* Speed selector */} {/* Speed selector */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-[#4b5563] text-xs mr-1">Vitesse</span> {([1, 2, 4] as const).map((s) => (
{([1, 2, 4] as const).map((s) => (
<button <button
key={s} key={s}
onClick={() => onSpeedChange(s)} onClick={() => onSpeedChange(s)}
@@ -94,14 +91,6 @@ export function AnimationPlayer({
))} ))}
</div> </div>
{/* Close */}
<button
onClick={onClose}
className="text-[#4b5563] hover:text-white transition-colors px-2 py-1 text-sm ml-2"
title="Quitter l'animation"
>
</button>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -17,7 +17,7 @@ function lerpColor(hex1: string, hex2: string, t: number): string {
} }
const COLOR_NEUTRAL = '#d4a843'; // or Ğ1 const COLOR_NEUTRAL = '#d4a843'; // or Ğ1
const COLOR_NEG = '#ff6d00'; // orange vif const COLOR_NEG = '#e53935'; // rouge vif
const COLOR_POS = '#00c853'; // vert vif const COLOR_POS = '#00c853'; // vert vif
const NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre const NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre
const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux villes const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux villes
+2 -2
View File
@@ -54,8 +54,8 @@ export function InfoPanel({ onClose }: InfoPanelProps) {
</Feature> </Feature>
<Feature icon="●" name="Couleur des nœuds"> <Feature icon="●" name="Couleur des nœuds">
<span className="text-green-400">Vert</span> = receveur net (reçoit plus que ce qu'il émet) ·{' '} <span className="text-green-400">Vert</span> = receveur net (reçoit plus que ce qu'il émet) ·{' '}
<span className="text-[#d4a843]">Or</span> = équilibré ·{' '} <span className="text-[#d4a843]">Or</span> = équilibré (dégradé or → vert selon l'excédent reçu) ·{' '}
<span className="text-orange-400">Orange</span> = émetteur net. <span className="text-[#e53935]">Rouge</span> = émetteur net (dégradé or rouge selon l'excédent émis).
</Feature> </Feature>
<Feature icon="↗" name="Clic sur un nœud"> <Feature icon="↗" name="Clic sur un nœud">
Affiche la liste des villes du cluster avec leur balance individuelle, Affiche la liste des villes du cluster avec leur balance individuelle,
+1 -11
View File
@@ -8,7 +8,6 @@ interface PeriodSelectorProps {
viewMode: 'heatmap' | 'flow'; viewMode: 'heatmap' | 'flow';
onViewModeChange: (mode: 'heatmap' | 'flow') => void; onViewModeChange: (mode: 'heatmap' | 'flow') => void;
geoPercent?: number | null; geoPercent?: number | null;
onInfo: () => void;
} }
const PERIODS = [ const PERIODS = [
@@ -19,7 +18,7 @@ const PERIODS = [
const PRESET_DAYS = new Set([1, 7, 30]); const PRESET_DAYS = new Set([1, 7, 30]);
export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent, onInfo }: PeriodSelectorProps) { export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent }: PeriodSelectorProps) {
const [customOpen, setCustomOpen] = useState(false); const [customOpen, setCustomOpen] = useState(false);
const [inputVal, setInputVal] = useState(''); const [inputVal, setInputVal] = useState('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -132,15 +131,6 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi
</span> </span>
)} )}
<div className="w-px mx-1 bg-[#2e2f3a] self-stretch" />
<button
onClick={onInfo}
className="px-2 py-1.5 rounded-md text-sm text-[#6b7280] hover:text-[#d4a843] hover:bg-[#1a1b23] transition-all duration-200 cursor-pointer leading-none"
aria-label="Aide"
>
</button>
</div> </div>
); );
} }
+10 -5
View File
@@ -12,7 +12,7 @@
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local * Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
*/ */
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD } from './adapters/SubsquidAdapter'; import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD, ss58ToDuniterKey } from './adapters/SubsquidAdapter';
import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter'; import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter';
import { import {
getTransactionsForPeriod, getTransactionsForPeriod,
@@ -69,9 +69,14 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
} }
// Clés Duniter uniques des émetteurs ET destinataires (un seul appel Cesium+) // Clés Duniter uniques des émetteurs ET destinataires (un seul appel Cesium+)
// Pour les membres WoT : via keyMap (genesis key = _id Cesium+)
// Pour les non-membres : conversion directe SS58 → Duniter key
const resolveKey = (ss58: string): string =>
keyMap.get(ss58) ?? ss58ToDuniterKey(ss58);
const allDuniterKeys = [...new Set([ const allDuniterKeys = [...new Set([
...rawTransfers.map((t) => keyMap.get(t.fromId)), ...rawTransfers.map((t) => t.fromId ? resolveKey(t.fromId) : undefined),
...rawTransfers.map((t) => keyMap.get(t.toId)), ...rawTransfers.map((t) => t.toId ? resolveKey(t.toId) : undefined),
].filter(Boolean) as string[])]; ].filter(Boolean) as string[])];
// Résolution géo par clé Duniter (_id Cesium+) // Résolution géo par clé Duniter (_id Cesium+)
@@ -89,7 +94,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
const arcs: TransactionArc[] = []; const arcs: TransactionArc[] = [];
for (const t of rawTransfers) { for (const t of rawTransfers) {
const fromDuniterKey = keyMap.get(t.fromId); const fromDuniterKey = t.fromId ? resolveKey(t.fromId) : undefined;
if (!fromDuniterKey) continue; if (!fromDuniterKey) continue;
const fromGeo = geoMap.get(fromDuniterKey); const fromGeo = geoMap.get(fromDuniterKey);
if (!fromGeo) continue; if (!fromGeo) continue;
@@ -110,7 +115,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
}); });
// Arc : les deux extrémités géolocalisées + villes différentes // Arc : les deux extrémités géolocalisées + villes différentes
const toDuniterKey = keyMap.get(t.toId); const toDuniterKey = t.toId ? resolveKey(t.toId) : undefined;
if (!toDuniterKey) continue; if (!toDuniterKey) continue;
const toGeo = geoMap.get(toDuniterKey); const toGeo = geoMap.get(toDuniterKey);
if (!toGeo) continue; if (!toGeo) continue;