initial commit

This commit is contained in:
Will Bradley
2025-10-11 17:03:31 -07:00
commit 4767b67460
25 changed files with 5098 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
// Ensure data directory exists
const dataDir = path.join(__dirname, '..', '..', 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const dbPath = path.join(dataDir, 'meshtastic.db');
const db = new Database(dbPath);
// Enable WAL mode for better performance
db.pragma('journal_mode = WAL');
// Initialize database schema
function initializeDatabase() {
// Users table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
)
`);
// Nodes table - stores information about Meshtastic nodes
db.exec(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT UNIQUE NOT NULL,
short_name TEXT,
long_name TEXT,
hardware_model TEXT,
role TEXT,
firmware_version TEXT,
last_heard DATETIME,
battery_level INTEGER,
voltage REAL,
channel_utilization REAL,
air_util_tx REAL,
uptime_seconds INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Create index on node_id
db.exec(`
CREATE INDEX IF NOT EXISTS idx_nodes_node_id ON nodes(node_id)
`);
// Positions table - stores GPS position data
db.exec(`
CREATE TABLE IF NOT EXISTS positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
altitude INTEGER,
precision_bits INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (node_id) REFERENCES nodes(node_id)
)
`);
// Create indexes for positions
db.exec(`
CREATE INDEX IF NOT EXISTS idx_positions_node_id ON positions(node_id)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_positions_timestamp ON positions(timestamp)
`);
// Messages table - stores text messages
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id TEXT,
from_node TEXT NOT NULL,
to_node TEXT,
channel INTEGER,
text TEXT,
rx_time DATETIME,
rx_snr REAL,
rx_rssi INTEGER,
hop_limit INTEGER,
want_ack BOOLEAN,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (from_node) REFERENCES nodes(node_id)
)
`);
// Create indexes for messages
db.exec(`
CREATE INDEX IF NOT EXISTS idx_messages_from_node ON messages(from_node)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at)
`);
// Telemetry table - stores device telemetry data
db.exec(`
CREATE TABLE IF NOT EXISTS telemetry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id TEXT NOT NULL,
battery_level INTEGER,
voltage REAL,
channel_utilization REAL,
air_util_tx REAL,
uptime_seconds INTEGER,
temperature REAL,
relative_humidity REAL,
barometric_pressure REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (node_id) REFERENCES nodes(node_id)
)
`);
// Create indexes for telemetry
db.exec(`
CREATE INDEX IF NOT EXISTS idx_telemetry_node_id ON telemetry(node_id)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_telemetry_timestamp ON telemetry(timestamp)
`);
// Activity log table
db.exec(`
CREATE TABLE IF NOT EXISTS activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
details TEXT,
ip_address TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
console.log('Database initialized successfully');
}
// Initialize the database
initializeDatabase();
module.exports = db;
+244
View File
@@ -0,0 +1,244 @@
const db = require('./db');
// User queries
const userQueries = {
createUser: db.prepare(`
INSERT INTO users (username, password_hash)
VALUES (?, ?)
`),
getUserByUsername: db.prepare(`
SELECT * FROM users WHERE username = ?
`),
updateLastLogin: db.prepare(`
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?
`),
getAllUsers: db.prepare(`
SELECT id, username, created_at, last_login FROM users
`)
};
// Node queries
const nodeQueries = {
upsertNode: db.prepare(`
INSERT INTO nodes (
node_id, short_name, long_name, hardware_model, role,
firmware_version, last_heard, battery_level, voltage,
channel_utilization, air_util_tx, uptime_seconds, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(node_id) DO UPDATE SET
short_name = COALESCE(excluded.short_name, short_name),
long_name = COALESCE(excluded.long_name, long_name),
hardware_model = COALESCE(excluded.hardware_model, hardware_model),
role = COALESCE(excluded.role, role),
firmware_version = COALESCE(excluded.firmware_version, firmware_version),
last_heard = COALESCE(excluded.last_heard, last_heard),
battery_level = COALESCE(excluded.battery_level, battery_level),
voltage = COALESCE(excluded.voltage, voltage),
channel_utilization = COALESCE(excluded.channel_utilization, channel_utilization),
air_util_tx = COALESCE(excluded.air_util_tx, air_util_tx),
uptime_seconds = COALESCE(excluded.uptime_seconds, uptime_seconds),
updated_at = CURRENT_TIMESTAMP
`),
getNodeById: db.prepare(`
SELECT
n.*,
(SELECT latitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as latitude,
(SELECT longitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as longitude,
(SELECT altitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as altitude,
(SELECT timestamp FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as position_timestamp
FROM nodes n
WHERE n.node_id = ?
`),
getAllNodes: db.prepare(`
SELECT
n.*,
(SELECT latitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as latitude,
(SELECT longitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as longitude,
(SELECT altitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as altitude,
(SELECT timestamp FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as position_timestamp
FROM nodes n
ORDER BY n.last_heard DESC
`),
updateNodeLastHeard: db.prepare(`
UPDATE nodes SET last_heard = CURRENT_TIMESTAMP WHERE node_id = ?
`)
};
// Position queries
const positionQueries = {
insertPosition: db.prepare(`
INSERT INTO positions (node_id, latitude, longitude, altitude, precision_bits)
VALUES (?, ?, ?, ?, ?)
`),
getLatestPositions: db.prepare(`
SELECT p.*, n.short_name, n.long_name, n.last_heard
FROM positions p
LEFT JOIN nodes n ON p.node_id = n.node_id
WHERE p.id IN (
SELECT MAX(id) FROM positions GROUP BY node_id
)
ORDER BY p.timestamp DESC
`),
getPositionsByNode: db.prepare(`
SELECT * FROM positions
WHERE node_id = ?
ORDER BY timestamp DESC
LIMIT ?
`),
deleteOldPositions: db.prepare(`
DELETE FROM positions WHERE timestamp < datetime('now', '-' || ? || ' days')
`),
getPositionTrails: db.prepare(`
SELECT
node_id,
latitude,
longitude,
altitude,
timestamp,
id
FROM (
SELECT
node_id,
latitude,
longitude,
altitude,
timestamp,
id,
ROW_NUMBER() OVER (PARTITION BY node_id ORDER BY timestamp DESC) as rn
FROM positions
) AS ranked
WHERE rn <= ?
ORDER BY node_id, timestamp DESC
`)
};
// Message queries
const messageQueries = {
insertMessage: db.prepare(`
INSERT INTO messages (
message_id, from_node, to_node, channel, text,
rx_time, rx_snr, rx_rssi, hop_limit, want_ack
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
getRecentMessages: db.prepare(`
SELECT m.*,
n1.short_name as from_short_name,
n1.long_name as from_long_name,
n2.short_name as to_short_name,
n2.long_name as to_long_name
FROM messages m
LEFT JOIN nodes n1 ON m.from_node = n1.node_id
LEFT JOIN nodes n2 ON m.to_node = n2.node_id
ORDER BY m.created_at DESC
LIMIT ?
`),
getRecentMessagesWithTimeLimit: db.prepare(`
SELECT m.*,
n1.short_name as from_short_name,
n1.long_name as from_long_name,
n2.short_name as to_short_name,
n2.long_name as to_long_name
FROM messages m
LEFT JOIN nodes n1 ON m.from_node = n1.node_id
LEFT JOIN nodes n2 ON m.to_node = n2.node_id
WHERE m.created_at >= datetime('now', '-24 hours')
ORDER BY m.created_at DESC
LIMIT ?
`),
getMessagesByNode: db.prepare(`
SELECT m.*,
n1.short_name as from_short_name,
n1.long_name as from_long_name
FROM messages m
LEFT JOIN nodes n1 ON m.from_node = n1.node_id
WHERE m.from_node = ? OR m.to_node = ?
ORDER BY m.created_at DESC
LIMIT ?
`),
deleteOldMessages: db.prepare(`
DELETE FROM messages WHERE created_at < datetime('now', '-' || ? || ' days')
`)
};
// Telemetry queries
const telemetryQueries = {
insertTelemetry: db.prepare(`
INSERT INTO telemetry (
node_id, battery_level, voltage, channel_utilization,
air_util_tx, uptime_seconds, temperature, relative_humidity,
barometric_pressure
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
getLatestTelemetryByNode: db.prepare(`
SELECT * FROM telemetry
WHERE node_id = ?
ORDER BY timestamp DESC
LIMIT 1
`),
getTelemetryHistory: db.prepare(`
SELECT * FROM telemetry
WHERE node_id = ?
ORDER BY timestamp DESC
LIMIT ?
`),
deleteOldTelemetry: db.prepare(`
DELETE FROM telemetry WHERE timestamp < datetime('now', '-' || ? || ' days')
`)
};
// Activity log queries
const activityLogQueries = {
logActivity: db.prepare(`
INSERT INTO activity_log (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
`),
getRecentActivity: db.prepare(`
SELECT a.*, u.username
FROM activity_log a
LEFT JOIN users u ON a.user_id = u.id
ORDER BY a.timestamp DESC
LIMIT ?
`)
};
// Statistics queries
const statsQueries = {
getMessageCount: db.prepare(`SELECT COUNT(*) as count FROM messages`),
getNodeCount: db.prepare(`SELECT COUNT(*) as count FROM nodes`),
getPositionCount: db.prepare(`SELECT COUNT(*) as count FROM positions`),
getDbSize: () => {
const result = db.prepare(`
SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()
`).get();
return result.size;
}
};
module.exports = {
userQueries,
nodeQueries,
positionQueries,
messageQueries,
telemetryQueries,
activityLogQueries,
statsQueries
};