Fix coverage: add /api/coverage route, remove stray gather code from loadCoverage
This commit is contained in:
246
frontend_src/components/H5Dashboard.tsx
Normal file
246
frontend_src/components/H5Dashboard.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface H5File {
|
||||
filename: string;
|
||||
nodeId: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface WaveformData {
|
||||
samples: number[];
|
||||
sample_rate: number;
|
||||
duration_sec: number;
|
||||
channel: number;
|
||||
channel_name: string;
|
||||
unit: string;
|
||||
stats: {
|
||||
min: number;
|
||||
max: number;
|
||||
mean: number;
|
||||
std: number;
|
||||
rms: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function H5Dashboard() {
|
||||
const [files, setFiles] = useState<H5File[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [waveformData, setWaveformData] = useState<WaveformData | null>(null);
|
||||
const [channel, setChannel] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [startTime, setStartTime] = useState(0);
|
||||
const [duration, setDuration] = useState(10);
|
||||
const [globalView, setGlobalView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/seismic/api/h5/files')
|
||||
.then(r => r.json())
|
||||
.then(data => setFiles(data.files || []))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const loadWaveform = async (file: string, ch: number, start: number, dur: number, global: boolean = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
file,
|
||||
channel: String(ch),
|
||||
start: String(start),
|
||||
duration: global ? '0' : String(dur)
|
||||
});
|
||||
const response = await fetch(`/seismic/api/h5/data?${params}`);
|
||||
const data = await response.json();
|
||||
setWaveformData(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (file: string) => {
|
||||
setSelectedFile(file);
|
||||
setGlobalView(false);
|
||||
loadWaveform(file, channel, startTime, duration, false);
|
||||
};
|
||||
|
||||
const handleChannelChange = (ch: number) => {
|
||||
setChannel(ch);
|
||||
if (selectedFile) loadWaveform(selectedFile, ch, startTime, duration, globalView);
|
||||
};
|
||||
|
||||
const handleGlobalView = () => {
|
||||
if (selectedFile) {
|
||||
setGlobalView(true);
|
||||
loadWaveform(selectedFile, channel, 0, 0, true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1400px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '10px' }}>
|
||||
🎯 Dashboard H5 - Visualisation Waveforms
|
||||
</h1>
|
||||
<p style={{ color: '#64748b', fontSize: '1.1rem' }}>
|
||||
{files.length} fichiers H5 calibrés disponibles
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '20px' }}>
|
||||
{/* Sidebar - Liste des fichiers */}
|
||||
<div style={{ background: 'white', padding: '20px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', maxHeight: '80vh', overflowY: 'auto' }}>
|
||||
<h2 style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>
|
||||
📁 Fichiers H5
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{files.map((file, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleFileSelect(file.filename)}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '12px',
|
||||
background: selectedFile === file.filename ? '#3b82f6' : '#f8fafc',
|
||||
color: selectedFile === file.filename ? 'white' : '#334155',
|
||||
border: selectedFile === file.filename ? '2px solid #2563eb' : '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.85rem',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedFile !== file.filename) {
|
||||
e.currentTarget.style.background = '#f1f5f9';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedFile !== file.filename) {
|
||||
e.currentTarget.style.background = '#f8fafc';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>Node {file.nodeId}</div>
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.8 }}>{file.date}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content - Waveform */}
|
||||
<div style={{ background: 'white', padding: '20px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||
{!selectedFile && (
|
||||
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '3rem', marginBottom: '20px' }}>📊</div>
|
||||
<p style={{ fontSize: '1.2rem' }}>Sélectionnez un fichier H5 pour visualiser les waveforms</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<>
|
||||
{/* Controls */}
|
||||
<div style={{ marginBottom: '20px', display: 'flex', gap: '15px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.9rem', fontWeight: 'bold', color: '#475569', display: 'block', marginBottom: '5px' }}>
|
||||
Canal :
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
{[1, 2, 3, 4].map(ch => (
|
||||
<button
|
||||
key={ch}
|
||||
onClick={() => handleChannelChange(ch)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: channel === ch ? '#3b82f6' : '#f1f5f9',
|
||||
color: channel === ch ? 'white' : '#334155',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{ch <= 3 ? `Geo ${ch}` : 'Hydro'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGlobalView}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: globalView ? '#10b981' : '#8b5cf6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
🌍 Vue Globale
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Waveform Display */}
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '10px' }}>⏳</div>
|
||||
<p>Chargement des données...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{waveformData && !loading && (
|
||||
<div>
|
||||
{/* Info bar */}
|
||||
<div style={{ background: '#f8fafc', padding: '15px', borderRadius: '8px', marginBottom: '20px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '10px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Canal</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.channel_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Unité</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.unit}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Durée</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.duration_sec.toFixed(1)} s</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Échantillons</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.samples.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: '10px', marginBottom: '20px' }}>
|
||||
{Object.entries(waveformData.stats).map(([key, value]) => (
|
||||
<div key={key} style={{ background: '#f0f9ff', padding: '10px', borderRadius: '6px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '0.75rem', color: '#0369a1', marginBottom: '3px', textTransform: 'uppercase' }}>{key}</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#0c4a6e', fontSize: '0.9rem' }}>{value.toExponential(3)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Waveform visualization (simple text representation for now) */}
|
||||
<div style={{ background: '#0f172a', padding: '20px', borderRadius: '8px', color: '#94a3b8', fontFamily: 'monospace', fontSize: '0.75rem', overflowX: 'auto' }}>
|
||||
<div style={{ marginBottom: '10px', color: '#f1f5f9', fontWeight: 'bold' }}>
|
||||
Waveform Preview (premiers échantillons) :
|
||||
</div>
|
||||
<pre style={{ margin: 0 }}>
|
||||
{waveformData.samples.slice(0, 50).map((v, i) =>
|
||||
`[${i.toString().padStart(4, '0')}] ${v.toExponential(4)} ${waveformData.unit}`
|
||||
).join('\n')}
|
||||
{waveformData.samples.length > 50 && `\n... (${waveformData.samples.length - 50} échantillons restants)`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user