import express from 'express'; import cors from 'cors'; import { readFileSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { join } from 'path'; import { spawn } from 'child_process'; import { Pool } from 'pg'; const app = express(); const PORT = 3001; app.use(cors()); app.use(express.json()); const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://postgres:seismic_pass@db:5432/seismic_data' }); const INDEX_PATH = '/mnt/kingston/seismic_webapp/data/index.json'; const EXPORT_DIR = '/mnt/kingston/seismic_webapp/exports'; if (!existsSync(EXPORT_DIR)) mkdirSync(EXPORT_DIR, { recursive: true }); // GET /api/migration-status - Renvoie l'état de la migration en cours app.get('/api/migration-status', async (req, res) => { try { const result = await pool.query('SELECT total_files, processed_files, current_file FROM migration_status WHERE id = 1'); if (result.rows.length > 0) { res.json(result.rows[0]); } else { res.status(404).json({ error: 'Status not found' }); } } catch (err) { res.status(500).json({ error: 'DB Error' }); } }); app.get('/api/nodes', (req, res) => { try { const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8')); const nodes = Object.values(index?.nodes || {}).map((n: any) => ({ id: n.id, position: n.position, hasDates: n.files && n.files.length > 0 })); res.json({ nodes }); } catch (e) { res.status(500).json({ error: 'Index read error' }); } }); app.get('/api/dates', (req, res) => { try { const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8')); res.json({ dates: index?.dates || [] }); } catch (e) { res.json({ dates: [] }); } }); app.get('/api/rms-timeline', (req, res) => { const { date, channel } = req.query; const cachePath = `/mnt/kingston/seismic_webapp/data/rms_cache/rms_${date}_${channel}.json`; if (existsSync(cachePath)) res.json(JSON.parse(readFileSync(cachePath, 'utf-8'))); else res.json({ nodes: {} }); }); app.get('/api/global-history', (req, res) => { const { channel } = req.query; const dir = '/mnt/kingston/seismic_webapp/data/rms_cache'; const nodes: Record = {}; try { if (existsSync(dir)) { readdirSync(dir).filter((f:string) => f.endsWith(`_${channel}.json`)).forEach((f:string) => { const data = JSON.parse(readFileSync(`${dir}/${f}`, 'utf-8')); Object.entries(data.nodes).forEach(([id, pts]: [string, any]) => { if (!nodes[id]) nodes[id] = []; nodes[id].push(...pts); }); }); } } catch (e) {} res.json({ nodes }); }); app.get('/api/data', async (req, res) => { const { node, start, channel } = req.query; const ts = parseInt(start as string); const startTime = new Date(ts * 1000); const endTime = new Date((ts + 10) * 1000); try { const dbRes = await pool.query('SELECT value FROM adc_samples WHERE node_id = $1 AND channel = $2 AND time >= $3 AND time < $4 ORDER BY time ASC', [node, channel, startTime, endTime]); if (dbRes.rows.length > 0) return res.json({ samples: dbRes.rows.map(r => r.value), source: 'db' }); const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8')); const targetFile = index?.nodes[node as string]?.files?.find((f: any) => ts >= f.start && ts <= f.end && (f.channel === channel || f.path.includes(`_${channel}_`))); if (!targetFile) return res.status(404).json({ error: 'File not found' }); const fixPath = (p: string) => p.replace(/\\/g, '/').replace('F:/', '/mnt/kingston/').replace('E:/', '/mnt/data_sdb1/'); const proc = spawn('python3', ['/app/scripts/extract_hdf5_window.py', '--file', fixPath(targetFile.path), '--channel', (channel as string).replace('ch',''), '--start', ts.toString(), '--duration', '10']); let stdout = ''; proc.stdout.on('data', (d) => stdout += d.toString()); proc.on('close', () => { try { res.json(JSON.parse(stdout)); } catch (e) { res.status(500).json({ error: 'Python error' }); } }); } catch (err) { res.status(500).json({ error: 'Server error' }); } }); app.post('/api/chat', (req, res) => { const { message } = req.body; const prompt = `Assistant Seismic. User: ${message}`; const proc = spawn('/home/floppyrj45/.local/bin/cursor-agent', ['--print', '--force', prompt]); let output = ''; proc.stdout.on('data', (d) => output += d.toString()); proc.on('close', () => res.json({ response: output })); }); app.get('/api/export', (req, res) => { const { node, date, channel, start } = req.query; try { const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8')); const targetFile = index?.nodes[node as string]?.files?.find((f:any) => f.path.includes(`_${channel}_`)); if (!targetFile) return res.status(404).send('Not found'); const fixPath = (p: string) => p.replace(/\\/g, '/').replace('F:/', '/mnt/kingston/').replace('E:/', '/mnt/data_sdb1/'); const proc = spawn('python3', ['/app/scripts/export_csv.py', '--file', fixPath(targetFile.path), '--start', start as string, '--duration', '3600', '--output', `/tmp/export_${node}.csv`]); proc.on('close', () => res.download(`/tmp/export_${node}.csv`)); } catch (e) { res.status(500).send('Export error'); } }); app.listen(PORT, () => console.log(`API port ${PORT}`)); // H5 Coverage Endpoints import sqlite3 from 'sqlite3'; const h5db = new sqlite3.Database('/app/h5_data.db', sqlite3.OPEN_READONLY, (err: any) => { if (err) console.error('H5 DB not available:', err.message); }); app.get('/api/h5/coverage', (req, res) => { h5db.all(` SELECT COUNT(*) as total_positions, SUM(CASE WHEN has_data = 1 THEN 1 ELSE 0 END) as with_data, SUM(CASE WHEN has_aux = 1 THEN 1 ELSE 0 END) as with_aux, SUM(sample_count) as total_files FROM positions `, (err: any, rows: any) => { if (err) return res.status(500).json({ error: err.message }); const stats = rows[0]; const coverage_pct = ((stats.with_data / stats.total_positions) * 100).toFixed(1); res.json({ total_positions: stats.total_positions, with_data: stats.with_data, with_aux: stats.with_aux, total_files: stats.total_files, coverage_pct: parseFloat(coverage_pct), missing: stats.total_positions - stats.with_data }); }); }); app.get('/api/h5/gaps', (req, res) => { h5db.all('SELECT node_code FROM positions WHERE has_data = 0 ORDER BY node_code', (err: any, rows: any) => { if (err) return res.status(500).json({ error: err.message }); const missing = rows.map((r: any) => r.node_code); const gaps = []; let gapStart = null; for (let i = 0; i < missing.length; i++) { if (i === 0 || missing[i] !== missing[i-1] + 1) { if (gapStart !== null) { gaps.push({ start: gapStart, end: missing[i-1], length: missing[i-1] - gapStart + 1 }); } gapStart = missing[i]; } } if (gapStart !== null) { gaps.push({ start: gapStart, end: missing[missing.length - 1], length: missing[missing.length - 1] - gapStart + 1 }); } res.json({ gaps, total_missing: missing.length }); }); }); // GET /api/conversion-status - Proxy vers la VM de conversion app.get('/api/conversion-status', async (req, res) => { try { const response = await fetch('http://192.168.0.81:8000/conversion-progress.json'); const data = await response.json(); res.json(data); } catch (err) { res.status(503).json({ error: 'VM conversion inaccessible', total: 345, processed: 0, errors: 0, percent: 0, eta_minutes: 0 }); } }); // ============ H5 2026 Format Endpoints ============ app.get('/api/h5/files', (req, res) => { const h5Dir = '/home/floppyrj45/docker/seismic-nodes-viewer/data/h5'; try { const files = readdirSync(h5Dir) .filter(f => f.endsWith('.h5')) .map(filename => { const match = filename.match(/rsn(\d+)/); const nodeId = match ? match[1] : 'unknown'; const matchDate = filename.match(/_(\d{6})_/); const date = matchDate ? matchDate[1] : ''; return { filename, nodeId, date, path: `${h5Dir}/${filename}` }; }); res.json({ files, count: files.length }); } catch (e) { res.status(500).json({ error: 'Cannot list H5 files' }); } }); app.get('/api/h5/data', (req, res) => { const { file, channel, start, duration } = req.query; const filePath = `/home/floppyrj45/docker/seismic-nodes-viewer/data/h5/${file}`; const proc = spawn('python3', [ '/home/floppyrj45/docker/seismic-nodes-viewer/scripts/extract_h5_calibrated.py', '--file', filePath, '--channel', channel as string, '--start', (start || '0') as string, '--duration', (duration || '10') as string ]); let stdout = ''; proc.stdout.on('data', d => stdout += d.toString()); proc.on('close', () => { try { res.json(JSON.parse(stdout)); } catch (e) { res.status(500).json({ error: 'Python script error', raw: stdout }); } }); }); // Docs endpoints app.get('/api/docs/manifest', (req, res) => { res.sendFile('/data/docs/campaign_manifest.json'); }); app.get('/api/docs/:filename', (req, res) => { const file = req.params.filename; res.sendFile(`/data/docs/${file}`); });