//! Storage: SQLite-backed persistence (CLAUDE.md section 3). //! //! Section 1.5.3 requires lock and Strict state to live in the core database //! rather than in editable plaintext config, so that editing a file is not //! enough to defeat a lock. The schema therefore keeps schedules, lock state, //! and usage counters in one place with no config file beside them. //! //! # Deliberately thin in M0 //! //! rusqlite is not yet a dependency — the machine this was built on has 9.7 GB //! free and section 3 makes every crate justify its size cost. This module //! defines the schema and the statements M1 will use, with no I/O behind it //! yet, so the schema is reviewable before it is compiled. /// The schema, version 1. /// /// Applied by `PRAGMA user_version`. Migrations are additive and forward-only; /// downgrading a schema would risk losing lock state, which section 1.5.4 does /// not permit. pub const SCHEMA_VERSION: i64 = 1; pub const SCHEMA: &str = r#" CREATE TABLE IF NOT EXISTS schedule ( id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT, mode TEXT NOT NULL CHECK (mode IN ('Blocklist', 'Allowlist')), targets TEXT NOT NULL, -- JSON array of Target conditions TEXT NOT NULL, -- JSON array of Condition paused INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); -- Section 1.5.3: lock state lives in the DB, not in a plaintext config file, -- so that editing a file cannot defeat a lock. CREATE TABLE IF NOT EXISTS lock_config ( id INTEGER PRIMARY KEY CHECK (id = 1), level TEXT NOT NULL CHECK (level IN ('Normal', 'Strict', 'Stubborn')), schedules_locked INTEGER NOT NULL, blocks_uninstall INTEGER NOT NULL, keyholder_code TEXT, -- NULL if the user declined section 1.5.2.7 -- Monotonic deadlines for a filed disable request, section 1.5.2.2. -- NULL when no request is pending. request_ready_at INTEGER, request_window_closes_at INTEGER, -- Elapsed-so-far plus boot id, so a reboot resumes conservatively. boot_id INTEGER NOT NULL, last_monotonic_ms INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS disable_attempt ( id INTEGER PRIMARY KEY AUTOINCREMENT, at_monotonic INTEGER NOT NULL, boot_id INTEGER NOT NULL, day_index INTEGER NOT NULL -- for the "4 times today" count ); CREATE TABLE IF NOT EXISTS usage_daily ( day_index INTEGER NOT NULL, -- days since epoch, local target TEXT NOT NULL, minutes INTEGER NOT NULL DEFAULT 0, launches INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (day_index, target) ); CREATE TABLE IF NOT EXISTS usage_hourly ( hour_index INTEGER NOT NULL, -- hours since epoch, local target TEXT NOT NULL, minutes INTEGER NOT NULL DEFAULT 0, launches INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (hour_index, target) ); CREATE TABLE IF NOT EXISTS meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS usage_daily_target ON usage_daily (target); CREATE INDEX IF NOT EXISTS usage_hourly_target ON usage_hourly (target); CREATE INDEX IF NOT EXISTS disable_attempt_day ON disable_attempt (day_index); "#; /// Fail open on corruption (CLAUDE.md section 1.5.4). /// /// A corrupt database must never leave a device unusable, so recovery renames /// the bad file aside and starts clean rather than failing to launch. The /// damaged file is kept for diagnosis. /// /// `None` here means "fresh database", which is the fail-open outcome. pub const RECOVERY_NOTE: &str = "corrupt database renamed aside; blocking re-applied from defaults on next start";