The portal's v2 API replaced nested categories (proclamation_categories.parent_id)
with flat categories plus tag-like groups: a proclamation now belongs to one
category and optionally one group. This lets the Proclamation Options page drop
its special-cased two-level tree.
Database:
- Add the proclamation_groups table (id, name, sort_order) and a
proclamations.group_id column.
- Drop parent_id from the proclamation_categories DDL so fresh databases get a
flat table.
- runMigrations() creates the new table/column, then runs a one-time
transactional v1->v2 migration keyed on PRAGMA user_version: every legacy
sub-category becomes a group (its id is preserved), proclamations that
pointed at a sub-category are remapped to (parent category, new group), and
the now-stale sub-category rows are deleted. selected flags are never
touched, so user choices survive. No network is needed, so offline kiosks
migrate at startup; re-running is a no-op.
- Add replaceGroups() and getAllGroups(); stop reading/writing parent_id.
API sync:
- Add PATH_GROUPS ("/groups") and parseGroupsJson().
- parseProclamationsJson() now also reads groupId.
- syncProclamationsFromApi() fetches /proclamations, /categories and /groups,
leaves the database untouched if any request or parse fails, then replaces
categories, replaces groups and upserts proclamations.
Proclamation Options page:
- FlatItem is now Category / Group / Proclamation.
- Each category lists only the groups that have at least one member in that
category, so impossible pairings (e.g. Physical/Prayers) never render.
- Proclamations with no group, or with a group the portal has removed, sit
directly under the category. Groups are collapsible and use a distinct color.
Tests:
- test_db: groups round-trip plus a legacy->v2 migration test asserting the
group is created, proclamations are remapped, selection is preserved and a
second initialize() is a no-op.
- test_proclamation_sync / test_api_client: parse groupId, parse groups, and
the live smoke test now also fetches /groups.
Docs: README database table and AGENTS notes updated for groups.
This commit is contained in:
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
@@ -66,6 +66,7 @@ bool parseProclamationsJson(const std::string& body,
|
||||
rec.title = stripHtml(entry.stringField("name"));
|
||||
rec.content = stripHtml(entry.stringField("content"));
|
||||
rec.category_id = static_cast<int>(entry.numberField("categoryId"));
|
||||
rec.group_id = static_cast<int>(entry.numberField("groupId"));
|
||||
rec.solfeggio_frequency_id = static_cast<int>(entry.numberField("solfeggioFrequencyId"));
|
||||
out.push_back(std::move(rec));
|
||||
}
|
||||
@@ -88,7 +89,28 @@ bool parseCategoriesJson(const std::string& body,
|
||||
if (rec.id == 0) continue;
|
||||
|
||||
rec.name = entry.stringField("name");
|
||||
rec.parent_id = static_cast<int>(entry.numberField("parentId"));
|
||||
rec.sort_order = static_cast<int>(entry.numberField("sortOrder"));
|
||||
out.push_back(std::move(rec));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseGroupsJson(const std::string& body,
|
||||
std::vector<radiantwave::db::ProclamationGroupRecord>& out) {
|
||||
json::JsonValue root;
|
||||
if (!json::parseDocument(body, root) || root.type != json::JsonValue::Type::Array) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out.clear();
|
||||
for (const auto& entry : root.array) {
|
||||
if (entry.type != json::JsonValue::Type::Object) continue;
|
||||
|
||||
radiantwave::db::ProclamationGroupRecord rec;
|
||||
rec.id = static_cast<int>(entry.numberField("id"));
|
||||
if (rec.id == 0) continue;
|
||||
|
||||
rec.name = entry.stringField("name");
|
||||
rec.sort_order = static_cast<int>(entry.numberField("sortOrder"));
|
||||
out.push_back(std::move(rec));
|
||||
}
|
||||
@@ -114,16 +136,28 @@ bool syncProclamationsFromApi(radiantwave::db::DatabaseManager& db,
|
||||
return false;
|
||||
}
|
||||
|
||||
api::HttpResponse groupsResponse =
|
||||
api::httpGet(api::PATH_GROUPS);
|
||||
if (!groupsResponse.transportOk ||
|
||||
groupsResponse.status < 200 || groupsResponse.status >= 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<radiantwave::db::ProclamationRecord> proclamations;
|
||||
std::vector<radiantwave::db::ProclamationCategoryRecord> categories;
|
||||
std::vector<radiantwave::db::ProclamationGroupRecord> groups;
|
||||
if (!parseProclamationsJson(proclamationsResponse.body, proclamations) ||
|
||||
!parseCategoriesJson(categoriesResponse.body, categories)) {
|
||||
!parseCategoriesJson(categoriesResponse.body, categories) ||
|
||||
!parseGroupsJson(groupsResponse.body, groups)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!db.replaceCategories(categories)) {
|
||||
return false;
|
||||
}
|
||||
if (!db.replaceGroups(groups)) {
|
||||
return false;
|
||||
}
|
||||
if (!db.syncProclamations(proclamations, defaultProclamationId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@ bool parseProclamationsJson(const std::string& body,
|
||||
bool parseCategoriesJson(const std::string& body,
|
||||
std::vector<radiantwave::db::ProclamationCategoryRecord>& out);
|
||||
|
||||
// Fetches proclamations + categories from the API and stores them in the DB,
|
||||
// preserving any existing selection and forcing `defaultProclamationId` to
|
||||
// stay selected. Returns true only if the API sync succeeded; on failure the
|
||||
// Parse the JSON payload returned by the groups endpoint.
|
||||
bool parseGroupsJson(const std::string& body,
|
||||
std::vector<radiantwave::db::ProclamationGroupRecord>& out);
|
||||
|
||||
// Fetches proclamations + categories + groups from the API and stores them in
|
||||
// the DB, preserving any existing selection and forcing `defaultProclamationId`
|
||||
// to stay selected. Returns true only if the API sync succeeded; on failure the
|
||||
// database is left untouched so the app can fall back to cached data.
|
||||
bool syncProclamationsFromApi(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& defaultProclamationId);
|
||||
|
||||
Reference in New Issue
Block a user