#pragma once #include #include #include namespace radiantwave::api { // Base URL of the RadiantWave content API. constexpr const char* API_BASE_URL = "https://portal.radiantwavetech.com/api/v2"; // Paths relative to API_BASE_URL. constexpr const char* PATH_PROCLAMATIONS = "/proclamations"; constexpr const char* PATH_CATEGORIES = "/categories"; constexpr const char* PATH_GROUPS = "/groups"; constexpr const char* PATH_FREQUENCIES = "/frequencies"; constexpr const char* PATH_LICENSE = "/license"; constexpr const char* PATH_CREATE_ACCOUNT = "/automations/create/account"; constexpr const char* PATH_COLOR_PALETTES = "/color-palettes"; constexpr const char* PATH_SCHEDULE = "/schedule"; // Result of a single HTTP request. struct HttpResponse { long status = 0; // HTTP status code; 0 if the request never completed std::string body; // raw response body (any status) std::string etag; // response ETag header, if any bool transportOk = false; // true when the request completed without a transport error }; // Performs a GET/POST/PUT against `path` (relative to API_BASE_URL). HttpResponse httpGet(const std::string& path); HttpResponse httpPost(const std::string& path, const std::string& jsonBody); HttpResponse httpPut(const std::string& path, const std::string& jsonBody); // GET with an optional If-None-Match header. Captures the response ETag. HttpResponse httpGetConditional(const std::string& path, const std::string& ifNoneMatch); // ─── GET /frequencies ────────────────────────────────────────────────── struct Frequency { int id = 0; std::string name; int value = 0; // frequency in Hz }; // Parse the JSON payload returned by the frequencies endpoint. bool parseFrequenciesJson(const std::string& body, std::vector& out); // Fetch the solfeggio frequencies. Returns true only on HTTP 2xx + successful parse. bool fetchFrequencies(std::vector& out); // ─── GET /color-palettes ─────────────────────────────────────────────── // A single color swatch (RGBA). Mirrors db::PaletteColor without coupling the // API layer to the database. struct PaletteColor { uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; uint8_t a = 255; }; // The ordered 7-color palette for one solfeggio frequency. struct ColorPalette { int solfeggio_frequency_id = 0; std::vector colors; }; // Parse the JSON payload returned by the color-palettes endpoint. Each entry // is expected to look like {"solfeggioFrequencyId": 1, "colors": ["#RRGGBB", ...]}. // Returns false on malformed input. bool parseColorPalettesJson(const std::string& body, std::vector& out); // Fetch the color palettes for every solfeggio frequency. Returns true only on // HTTP 2xx + successful parse. // // NOTE: the portal does not expose this route yet. This is readiness code — it // is deliberately not called anywhere in the app until the route ships, so // palettes can be updated from the portal rather than code releases. bool fetchColorPalettes(std::vector& out); // ─── POST /license ───────────────────────────────────────────────────── struct LicenseInfo { std::string key; std::string email_address; // Advisory System Name status returned by the portal: one of "ok", // "unchanged", "conflict", "invalid", or empty when the portal did not // report one (e.g. older portal). std::string system_name_status; }; // Parse the 200 JSON payload returned by the license endpoint. bool parseLicenseJson(const std::string& body, LicenseInfo& out); struct LicenseResult { int status = 0; LicenseInfo info; std::string error_message; // extracted from the error body on failure bool ok() const { return status >= 200 && status < 300; } }; // Validate a license key / email pair against the portal. Never throws; failures // (network, HTTP error status) are reported through the returned LicenseResult. // A 2xx response means the license is valid and the subscription is active. // When `systemName` is non-empty it is sent as the additive `systemName` field // (the Kiosk is the only writer); the response's `systemNameStatus` is advisory. LicenseResult validateLicense(const std::string& key, const std::string& email, const std::string& systemName = ""); // ─── PUT /licenses/{key}/system-name ─────────────────────────────────── // Result of a System Name PUT. `status` is the HTTP status (0 on transport // failure); `transport_ok` is false when the request never completed. struct PutNameResult { int status = 0; bool transport_ok = false; bool ok() const { return status >= 200 && status < 300; } }; // Set the System Name for a License Key. 200 on success, 400 invalid_name, // 404 unknown_license, 409 name_taken. Never throws. PutNameResult putSystemName(const std::string& key, const std::string& name); // ─── POST /automations/create/account ────────────────────────────────── struct AccountBody { bool success = false; std::string message; std::string user_id; }; // Parse the 200 JSON payload returned by the create-account endpoint. bool parseCreateAccountJson(const std::string& body, AccountBody& out); struct AccountResult { int status = 0; AccountBody body; std::string error_message; bool ok() const { return status >= 200 && status < 300; } }; // Creates (or reuses) a user account and issues a license key. The key is emailed // to the user and is NOT part of the response. `name` may be empty. AccountResult createAccount(const std::string& email, const std::string& name = ""); // ─── GET /schedule ───────────────────────────────────────────────────── // One scheduled listening block. Wall-clock times are carried as minutes from // midnight; overnight blocks wrap past midnight (end clock time <= start). struct ScheduleBlock { std::string id; int start_minutes = 0; int end_minutes = 0; bool overnight = false; int duration_minutes = 0; // When true the kiosk enables dynamic audio for the block's duration, // overriding the device's audio selection. Defaults to true: schedules are // dynamic unless the portal explicitly says otherwise. bool dynamic_audio = true; // Selected proclamation ids, in play order. std::vector proclamation_ids; }; struct ScheduleResponse { std::string email; std::vector blocks; }; // Parse the JSON payload returned by the schedule endpoint. `dynamicAudio` is // optional and defaults to true when absent. Returns false on malformed input. bool parseScheduleJson(const std::string& body, ScheduleResponse& out); // Fetch a user's schedule. Authenticates via the paired `email` + `license_key` // query parameters. Returns true only on HTTP 2xx + successful parse; failures // (including a missing/blank pair) leave `out` untouched. bool fetchSchedule(const std::string& email, const std::string& license_key, ScheduleResponse& out); // ─── GET /licenses/{key}/schedule (Phase 3, conditional) ─────────────── // One Block of the target payload. `day` is "" for daily schedules and // "mon".."sun" for weekly schedules. Times are wall-clock minutes. struct TargetBlock { std::string id; std::string day; // "" for daily int start_minutes = 0; int end_minutes = 0; bool overnight = false; int duration_minutes = 0; bool dynamic_audio = true; std::vector proclamation_ids; }; // The target schedule payload (version/type/daily/weekly). struct SchedulePayload { std::string version; // "daily", "weekly", or "" for no schedule (type null). std::string type; std::vector daily; // Weekly blocks keyed by day (mon..sun); empty for daily/none. std::vector weekly; // Legacy `blocks` flattening carried for backward compatibility. std::vector legacy_blocks; bool hasSchedule() const { return type == "daily" || type == "weekly"; } }; // Parse the target payload (with version/type/daily/weekly). `blocks` (legacy) // is also read so older responses still populate `legacy_blocks`. Returns false // on malformed input. bool parseSchedulePayloadJson(const std::string& body, SchedulePayload& out); // Result of a conditional schedule fetch. enum class ScheduleFetchStatus { Ok, // 200; payload replaced NotModified, // 304; caller keeps its cached payload NotFound, // 404 unknown license NetworkError, // transport/other failure }; // Fetch a License Key's schedule with conditional GET. When `etag` is // non-empty it is sent as If-None-Match. On success `out` is replaced and // `outEtag` receives the response ETag. Never throws. ScheduleFetchStatus fetchScheduleForLicense(const std::string& license_key, const std::string& etag, SchedulePayload& out, std::string& outEtag); } // namespace radiantwave::api