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:
2026-05-21 14:53:08 -04:00
parent ed0f93036d
commit a67815c61a
22 changed files with 9791 additions and 228 deletions
+128 -45
View File
@@ -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,
};