Fix coverage: add /api/coverage route, remove stray gather code from loadCoverage
This commit is contained in:
250
backend_src/index.ts
Normal file
250
backend_src/index.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user