import { useState, useEffect, useCallback } from 'react'; import { MapContainer, TileLayer } from 'react-leaflet'; import NodeMarkers from './components/NodeMarkers'; import Sidebar from './components/Sidebar'; import SeismicSection from './SeismicSection'; import H5Coverage from './components/H5Coverage'; import DataDocumentation from './components/DataDocumentation'; import H5Dashboard from './components/H5Dashboard'; import CampaignDocs from "./components/CampaignDocs"; import { Node, DataWindow } from './types'; const API_BASE = '/seismic/api'; const DEFAULT_CENTER: [number, number] = [43.40, 3.70]; function App() { const [nodes, setNodes] = useState([]); const [dates, setDates] = useState([]); const [selectedDate, setSelectedDate] = useState(''); const [selectedChannel, setSelectedChannel] = useState('ch0'); const [selectedNode, setSelectedNode] = useState(null); const [currentTime, setCurrentTime] = useState(0); const [dataWindow, setDataWindow] = useState(null); const [loading, setLoading] = useState(true); const [sampleRate, setSampleRate] = useState(200); const [showOnlyWithData, setShowOnlyWithData] = useState(false); const [rmsTimeline, setRmsTimeline] = useState(null); const [allAdcValues, setAllAdcValues] = useState>({}); const [isPlaying, setIsPlaying] = useState(false); const [playSpeed, setPlaySpeed] = useState(1); const [chatOpen, setChatOpen] = useState(false); const [chatMsg, setChatMsg] = useState(''); const [chatHistory, setChatHistory] = useState([]); const [showSection, setShowSection] = useState(false); const [showDashboard, setShowDashboard] = useState(false); const [showCampaignDocs, setShowCampaignDocs] = useState(false); const [showDocumentation, setShowDocumentation] = useState(false); const [showCoverage, setShowCoverage] = useState(false); const [migrationStatus, setMigrationStatus] = useState(null); // 🔗 Deep-linking: Lire les paramètres URL au démarrage useEffect(() => { const params = new URLSearchParams(window.location.search); const urlDate = params.get('date'); const urlChannel = params.get('channel'); const urlTime = params.get('time'); const urlNode = params.get('node'); if (urlDate) setSelectedDate(urlDate); if (urlChannel) setSelectedChannel(urlChannel); if (urlTime) setCurrentTime(parseFloat(urlTime)); if (urlNode && nodes.length > 0) { const node = nodes.find(n => n.id === urlNode); if (node) setSelectedNode(node); } }, [nodes]); // Se déclenche quand les nodes sont chargés // 🔗 Deep-linking: Synchroniser l'URL quand les états changent useEffect(() => { const params = new URLSearchParams(); if (selectedDate) params.set('date', selectedDate); if (selectedChannel) params.set('channel', selectedChannel); if (currentTime > 1000000) params.set('time', currentTime.toString()); if (selectedNode) params.set('node', selectedNode.id); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.pushState({}, '', newUrl); }, [selectedDate, selectedChannel, currentTime, selectedNode]); useEffect(() => { fetch(`${API_BASE}/nodes`).then(r => r.json()).then(data => { setNodes(data.nodes || []); setSampleRate(data.sampleRateHz || 200); setLoading(false); }); }, []); useEffect(() => { fetch(`${API_BASE}/dates`).then(r => r.json()).then(data => { const dateList = data.dates || []; setDates(dateList); if (dateList.length > 0 && !selectedDate) setSelectedDate(dateList[dateList.length - 1]); }); }, []); // Polling de la migration useEffect(() => { const checkStatus = () => { fetch(`${API_BASE}/migration-status`) .then(r => r.json()) .then(data => setMigrationStatus(data)) .catch(() => {}); }; const interval = setInterval(checkStatus, 5000); checkStatus(); return () => clearInterval(interval); }, []); useEffect(() => { if (!selectedDate || !selectedChannel) return; setRmsTimeline(null); fetch(`${API_BASE}/rms-timeline?date=${selectedDate}&channel=${selectedChannel}`) .then(res => res.json()) .then(data => { if (data && data.nodes && Object.keys(data.nodes).length > 0) { setRmsTimeline(data); const allPts = Object.values(data.nodes).flat() as any[]; if (allPts.length > 0) { const minTs = Math.min(...allPts.map(p => p.ts)); if (minTs > 1000000) setCurrentTime(minTs); } } else { const ts = new Date(selectedDate).getTime() / 1000; if (!isNaN(ts)) setCurrentTime(ts); } }); }, [selectedDate, selectedChannel]); useEffect(() => { if (!isPlaying) return; const interval = setInterval(() => { setCurrentTime(prev => prev + (60 * (playSpeed / 5))); }, 200); return () => clearInterval(interval); }, [isPlaying, playSpeed]); useEffect(() => { if (!rmsTimeline || !rmsTimeline.nodes || !currentTime) { setAllAdcValues({}); return; } const values: Record = {}; Object.entries(rmsTimeline.nodes).forEach(([nodeId, dataPoints]) => { const pts = dataPoints as any[]; const point = pts.find(p => p.ts >= currentTime) || pts[pts.length - 1]; if (point) values[nodeId] = point.rms; }); setAllAdcValues(values); }, [currentTime, rmsTimeline]); const loadNodeData = useCallback(async () => { if (!selectedNode || !selectedDate || currentTime < 1000000) return; setLoading(true); try { const res = await fetch(`${API_BASE}/data?node=${selectedNode.id}&date=${selectedDate}&channel=${selectedChannel}&start=${Math.floor(currentTime)}`); const data = await res.json(); if (data && data.samples) setDataWindow({ ...data, node: selectedNode.id, date: selectedDate, channel: selectedChannel }); else setDataWindow(null); } catch (e) { setDataWindow(null); } setLoading(false); }, [selectedNode, selectedDate, selectedChannel, currentTime]); useEffect(() => { loadNodeData(); }, [selectedNode, selectedDate, selectedChannel]); const formatTime = (ts: number) => { if (ts < 1000000) return '--:--:--'; return new Date(ts * 1000).toISOString().substr(11, 8); }; const sendChat = async () => { if (!chatMsg) return; const newHist = [...chatHistory, { role: 'user', text: chatMsg }]; setChatHistory(newHist); setChatMsg(''); try { const res = await fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: chatMsg }) }); const data = await res.json(); setChatHistory([...newHist, { role: 'agent', text: data.response }]); } catch (e) { setChatHistory([...newHist, { role: 'agent', text: "Erreur agent." }]); } }; return (<>

Seismic Viewer

{migrationStatus && migrationStatus.processed_files < migrationStatus.total_files && (
âš¡ Migration : {Math.round((migrationStatus.processed_files / migrationStatus.total_files) * 100)}% ({migrationStatus.processed_files}/{migrationStatus.total_files})
)}
Nodes: {nodes.length} | Actifs: {nodes.filter(n=>n.hasDates).length}
{[1, 10, 100, 1000].map(s => ( ))}
{formatTime(currentTime)} { setCurrentTime(Number(e.target.value)); setIsPlaying(false); }} />
setShowSection(false)} />
{!chatOpen ? ( ) : (
Assistant
{chatHistory.map((m, i) => (
{m.text}
))}
setChatMsg(e.target.value)} onKeyPress={e => e.key === 'Enter' && sendChat()} aria-label="Poser une question" style={{ flex: 1, background: '#0f172a', border: '1px solid #334155', color: '#fff', padding: '5px', borderRadius: '4px', fontSize: '0.8rem' }} placeholder="Question…" />
)}
{showDashboard && (
)} {showCampaignDocs && (
)} {showDocumentation && setShowDocumentation(false)} />} {showCoverage && setShowCoverage(false)} />}
); } export default App;