Files
seisee/backend_src/index.ts

251 lines
9.1 KiB
TypeScript

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<string, any[]> = {};
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}`);
});