14 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
syoul 70de7e4c06 chore: merge dev → main v1.3.2
ci/woodpecker/push/woodpecker Pipeline was successful
- fix: bouton Clusters bottom-44 mobile / bottom-24 desktop
- fix: bouton Clusters bottom-36/32 (iterations précédentes)
- fix: plan historique-genesis.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:08:09 +01:00
syoul 65f26e2b58 chore: bump version to 1.3.2
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:07:52 +01:00
syoul 104949427c fix: bouton Clusters bottom-44 mobile / bottom-24 desktop
ci/woodpecker/push/woodpecker Pipeline was successful
Sur mobile réel, la police forcée à 16px fait wrapper les contrôles
AnimationPlayer en 2 lignes (~165px). bottom-44 (176px) sur mobile,
bottom-24 (96px) sur sm+ où les contrôles ne wrappent pas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:05:01 +01:00
syoul 9ec95dfc91 fix: bouton Clusters bottom-36 — dépasse le bord supérieur de l'AnimationPlayer
ci/woodpecker/push/woodpecker Pipeline was successful
Player: bottom-4 + ~114px hauteur → bord sup à ~130px.
bottom-32 (128px) chevauchait de 2px. bottom-36 (144px) donne 14px de marge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:52:51 +01:00
7 changed files with 26 additions and 35 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "g1flux",
"private": true,
"version": "1.3.1",
"version": "1.4.1",
"type": "module",
"scripts": {
"dev": "vite",
+9 -2
View File
@@ -131,6 +131,15 @@ export default function App() {
</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 */}
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
<PeriodSelector
@@ -143,7 +152,6 @@ export default function App() {
geoPercent={visibleStats && visibleStats.transactionCount > 0
? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100)
: null}
onInfo={() => setInfoOpen(true)}
/>
</div>
@@ -184,7 +192,6 @@ export default function App() {
onPlay={animation.play}
onPause={animation.pause}
onSpeedChange={animation.setSpeed}
onClose={animation.deactivate}
/>
)}
+1 -12
View File
@@ -9,7 +9,6 @@ interface AnimationPlayerProps {
onPlay: () => void;
onPause: () => void;
onSpeedChange: (s: 1 | 2 | 4) => void;
onClose: () => void;
}
export function AnimationPlayer({
@@ -21,7 +20,6 @@ export function AnimationPlayer({
onPlay,
onPause,
onSpeedChange,
onClose,
}: AnimationPlayerProps) {
const frame = frames[currentIndex];
@@ -78,8 +76,7 @@ export function AnimationPlayer({
{/* Speed selector */}
<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
key={s}
onClick={() => onSpeedChange(s)}
@@ -94,14 +91,6 @@ export function AnimationPlayer({
))}
</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>
+2 -2
View File
@@ -17,7 +17,7 @@ function lerpColor(hex1: string, hex2: string, t: number): string {
}
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 NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre
const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux villes
@@ -350,7 +350,7 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) {
{/* Bouton cluster / villes */}
<button
onClick={() => setClustered(c => !c)}
className={`absolute bottom-32 left-4 z-[1002] px-3 py-1.5 rounded-lg text-xs font-medium border transition-all duration-200 ${
className={`absolute bottom-44 sm:bottom-24 left-4 z-[1002] px-3 py-1.5 rounded-lg text-xs font-medium border transition-all duration-200 ${
clustered
? 'bg-[#d4a843] text-[#0a0b0f] border-[#d4a843] shadow-[0_0_10px_rgba(212,168,67,0.35)]'
: 'bg-[#0a0b0f]/90 text-[#6b7280] border-[#2e2f3a] hover:text-[#d4a843] hover:border-[#d4a843]'
+2 -2
View File
@@ -54,8 +54,8 @@ export function InfoPanel({ onClose }: InfoPanelProps) {
</Feature>
<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-[#d4a843]">Or</span> = équilibré ·{' '}
<span className="text-orange-400">Orange</span> = émetteur net.
<span className="text-[#d4a843]">Or</span> = équilibré (dégradé or vert selon l'excédent reçu) ·{' '}
<span className="text-[#e53935]">Rouge</span> = émetteur net (dégradé or rouge selon l'excédent émis).
</Feature>
<Feature icon="↗" name="Clic sur un nœud">
Affiche la liste des villes du cluster avec leur balance individuelle,
+1 -11
View File
@@ -8,7 +8,6 @@ interface PeriodSelectorProps {
viewMode: 'heatmap' | 'flow';
onViewModeChange: (mode: 'heatmap' | 'flow') => void;
geoPercent?: number | null;
onInfo: () => void;
}
const PERIODS = [
@@ -19,7 +18,7 @@ const PERIODS = [
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 [inputVal, setInputVal] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
@@ -132,15 +131,6 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi
</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>
);
}
+10 -5
View File
@@ -12,7 +12,7 @@
* 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 {
getTransactionsForPeriod,
@@ -69,9 +69,14 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
}
// 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([
...rawTransfers.map((t) => keyMap.get(t.fromId)),
...rawTransfers.map((t) => keyMap.get(t.toId)),
...rawTransfers.map((t) => t.fromId ? resolveKey(t.fromId) : undefined),
...rawTransfers.map((t) => t.toId ? resolveKey(t.toId) : undefined),
].filter(Boolean) as string[])];
// Résolution géo par clé Duniter (_id Cesium+)
@@ -89,7 +94,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
const arcs: TransactionArc[] = [];
for (const t of rawTransfers) {
const fromDuniterKey = keyMap.get(t.fromId);
const fromDuniterKey = t.fromId ? resolveKey(t.fromId) : undefined;
if (!fromDuniterKey) continue;
const fromGeo = geoMap.get(fromDuniterKey);
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
const toDuniterKey = keyMap.get(t.toId);
const toDuniterKey = t.toId ? resolveKey(t.toId) : undefined;
if (!toDuniterKey) continue;
const toGeo = geoMap.get(toDuniterKey);
if (!toGeo) continue;