#include "database_manager.h" #include #include #include #include #include #include #include namespace radiantwave::db { // ─── Constructor & Destructor ────────────────────────────────────────── DatabaseManager::DatabaseManager() = default; DatabaseManager::~DatabaseManager() { close(); } // ─── Path Resolution ─────────────────────────────────────────────────── std::string DatabaseManager::resolvePath(const std::string& raw_path) const { std::string path = raw_path; if (path.empty()) { const char* home = std::getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); if (pw) { home = pw->pw_dir; } } if (home) { path = std::string(home) + "/.local/radiantwave/radiantwave.db"; } else { path = "/tmp/radiantwave/radiantwave.db"; } } else if (path.front() == '~') { const char* home = std::getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); if (pw) { home = pw->pw_dir; } } if (home) { path = std::string(home) + path.substr(1); } } return path; } // ─── Lifecycle & Migrations ──────────────────────────────────────────── bool DatabaseManager::initialize(const std::string& custom_path) { db_path_ = resolvePath(custom_path); // Create directories if needed try { std::filesystem::path p(db_path_); if (p.has_parent_path()) { std::filesystem::create_directories(p.parent_path()); } } catch (const std::exception& e) { std::cerr << "[DB] Failed to create directories: " << e.what() << std::endl; return false; } int rc = sqlite3_open_v2( db_path_.c_str(), &db_, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr ); if (rc != SQLITE_OK) { std::cerr << "[DB] Cannot open database " << db_path_ << ": " << sqlite3_errmsg(db_) << std::endl; return false; } // Enable WAL mode and foreign keys for optimal kiosk performance executeSql("PRAGMA journal_mode=WAL;"); executeSql("PRAGMA foreign_keys=ON;"); return runMigrations(); } void DatabaseManager::close() { std::lock_guard lock(mutex_); if (db_) { sqlite3_close_v2(db_); db_ = nullptr; } } bool DatabaseManager::isOpen() const { return db_ != nullptr; } bool DatabaseManager::executeSql(const std::string& sql) { char* err_msg = nullptr; int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err_msg); if (rc != SQLITE_OK) { std::cerr << "[DB] SQL error: " << (err_msg ? err_msg : "unknown") << std::endl; if (err_msg) sqlite3_free(err_msg); return false; } return true; } bool DatabaseManager::runMigrations() { std::lock_guard lock(mutex_); const char* schema = R"( CREATE TABLE IF NOT EXISTS system_kv ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS proclamations ( id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, solfeggio_freq_hz REAL DEFAULT 528.0, category TEXT DEFAULT 'general', is_active INTEGER DEFAULT 1, selected INTEGER DEFAULT 0, category_id INTEGER DEFAULT 0, group_id INTEGER DEFAULT 0, solfeggio_frequency_id INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS proclamation_categories ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS proclamation_groups ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS asset_downloads ( id TEXT PRIMARY KEY, filename TEXT NOT NULL, url TEXT NOT NULL, status TEXT CHECK (status IN ('pending', 'downloading', 'completed', 'failed')) DEFAULT 'pending', bytes_downloaded INTEGER DEFAULT 0, total_bytes INTEGER DEFAULT 0, error_message TEXT DEFAULT '', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS audio_files ( id INTEGER PRIMARY KEY, display_name TEXT NOT NULL, filename TEXT NOT NULL UNIQUE, display_order INTEGER DEFAULT 0, solfeggio_frequency_id INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS event_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, level TEXT CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', component TEXT NOT NULL, message TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS color_palettes ( solfeggio_frequency_id INTEGER NOT NULL, color_index INTEGER NOT NULL, hex TEXT NOT NULL, PRIMARY KEY (solfeggio_frequency_id, color_index) ); CREATE TABLE IF NOT EXISTS schedule_blocks ( id TEXT PRIMARY KEY, start_minutes INTEGER NOT NULL DEFAULT 0, end_minutes INTEGER NOT NULL DEFAULT 0, overnight INTEGER NOT NULL DEFAULT 0, duration_minutes INTEGER NOT NULL DEFAULT 0, dynamic_audio INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS schedule_block_proclamations ( block_id TEXT NOT NULL, position INTEGER NOT NULL, proclamation_id TEXT NOT NULL, PRIMARY KEY (block_id, position) ); )"; bool ok = executeSql(schema); if (ok) { // Back-fill columns for databases created before these fields existed. ensureColumn("proclamations", "selected", "selected INTEGER DEFAULT 0"); ensureColumn("proclamations", "category_id", "category_id INTEGER DEFAULT 0"); ensureColumn("proclamations", "group_id", "group_id INTEGER DEFAULT 0"); ensureColumn("proclamations", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0"); ensureColumn("audio_files", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0"); // Clean up the pre-active/inactive-license subscription level keys. // Existing devices may still carry them; nothing reads them anymore. executeSql("DELETE FROM system_kv WHERE key IN " "('subscription_level_id','subscription_level_name','subscription_level_description');"); } if (ok) { ok = migrateSubCategoriesToGroups(); } if (ok) { seedColorPalettes(); } return ok; } // Schema migration v2: sub-categories -> tag-like groups. // // Pre-v2 databases modelled a two-level category tree via // `proclamation_categories.parent_id`, and proclamations pointed at either a // root or a sub-category. v2 flattens categories and turns each former // sub-category into a group, remapping the proclamations that referenced it to // its parent category + the new group. Selection flags are never touched, so // the user's picks survive the upgrade. `PRAGMA user_version` records the // migration so it runs exactly once; the whole thing is one transaction so a // failure leaves the database untouched. No network is required, so this // always runs at startup, including on offline kiosks. bool DatabaseManager::migrateSubCategoriesToGroups() { if (!db_) return false; int userVersion = 0; { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, "PRAGMA user_version;", -1, &stmt, nullptr) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { userVersion = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); } } if (userVersion >= 2) { return true; // already migrated } // Fresh databases (created by the v2 schema) have no legacy parent_id // column, so there is nothing to convert: just stamp the version. if (!columnExists("proclamation_categories", "parent_id")) { return executeSql("PRAGMA user_version = 2;"); } bool migrated = executeSql( "BEGIN;" "INSERT OR IGNORE INTO proclamation_groups (id, name, sort_order) " " SELECT id, name, sort_order FROM proclamation_categories WHERE parent_id != 0;" "UPDATE proclamations " " SET group_id = category_id, " " category_id = COALESCE(" " (SELECT parent_id FROM proclamation_categories " " WHERE id = proclamations.category_id), " " category_id) " " WHERE category_id IN " " (SELECT id FROM proclamation_categories WHERE parent_id != 0);" "DELETE FROM proclamation_categories WHERE parent_id != 0;" "PRAGMA user_version = 2;" "COMMIT;"); if (!migrated) { executeSql("ROLLBACK;"); } return migrated; } void DatabaseManager::seedColorPalettes() { static const char* kSeedSql = "INSERT OR IGNORE INTO color_palettes (solfeggio_frequency_id, color_index, hex) VALUES " // 396 Hz "(2,0,'#8B0000'),(2,1,'#B22222'),(2,2,'#DC143C'),(2,3,'#FF6347'),(2,4,'#FF8C69'),(2,5,'#FFA500'),(2,6,'#FFDAB9')," // 417 Hz "(3,0,'#FF4500'),(3,1,'#FF7F50'),(3,2,'#FF8C00'),(3,3,'#FFA500'),(3,4,'#FFC87F'),(3,5,'#FFDFBA'),(3,6,'#FFEBCD')," // 528 Hz "(5,0,'#32CD32'),(5,1,'#3CB371'),(5,2,'#90EE90'),(5,3,'#98FB98'),(5,4,'#ADFFA9'),(5,5,'#C8FFD2'),(5,6,'#F0FFF0')," // 639 Hz "(6,0,'#00FF7F'),(6,1,'#00CED1'),(6,2,'#40E0D0'),(6,3,'#5F9EA0'),(6,4,'#7FFFD4'),(6,5,'#AFEEEE'),(6,6,'#E0FFFF')," // 741 Hz "(7,0,'#00BFFF'),(7,1,'#1E90FF'),(7,2,'#4169E1'),(7,3,'#6495ED'),(7,4,'#87CEEB'),(7,5,'#ADD8E6'),(7,6,'#F0F8FF')," // 852 Hz "(8,0,'#4B0082'),(8,1,'#9370DB'),(8,2,'#8A2BE2'),(8,3,'#7B68EE'),(8,4,'#DA70D6'),(8,5,'#D8BFD8'),(8,6,'#E6E6FA')," // 963 Hz "(9,0,'#9400D3'),(9,1,'#BA55D3'),(9,2,'#C71585'),(9,3,'#DDA0DD'),(9,4,'#EE82EE'),(9,5,'#FFF0F5'),(9,6,'#FAF0FA')," // 174/285 Hz (grounding/relief): warm gold "(1,0,'#8B6914'),(1,1,'#B8860B'),(1,2,'#DAA520'),(1,3,'#E8B64C'),(1,4,'#FFD700'),(1,5,'#FFE9A8'),(1,6,'#FFF4CC')," // 432 Hz (clarity/wellbeing): soft rose "(4,0,'#8B2252'),(4,1,'#CD3278'),(4,2,'#DB7093'),(4,3,'#FF69B4'),(4,4,'#FFB6C1'),(4,5,'#FFC0CB'),(4,6,'#FFE4E1');"; executeSql(kSeedSql); } bool DatabaseManager::columnExists(const std::string& table, const std::string& column) { if (!db_) return false; const char* sql = "SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, table.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, column.c_str(), -1, SQLITE_TRANSIENT); bool exists = (sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_int(stmt, 0) > 0); sqlite3_finalize(stmt); return exists; } bool DatabaseManager::ensureColumn(const std::string& table, const std::string& column, const std::string& column_ddl) { if (columnExists(table, column)) { return true; } return executeSql("ALTER TABLE " + table + " ADD COLUMN " + column_ddl + ";"); } // ─── Key-Value Storage (system_kv table) ─────────────────────────────── bool DatabaseManager::setKV(const std::string& key, const std::string& value) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "INSERT INTO system_kv (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) " "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } std::string DatabaseManager::getKV(const std::string& key, const std::string& defaultValue) const { std::lock_guard lock(mutex_); if (!db_) return defaultValue; const char* sql = "SELECT value FROM system_kv WHERE key = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return defaultValue; } sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); std::string result = defaultValue; if (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char* text = sqlite3_column_text(stmt, 0); if (text) { result = reinterpret_cast(text); } } sqlite3_finalize(stmt); return result; } bool DatabaseManager::hasKV(const std::string& key) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "SELECT 1 FROM system_kv WHERE key = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); bool exists = (sqlite3_step(stmt) == SQLITE_ROW); sqlite3_finalize(stmt); return exists; } bool DatabaseManager::deleteKV(const std::string& key) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "DELETE FROM system_kv WHERE key = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } // ─── Proclamations ───────────────────────────────────────────────────── void DatabaseManager::bindProclamationRecord(sqlite3_stmt* stmt, const ProclamationRecord& record) { sqlite3_bind_text(stmt, 1, record.id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, record.title.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, record.content.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 4, record.solfeggio_freq_hz); sqlite3_bind_text(stmt, 5, record.category.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 6, record.is_active ? 1 : 0); sqlite3_bind_int(stmt, 7, record.selected ? 1 : 0); sqlite3_bind_int(stmt, 8, record.category_id); sqlite3_bind_int(stmt, 9, record.group_id); sqlite3_bind_int(stmt, 10, record.solfeggio_frequency_id); } bool DatabaseManager::saveProclamation(const ProclamationRecord& record) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, " "is_active, selected, category_id, group_id, solfeggio_frequency_id) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " "ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, " "solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, " "is_active=excluded.is_active, selected=excluded.selected, " "category_id=excluded.category_id, group_id=excluded.group_id, " "solfeggio_frequency_id=excluded.solfeggio_frequency_id;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } bindProclamationRecord(stmt, record); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } static ProclamationRecord readProclamationRecord(sqlite3_stmt* stmt, int firstCol) { ProclamationRecord rec; rec.id = reinterpret_cast(sqlite3_column_text(stmt, firstCol + 0)); rec.title = reinterpret_cast(sqlite3_column_text(stmt, firstCol + 1)); rec.content = reinterpret_cast(sqlite3_column_text(stmt, firstCol + 2)); rec.solfeggio_freq_hz = sqlite3_column_double(stmt, firstCol + 3); rec.category = reinterpret_cast(sqlite3_column_text(stmt, firstCol + 4)); rec.is_active = (sqlite3_column_int(stmt, firstCol + 5) != 0); rec.selected = (sqlite3_column_int(stmt, firstCol + 6) != 0); rec.category_id = sqlite3_column_int(stmt, firstCol + 7); rec.group_id = sqlite3_column_int(stmt, firstCol + 8); rec.solfeggio_frequency_id = sqlite3_column_int(stmt, firstCol + 9); return rec; } std::vector DatabaseManager::getActiveProclamations() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, " "selected, category_id, group_id, solfeggio_frequency_id " "FROM proclamations WHERE is_active = 1;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { result.push_back(readProclamationRecord(stmt, 0)); } sqlite3_finalize(stmt); return result; } bool DatabaseManager::replaceCategories(const std::vector& records) { std::lock_guard lock(mutex_); if (!db_) return false; if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; const char* delSql = "DELETE FROM proclamation_categories;"; if (sqlite3_exec(db_, delSql, nullptr, nullptr, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } const char* insSql = "INSERT INTO proclamation_categories (id, name, sort_order) " "VALUES (?, ?, ?);"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, insSql, -1, &stmt, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } for (const auto& cat : records) { sqlite3_bind_int(stmt, 1, cat.id); sqlite3_bind_text(stmt, 2, cat.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, cat.sort_order); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } sqlite3_finalize(stmt); if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } bool DatabaseManager::replaceGroups(const std::vector& records) { std::lock_guard lock(mutex_); if (!db_) return false; if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; if (sqlite3_exec(db_, "DELETE FROM proclamation_groups;", nullptr, nullptr, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } const char* insSql = "INSERT INTO proclamation_groups (id, name, sort_order) " "VALUES (?, ?, ?);"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, insSql, -1, &stmt, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } for (const auto& group : records) { sqlite3_bind_int(stmt, 1, group.id); sqlite3_bind_text(stmt, 2, group.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, group.sort_order); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } sqlite3_finalize(stmt); if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } bool DatabaseManager::syncProclamations(const std::vector& records, const std::string& defaultProclamationId) { std::lock_guard lock(mutex_); if (!db_) return false; // Stash current selection so a re-sync doesn't reset what the user picked. std::unordered_map stash; { const char* selSql = "SELECT id, selected FROM proclamations;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, selSql, -1, &stmt, nullptr) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { std::string id = reinterpret_cast(sqlite3_column_text(stmt, 0)); stash[id] = (sqlite3_column_int(stmt, 1) != 0); } sqlite3_finalize(stmt); } } if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; // Remove proclamations the API no longer returns, but never the default one. { std::string delSql = "DELETE FROM proclamations WHERE id NOT IN ("; if (!records.empty()) { for (size_t i = 0; i < records.size(); ++i) { delSql += i == 0 ? "?" : ",?"; } } else { delSql += "''"; } delSql += ") AND id != ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, delSql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { ok = false; } else { for (size_t i = 0; i < records.size(); ++i) { sqlite3_bind_text(stmt, static_cast(i + 1), records[i].id.c_str(), -1, SQLITE_TRANSIENT); } sqlite3_bind_text(stmt, static_cast(records.size() + 1), defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; } sqlite3_finalize(stmt); } } // Upsert each proclamation, preserving the stashed selection state. if (ok) { const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, " "is_active, selected, category_id, group_id, solfeggio_frequency_id) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " "ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, " "solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, " "is_active=excluded.is_active, category_id=excluded.category_id, " "group_id=excluded.group_id, solfeggio_frequency_id=excluded.solfeggio_frequency_id;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { ok = false; } else { for (const auto& rec : records) { ProclamationRecord r = rec; auto it = stash.find(r.id); r.selected = (it != stash.end()) ? it->second : false; bindProclamationRecord(stmt, r); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } sqlite3_finalize(stmt); } } // Force the default proclamation to remain selected. if (ok) { const char* defSql = "UPDATE proclamations SET selected = 1 WHERE id = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, defSql, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; } sqlite3_finalize(stmt); } else { ok = false; } } if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } std::vector DatabaseManager::getAllProclamations() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, " "selected, category_id, group_id, solfeggio_frequency_id FROM proclamations;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { result.push_back(readProclamationRecord(stmt, 0)); } sqlite3_finalize(stmt); return result; } std::vector DatabaseManager::getAllCategories() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, name, sort_order FROM proclamation_categories " "ORDER BY sort_order ASC, id ASC;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { ProclamationCategoryRecord rec; rec.id = sqlite3_column_int(stmt, 0); rec.name = reinterpret_cast(sqlite3_column_text(stmt, 1)); rec.sort_order = sqlite3_column_int(stmt, 2); result.push_back(rec); } sqlite3_finalize(stmt); return result; } std::vector DatabaseManager::getAllGroups() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, name, sort_order FROM proclamation_groups " "ORDER BY sort_order ASC, id ASC;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { ProclamationGroupRecord rec; rec.id = sqlite3_column_int(stmt, 0); rec.name = reinterpret_cast(sqlite3_column_text(stmt, 1)); rec.sort_order = sqlite3_column_int(stmt, 2); result.push_back(rec); } sqlite3_finalize(stmt); return result; } bool DatabaseManager::setProclamationSelected(const std::string& id, bool selected) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "UPDATE proclamations SET selected = ? WHERE id = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_int(stmt, 1, selected ? 1 : 0); sqlite3_bind_text(stmt, 2, id.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } bool DatabaseManager::saveSelectedProclamations(const std::vector& selectedIds, const std::string& defaultProclamationId) { std::lock_guard lock(mutex_); if (!db_) return false; if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; if (sqlite3_exec(db_, "UPDATE proclamations SET selected = 0;", nullptr, nullptr, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } const char* sql = "UPDATE proclamations SET selected = 1 WHERE id = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } for (const auto& id : selectedIds) { sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } sqlite3_finalize(stmt); if (ok) { ok = ensureDefaultProclamationSelectedUnlocked(defaultProclamationId); } if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } bool DatabaseManager::ensureDefaultProclamationSelected(const std::string& defaultProclamationId) { std::lock_guard lock(mutex_); return ensureDefaultProclamationSelectedUnlocked(defaultProclamationId); } bool DatabaseManager::ensureDefaultProclamationSelectedUnlocked(const std::string& defaultProclamationId) { if (!db_) return false; const char* sql = "UPDATE proclamations SET selected = 1 WHERE id = ?;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, defaultProclamationId.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } // ─── Asset Downloads ─────────────────────────────────────────────────── bool DatabaseManager::updateAssetDownload(const AssetDownloadRecord& record) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "INSERT INTO asset_downloads (id, filename, url, status, bytes_downloaded, total_bytes, error_message, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) " "ON CONFLICT(id) DO UPDATE SET filename=excluded.filename, url=excluded.url, status=excluded.status, " "bytes_downloaded=excluded.bytes_downloaded, total_bytes=excluded.total_bytes, error_message=excluded.error_message, " "updated_at=CURRENT_TIMESTAMP;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, record.id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, record.filename.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, record.url.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, record.status.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 5, record.bytes_downloaded); sqlite3_bind_int64(stmt, 6, record.total_bytes); sqlite3_bind_text(stmt, 7, record.error_message.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } std::vector DatabaseManager::getPendingAssetDownloads() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, filename, url, status, bytes_downloaded, total_bytes, error_message FROM asset_downloads WHERE status = 'pending';"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { AssetDownloadRecord rec; rec.id = reinterpret_cast(sqlite3_column_text(stmt, 0)); rec.filename = reinterpret_cast(sqlite3_column_text(stmt, 1)); rec.url = reinterpret_cast(sqlite3_column_text(stmt, 2)); rec.status = reinterpret_cast(sqlite3_column_text(stmt, 3)); rec.bytes_downloaded = sqlite3_column_int64(stmt, 4); rec.total_bytes = sqlite3_column_int64(stmt, 5); rec.error_message = reinterpret_cast(sqlite3_column_text(stmt, 6)); result.push_back(rec); } sqlite3_finalize(stmt); return result; } // ─── Event Logging ───────────────────────────────────────────────────── bool DatabaseManager::logEvent(const std::string& level, const std::string& component, const std::string& message) { std::lock_guard lock(mutex_); if (!db_) return false; const char* sql = "INSERT INTO event_logs (level, component, message) VALUES (?, ?, ?);"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, level.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, component.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, message.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } // ─── Color Palettes Table ────────────────────────────────────────────── static std::vector readPaletteUnlocked(sqlite3* db, int solfeggio_frequency_id) { std::vector result; if (!db) return result; const char* sql = "SELECT hex FROM color_palettes WHERE solfeggio_frequency_id = ? " "ORDER BY color_index ASC;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } sqlite3_bind_int(stmt, 1, solfeggio_frequency_id); auto parseHex = [](const std::string& hex) -> PaletteColor { PaletteColor c; auto nibble = [&hex](size_t i) -> int { char ch = hex.size() > i ? hex[i] : '0'; if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; return 0; }; // Accept "#RRGGBB" or "RRGGBB". size_t base = (!hex.empty() && hex[0] == '#') ? 1 : 0; c.r = static_cast(nibble(base) * 16 + nibble(base + 1)); c.g = static_cast(nibble(base + 2) * 16 + nibble(base + 3)); c.b = static_cast(nibble(base + 4) * 16 + nibble(base + 5)); return c; }; while (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char* text = sqlite3_column_text(stmt, 0); if (text) { result.push_back(parseHex(reinterpret_cast(text))); } } sqlite3_finalize(stmt); return result; } std::vector DatabaseManager::getPaletteForFrequencyId(int solfeggio_frequency_id) { std::lock_guard lock(mutex_); if (!db_) return {}; std::vector result = readPaletteUnlocked(db_, solfeggio_frequency_id); if (result.empty()) { result = readPaletteUnlocked(db_, 5); // 528 Hz fallback } return result; } bool DatabaseManager::replaceColorPalettes(const std::vector& records) { std::lock_guard lock(mutex_); if (!db_) return false; if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; if (sqlite3_exec(db_, "DELETE FROM color_palettes;", nullptr, nullptr, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } const char* insSql = "INSERT INTO color_palettes (solfeggio_frequency_id, color_index, hex) " "VALUES (?, ?, ?);"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, insSql, -1, &stmt, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } for (const auto& rec : records) { for (size_t i = 0; i < rec.colors.size(); ++i) { const PaletteColor& c = rec.colors[i]; char hex[8]; std::snprintf(hex, sizeof(hex), "#%02x%02x%02x", c.r, c.g, c.b); sqlite3_bind_int(stmt, 1, rec.solfeggio_frequency_id); sqlite3_bind_int(stmt, 2, static_cast(i)); sqlite3_bind_text(stmt, 3, hex, -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; } sqlite3_reset(stmt); sqlite3_clear_bindings(stmt); } if (!ok) break; } sqlite3_finalize(stmt); if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } // ─── Audio Files Table ───────────────────────────────────────────────── bool DatabaseManager::syncAudioFiles(const std::vector& records) { std::lock_guard lock(mutex_); if (!db_) return false; sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr); bool ok = true; // Upsert each record by filename. The manifest is the source of truth for // display_name / display_order / solfeggio_frequency_id, so all are // refreshed on every sync. const char* upsertSql = "INSERT INTO audio_files (display_name, filename, display_order, solfeggio_frequency_id) " "VALUES (?, ?, ?, ?) " "ON CONFLICT(filename) DO UPDATE SET " "display_name=excluded.display_name, display_order=excluded.display_order, " "solfeggio_frequency_id=excluded.solfeggio_frequency_id;"; for (const auto& rec : records) { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, upsertSql, -1, &stmt, nullptr) != SQLITE_OK) { ok = false; continue; } sqlite3_bind_text(stmt, 1, rec.display_name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, rec.filename.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, rec.display_order); sqlite3_bind_int(stmt, 4, rec.solfeggio_frequency_id); if (sqlite3_step(stmt) != SQLITE_DONE) ok = false; sqlite3_finalize(stmt); } // Delete rows whose filename is not part of the provided set. sqlite3_stmt* delStmt = nullptr; if (records.empty()) { ok = ok && executeSql("DELETE FROM audio_files;"); } else { std::string deleteSql = "DELETE FROM audio_files WHERE filename NOT IN ("; for (size_t i = 0; i < records.size(); ++i) { if (i > 0) deleteSql += ", "; deleteSql += "?"; } deleteSql += ");"; if (sqlite3_prepare_v2(db_, deleteSql.c_str(), -1, &delStmt, nullptr) == SQLITE_OK) { for (size_t i = 0; i < records.size(); ++i) { sqlite3_bind_text(delStmt, static_cast(i + 1), records[i].filename.c_str(), -1, SQLITE_TRANSIENT); } if (sqlite3_step(delStmt) != SQLITE_DONE) ok = false; sqlite3_finalize(delStmt); } else { ok = false; } } sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); return ok; } std::vector DatabaseManager::getAudioFiles() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, display_name, filename, display_order, solfeggio_frequency_id " "FROM audio_files ORDER BY display_order ASC;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { AudioFileRecord rec; rec.id = sqlite3_column_int(stmt, 0); rec.display_name = reinterpret_cast(sqlite3_column_text(stmt, 1)); rec.filename = reinterpret_cast(sqlite3_column_text(stmt, 2)); rec.display_order = sqlite3_column_int(stmt, 3); rec.solfeggio_frequency_id = sqlite3_column_int(stmt, 4); result.push_back(rec); } sqlite3_finalize(stmt); return result; } // ─── Schedule Blocks Table ───────────────────────────────────────────── bool DatabaseManager::replaceScheduleBlocks(const std::vector& records) { std::lock_guard lock(mutex_); if (!db_) return false; if (sqlite3_exec(db_, "BEGIN;", nullptr, nullptr, nullptr) != SQLITE_OK) { return false; } bool ok = true; if (sqlite3_exec(db_, "DELETE FROM schedule_block_proclamations;", nullptr, nullptr, nullptr) != SQLITE_OK || sqlite3_exec(db_, "DELETE FROM schedule_blocks;", nullptr, nullptr, nullptr) != SQLITE_OK) { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); return false; } const char* blockSql = "INSERT INTO schedule_blocks " "(id, start_minutes, end_minutes, overnight, duration_minutes, " " dynamic_audio, sort_order) " "VALUES (?, ?, ?, ?, ?, ?, ?);"; const char* procSql = "INSERT INTO schedule_block_proclamations " "(block_id, position, proclamation_id) VALUES (?, ?, ?);"; for (size_t i = 0; i < records.size() && ok; ++i) { const ScheduleBlockRecord& rec = records[i]; sqlite3_stmt* blockStmt = nullptr; if (sqlite3_prepare_v2(db_, blockSql, -1, &blockStmt, nullptr) != SQLITE_OK) { ok = false; break; } sqlite3_bind_text(blockStmt, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(blockStmt, 2, rec.start_minutes); sqlite3_bind_int(blockStmt, 3, rec.end_minutes); sqlite3_bind_int(blockStmt, 4, rec.overnight ? 1 : 0); sqlite3_bind_int(blockStmt, 5, rec.duration_minutes); sqlite3_bind_int(blockStmt, 6, rec.dynamic_audio ? 1 : 0); sqlite3_bind_int(blockStmt, 7, static_cast(i)); if (sqlite3_step(blockStmt) != SQLITE_DONE) ok = false; sqlite3_finalize(blockStmt); if (!ok) break; for (size_t p = 0; p < rec.proclamation_ids.size(); ++p) { sqlite3_stmt* procStmt = nullptr; if (sqlite3_prepare_v2(db_, procSql, -1, &procStmt, nullptr) != SQLITE_OK) { ok = false; break; } sqlite3_bind_text(procStmt, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(procStmt, 2, static_cast(p)); sqlite3_bind_text(procStmt, 3, rec.proclamation_ids[p].c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(procStmt) != SQLITE_DONE) ok = false; sqlite3_finalize(procStmt); if (!ok) break; } } if (ok) { sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr); } else { sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr); } return ok; } std::vector DatabaseManager::getScheduleBlocks() { std::lock_guard lock(mutex_); std::vector result; if (!db_) return result; const char* sql = "SELECT id, start_minutes, end_minutes, overnight, duration_minutes, " "dynamic_audio FROM schedule_blocks ORDER BY sort_order ASC;"; sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return result; } while (sqlite3_step(stmt) == SQLITE_ROW) { ScheduleBlockRecord rec; rec.id = reinterpret_cast(sqlite3_column_text(stmt, 0)); rec.start_minutes = sqlite3_column_int(stmt, 1); rec.end_minutes = sqlite3_column_int(stmt, 2); rec.overnight = (sqlite3_column_int(stmt, 3) != 0); rec.duration_minutes = sqlite3_column_int(stmt, 4); rec.dynamic_audio = (sqlite3_column_int(stmt, 5) != 0); result.push_back(std::move(rec)); } sqlite3_finalize(stmt); // Attach each block's proclamation ids in play order. const char* procSql = "SELECT proclamation_id FROM schedule_block_proclamations " "WHERE block_id = ? ORDER BY position ASC;"; for (auto& block : result) { sqlite3_stmt* procStmt = nullptr; if (sqlite3_prepare_v2(db_, procSql, -1, &procStmt, nullptr) != SQLITE_OK) { break; } sqlite3_bind_text(procStmt, 1, block.id.c_str(), -1, SQLITE_TRANSIENT); while (sqlite3_step(procStmt) == SQLITE_ROW) { const unsigned char* text = sqlite3_column_text(procStmt, 0); if (text) { block.proclamation_ids.push_back(reinterpret_cast(text)); } } sqlite3_finalize(procStmt); } return result; } } // namespace radiantwave::db