166 lines
5.2 KiB
TypeScript
166 lines
5.2 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import Database from "better-sqlite3";
|
|
|
|
export type CompanionDatabase = Database.Database;
|
|
|
|
export function openDatabase(path: string): CompanionDatabase {
|
|
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true, mode: 0o750 });
|
|
|
|
const database = new Database(path);
|
|
database.pragma("journal_mode = WAL");
|
|
database.pragma("foreign_keys = ON");
|
|
database.pragma("busy_timeout = 5000");
|
|
migrate(database);
|
|
return database;
|
|
}
|
|
|
|
function migrate(database: CompanionDatabase): void {
|
|
database.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL CHECK (role = 'admin'),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id_hash TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions(expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
action TEXT NOT NULL,
|
|
result TEXT NOT NULL CHECK (result IN ('success', 'failure')),
|
|
ip_address TEXT,
|
|
details_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS job_runs (
|
|
id TEXT PRIMARY KEY,
|
|
job_id TEXT NOT NULL,
|
|
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
status TEXT NOT NULL CHECK (status IN (
|
|
'queued', 'running', 'succeeded', 'failed', 'timed_out', 'interrupted'
|
|
)),
|
|
input_json TEXT NOT NULL DEFAULT '{}',
|
|
output_json TEXT,
|
|
error_code TEXT,
|
|
created_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS job_runs_created_idx ON job_runs(created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS adb_shortcuts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
target_type TEXT NOT NULL CHECK (target_type IN ('component', 'package')),
|
|
target_value TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(target_type, target_value)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mobile_pairing_codes (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
code_hash TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mobile_devices (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
revoked_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mobile_phone_state (
|
|
device_id TEXT PRIMARY KEY REFERENCES mobile_devices(id) ON DELETE CASCADE,
|
|
received_at TEXT NOT NULL,
|
|
captured_at TEXT NOT NULL,
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
accuracy_meters REAL NOT NULL,
|
|
altitude_meters REAL,
|
|
speed_kph REAL,
|
|
bearing_degrees REAL,
|
|
battery_percent REAL,
|
|
network_type TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS car_telemetry_samples (
|
|
recorded_at INTEGER PRIMARY KEY,
|
|
speed_kph REAL,
|
|
wheel_angle_degrees REAL,
|
|
battery_percent REAL,
|
|
range_km REAL,
|
|
battery_voltage REAL,
|
|
total_consumption_kwh REAL,
|
|
charging_status INTEGER,
|
|
charging_power_kw REAL
|
|
) WITHOUT ROWID;
|
|
|
|
CREATE TABLE IF NOT EXISTS car_telemetry_hourly (
|
|
bucket_start INTEGER PRIMARY KEY,
|
|
sample_count INTEGER NOT NULL,
|
|
speed_sum REAL NOT NULL,
|
|
speed_count INTEGER NOT NULL,
|
|
speed_max REAL,
|
|
soc_sum REAL NOT NULL,
|
|
soc_count INTEGER NOT NULL,
|
|
soc_min REAL,
|
|
soc_max REAL,
|
|
range_sum REAL NOT NULL,
|
|
range_count INTEGER NOT NULL,
|
|
range_max REAL,
|
|
charge_power_sum REAL NOT NULL,
|
|
charge_power_count INTEGER NOT NULL,
|
|
charge_power_max REAL,
|
|
distance_km REAL NOT NULL,
|
|
driving_seconds REAL NOT NULL,
|
|
charging_seconds REAL NOT NULL
|
|
) WITHOUT ROWID;
|
|
|
|
CREATE TABLE IF NOT EXISTS car_telemetry_stats (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
first_sample_at INTEGER,
|
|
last_sample_at INTEGER,
|
|
sample_count INTEGER NOT NULL DEFAULT 0,
|
|
peak_speed_kph REAL,
|
|
peak_speed_at INTEGER,
|
|
peak_charging_power_kw REAL,
|
|
peak_charging_power_at INTEGER,
|
|
max_observed_range_km REAL,
|
|
max_observed_range_at INTEGER,
|
|
tracked_distance_km REAL NOT NULL DEFAULT 0,
|
|
driving_seconds REAL NOT NULL DEFAULT 0,
|
|
charging_seconds REAL NOT NULL DEFAULT 0,
|
|
charge_sessions INTEGER NOT NULL DEFAULT 0,
|
|
soc_used_percent REAL NOT NULL DEFAULT 0
|
|
);
|
|
|
|
INSERT OR IGNORE INTO car_telemetry_stats (id) VALUES (1);
|
|
`);
|
|
|
|
database
|
|
.prepare(
|
|
`UPDATE job_runs
|
|
SET status = 'interrupted', error_code = 'PROCESS_RESTARTED', finished_at = ?
|
|
WHERE status IN ('queued', 'running')`
|
|
)
|
|
.run(new Date().toISOString());
|
|
}
|