Files
seisee/index.html.final.broken-20260219

1652 lines
76 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3E%3Ctext y=%27.9em%27 font-size=%2790%27%3E🌊%3C/text%3E%3C/svg%3E">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌊 SeiSee Web — Seismic Viewer</title>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
:root{--bg:#1a1a2e;--panel:#16213e;--header:#0f3460;--accent:#00d4ff;--accent2:#e94560;--text:#eee;--muted:#8899aa;--border:#2a2a4a;--green:#4ade80;--yellow:#fde047;--hover:#1a3a6a}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column}
.hdr{background:var(--header);padding:6px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);flex-shrink:0}
.hdr h1{font-size:1rem;color:var(--accent);white-space:nowrap}
.hdr .sep{width:1px;height:24px;background:var(--border)}
.toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:1}
.toolbar label{font-size:.7rem;color:var(--muted);text-transform:uppercase}
.toolbar select,.toolbar input[type=number]{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:3px 6px;border-radius:3px;font-size:.8rem}
.toolbar input[type=number]{width:65px}
.toolbar select{min-width:80px}
.tbtn{background:var(--accent);color:#000;border:none;padding:4px 10px;border-radius:3px;cursor:pointer;font-weight:600;font-size:.75rem}
.tbtn:hover{opacity:.85}
.tbtn.active{background:var(--accent2);color:#fff}
.tgroup{display:flex;align-items:center;gap:4px;padding:0 6px;border-left:1px solid var(--border)}
.main{display:flex;flex:1;overflow:hidden}
.sidebar{width:260px;min-width:200px;max-width:400px;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
.sidebar .stitle{padding:8px 12px;font-size:.75rem;color:var(--muted);text-transform:uppercase;border-bottom:1px solid var(--border);display:flex;justify-content:space-between}
.sidebar .search{width:100%;padding:6px 10px;background:var(--bg);border:none;border-bottom:1px solid var(--border);color:var(--text);font-size:.8rem}
.sidebar .search:focus{outline:none;background:#0d1117}
.flist{flex:1;overflow-y:auto}
.fitem{padding:5px 12px;cursor:pointer;border-bottom:1px solid var(--border);font-size:.8rem}
.fitem:hover{background:var(--hover)}
.fitem.active{background:var(--hover);border-left:3px solid var(--accent)}
.fitem .fhead{display:flex;justify-content:space-between;align-items:center;margin-bottom:2px}
.fitem .fn{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.fitem .ft{font-size:.65rem;padding:1px 5px;border-radius:3px;font-weight:700;flex-shrink:0;margin-left:6px}
.fitem .finfo{font-size:.65rem;color:var(--muted)}
.ft-h5{background:#1e3a8a;color:#60a5fa}
.ft-segy{background:#166534;color:var(--green)}
.content{flex:1;display:flex;flex-direction:column;overflow:hidden}
.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0}
.tab{padding:6px 16px;cursor:pointer;font-size:.8rem;color:var(--muted);border-bottom:2px solid transparent}
.tab:hover{color:var(--text)}
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.view{flex:1;overflow:hidden;display:none;position:relative}
.view.active{display:flex;flex-direction:column}
#sectionView{position:relative}
#heatmapContainer{flex:1;min-height:0}
#wiggleCanvas{position:absolute;top:0;left:0;pointer-events:none}
.htable-wrap{flex:1;overflow:auto;padding:8px}
.htable{width:100%;border-collapse:collapse;font-size:.75rem}
.htable th{background:var(--header);padding:4px 8px;text-align:left;position:sticky;top:0;z-index:1;color:var(--accent)}
.htable td{padding:3px 8px;border-bottom:1px solid var(--border)}
.htable tr:hover td{background:var(--hover)}
.infopanel{background:var(--panel);border-top:1px solid var(--border);padding:4px 12px;font-size:.7rem;color:var(--muted);display:flex;gap:16px;flex-shrink:0}
.infopanel span{white-space:nowrap}
.empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:1rem}
.resize-handle{width:4px;cursor:col-resize;background:transparent;flex-shrink:0}
.resize-handle:hover{background:var(--accent)}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--bg)}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
.ch-section{border-top:1px solid var(--border);padding:8px 12px;flex-shrink:0;max-height:40vh;overflow-y:auto}
.ch-section .stitle{padding:0 0 6px 0;border:none}
.ch-btn{display:block;width:100%;text-align:left;padding:3px 8px;background:none;border:1px solid var(--border);color:var(--text);cursor:pointer;font-size:.75rem;border-radius:2px;margin-bottom:2px}
.ch-btn:hover{border-color:var(--accent)}
.ch-btn.active{background:var(--hover);border-color:var(--accent);color:var(--accent)}
.meta-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;padding:16px;overflow:auto}
.meta-card{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:12px}
.meta-card h3{color:var(--accent);font-size:.9rem;margin-bottom:8px;border-bottom:1px solid var(--border);padding-bottom:4px}
.meta-row{display:flex;justify-content:space-between;padding:4px 0;font-size:.75rem;border-bottom:1px solid rgba(255,255,255,0.05)}
.meta-row .label{color:var(--muted);font-weight:600}
.meta-row .value{color:var(--text);text-align:right;max-width:60%;overflow:hidden;text-overflow:ellipsis}
#mapContainer{flex:1;position:relative}
#map{width:100%;height:100%}
.map-controls{position:absolute;top:10px;right:10px;z-index:1000;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px}
.map-controls button{background:var(--accent);color:#000;border:none;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:.75rem;margin:2px}
.map-controls button:hover{opacity:.85}
.extract-panel{padding:16px;overflow:auto}
.extract-form{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:16px;max-width:600px}
.extract-form .form-row{margin-bottom:12px}
.extract-form label{display:block;font-size:.75rem;color:var(--muted);margin-bottom:4px;text-transform:uppercase}
.extract-form input,.extract-form select{width:100%;padding:6px 10px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:3px;font-size:.8rem}
.extract-form button{background:var(--green);color:#000;border:none;padding:8px 16px;border-radius:3px;cursor:pointer;font-weight:600;font-size:.85rem;width:100%}
.extract-form button:hover{opacity:.85}
.extract-info{margin-top:12px;padding:8px;background:rgba(0,212,255,0.1);border-left:3px solid var(--accent);font-size:.75rem;color:var(--text)}
.pipeline-panel{padding:20px;overflow:auto;display:flex;flex-direction:column;gap:20px}
.pipeline-diagram{display:flex;align-items:stretch;justify-content:space-between;background:var(--panel);padding:20px;border-radius:8px;border:1px solid var(--border);flex-wrap:wrap;gap:16px}
.p-step{flex:1;min-width:240px;background:var(--bg);padding:16px;border-radius:6px;border:1px solid var(--border);display:flex;flex-direction:column;position:relative}
.p-step h4{color:var(--accent);font-size:1.1rem;margin-bottom:6px}
.p-step p{font-size:.8rem;color:var(--muted);margin-bottom:8px;line-height:1.4}
.p-step .count{font-size:1.5rem;font-weight:bold;color:var(--text);margin:8px 0}
.p-step .tech{font-size:.7rem;background:var(--hover);padding:4px 8px;border-radius:3px;display:inline-block;margin-top:auto;margin-bottom:8px}
.p-step .formulas{margin-top:8px;padding-top:8px;border-top:1px solid var(--border);font-size:.7rem;line-height:1.6}
.p-step .formulas div{margin:3px 0;color:var(--muted)}
.p-step .formulas code{background:rgba(0,212,255,0.1);padding:2px 5px;border-radius:2px;color:var(--accent);font-family:monospace;font-size:.68rem}
.p-arrow{color:var(--muted);font-size:2rem;display:flex;align-items:center}
.p-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}
.stat-card{background:var(--panel);padding:16px;border-radius:8px;border:1px solid var(--border)}
.stat-card h5{color:var(--muted);font-size:.75rem;text-transform:uppercase;margin-bottom:8px}
.stat-card .val{font-size:1.5rem;font-weight:bold;color:var(--accent)}
.progress-bar{height:6px;background:var(--bg);border-radius:3px;margin-top:8px;overflow:hidden}
.progress-fill{height:100%;background:var(--green);width:0%}
.geosup-panel{padding:20px;overflow:auto;display:flex;flex-direction:column;gap:20px}
.geosup-section{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:16px}
.geosup-section h3{color:var(--accent);font-size:1rem;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.doc-list{display:flex;flex-direction:column;gap:8px}
.doc-item{display:flex;align-items:center;gap:12px;padding:10px;background:var(--bg);border-radius:4px;cursor:pointer;transition:background 0.2s}
.doc-item:hover{background:var(--hover)}
.doc-item .icon{font-size:1.5rem}
.doc-item .info{flex:1}
.doc-item .name{color:var(--text);font-weight:500;font-size:.85rem}
.doc-item .meta{color:var(--muted);font-size:.7rem;margin-top:2px}
.doc-item .badge{background:var(--accent);color:#000;padding:2px 6px;border-radius:3px;font-size:.65rem;font-weight:700}
.doc-item .badge.gundalf{background:var(--accent2);color:#fff}
.geosup-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
.geosup-stat{background:var(--bg);padding:12px;border-radius:4px;text-align:center}
.geosup-stat .val{font-size:1.5rem;font-weight:bold;color:var(--accent)}
.geosup-stat .label{font-size:.7rem;color:var(--muted);margin-top:4px}
.map-layers{position:absolute;top:10px;left:10px;z-index:1000;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px;max-width:200px}
.map-layers h5{font-size:.7rem;color:var(--muted);margin-bottom:6px;text-transform:uppercase}
.layer-toggle{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:.75rem;cursor:pointer}
.layer-toggle input{cursor:pointer}
.layer-toggle .dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.dot-source{background:#3b82f6}
.dot-receiver{background:#ef4444}
.dot-deployment{background:#22c55e}
.dot-file{background:#fbbf24}
.load-info{background:var(--panel);border:1px solid var(--accent);border-radius:4px;padding:8px 12px;margin-bottom:8px;font-size:.75rem;line-height:1.6}
.load-info .label{color:var(--muted);font-weight:600;margin-right:4px}
.load-info .value{color:var(--accent)}
</style>
</head>
<body>
<div class="hdr">
<h1>🌊 SeiSee Web</h1>
<div class="sep"></div>
<div class="toolbar">
<div class="tgroup" style="border:none">
<label>View:</label>
<button class="tbtn active" id="btnDensity" onclick="setView('density')">Density</button>
<button class="tbtn" id="btnWiggle" onclick="setView('wiggle')">Wiggle</button>
<button class="tbtn" id="btnBoth" onclick="setView('both')">Both</button>
</div>
<div class="tgroup">
<label>Colormap:</label>
<select id="selCmap" onchange="renderSection()">
<option value="RdBu_r">Seismic (RdBu)</option>
<option value="Greys">Grayscale</option>
<option value="Viridis">Viridis</option>
<option value="Plasma">Plasma</option>
<option value="Hot">Hot</option>
<option value="Picnic">Picnic</option>
</select>
</div>
<div class="tgroup">
<label>Gain:</label>
<input type="number" id="inGain" value="1" min="0.1" max="100" step="0.5" onchange="renderSection()">
</div>
<div class="tgroup">
<label>Clip %:</label>
<input type="number" id="inClip" value="99" min="50" max="100" step="1" onchange="renderSection()">
</div>
<div class="tgroup">
<label>Traces:</label>
<input type="number" id="inTraceStart" value="0" min="0" step="10">
<span style="color:var(--muted)"></span>
<input type="number" id="inTraceCount" value="200" min="10" max="1000" step="10">
</div>
<div class="tgroup">
<label>Samp/Tr:</label>
<input type="number" id="inSPT" value="1000" min="100" max="50000" step="100">
</div>
<div class="tgroup">
<label>Norm:</label>
<select id="selNorm" onchange="renderSection()">
<option value="global">Global</option>
<option value="trace">Per Trace</option>
</select>
</div>
<div class="tgroup">
<button class="tbtn" onclick="loadSection()" style="background:var(--green);color:#000">▶ Load</button>
</div>
</div>
</div>
<div class="main">
<div class="sidebar" id="sidebar">
<div class="stitle"><span>Files</span><span id="fileCount">0</span></div>
<input type="text" class="search" id="searchInput" placeholder="Search..." oninput="filterFiles()">
<div class="flist" id="fileList"></div>
<div class="ch-section" id="channelSection" style="display:none">
<div class="stitle">Channel</div>
<div id="channelList"></div>
</div>
</div>
<div class="resize-handle" id="resizeHandle"></div>
<div class="content">
<div class="tabs">
<div class="tab active" onclick="switchTab('section')">Section</div>
<div class="tab" onclick="switchTab('metadata')">Metadata</div>
<div class="tab" onclick="switchTab('map')">Map</div>
<div class="tab" onclick="switchTab('extract')">Extract</div>
<div class="tab" onclick="switchTab('pipeline')">Pipeline</div>
<div class="tab" onclick="switchTab('geosup')">Geosup</div>
<div class="tab" onclick="switchTab('headers')">Headers</div>
<div class="tab" onclick="switchTab('waveform')">Waveform</div>
<div class="tab" onclick="switchTab('gather')">Gather</div>
<div class="tab" onclick="switchTab('coverage')">Coverage</div>
</div>
<div class="view active" id="sectionView">
<div id="loadInfoPanel" style="display:none" class="load-info">
<span class="label">Loaded:</span>
<span class="value" id="loadedTracesInfo">—</span>
</div>
<div id="heatmapContainer"></div>
<canvas id="wiggleCanvas"></canvas>
</div>
<div class="view" id="metadataView">
<div class="meta-grid" id="metadataContent">
<div class="empty">Select a file to view metadata</div>
</div>
</div>
<div class="view" id="mapView">
<div id="mapContainer">
<div id="map"></div>
<div class="map-layers" id="mapLayers">
<h5>Geosup Layers</h5>
<label class="layer-toggle"><input type="checkbox" id="layerSources" onchange="toggleLayer('sources')"><span class="dot dot-source"></span> Sources</label>
<label class="layer-toggle"><input type="checkbox" id="layerReceivers" onchange="toggleLayer('receivers')"><span class="dot dot-receiver"></span> Receivers</label>
<label class="layer-toggle"><input type="checkbox" id="layerDeployments" onchange="toggleLayer('deployments')"><span class="dot dot-deployment"></span> Deployments</label>
<label class="layer-toggle"><input type="checkbox" id="layerFiles" checked onchange="toggleLayer('files')"><span class="dot dot-file"></span> Files</label>
</div>
<div class="map-controls">
<div style="margin-bottom:4px;font-size:.7rem;color:var(--muted)">Edit Location</div>
<button onclick="enableLocationEdit()">📍 Set Location</button>
<button onclick="loadGeosupLayers()" style="margin-top:4px">🔄 Load Geosup</button>
</div>
</div>
</div>
<div class="view" id="extractView">
<div class="extract-panel">
<div class="extract-form">
<h3 style="color:var(--accent);margin-bottom:12px">Extract Segment</h3>
<div class="form-row">
<label>Channel</label>
<select id="extractChannel"></select>
</div>
<div class="form-row">
<label>Start Time (seconds)</label>
<input type="number" id="extractStart" value="0" min="0" step="60">
</div>
<div class="form-row">
<label>Duration (seconds)</label>
<input type="number" id="extractDuration" value="1200" min="1" step="60">
<div style="font-size:.65rem;color:var(--muted);margin-top:2px">Default: 1200s (20 minutes)</div>
</div>
<div class="form-row">
<label>Format</label>
<select id="extractFormat">
<option value="csv">CSV</option>
<option value="h5">HDF5 (.h5)</option>
</select>
</div>
<button onclick="downloadExtract()">⬇️ Download Extract</button>
<div class="extract-info" id="extractInfo" style="display:none"></div>
</div>
</div>
</div>
<div class="view" id="pipelineView">
<div class="pipeline-panel">
<h2 style="color:var(--accent);font-size:1.2rem;border-bottom:1px solid var(--border);padding-bottom:10px">Processing Chain — RAW → H5</h2>
<div class="pipeline-diagram">
<div class="p-step">
<h4>1. RAW</h4>
<p>Données brutes Manta (format propriétaire)</p>
<div class="count" id="countRaw">--</div>
<div class="tech">Format Propriétaire</div>
<div class="formulas">
<div><strong>Source:</strong> Hydrophones + Géophones Manta</div>
<div><strong>Résolution ADC:</strong> 24-bit, ±2.5V</div>
</div>
</div>
<div class="p-arrow">➔</div>
<div class="p-step">
<h4>2. SEGY</h4>
<p>Conversion intermédiaire via mantasegy (Box64/ARM)</p>
<div class="count" id="countSegy">--</div>
<div class="tech">mantasegy (Box64/ARM)</div>
<div class="formulas">
<div><strong>Format:</strong> SEG-Y Rev.1</div>
<div><strong>Conversion:</strong> Binaire Manta → SEG-Y</div>
</div>
</div>
<div class="p-arrow">➔</div>
<div class="p-step">
<h4>3. Normalisation</h4>
<p>Conversion ADC → valeurs physiques calibrées</p>
<div class="tech">Python + Calibration</div>
<div class="formulas">
<div><strong>Géophones (SM-24):</strong></div>
<div><code>v(m/s) = (ADC / gain) × 28.8 V/(m/s)</code></div>
<div style="margin-top:6px"><strong>Hydrophones:</strong></div>
<div><code>P(µPa) = (ADC / gain) × sens(µPa/V)</code></div>
<div style="margin-top:6px"><strong>ADC → Voltage:</strong></div>
<div><code>V = ADC × (5.0 / 2²⁴)</code></div>
<div style="margin-top:6px"><strong>Normalisation trace:</strong></div>
<div><code>(trace - mean) / std</code></div>
</div>
</div>
<div class="p-arrow">➔</div>
<div class="p-step">
<h4>4. HDF5</h4>
<p>Export HDF5 avec Python + calibration</p>
<div class="count" id="countH5">--</div>
<div class="tech">Python + h5py</div>
<div class="formulas">
<div><strong>Format:</strong> HDF5 (.h5)</div>
<div><strong>Groupes:</strong> calibrated_data, raw_data, metadata</div>
<div><strong>Compression:</strong> gzip</div>
</div>
</div>
</div>
<div class="p-stats">
<div class="stat-card">
<h5>Espace Disque (USB)</h5>
<div class="val" id="diskUsage">-- %</div>
<div class="progress-bar"><div class="progress-fill" id="diskBar" style="width:0%"></div></div>
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="diskDetail">-- GB utilisés sur -- GB</div>
</div>
<div class="stat-card">
<h5>Status Conversion</h5>
<div class="val" id="convertStatus" style="font-size:1rem">Inactif</div>
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="convertDetail">Aucune tâche en cours</div>
</div>
<div class="stat-card">
<h5>Complétion H5</h5>
<div class="val" id="completionPct">-- %</div>
<div class="progress-bar"><div class="progress-fill" id="completionBar" style="width:0%"></div></div>
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="completionDetail">-- / 345 fichiers traités</div>
</div>
</div>
<button onclick="loadPipelineStatus()" style="width:140px;padding:8px;background:var(--panel);border:1px solid var(--border);color:var(--text);border-radius:4px;cursor:pointer">↻ Actualiser</button>
</div>
</div>
<div class="view" id="geosupView">
<div class="geosup-panel">
<h2 style="color:var(--accent);font-size:1.2rem;border-bottom:1px solid var(--border);padding-bottom:10px">📍 Geosup — Positions & Documentation</h2>
<div class="geosup-stats" id="geosupStats">
<div class="geosup-stat"><div class="val" id="statSources">--</div><div class="label">Sources</div></div>
<div class="geosup-stat"><div class="val" id="statReceivers">--</div><div class="label">Receivers</div></div>
<div class="geosup-stat"><div class="val" id="statDeployments">--</div><div class="label">Deployments</div></div>
<div class="geosup-stat"><div class="val" id="statDocs">--</div><div class="label">Documents</div></div>
</div>
<div class="geosup-section">
<h3>📚 Documentation</h3>
<div class="doc-list" id="docList">
<div class="empty">Chargement...</div>
</div>
</div>
<div class="geosup-section">
<h3>🗺️ Position Coverage</h3>
<p style="font-size:.8rem;color:var(--muted);margin-bottom:8px">Use the <strong>Map</strong> tab to visualize source, receiver, and deployment positions. Toggle layers using the controls.</p>
<button onclick="switchTab('map');setTimeout(loadGeosupLayers,500)" class="tbtn" style="background:var(--green)">Open Map with Positions →</button>
</div>
</div>
</div>
<div class="view" id="headersView">
<div class="htable-wrap" id="headersContent">
<div class="empty">Select a file to view headers</div>
</div>
</div>
<div class="view" id="waveformView">
<div id="waveformChart" style="flex:1"></div>
<div class="view" id="coverageView">
<div class="coverage-panel">
<div class="coverage-stats" id="coverageStats">
<div class="coverage-stat"><div class="val" id="covNodes">--</div><div class="label">Nodes</div></div>
<div class="coverage-stat"><div class="val" id="covFiles">--</div><div class="label">Files</div></div>
<div class="coverage-stat"><div class="val" id="covHours">--</div><div class="label">Total Hours</div></div>
<div class="coverage-stat"><div class="val" id="covLines">--</div><div class="label">Lines</div></div>
<div class="coverage-legend" id="coverageLegend"></div>
<button class="tbtn" onclick="loadCoverage()" style="background:var(--green);color:#000">Refresh</button>
</div>
<div class="coverage-chart-wrap" id="coverageChartWrap">
<div class="empty" id="coverageEmpty">Loading coverage data...</div>
</div>
</div>
</div>
<div class="view" id="gatherView">
<div class="gather-panel">
<div class="gather-line-select" id="gatherLineSelect">
<label>Select Line:</label>
<select id="gatherLineDropdown" onchange="selectGatherLine(this.value)">
<option value="">-- Choose --</option>
</select>
<button onclick="selectAllGatherFiles()">Select All</button>
<button onclick="clearGatherFiles()" style="background:var(--accent2);color:#fff">Clear</button>
<span id="gatherSelCount" style="font-size:.75rem;color:var(--accent);margin-left:8px">0 selected</span>
</div>
<div class="gather-controls">
<label>Sort:</label>
<select id="gatherSort" onchange="renderGather()">
<option value="board_id">Board ID</option>
<option value="line_point">Line + Point</option>
<option value="distance">Distance</option>
</select>
<label>Norm:</label>
<select id="gatherNorm" onchange="renderGather()">
<option value="global">Global</option>
<option value="trace" selected>Per-trace</option>
</select>
<label>Gain:</label>
<input type="number" id="gatherGain" value="5" min="0.1" max="100" step="0.5" onchange="renderGather()">
<label>Spacing:</label>
<input type="range" id="gatherSpacing" min="20" max="200" value="60" oninput="renderGather()">
<label>Mode:</label>
<select id="gatherMode" onchange="renderGather()">
<option value="wiggle">Wiggle</option>
<option value="density">Density</option>
</select>
<label>Channel:</label>
<select id="gatherChannel">
<option value="calibrated_data/channel_1">channel_1</option>
<option value="calibrated_data/channel_2">channel_2</option>
<option value="calibrated_data/channel_3">channel_3</option>
<option value="calibrated_data/channel_4">channel_4</option>
</select>
<label>START:</label>
<input type="number" id="gatherStart" value="0" min="0" step="10" style="width:70px" onchange="onGatherParamChange()">
<label>DURATION:</label>
<input type="number" id="gatherDuration" value="60" min="1" step="10" style="width:70px" onchange="onGatherParamChange()">
<div style="display:flex;align-items:center;gap:6px;flex:1;min-width:300px">
<span style="font-size:.7rem;color:var(--accent);font-weight:600;min-width:55px" id="gatherSliderLabel">0:00</span>
<input type="range" id="gatherTimeSlider" min="0" max="8000" value="0" step="1"
style="flex:1;height:24px;accent-color:var(--accent);cursor:pointer"
oninput="onGatherSliderInput(this.value)">
<span style="font-size:.7rem;color:var(--muted);min-width:55px;text-align:right" id="gatherSliderMax">2:13:20</span>
</div>
<button class="tbtn" onclick="loadGather()" style="background:var(--green);color:#000">Load Gather</button>
<button class="tbtn" onclick="extractGather()" style="background:#f59e0b;color:#000;margin-left:4px" title="Export full resolution data (all channels, no decimation)">📥 Extract</button>
</div>
<div class="gather-canvas-wrap" id="gatherCanvasWrap" style="flex:1;overflow:auto;position:relative;min-height:0;border:1px solid var(--border);border-radius:4px;margin:4px 8px">
<canvas id="gatherCanvas" style="display:block;width:100%"></canvas>
<div class="empty" id="gatherEmpty">Select files using checkboxes in the file list, then click "Load Gather"</div>
</div>
<div class="gather-stats" id="gatherStats" style="padding:4px 12px;font-size:.7rem;color:var(--muted)"></div>
</div>
</div>
</div>
<div class="infopanel" id="infopanel">
<span>Ready</span>
</div>
</div>
</div>
<script>
const API = '/seismic/api';
let files = [], selectedFile = null, selectedChannel = 'calibrated_data/channel_1';
let sectionData = null;
let currentView = 'density';
let currentTab = 'section';
let map = null;
let locations = {};
let currentMetadata = null;
let fileInfoCache = {};
async function init() {
const r = await fetch(API + '/files');
const d = await r.json();
files = d.files || [];
document.getElementById('fileCount').textContent = files.length;
renderFiles(files);
setupResize();
try {
const lr = await fetch(API + '/locations');
locations = await lr.json();
} catch (e) {
console.error('Failed to load locations:', e);
}
}
function renderFiles(list) {
document.getElementById('fileList').innerHTML = list.map(f => {
const info = fileInfoCache[f.name] || {};
let infoText = '';
if (info.duration_human && info.num_channels && info.sample_rate_hz) {
infoText = `<div class="finfo">${info.duration_human} · ${info.num_channels} ch · ${info.sample_rate_hz} Hz</div>`;
}
return `<div class="fitem${selectedFile===f.name?' active':''}" onclick="selectFile('${f.name}')">
<div class="fhead">
<span class="fn" title="${f.name}">${f.name}</span>
<span class="ft ft-${f.type}">${f.type.toUpperCase()}</span>
</div>
${infoText}
</div>`;
}).join('');
}
function filterFiles() {
const q = document.getElementById('searchInput').value.toLowerCase();
renderFiles(files.filter(f => f.name.toLowerCase().includes(q)));
}
async function selectFile(name) {
selectedFile = name;
filterFiles();
setInfo(`Loading ${name}...`);
const r = await fetch(`${API}/file/${encodeURIComponent(name)}/info`);
const info = await r.json();
fileInfoCache[name] = info;
const chSec = document.getElementById('channelSection');
if (info.type === 'h5' && info.datasets) {
chSec.style.display = 'block';
const cals = info.datasets.filter(d => d.path.includes('calibrated'));
const raws = info.datasets.filter(d => d.path.includes('raw'));
let html = '<div style="font-size:.65rem;color:var(--muted);margin-bottom:4px">Calibrated:</div>';
cals.forEach(d => {
html += `<button class="ch-btn${d.path===selectedChannel?' active':''}" onclick="selectChannel('${d.path}')">${d.path.split('/').pop()} <span style="color:var(--muted)">(${d.dtype})</span></button>`;
});
if (raws.length) {
html += '<div style="font-size:.65rem;color:var(--muted);margin:4px 0">Raw:</div>';
raws.forEach(d => {
html += `<button class="ch-btn${d.path===selectedChannel?' active':''}" onclick="selectChannel('${d.path}')">${d.path.split('/').pop()} <span style="color:var(--muted)">(${d.dtype})</span></button>`;
});
}
document.getElementById('channelList').innerHTML = html;
const extractCh = document.getElementById('extractChannel');
extractCh.innerHTML = '';
[...cals, ...raws].forEach(d => {
const opt = document.createElement('option');
opt.value = d.path;
opt.textContent = d.path;
extractCh.appendChild(opt);
});
if (info.duration_human) {
setInfo(`${name} — ${info.duration_human}, ${info.num_channels} channels @ ${info.sample_rate_hz} Hz`);
} else if (info.samples_per_channel) {
const spt = parseInt(document.getElementById('inSPT').value);
const totalTr = Math.floor(info.samples_per_channel / spt);
setInfo(`${name} — ${info.samples_per_channel.toLocaleString()} samples, ${totalTr} virtual traces @ ${spt} spt`);
}
} else {
chSec.style.display = 'none';
if (info.num_traces) {
setInfo(`${name} — ${info.num_traces} traces, ${info.samples_per_trace} samples/trace`);
document.getElementById('inSPT').value = info.samples_per_trace || 1000;
}
}
renderFiles(files.filter(f => f.name.toLowerCase().includes(document.getElementById('searchInput').value.toLowerCase())));
loadMetadata();
if (currentTab === 'section') loadSection();
if (currentTab === 'headers') loadHeaders();
if (currentTab === 'map') initMap();
}
function selectChannel(ch) {
selectedChannel = ch;
document.querySelectorAll('.ch-btn').forEach(el => {
el.classList.toggle('active', el.onclick.toString().includes(ch));
});
if (currentTab === 'section') loadSection();
}
async function loadMetadata() {
if (!selectedFile) return;
const mc = document.getElementById('metadataContent');
mc.innerHTML = '<div class="empty">Loading metadata...</div>';
try {
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/metadata`);
const data = await r.json();
currentMetadata = data;
if (data.error) {
mc.innerHTML = `<div class="empty" style="color:var(--accent2)">${data.error}</div>`;
return;
}
let html = '';
html += '<div class="meta-card"><h3>📄 File Information</h3>';
html += `<div class="meta-row"><span class="label">Filename</span><span class="value">${data.filename || '—'}</span></div>`;
html += `<div class="meta-row"><span class="label">Size</span><span class="value">${data.file_size_mb || '—'} MB</span></div>`;
html += `<div class="meta-row"><span class="label">Modified</span><span class="value">${data.modified ? new Date(data.modified).toLocaleString('fr') : '—'}</span></div>`;
html += '</div>';
html += '<div class="meta-card"><h3>⏱️ Recording Information</h3>';
html += `<div class="meta-row"><span class="label">Start Time</span><span class="value">${data.start_timestamp ? new Date(data.start_timestamp).toLocaleString('fr') : '—'}</span></div>`;
html += `<div class="meta-row"><span class="label">End Time</span><span class="value">${data.end_timestamp ? new Date(data.end_timestamp).toLocaleString('fr') : '—'}</span></div>`;
html += `<div class="meta-row"><span class="label">Duration</span><span class="value">${data.duration_human || '—'}</span></div>`;
html += `<div class="meta-row"><span class="label">Sample Rate</span><span class="value">${data.sample_rate_hz || data.sample_rate || '—'} Hz</span></div>`;
html += '</div>';
html += '<div class="meta-card"><h3>📊 Data Information</h3>';
html += `<div class="meta-row"><span class="label">Channels</span><span class="value">${data.n_channels || data.num_traces || '—'}</span></div>`;
html += `<div class="meta-row"><span class="label">Total Samples</span><span class="value">${(data.n_samples || data.total_samples || 0).toLocaleString()}</span></div>`;
html += `<div class="meta-row"><span class="label">Samples/Channel</span><span class="value">${(data.n_samples || 0).toLocaleString()}</span></div>`;
html += `<div class="meta-row"><span class="label">Source File</span><span class="value" title="${data.source_file || '—'}">${data.source_file || '—'}</span></div>`;
html += '</div>';
if (data.calibration) {
html += '<div class="meta-card"><h3>⚙️ Calibration</h3>';
for (const [k, v] of Object.entries(data.calibration)) {
html += `<div class="meta-row"><span class="label">${k.replace(/_/g, ' ')}</span><span class="value">${v}</span></div>`;
}
html += '</div>';
}
if (data.channels && data.channels.length) {
html += '<div class="meta-card" style="grid-column:1/-1"><h3>📡 Channels</h3>';
html += '<table class="htable"><tr><th>Channel</th><th>Sensor</th><th>Unit</th><th>Samples</th><th>Type</th></tr>';
data.channels.forEach(ch => {
html += `<tr>
<td>${ch.name}</td>
<td>${ch.sensor || '—'}</td>
<td>${ch.unit || '—'}</td>
<td>${(ch.samples || 0).toLocaleString()}</td>
<td>${ch.dtype}</td>
</tr>`;
});
html += '</table></div>';
}
mc.innerHTML = html;
} catch (e) {
mc.innerHTML = `<div class="empty" style="color:var(--accent2)">Error: ${e.message}</div>`;
}
}
function initMap() {
if (!map) {
setTimeout(() => {
map = L.map('map').setView([43.2965, 5.3698], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
map.on('click', onMapClick);
}, 100);
}
updateMapMarkers();
}
function updateMapMarkers() {
if (!map) return;
map.eachLayer(layer => {
if (layer instanceof L.Marker) {
map.removeLayer(layer);
}
});
for (const [filename, loc] of Object.entries(locations)) {
const marker = L.marker([loc.lat, loc.lon]).addTo(map);
marker.bindPopup(`<b>${filename}</b><br>Lat: ${loc.lat.toFixed(6)}<br>Lon: ${loc.lon.toFixed(6)}`);
if (filename === selectedFile) {
marker.openPopup();
map.setView([loc.lat, loc.lon], 12);
}
}
}
let editingLocation = false;
function enableLocationEdit() {
if (!selectedFile) {
alert('Please select a file first');
return;
}
editingLocation = true;
alert(`Click on the map to set location for ${selectedFile}`);
}
async function onMapClick(e) {
if (!editingLocation || !selectedFile) return;
const lat = e.latlng.lat;
const lon = e.latlng.lng;
try {
const r = await fetch(API + '/locations', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({filename: selectedFile, lat, lon})
});
const data = await r.json();
if (data.success) {
locations = data.locations;
updateMapMarkers();
setInfo(`Location set for ${selectedFile}: ${lat.toFixed(6)}, ${lon.toFixed(6)}`);
}
} catch (e) {
setInfo(`Error setting location: ${e.message}`);
}
editingLocation = false;
}
async function downloadExtract() {
if (!selectedFile) {
alert('Please select a file first');
return;
}
const channel = document.getElementById('extractChannel').value;
const start = parseFloat(document.getElementById('extractStart').value);
const duration = parseFloat(document.getElementById('extractDuration').value);
const format = document.getElementById('extractFormat').value;
const info = document.getElementById('extractInfo');
info.style.display = 'block';
info.innerHTML = `⏳ Extracting ${duration}s from ${selectedFile}...`;
try {
const url = `${API}/file/${encodeURIComponent(selectedFile)}/extract?start_sec=${start}&duration_sec=${duration}&channel=${encodeURIComponent(channel)}&format=${format}`;
const response = await fetch(url);
if (!response.ok) {
const err = await response.json();
info.innerHTML = `❌ Error: ${err.error || 'Unknown error'}`;
return;
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `extract_${selectedFile.replace('.h5', '')}_${start}_${duration}s.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
info.innerHTML = `✅ Downloaded ${(blob.size / 1024 / 1024).toFixed(2)} MB`;
} catch (e) {
info.innerHTML = `❌ Error: ${e.message}`;
}
}
async function loadPipelineStatus() {
try {
const r = await fetch(API + '/pipeline-status');
const d = await r.json();
if (d.error) return;
document.getElementById('countRaw').textContent = d.counts.raw;
document.getElementById('countSegy').textContent = d.counts.segy;
document.getElementById('countH5').textContent = d.counts.h5;
const usedPct = d.disk.percent;
document.getElementById('diskUsage').textContent = `${usedPct}%`;
document.getElementById('diskBar').style.width = `${usedPct}%`;
document.getElementById('diskDetail').textContent = `${d.disk.used_gb} GB utilisés sur ${d.disk.total_gb} GB`;
const total = d.counts.total_expected || 345;
const h5 = d.counts.h5;
const pct = Math.round((h5 / total) * 100);
document.getElementById('completionPct').textContent = `${pct}%`;
document.getElementById('completionBar').style.width = `${pct}%`;
document.getElementById('completionDetail').textContent = `${h5} / ${total} fichiers H5 prêts`;
if (d.progress) {
document.getElementById('convertStatus').textContent = 'En cours...';
document.getElementById('convertStatus').style.color = 'var(--yellow)';
document.getElementById('convertDetail').textContent = `Fichier: ${d.progress.current_file}`;
} else {
document.getElementById('convertStatus').textContent = 'Inactif';
document.getElementById('convertStatus').style.color = 'var(--muted)';
document.getElementById('convertDetail').textContent = 'Aucune tâche en cours';
}
} catch (e) {
console.error('Pipeline status error:', e);
}
}
async function loadSection() {
if (!selectedFile) return;
setInfo('Loading section...');
const ts = parseInt(document.getElementById('inTraceStart').value);
const tc = parseInt(document.getElementById('inTraceCount').value);
const spt = parseInt(document.getElementById('inSPT').value);
const params = new URLSearchParams({
trace_start: ts, trace_count: tc,
samples_per_trace: spt, channel: selectedChannel,
max_samples: 500
});
try {
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/section?${params}`);
const d = await r.json();
if (d.error) { setInfo(`Error: ${d.error}`); return; }
const bytes = Uint8Array.from(atob(d.data_b64), c => c.charCodeAt(0));
const floats = new Float32Array(bytes.buffer);
sectionData = {
num_traces: d.num_traces,
samples_per_trace: d.samples_per_trace,
trace_start: d.trace_start,
total_traces: d.total_traces,
vmin: d.vmin, vmax: d.vmax,
data: floats
};
const loadInfo = document.getElementById('loadInfoPanel');
const loadText = document.getElementById('loadedTracesInfo');
loadInfo.style.display = 'block';
loadText.innerHTML = `Traces <strong>${d.trace_start}${d.trace_start + d.num_traces}</strong> (${d.num_traces} traces × ${d.samples_per_trace} samp) · Channel: <strong>${selectedChannel.split('/').pop()}</strong> · Range: [${d.vmin.toFixed(3)}, ${d.vmax.toFixed(3)}]`;
setInfo(`${selectedFile} — ${d.num_traces} traces × ${d.samples_per_trace} samples | range [${d.vmin.toFixed(2)}, ${d.vmax.toFixed(2)}] | total: ${d.total_traces} traces`);
renderSection();
} catch (e) {
setInfo(`Error: ${e.message}`);
}
}
function renderSection() {
if (!sectionData) return;
const gain = parseFloat(document.getElementById('inGain').value);
const clipPct = parseFloat(document.getElementById('inClip').value) / 100;
const cmap = document.getElementById('selCmap').value;
const norm = document.getElementById('selNorm').value;
const { num_traces, samples_per_trace, data, trace_start } = sectionData;
const z = [];
for (let s = 0; s < samples_per_trace; s++) {
const row = [];
for (let t = 0; t < num_traces; t++) {
row.push(data[t * samples_per_trace + s] * gain);
}
z.push(row);
}
let clipVal;
if (norm === 'global') {
const allVals = Array.from(data).map(v => Math.abs(v * gain));
allVals.sort((a, b) => a - b);
clipVal = allVals[Math.floor(allVals.length * clipPct)] || allVals[allVals.length - 1];
} else {
for (let s = 0; s < samples_per_trace; s++) {
for (let t = 0; t < num_traces; t++) {
const traceVals = [];
for (let ss = 0; ss < samples_per_trace; ss++) {
traceVals.push(Math.abs(data[t * samples_per_trace + ss]));
}
traceVals.sort((a, b) => a - b);
const trClip = traceVals[Math.floor(traceVals.length * clipPct)] || 1;
z[s][t] = (data[t * samples_per_trace + s] * gain) / trClip;
}
}
clipVal = 1;
}
const showDensity = currentView === 'density' || currentView === 'both';
const showWiggle = currentView === 'wiggle' || currentView === 'both';
const container = document.getElementById('heatmapContainer');
if (showDensity) {
const traceLabels = Array.from({length: num_traces}, (_, i) => i + trace_start);
const sampleLabels = Array.from({length: samples_per_trace}, (_, i) => i);
Plotly.react('heatmapContainer', [{
z: z,
x: traceLabels,
y: sampleLabels,
type: 'heatmap',
colorscale: cmap,
zmin: -clipVal,
zmax: clipVal,
colorbar: { title: 'Amplitude', titlefont: { color: '#8899aa', size: 10 }, tickfont: { color: '#8899aa', size: 9 } },
hovertemplate: 'Trace: %{x}<br>Sample: %{y}<br>Amp: %{z:.4f}<extra></extra>'
}], {
paper_bgcolor: '#1a1a2e',
plot_bgcolor: '#1a1a2e',
font: { color: '#eee' },
xaxis: { title: 'Trace', color: '#8899aa', gridcolor: '#2a2a4a' },
yaxis: { title: 'Sample', color: '#8899aa', gridcolor: '#2a2a4a', autorange: 'reversed' },
margin: { t: 10, r: 80, b: 40, l: 60 }
}, { responsive: true });
} else {
container.innerHTML = '';
}
const canvas = document.getElementById('wiggleCanvas');
if (showWiggle) {
drawWiggle(canvas, container, clipVal);
} else {
canvas.width = 0;
canvas.height = 0;
}
}
function drawWiggle(canvas, container, clipVal) {
if (!sectionData) return;
const { num_traces, samples_per_trace, data, trace_start } = sectionData;
const gain = parseFloat(document.getElementById('inGain').value);
const rect = container.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentView === 'wiggle') {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.style.pointerEvents = 'auto';
} else {
canvas.style.pointerEvents = 'none';
}
const ml = 60, mr = 80, mt = 10, mb = 40;
const pw = canvas.width - ml - mr;
const ph = canvas.height - mt - mb;
if (pw <= 0 || ph <= 0) return;
const traceSpacing = pw / num_traces;
const halfSpacing = traceSpacing * 0.45;
for (let t = 0; t < num_traces; t++) {
const cx = ml + (t + 0.5) * traceSpacing;
ctx.beginPath();
const fillPath = new Path2D();
fillPath.moveTo(cx, mt);
for (let s = 0; s < samples_per_trace; s++) {
const val = (data[t * samples_per_trace + s] * gain) / clipVal;
const x = cx + val * halfSpacing;
const y = mt + (s / (samples_per_trace - 1)) * ph;
if (s === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
fillPath.lineTo(x, y);
}
ctx.strokeStyle = currentView === 'both' ? 'rgba(0,0,0,0.6)' : '#00d4ff';
ctx.lineWidth = 0.5;
ctx.stroke();
fillPath.lineTo(cx, mt + ph);
fillPath.closePath();
ctx.save();
ctx.clip(fillPath);
ctx.fillStyle = currentView === 'both' ? 'rgba(0,0,0,0.3)' : 'rgba(0,212,255,0.3)';
ctx.fillRect(cx, mt, halfSpacing + 5, ph);
ctx.restore();
}
if (currentView === 'wiggle') {
ctx.strokeStyle = '#2a2a4a';
ctx.lineWidth = 1;
ctx.strokeRect(ml, mt, pw, ph);
ctx.fillStyle = '#8899aa';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
const step = Math.max(1, Math.floor(num_traces / 10));
for (let t = 0; t < num_traces; t += step) {
const x = ml + (t + 0.5) * traceSpacing;
ctx.fillText(t + trace_start, x, mt + ph + 15);
}
ctx.textAlign = 'right';
const ystep = Math.max(1, Math.floor(samples_per_trace / 8));
for (let s = 0; s < samples_per_trace; s += ystep) {
const y = mt + (s / (samples_per_trace - 1)) * ph;
ctx.fillText(s, ml - 5, y + 3);
}
ctx.textAlign = 'center';
ctx.fillText('Trace', ml + pw / 2, mt + ph + 32);
ctx.save();
ctx.translate(15, mt + ph / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Sample', 0, 0);
ctx.restore();
}
}
function setView(v) {
currentView = v;
document.getElementById('btnDensity').classList.toggle('active', v === 'density');
document.getElementById('btnWiggle').classList.toggle('active', v === 'wiggle');
document.getElementById('btnBoth').classList.toggle('active', v === 'both');
renderSection();
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab').forEach((el, i) => {
el.classList.toggle('active', ['section', 'metadata', 'map', 'extract', 'pipeline', 'geosup', 'headers', 'waveform'][i] === tab);
});
document.getElementById('sectionView').classList.toggle('active', tab === 'section');
document.getElementById('metadataView').classList.toggle('active', tab === 'metadata');
document.getElementById('mapView').classList.toggle('active', tab === 'map');
document.getElementById('extractView').classList.toggle('active', tab === 'extract');
document.getElementById('pipelineView').classList.toggle('active', tab === 'pipeline');
document.getElementById('geosupView').classList.toggle('active', tab === 'geosup');
document.getElementById('headersView').classList.toggle('active', tab === 'headers');
document.getElementById('waveformView').classList.toggle('active', tab === 'waveform');
if (tab === 'metadata' && selectedFile) loadMetadata();
if (tab === 'map') initMap();
if (tab === 'pipeline') loadPipelineStatus();
if (tab === 'geosup') loadGeosupData();
if (tab === 'waveform' && selectedFile) loadWaveform();
if (tab === 'section') renderSection();
if (tab === 'headers' && selectedFile) loadHeaders();
}
async function loadHeaders() {
if (!selectedFile) return;
const hc = document.getElementById('headersContent');
hc.innerHTML = '<div class="empty">Loading headers...</div>';
try {
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/headers`);
const d = await r.json();
if (d.error) { hc.innerHTML = `<div class="empty" style="color:var(--accent2)">${d.error}</div>`; return; }
let html = '';
if (d.type === 'h5') {
if (d.file_attrs && Object.keys(d.file_attrs).length) {
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">File Attributes</h3>';
html += '<table class="htable"><tr><th>Key</th><th>Value</th></tr>';
for (const [k, v] of Object.entries(d.file_attrs)) { html += `<tr><td>${k}</td><td>${v}</td></tr>`; }
html += '</table>';
}
if (d.groups && d.groups.length) {
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Groups</h3>';
html += '<table class="htable"><tr><th>Path</th><th>Attributes</th></tr>';
d.groups.forEach(g => {
const attrs = Object.entries(g).filter(([k]) => k !== 'path').map(([k, v]) => `${k}: ${v}`).join(', ');
html += `<tr><td>${g.path}</td><td>${attrs}</td></tr>`;
});
html += '</table>';
}
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Datasets</h3>';
html += '<table class="htable"><tr><th>Path</th><th>Shape</th><th>Dtype</th><th>Size</th><th>Attributes</th></tr>';
(d.datasets || []).forEach(ds => {
const attrs = Object.entries(ds).filter(([k]) => k.startsWith('attr:')).map(([k, v]) => `${k.slice(5)}: ${v}`).join(', ');
html += `<tr><td>${ds.path}</td><td>${JSON.stringify(ds.shape)}</td><td>${ds.dtype}</td><td>${ds.size?.toLocaleString()}</td><td>${attrs || '—'}</td></tr>`;
});
html += '</table>';
} else if (d.type === 'segy') {
if (d.binary_header) {
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Binary File Header</h3>';
html += '<table class="htable"><tr><th>Field</th><th>Value</th></tr>';
for (const [k, v] of Object.entries(d.binary_header)) { html += `<tr><td>${k.replace(/_/g, ' ')}</td><td>${v}</td></tr>`; }
html += '</table>';
}
if (d.trace_headers && d.trace_headers.length) {
html += `<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Trace Headers (${d.trace_headers.length} of ${d.total_traces})</h3>`;
const cols = Object.keys(d.trace_headers[0]);
html += '<table class="htable"><tr>' + cols.map(c => `<th>${c}</th>`).join('') + '</tr>';
d.trace_headers.forEach(th => { html += '<tr>' + cols.map(c => `<td>${th[c] ?? '—'}</td>`).join('') + '</tr>'; });
html += '</table>';
}
}
hc.innerHTML = html || '<div class="empty">No header data</div>';
} catch (e) { hc.innerHTML = `<div class="empty" style="color:var(--accent2)">Error: ${e.message}</div>`; }
}
async function loadWaveform() {
if (!selectedFile || !selectedFile.endsWith('.h5')) {
document.getElementById('waveformChart').innerHTML = '<div class="empty">Waveform view available for H5 files</div>';
return;
}
document.getElementById('waveformChart').innerHTML = '<div class="empty">Loading...</div>';
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/waveform?channel=${encodeURIComponent(selectedChannel)}&points=5000`);
const d = await r.json();
if (d.error) { document.getElementById('waveformChart').innerHTML = `<div class="empty">${d.error}</div>`; return; }
Plotly.newPlot('waveformChart', [{
x: d.x, y: d.y, type: 'scattergl', mode: 'lines',
line: { color: '#00d4ff', width: 1 }
}], {
paper_bgcolor: '#1a1a2e', plot_bgcolor: '#1a1a2e',
font: { color: '#eee' },
xaxis: { title: 'Sample', color: '#8899aa', gridcolor: '#2a2a4a' },
yaxis: { title: 'Amplitude', color: '#8899aa', gridcolor: '#2a2a4a' },
margin: { t: 10, r: 20, b: 40, l: 60 },
annotations: [{
x: 0.01, y: 0.98, xref: 'paper', yref: 'paper', showarrow: false,
text: `${d.total_samples.toLocaleString()} samples → ${d.displayed_points} pts | min=${d.min.toFixed(4)} max=${d.max.toFixed(4)}`,
font: { size: 10, color: '#8899aa' }, bgcolor: '#16213e', borderpad: 4
}]
}, { responsive: true });
}
function setupResize() {
const handle = document.getElementById('resizeHandle');
const sidebar = document.getElementById('sidebar');
let dragging = false, startX, startW;
handle.addEventListener('mousedown', e => {
dragging = true; startX = e.clientX; startW = sidebar.offsetWidth;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
sidebar.style.width = Math.max(150, Math.min(500, startW + e.clientX - startX)) + 'px';
});
document.addEventListener('mouseup', () => {
if (dragging) { dragging = false; document.body.style.cursor = ''; }
});
}
function setInfo(msg) {
document.getElementById('infopanel').innerHTML = `<span>${msg}</span><span>${new Date().toLocaleTimeString('fr')}</span>`;
}
let geosupData = null;
let geosupLayers = { sources: null, receivers: null, deployments: null };
let layerVisibility = { sources: false, receivers: false, deployments: false, files: true };
async function loadGeosupData() {
try {
const posResp = await fetch(API + '/geosup/positions');
geosupData = await posResp.json();
if (geosupData.counts) {
document.getElementById('statSources').textContent = geosupData.counts.sources.toLocaleString();
document.getElementById('statReceivers').textContent = geosupData.counts.receivers.toLocaleString();
document.getElementById('statDeployments').textContent = geosupData.counts.deployments.toLocaleString();
}
const docResp = await fetch(API + '/geosup/documents');
const docData = await docResp.json();
document.getElementById('statDocs').textContent = docData.count || 0;
const docList = document.getElementById('docList');
if (docData.documents && docData.documents.length) {
docList.innerHTML = docData.documents.map(doc => `
<div class="doc-item" onclick="downloadDoc('${doc.name}')">
<span class="icon">${doc.icon}</span>
<div class="info">
<div class="name">${doc.name}</div>
<div class="meta">${doc.type} • ${doc.size_human}</div>
</div>
${doc.is_gundalf ? '<span class="badge gundalf">GUNDALF</span>' : ''}
${doc.is_params ? '<span class="badge">PARAMS</span>' : ''}
</div>
`).join('');
} else {
docList.innerHTML = '<div class="empty">No documents found</div>';
}
} catch (e) {
console.error('Geosup load error:', e);
}
}
function downloadDoc(filename) {
window.open(API + '/geosup/documents/' + encodeURIComponent(filename), '_blank');
}
async function loadGeosupLayers() {
if (!map) {
setTimeout(loadGeosupLayers, 500);
return;
}
if (!geosupData) {
const resp = await fetch(API + '/geosup/positions');
geosupData = await resp.json();
}
if (!geosupData.features) return;
Object.values(geosupLayers).forEach(layer => {
if (layer) map.removeLayer(layer);
});
const sourceMarkers = [];
const receiverMarkers = [];
const deploymentMarkers = [];
geosupData.features.forEach(feature => {
const coords = feature.geometry.coordinates;
const props = feature.properties;
const popup = `<b>${props.category.toUpperCase()}</b><br>
Line: ${props.line}, Point: ${props.point}<br>
E: ${props.easting?.toFixed(1)}, N: ${props.northing?.toFixed(1)}<br>
Depth: ${props.depth}m${props.timestamp ? '<br>Time: ' + props.timestamp : ''}`;
const color = props.category === 'source' ? '#3b82f6' :
props.category === 'receiver' ? '#ef4444' : '#22c55e';
const marker = L.circleMarker([coords[1], coords[0]], {
radius: 4,
fillColor: color,
color: color,
weight: 1,
opacity: 0.8,
fillOpacity: 0.6
}).bindPopup(popup);
if (props.category === 'source') sourceMarkers.push(marker);
else if (props.category === 'receiver') receiverMarkers.push(marker);
else deploymentMarkers.push(marker);
});
geosupLayers.sources = L.layerGroup(sourceMarkers);
geosupLayers.receivers = L.layerGroup(receiverMarkers);
geosupLayers.deployments = L.layerGroup(deploymentMarkers);
document.getElementById('layerSources').checked = true;
document.getElementById('layerReceivers').checked = true;
document.getElementById('layerDeployments').checked = true;
layerVisibility.sources = true;
layerVisibility.receivers = true;
layerVisibility.deployments = true;
geosupLayers.sources.addTo(map);
geosupLayers.receivers.addTo(map);
geosupLayers.deployments.addTo(map);
if (geosupData.features.length > 0) {
const bounds = L.latLngBounds(geosupData.features.map(f => [f.geometry.coordinates[1], f.geometry.coordinates[0]]));
map.fitBounds(bounds, { padding: [20, 20] });
}
setInfo(`Loaded ${geosupData.counts.total} geosup positions`);
}
function toggleLayer(layerName) {
const checkbox = document.getElementById('layer' + layerName.charAt(0).toUpperCase() + layerName.slice(1));
layerVisibility[layerName] = checkbox.checked;
if (layerName === 'files') {
updateMapMarkers();
} else if (geosupLayers[layerName]) {
if (layerVisibility[layerName]) {
geosupLayers[layerName].addTo(map);
} else {
map.removeLayer(geosupLayers[layerName]);
}
}
}
window.addEventListener('resize', () => {
if (currentTab === 'section') renderSection();
if (map) map.invalidateSize();
});
(async function bootInit() {
for (let attempt = 0; attempt < 5; attempt++) {
try {
await init();
console.log('init OK, files=' + files.length);
return;
} catch(e) {
console.warn('init attempt ' + (attempt+1) + ' failed:', e.message);
await new Promise(r => setTimeout(r, 2000));
}
}
console.error('init failed after 5 attempts');
})();
function clearGatherFiles() {
gatherSelectedFiles.clear();
updateGatherSelCount();
filterFiles();
}
function closeExtractModal() {
activeExtractTask = null; // Stop polling
document.getElementById('extractModal').style.display = 'none';
}
async function doExtract() {
var channels = document.getElementById('extChannels').value;
var scope = document.getElementById('extScope').value;
var start = parseFloat(document.getElementById('extStart').value);
var duration = parseFloat(document.getElementById('extDuration').value);
var incNodes = document.getElementById('extIncNodes').checked;
var incShots = document.getElementById('extIncShots').checked;
var incMeta = document.getElementById('extIncMeta').checked;
var filesParam;
if (scope === 'all_lines') {
filesParam = files.filter(f=>f.name.endsWith('.h5')).map(f=>f.name).join(',');
} else if (scope === 'line') {
var lineDropdown = document.getElementById('gatherLineDropdown');
var currentLine = lineDropdown ? lineDropdown.value : '';
if (currentLine) {
var lineNum = parseInt(currentLine);
var lineFiles = [];
files.forEach(function(f) {
var ni = getNodeInfo(f.name);
if (ni && ni.line === lineNum) lineFiles.push(f.name);
});
filesParam = lineFiles.join(',');
} else {
filesParam = Array.from(gatherSelectedFiles).join(',');
}
} else {
if (gatherOverlapInfo && gatherOverlapInfo.groups && gatherOverlapInfo.groups[gatherActiveGroup]) {
filesParam = gatherOverlapInfo.groups[gatherActiveGroup].files.join(',');
} else {
filesParam = Array.from(gatherSelectedFiles).join(',');
}
}
// UI update
document.getElementById('extDownloadBtn').disabled = true;
document.getElementById('extDownloadBtn').style.opacity = '0.5';
document.getElementById('extCancelBtn').style.display = 'none';
document.getElementById('extProgressContainer').style.display = 'block';
document.getElementById('extEstimate').style.display = 'none';
try {
var r = await fetch(API + '/gather_extract', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
files: filesParam, start: start, duration: duration,
channels: channels,
include_nodes: incNodes,
include_shots: incShots,
include_metadata: incMeta
})
});
if (!r.ok) {
var err = await r.json();
throw new Error(err.error || r.statusText);
}
var data = await r.json();
activeExtractTask = data.task_id;
pollTaskStatus(activeExtractTask);
} catch(e) {
alert('Extract error: ' + e.message);
resetExtractModal();
}
}
function extractBoardIdJs(filename) {
const m = filename.match(/_b(\d+)_/);
return m ? m[1] : null;
}
function extractGather() {
if (gatherSelectedFiles.size === 0) {
setInfo('Extract: select files first');
return;
}
document.getElementById('extStart').value = document.getElementById('gatherStart').value;
document.getElementById('extDuration').value = document.getElementById('gatherDuration').value;
var currentCount = 0;
if (gatherOverlapInfo && gatherOverlapInfo.groups && gatherOverlapInfo.groups[gatherActiveGroup]) {
currentCount = gatherOverlapInfo.groups[gatherActiveGroup].files.length;
} else {
currentCount = gatherSelectedFiles.size;
}
var lineDropdown = document.getElementById('gatherLineDropdown');
var currentLine = lineDropdown ? lineDropdown.value : '';
var scopeEl = document.getElementById('extScope');
scopeEl.options[0].text = 'Current selection (' + currentCount + ' files)';
if (currentLine) {
scopeEl.options[1].text = 'All files on ' + currentLine + ' line';
}
scopeEl.options[2].text = 'All 8 lines (' + files.filter(f=>f.name.endsWith(".h5")).length + ' files)';
updateExtEstimate();
resetExtractModal(); // Reset UI state
document.getElementById('extractModal').style.display = 'flex';
}
function fmtFileName(fn) {
var m = fn.match(/auto_(\d+)_(\d{2})(\d{2})(\d{2})_b(\d+)/);
if (m) {
var day = m[1], hh = m[2], mm = m[3], bid = m[5];
var node = darfNodes && darfNodes[bid];
var label = 'b' + bid;
if (node && node.line) label += ' L' + node.line;
label += ' (D' + day + ' ' + hh + ':' + mm + ')';
return label;
}
return fn.replace('.h5','').substring(0,25);
}
function fmtTime(s) {
s = Math.round(s);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0');
return m + ':' + String(sec).padStart(2,'0');
}
function getNodeInfo(fname) {
var m = fname.match(/auto_(\d+)_(\d{2})(\d{2})(\d{2})_b(\d+)/);
if (!m) return null;
var bid = m[5];
var node = darfNodes && darfNodes[bid];
return { day: m[1], hh: m[2], mm: m[3], bid: bid, node: node,
line: node ? node.line : 0, point: node ? node.point : 0 };
}
function onGatherParamChange() {
const start = parseInt(document.getElementById('gatherStart').value) || 0;
document.getElementById('gatherTimeSlider').value = start;
document.getElementById('gatherSliderLabel').textContent = fmtTime(start);
if (gatherSelectedFiles.size > 0) loadGather();
}
function onGatherSliderInput(val) {
val = parseInt(val);
document.getElementById('gatherStart').value = val;
document.getElementById('gatherSliderLabel').textContent = fmtTime(val);
clearTimeout(gatherSliderDebounce);
gatherSliderDebounce = setTimeout(() => { if (gatherSelectedFiles.size > 0) loadGather(); }, 300);
}
async function pollTaskStatus(taskId) {
if (activeExtractTask !== taskId) return;
try {
var r = await fetch(API + '/tasks/' + taskId);
if (!r.ok) throw new Error('Status check failed');
var status = await r.json();
if (status.status === 'processing' || status.status === 'pending') {
var pct = status.progress || 0;
var file = status.current_file ? status.current_file.substring(0,20)+'...' : '';
document.getElementById('extProgressBar').style.width = pct + '%';
document.getElementById('extPercentText').textContent = pct + '%';
document.getElementById('extStatusText').textContent = 'Processing ' + status.processed_files + '/' + status.total_files + (file ? ': ' + file : '');
setTimeout(function() { pollTaskStatus(taskId); }, 1000);
} else if (status.status === 'completed') {
document.getElementById('extProgressBar').style.width = '100%';
document.getElementById('extPercentText').textContent = '100%';
document.getElementById('extStatusText').textContent = 'Compressing & Downloading...';
window.location.href = API + '/tasks/' + taskId + '/download';
setTimeout(function() {
closeExtractModal();
resetExtractModal();
}, 2000);
} else if (status.status === 'error') {
throw new Error(status.error || 'Unknown error');
}
} catch(e) {
alert('Task error: ' + e.message);
resetExtractModal();
}
}
function populateGatherLineDropdown() {
const lines = new Set();
Object.values(darfNodes).forEach(n => { if (n.line) lines.add(n.line); });
const sorted = Array.from(lines).sort((a, b) => a - b);
const dd = document.getElementById('gatherLineDropdown');
dd.innerHTML = '<option value="">-- All lines --</option>' + sorted.map(l => `<option value="${l}">Line ${l}</option>`).join('');
}
function populateWaveformChannels(info) { var container = document.getElementById("waveformChannelChecks"); if (!container) return; container.innerHTML = ""; var channels = (info.calibrated_channels || []).concat(info.raw_channels || []); channels.forEach(function(d) { var label = document.createElement("label"); label.style.cssText = "display:flex;align-items:center;gap:4px;color:var(--text);font-size:.8rem;cursor:pointer"; var cb = document.createElement("input"); cb.type = "checkbox"; cb.value = d.path; cb.checked = (d.path === selectedChannel); label.appendChild(cb); label.appendChild(document.createTextNode(d.path.split("/").pop())); container.appendChild(label); });}
function renderFileItem(f, showCb, ni) {
const info = fileInfoCache[f.name] || {};
let infoText = '';
if (info.duration_human && info.num_channels && info.sample_rate_hz) {
infoText = '<div class="finfo">' + info.duration_human + ' · ' + info.num_channels + ' ch · ' + info.sample_rate_hz + ' Hz</div>';
}
const escapedName = f.name.replace(/'/g, "\\'");
const cb = showCb ? '<input type="checkbox" class="gather-cb" ' + (gatherSelectedFiles.has(f.name)?'checked':'') + ' onclick="event.stopPropagation();toggleGatherFile(\'' + escapedName + '\',this.checked)" style="margin-right:6px;cursor:pointer;accent-color:var(--accent)">' : '';
var label = fmtFileName(f.name);
if (ni && ni.point) label = 'P' + ni.point + ' — ' + label;
return '<div class="fitem' + (selectedFile===f.name?' active':'') + '" onclick="selectFile(\'' + escapedName + '\')">' +
'<div class="fhead">' + cb + '<span class="fn" title="' + f.name + '">' + label + '</span>' +
'<span class="ft ft-' + f.type + '">' + f.type.toUpperCase() + '</span></div>' +
infoText + '</div>';
}
function renderFilesFlat(list, showCb) {
var html = '<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:6px;padding:0 4px">';
html += '<button onclick="sidebarGroupBy=\'flat\';filterFiles()" style="font-size:10px;padding:2px 6px;background:var(--accent);border:none;color:#fff;border-radius:3px;cursor:pointer">Flat</button>';
html += '<button onclick="sidebarGroupBy=\'line\';filterFiles()" style="font-size:10px;padding:2px 6px;background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:3px;cursor:pointer">By Line</button>';
html += '</div>';
list.forEach(function(f) {
html += renderFileItem(f, showCb, getNodeInfo(f.name));
});
document.getElementById('fileList').innerHTML = html;
}
function resetExtractModal() {
activeExtractTask = null;
var btn = document.getElementById('extDownloadBtn');
if(btn) {
btn.disabled = false;
btn.style.opacity = '1';
}
var cancel = document.getElementById('extCancelBtn');
if(cancel) cancel.style.display = 'block';
var prog = document.getElementById('extProgressContainer');
if(prog) prog.style.display = 'none';
var est = document.getElementById('extEstimate');
if(est) est.style.display = 'block';
var bar = document.getElementById('extProgressBar');
if(bar) bar.style.width = '0%';
}
function selectAllGatherFiles() {
files.forEach(f => { if (f.name.endsWith('.h5')) gatherSelectedFiles.add(f.name); });
updateGatherSelCount();
filterFiles();
}
function selectGatherLine(lineVal) {
if (!lineVal) return;
const line = parseInt(lineVal);
const boardIds = new Set();
Object.entries(darfNodes).forEach(([bid, n]) => {
if (n.line === line) boardIds.add(bid);
});
files.forEach(f => {
const bid = extractBoardIdJs(f.name);
if (bid && boardIds.has(bid)) gatherSelectedFiles.add(f.name);
});
updateGatherSelCount();
filterFiles();
}
function toggleGatherFile(name, checked) {
if (checked) gatherSelectedFiles.add(name);
else gatherSelectedFiles.delete(name);
updateGatherSelCount();
}
function updateExtEstimate() {
var channels = document.getElementById('extChannels').value;
var scope = document.getElementById('extScope').value;
var duration = parseFloat(document.getElementById('extDuration').value) || 60;
var nCh = 8;
if (channels === 'all_cal' || channels === 'all_raw') nCh = 4;
else if (channels.startsWith('cal_') || channels.startsWith('raw_')) nCh = 1;
var nFiles = gatherSelectedFiles.size;
if (scope === 'all_lines') nFiles = files.filter(f=>f.name.endsWith('.h5')).length;
var samples = duration * 500 * nFiles;
var estMB = (samples * nCh * 10) / 1e6;
var estZipMB = estMB * 0.3;
document.getElementById('extEstimate').innerHTML =
'<b>' + nFiles + '</b> files × <b>' + nCh + '</b> ch × <b>' + duration + '</b>s @ 500Hz<br>' +
'Samples: <b>' + (samples).toLocaleString() + '</b> per channel<br>' +
'Estimated ZIP: ~<b>' + Math.round(estZipMB) + ' MB</b> (CSV ~' + Math.round(estMB) + ' MB uncompressed)<br>' +
'<span style="color:var(--green)">⚡ Full resolution — zero decimation</span>';
}
function updateGatherSelCount() {
document.getElementById('gatherSelCount').textContent = gatherSelectedFiles.size + ' selected';
}
function updateGatherSliderMax(maxDur) {
gatherMaxDuration = Math.floor(maxDur);
const slider = document.getElementById('gatherTimeSlider');
slider.max = gatherMaxDuration;
document.getElementById('gatherSliderMax').textContent = fmtTime(gatherMaxDuration);
const durEl = document.getElementById('gatherDuration');
durEl.max = gatherMaxDuration;
}
</script>
<div id="extractModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:9999;align-items:center;justify-content:center">
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;max-width:480px;width:90%;color:var(--text);box-shadow:0 8px 32px rgba(0,0,0,0.5)">
<h3 style="margin:0 0 16px;color:var(--accent)">📥 Gather Extract — Full Resolution</h3>
<div style="margin-bottom:12px">
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">📡 Channels</label>
<select id="extChannels" style="width:100%;padding:6px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px">
<option value="all8">All 8 channels (4 calibrated + 4 raw ADC)</option>
<option value="all_cal">4 calibrated channels</option>
<option value="all_raw">4 raw ADC channels</option>
<option value="cal_1">Calibrated channel 1 only</option>
<option value="cal_2">Calibrated channel 2 only</option>
<option value="cal_3">Calibrated channel 3 only</option>
<option value="cal_4">Calibrated channel 4 only</option>
<option value="raw_1">Raw ADC channel 1 only</option>
<option value="raw_2">Raw ADC channel 2 only</option>
<option value="raw_3">Raw ADC channel 3 only</option>
<option value="raw_4">Raw ADC channel 4 only</option>
</select>
</div>
<div style="margin-bottom:12px">
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">📐 Scope</label>
<select id="extScope" style="width:100%;padding:6px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px">
<option value="current">Current selection (23 files)</option>
<option value="line">All files on current line</option>
<option value="all_lines">All 8 lines (167 files)</option>
</select>
</div>
<div style="margin-bottom:12px">
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">⏱ Time Window</label>
<div style="display:flex;gap:8px">
<div style="flex:1"><label style="font-size:11px;color:var(--muted)">Start (s)</label><input type="number" id="extStart" style="width:100%;padding:4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px"></div>
<div style="flex:1"><label style="font-size:11px;color:var(--muted)">Duration (s)</label><input type="number" id="extDuration" style="width:100%;padding:4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px"></div>
</div>
</div>
<div style="margin-bottom:16px">
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:6px">📎 Include</label>
<label style="display:block;font-size:12px;margin-bottom:4px;cursor:pointer"><input type="checkbox" id="extIncNodes" checked> Node positions (DARF deployment data)</label>
<label style="display:block;font-size:12px;margin-bottom:4px;cursor:pointer"><input type="checkbox" id="extIncShots" checked> SPS shot points (matching time window)</label>
<label style="display:block;font-size:12px;cursor:pointer"><input type="checkbox" id="extIncMeta" checked> Metadata JSON</label>
</div>
<div id="extEstimate" style="background:var(--bg);padding:8px;border-radius:4px;font-size:12px;margin-bottom:16px;color:var(--muted)">
Estimating...
</div>
<!-- Progress Bar -->
<div id="extProgressContainer" style="display:none;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:4px">
<span id="extStatusText">Preparing...</span>
<span id="extPercentText">0%</span>
</div>
<div style="height:6px;background:var(--bg);border-radius:3px;overflow:hidden">
<div id="extProgressBar" style="width:0%;height:100%;background:var(--green);transition:width 0.3s"></div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="extCancelBtn" onclick="closeExtractModal()" style="padding:8px 16px;background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:4px;cursor:pointer">Cancel</button>
<button id="extDownloadBtn" onclick="doExtract()" style="padding:8px 16px;background:var(--accent);border:none;color:#fff;border-radius:4px;cursor:pointer;font-weight:600">📥 Download ZIP</button>
</div>
</div>
</div>
</body>
</html>