#pragma once #include #include #include #include namespace radiantwave::db { struct ProclamationRecord { std::string id; std::string title; std::string content; double solfeggio_freq_hz = 528.0; std::string category = "general"; bool is_active = true; bool selected = false; int category_id = 0; // maps to ProclamationCategoryRecord::id int group_id = 0; // maps to ProclamationGroupRecord::id (0 = ungrouped) int solfeggio_frequency_id = 0; // maps to the solfeggio frequency preset }; struct ProclamationCategoryRecord { int id = 0; std::string name; int sort_order = 0; }; // A group is a tag-like organizer: it is not owned by a category. A // proclamation belongs to one category and may additionally belong to one // group. Within the UI, a group appears under a category only when at least // one proclamation pairs that (category_id, group_id). struct ProclamationGroupRecord { int id = 0; std::string name; int sort_order = 0; }; struct AssetDownloadRecord { std::string id; std::string filename; std::string url; std::string status = "pending"; // pending, downloading, completed, failed int64_t bytes_downloaded = 0; int64_t total_bytes = 0; std::string error_message = ""; }; struct AudioFileRecord { int id = 0; std::string display_name; std::string filename; int display_order = 0; int solfeggio_frequency_id = 0; // 0 = not associated with a solfeggio frequency }; // One scheduled listening block, mirrored from GET /schedule. `proclamation_ids` // is the ordered play list for the block. struct ScheduleBlockRecord { std::string id; int start_minutes = 0; int end_minutes = 0; bool overnight = false; int duration_minutes = 0; bool dynamic_audio = true; // override the device audio for this block std::vector proclamation_ids; }; // A single color in a solfeggio frequency's color palette (RGBA). struct PaletteColor { uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; uint8_t a = 255; }; // The ordered palette for one solfeggio frequency (as stored in the // color_palettes table, keyed by color_index). struct PaletteRecord { int solfeggio_frequency_id = 0; std::vector colors; }; class DatabaseManager { public: DatabaseManager(); ~DatabaseManager(); // Non-copyable DatabaseManager(const DatabaseManager&) = delete; DatabaseManager& operator=(const DatabaseManager&) = delete; // Initialize & open database at path (default: ~/.local/radiantwave/radiantwave.db) bool initialize(const std::string& custom_path = ""); // Close database connection void close(); // True once initialize() has opened the database. bool isOpen() const; // ── Key-Value Storage (system_kv table) ─────────────────────────── bool setKV(const std::string& key, const std::string& value); std::string getKV(const std::string& key, const std::string& defaultValue = "") const; bool hasKV(const std::string& key); bool deleteKV(const std::string& key); // ── Proclamations Table ─────────────────────────────────────────── bool saveProclamation(const ProclamationRecord& record); std::vector getActiveProclamations(); // Full sync: replaces all categories with the given set. bool replaceCategories(const std::vector& records); // Full sync: replaces all groups with the given set. bool replaceGroups(const std::vector& records); // Full sync: upserts proclamations, deletes any not in `records`, // preserves the existing `selected` state, and forces the proclamation // identified by `defaultProclamationId` to remain selected. bool syncProclamations(const std::vector& records, const std::string& defaultProclamationId); // Fetch all proclamations (regardless of is_active). std::vector getAllProclamations(); // Fetch all categories ordered by sort_order then id. std::vector getAllCategories(); // Fetch all groups ordered by sort_order then id. std::vector getAllGroups(); // Toggle a single proclamation's selected flag. bool setProclamationSelected(const std::string& id, bool selected); // Atomically replace the selection with `selectedIds`, then force the // default proclamation to remain selected. bool saveSelectedProclamations(const std::vector& selectedIds, const std::string& defaultProclamationId); // Force the default proclamation to be flagged selected in the database. bool ensureDefaultProclamationSelected(const std::string& defaultProclamationId); // ── Asset Downloads Table ───────────────────────────────────────── bool updateAssetDownload(const AssetDownloadRecord& record); std::vector getPendingAssetDownloads(); // ── Audio Files Table ───────────────────────────────────────────── // Reconciles audio_files to the given set (the on-disk audio files). // Upserts by filename with the manifest as source of truth for // display_name / display_order / solfeggio_frequency_id, and deletes // any row whose filename is not in `records`. bool syncAudioFiles(const std::vector& records); std::vector getAudioFiles(); // ── Color Palettes Table ────────────────────────────────────────── // Returns the 7-color palette for a solfeggio frequency id, ordered by // color_index. Falls back to the 528 Hz palette when the id has no rows. std::vector getPaletteForFrequencyId(int solfeggio_frequency_id); // Full sync: replaces the entire color_palettes table with `records` // (the portal is the source of truth for palettes). Pass an empty vector // to clear all palettes. Colors are stored as "#RRGGBB" hex strings. // // NOTE: not currently called from the app flow. This is readiness code so // palettes can be pushed from the portal once the API route exists. bool replaceColorPalettes(const std::vector& records); // ── Schedule Blocks Table ───────────────────────────────────────── // Full sync: replaces the stored schedule with `records` (the portal is the // source of truth). Pass an empty vector to clear the schedule. Block order // and per-block proclamation order are preserved exactly as given. bool replaceScheduleBlocks(const std::vector& records); // Fetch all schedule blocks, ordered as stored, with their proclamation ids // in play order. std::vector getScheduleBlocks(); // ── Event Logging Table ─────────────────────────────────────────── bool logEvent(const std::string& level, const std::string& component, const std::string& message); private: std::string resolvePath(const std::string& raw_path) const; bool runMigrations(); bool migrateSubCategoriesToGroups(); bool executeSql(const std::string& sql); bool columnExists(const std::string& table, const std::string& column); bool ensureColumn(const std::string& table, const std::string& column, const std::string& column_ddl); void seedColorPalettes(); void bindProclamationRecord(sqlite3_stmt* stmt, const ProclamationRecord& record); bool ensureDefaultProclamationSelectedUnlocked(const std::string& defaultProclamationId); sqlite3* db_ = nullptr; std::string db_path_; mutable std::mutex mutex_; }; } // namespace radiantwave::db