mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 20:16:35 +00:00
252 lines
9.8 KiB
TypeScript
252 lines
9.8 KiB
TypeScript
/**
|
|
* Database migrations.
|
|
*
|
|
* Idempotent — `service-store.init()` runs ALL of these on every startup,
|
|
* inside one transaction. SQLite tolerates `IF NOT EXISTS` everywhere we
|
|
* need it. When schemas change non-additively, we'll graduate to a real
|
|
* versioned migrator; for v0.1 this is sufficient.
|
|
*
|
|
* NOTE on datetimes: stored as TEXT in ISO-8601 UTC ("YYYY-MM-DDTHH:MM:SS.sssZ").
|
|
* Application code uses `new Date().toISOString()` for writes and
|
|
* `new Date(value)` for reads. No tz-aware datetime gotcha because TEXT is
|
|
* pure string round-trip. (Old python build hit a pain point with
|
|
* SQLAlchemy's DateTime adapter — we avoid the whole class of issue here.)
|
|
*/
|
|
|
|
export const MIGRATIONS: readonly string[] = [
|
|
// ---- users ---------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'operator' CHECK(role IN ('admin', 'operator')),
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
|
totp_secret_encrypted TEXT,
|
|
recovery_codes_hashed TEXT NOT NULL DEFAULT '[]',
|
|
must_change_password INTEGER NOT NULL DEFAULT 0,
|
|
failed_login_count INTEGER NOT NULL DEFAULT 0,
|
|
locked_until TEXT,
|
|
last_login_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT`,
|
|
|
|
// ---- sessions ------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
csrf_token TEXT NOT NULL,
|
|
totp_pending INTEGER NOT NULL DEFAULT 0,
|
|
user_agent TEXT,
|
|
ip_address TEXT,
|
|
issued_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
expires_at TEXT NOT NULL,
|
|
revoked_at TEXT
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_sessions_active
|
|
ON sessions(expires_at)
|
|
WHERE revoked_at IS NULL`,
|
|
|
|
// ---- api_keys ------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS api_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
key_hash TEXT NOT NULL,
|
|
key_prefix TEXT NOT NULL,
|
|
scopes TEXT NOT NULL DEFAULT '[]',
|
|
expires_at TEXT,
|
|
last_used_at TEXT,
|
|
last_used_ip TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
revoked_at TEXT
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix)`,
|
|
|
|
// ---- setup_state (singleton row, id=1) -----------------------------------
|
|
`CREATE TABLE IF NOT EXISTS setup_state (
|
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
is_complete INTEGER NOT NULL DEFAULT 0,
|
|
cluster_key_provisioned INTEGER NOT NULL DEFAULT 0,
|
|
nodered_flows_deployed INTEGER NOT NULL DEFAULT 0,
|
|
completed_at TEXT,
|
|
extras TEXT NOT NULL DEFAULT '{}'
|
|
) STRICT`,
|
|
`INSERT OR IGNORE INTO setup_state (id) VALUES (1)`,
|
|
|
|
// ---- displays ------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS displays (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
"index" INTEGER NOT NULL UNIQUE,
|
|
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
width_px INTEGER NOT NULL DEFAULT 1920,
|
|
height_px INTEGER NOT NULL DEFAULT 1080,
|
|
default_layout_id INTEGER,
|
|
idle_timeout_seconds INTEGER NOT NULL DEFAULT 600,
|
|
sleep_timeout_seconds INTEGER NOT NULL DEFAULT 1800,
|
|
cec_enabled INTEGER NOT NULL DEFAULT 1,
|
|
cec_device_path TEXT,
|
|
cec_logical_address INTEGER,
|
|
desired_power_state TEXT NOT NULL DEFAULT 'follow_layout'
|
|
CHECK(desired_power_state IN ('follow_layout', 'on', 'standby')),
|
|
state_check_enabled INTEGER NOT NULL DEFAULT 0,
|
|
state_check_interval_seconds INTEGER NOT NULL DEFAULT 60
|
|
) STRICT`,
|
|
|
|
// ---- cameras -------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS cameras (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
type TEXT NOT NULL CHECK(type IN ('rtsp', 'onvif')),
|
|
rtsp_url TEXT,
|
|
onvif_host TEXT,
|
|
onvif_port INTEGER,
|
|
onvif_username TEXT,
|
|
onvif_password TEXT,
|
|
capabilities TEXT NOT NULL DEFAULT '[]',
|
|
stream_policy TEXT NOT NULL DEFAULT 'auto'
|
|
CHECK(stream_policy IN ('auto', 'always_main', 'always_sub')),
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
last_seen_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS camera_streams (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
camera_id INTEGER NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL CHECK(role IN ('main', 'sub', 'other')),
|
|
name TEXT NOT NULL,
|
|
profile_token TEXT,
|
|
rtsp_uri TEXT NOT NULL,
|
|
width INTEGER,
|
|
height INTEGER,
|
|
encoding TEXT,
|
|
framerate REAL,
|
|
bitrate_kbps INTEGER,
|
|
is_discovered INTEGER NOT NULL DEFAULT 0
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_camera_streams_camera ON camera_streams(camera_id)`,
|
|
|
|
// ---- layout templates + layouts + cells ----------------------------------
|
|
`CREATE TABLE IF NOT EXISTS layout_templates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
regions TEXT NOT NULL DEFAULT '[]',
|
|
grid_cols INTEGER NOT NULL DEFAULT 12,
|
|
grid_rows INTEGER NOT NULL DEFAULT 12,
|
|
is_builtin INTEGER NOT NULL DEFAULT 0
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS layouts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
template_id INTEGER NOT NULL REFERENCES layout_templates(id),
|
|
display_id INTEGER NOT NULL REFERENCES displays(id),
|
|
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('hot', 'normal', 'cold')),
|
|
cooling_timeout_seconds INTEGER,
|
|
preload_camera_ids TEXT NOT NULL DEFAULT '[]',
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
resets_idle_timer INTEGER NOT NULL DEFAULT 1
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS layout_cells (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
|
region_name TEXT NOT NULL,
|
|
content_type TEXT NOT NULL CHECK(content_type IN ('camera', 'web', 'html')),
|
|
camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL,
|
|
stream_selector TEXT NOT NULL DEFAULT 'auto'
|
|
CHECK(stream_selector IN ('auto', 'main', 'sub')),
|
|
web_url TEXT,
|
|
html_content TEXT,
|
|
cooling_timeout_seconds INTEGER,
|
|
options TEXT NOT NULL DEFAULT '{}'
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_layout_cells_layout ON layout_cells(layout_id)`,
|
|
|
|
// ---- kiosks --------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS kiosks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
key_hash TEXT NOT NULL,
|
|
key_prefix TEXT NOT NULL,
|
|
capabilities TEXT NOT NULL DEFAULT '[]',
|
|
hardware_model TEXT,
|
|
os_version TEXT,
|
|
kiosk_app_version TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
paired_at TEXT,
|
|
last_seen_at TEXT,
|
|
last_bundle_version TEXT,
|
|
display_id INTEGER REFERENCES displays(id) ON DELETE SET NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_kiosks_prefix ON kiosks(key_prefix)`,
|
|
|
|
// ---- labels --------------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS labels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
color TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS kiosk_labels (
|
|
kiosk_id INTEGER NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
|
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL CHECK(role IN ('consume', 'operate')),
|
|
PRIMARY KEY (kiosk_id, label_id, role)
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS camera_labels (
|
|
camera_id INTEGER NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
|
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (camera_id, label_id)
|
|
) STRICT`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS layout_labels (
|
|
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
|
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (layout_id, label_id)
|
|
) STRICT`,
|
|
|
|
// ---- pairing_codes -------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS pairing_codes (
|
|
code TEXT PRIMARY KEY,
|
|
kiosk_proposed_name TEXT,
|
|
kiosk_hardware_model TEXT,
|
|
kiosk_capabilities TEXT NOT NULL DEFAULT '[]',
|
|
issued_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
expires_at TEXT NOT NULL,
|
|
consumed_at TEXT,
|
|
consumed_by_kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL,
|
|
extras TEXT NOT NULL DEFAULT '{}'
|
|
) STRICT`,
|
|
|
|
// ---- event_log -----------------------------------------------------------
|
|
`CREATE TABLE IF NOT EXISTS event_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL,
|
|
source_camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL,
|
|
source_type TEXT NOT NULL CHECK(source_type IN ('onvif', 'gpio', 'synthetic', 'system')),
|
|
topic TEXT NOT NULL,
|
|
property_op TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
received_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
forwarded_to_nodered INTEGER NOT NULL DEFAULT 0
|
|
) STRICT`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_event_log_received ON event_log(received_at DESC)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_event_log_topic ON event_log(topic, received_at DESC)`,
|
|
];
|