feat: refactor parsing and scanning logic for test files
- Updated `parseFilename` to `parseTargetFilename` and modified its return structure to include `test_id` instead of `file_id`. - Introduced `parseResultFilename` to extract `test_id` and `device` from result file names. - Enhanced `fullScan` to separately handle target and results directories, improving clarity and functionality. - Updated database interactions to use `test_id` instead of `file_id` across various modules. - Added a new `/rescan` endpoint to trigger a full scan of target and results directories. - Improved logging and error handling throughout the scanning process. - Introduced `parseTputRssi` to extract throughput and RSSI data from log files.
This commit is contained in:
+128
-45
@@ -1,64 +1,147 @@
|
||||
'use strict';
|
||||
/**
|
||||
* In-memory test store + JSON-file config persistence.
|
||||
* No native modules required.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
||||
const DB_PATH = path.join(__dirname, 'dashboard.db');
|
||||
const CONFIG_JSON = path.join(__dirname, 'config.json');
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// Enable WAL for better concurrent read performance
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// ── Schema ───────────────────────────────────────────────────────────────────
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tests (
|
||||
id TEXT PRIMARY KEY,
|
||||
test_id TEXT,
|
||||
parent_dir TEXT,
|
||||
filename TEXT,
|
||||
interference TEXT,
|
||||
device TEXT,
|
||||
rotation TEXT,
|
||||
test_point TEXT,
|
||||
station TEXT,
|
||||
band TEXT,
|
||||
channel TEXT,
|
||||
bandwidth TEXT,
|
||||
rssi TEXT,
|
||||
direction TEXT,
|
||||
completed INTEGER NOT NULL DEFAULT 0,
|
||||
completed_at TEXT,
|
||||
duration_seconds REAL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tests_test_id ON tests (test_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tests_device ON tests (device);
|
||||
`);
|
||||
|
||||
// Add new columns to existing databases (idempotent)
|
||||
for (const colDef of ['tput_mbps REAL', 'dl_rssi_dbm REAL', 'ul_rssi_dbm REAL', 'tput_results TEXT']) {
|
||||
try { db.exec(`ALTER TABLE tests ADD COLUMN ${colDef}`); } catch { /* already exists */ }
|
||||
}
|
||||
|
||||
// ── One-time migration from config.json ──────────────────────────────────────
|
||||
{
|
||||
const alreadyMigrated = db.prepare("SELECT COUNT(*) AS n FROM config").get().n > 0;
|
||||
if (!alreadyMigrated && fs.existsSync(CONFIG_JSON)) {
|
||||
try {
|
||||
const legacy = JSON.parse(fs.readFileSync(CONFIG_JSON, 'utf8'));
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)');
|
||||
const migrate = db.transaction((obj) => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v != null) insert.run(k, String(v));
|
||||
}
|
||||
});
|
||||
migrate(legacy);
|
||||
console.log('[db] Migrated config.json → SQLite');
|
||||
} catch (e) {
|
||||
console.warn('[db] Could not migrate config.json:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────────────
|
||||
let _config = {};
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
_config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
}
|
||||
} catch { _config = {}; }
|
||||
const _stmtGetConfig = db.prepare('SELECT value FROM config WHERE key = ?');
|
||||
const _stmtSetConfig = db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)');
|
||||
const _stmtDelConfig = db.prepare('DELETE FROM config WHERE key = ?');
|
||||
|
||||
function _saveConfig() {
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(_config, null, 2));
|
||||
}
|
||||
function getConfig(key) { return _stmtGetConfig.get(key)?.value ?? null; }
|
||||
function setConfig(key, value) { _stmtSetConfig.run(key, value); }
|
||||
function delConfig(key) { _stmtDelConfig.run(key); }
|
||||
|
||||
function getConfig(key) { return _config[key] ?? null; }
|
||||
function setConfig(key, value) { _config[key] = value; _saveConfig(); }
|
||||
function delConfig(key) { delete _config[key]; _saveConfig(); }
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
const _stmtUpsertTest = db.prepare(`
|
||||
INSERT INTO tests
|
||||
(id, test_id, parent_dir, filename, interference, device, rotation,
|
||||
test_point, station, band, channel, bandwidth, rssi, direction)
|
||||
VALUES
|
||||
(@id, @test_id, @parent_dir, @filename, @interference, @device, @rotation,
|
||||
@test_point, @station, @band, @channel, @bandwidth, @rssi, @direction)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
test_id = excluded.test_id,
|
||||
parent_dir = excluded.parent_dir,
|
||||
filename = excluded.filename,
|
||||
interference = excluded.interference,
|
||||
device = excluded.device,
|
||||
rotation = excluded.rotation,
|
||||
test_point = excluded.test_point,
|
||||
station = excluded.station,
|
||||
band = excluded.band,
|
||||
channel = excluded.channel,
|
||||
bandwidth = excluded.bandwidth,
|
||||
rssi = excluded.rssi,
|
||||
direction = excluded.direction
|
||||
-- completed / completed_at / duration_seconds intentionally preserved
|
||||
`);
|
||||
|
||||
// ── Tests (keyed by full derived ID) ─────────────────────────────────────────
|
||||
/** @type {Map<string, object>} */
|
||||
const _tests = new Map();
|
||||
const _stmtMarkCompleted = db.prepare(`
|
||||
UPDATE tests
|
||||
SET completed = 1, completed_at = ?, duration_seconds = ?, tput_results = ?
|
||||
WHERE test_id = ? AND (? IS NULL OR device = ?)
|
||||
`);
|
||||
|
||||
const _stmtResetTest = db.prepare(`
|
||||
UPDATE tests
|
||||
SET completed = 0, completed_at = NULL, duration_seconds = NULL, tput_results = NULL
|
||||
WHERE test_id = ? AND (? IS NULL OR device = ?)
|
||||
`);
|
||||
|
||||
const _stmtGetStation = db.prepare('SELECT station FROM tests WHERE test_id = ? AND device = ? LIMIT 1');
|
||||
|
||||
const _stmtClearTests = db.prepare('DELETE FROM tests');
|
||||
const _stmtGetAllTests = db.prepare('SELECT * FROM tests');
|
||||
const _stmtCountTests = db.prepare('SELECT COUNT(*) AS n FROM tests');
|
||||
|
||||
function upsertTest(test) {
|
||||
const existing = _tests.get(test.id);
|
||||
_tests.set(test.id, {
|
||||
...test,
|
||||
// Preserve completion state when re-inserting from a target scan
|
||||
completed: existing ? existing.completed : 0,
|
||||
completed_at: existing ? existing.completed_at : null,
|
||||
duration_seconds: existing ? existing.duration_seconds : null,
|
||||
});
|
||||
_stmtUpsertTest.run(test);
|
||||
}
|
||||
|
||||
function markCompleted(file_id, device, completed_at, duration_seconds) {
|
||||
for (const [id, test] of _tests) {
|
||||
if (test.file_id === file_id && (!device || test.device === device)) {
|
||||
_tests.set(id, { ...test, completed: 1, completed_at, duration_seconds });
|
||||
}
|
||||
}
|
||||
function markCompleted(test_id, device, completed_at, duration_seconds, tputResults = null) {
|
||||
const json = tputResults && tputResults.length > 0 ? JSON.stringify(tputResults) : null;
|
||||
_stmtMarkCompleted.run(completed_at, duration_seconds, json, test_id, device, device);
|
||||
}
|
||||
|
||||
function resetByFileIdAndDevice(file_id, device) {
|
||||
for (const [id, test] of _tests) {
|
||||
if (test.file_id === file_id && (!device || test.device === device)) {
|
||||
_tests.set(id, { ...test, completed: 0, completed_at: null, duration_seconds: null });
|
||||
}
|
||||
}
|
||||
function getStationForTest(test_id, device) {
|
||||
return _stmtGetStation.get(test_id, device)?.station ?? null;
|
||||
}
|
||||
|
||||
function clearTests() { _tests.clear(); }
|
||||
function getAllTests() { return Array.from(_tests.values()); }
|
||||
function resetByFileIdAndDevice(test_id, device) {
|
||||
_stmtResetTest.run(test_id, device, device);
|
||||
}
|
||||
|
||||
function clearTests() { _stmtClearTests.run(); }
|
||||
function getAllTests() { return _stmtGetAllTests.all(); }
|
||||
function countTests() { return _stmtCountTests.get().n; }
|
||||
|
||||
module.exports = {
|
||||
getConfig, setConfig, delConfig,
|
||||
upsertTest, markCompleted, resetByFileIdAndDevice, clearTests, getAllTests,
|
||||
upsertTest, markCompleted, resetByFileIdAndDevice, clearTests, getAllTests, countTests,
|
||||
getStationForTest,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user