#include "storage/state_repository.hpp" #include "domain/layout_profile.hpp" #include "domain/layout_schema.hpp" #include "util/file_util.hpp" #include "util/id_util.hpp" #include "util/string_util.hpp" namespace lm { std::filesystem::path StateRepository::modelsDir() const { return app_.data_path.parent_path() / "models"; } std::filesystem::path StateRepository::profileFilePath(const std::string& id) const { return modelsDir() / (id + ".json"); } std::optional StateRepository::loadProfileFromDisk(const std::string& id) const { const auto raw = FileUtil::readBinary(profileFilePath(id)); if (raw.empty()) return std::nullopt; try { return nlohmann::json::parse(raw); } catch (...) { return std::nullopt; } } bool StateRepository::saveProfileToDisk(const nlohmann::json& profile) const { if (!profile.is_object() || !profile.contains("id") || !profile["id"].is_string()) return false; std::error_code ec; std::filesystem::create_directories(modelsDir(), ec); auto body = profile.dump(2); body.push_back('\n'); return FileUtil::writeBinaryAtomic(profileFilePath(profile["id"].get()), body); } bool StateRepository::deleteProfileFile(const std::string& id) const { std::error_code ec; std::filesystem::remove(profileFilePath(id), ec); return true; } void StateRepository::loadActiveCache() { nlohmann::json& state = app_.state; const auto idx = LayoutProfile::findActiveIndex(state); if (!idx) return; const std::string id = state["layouts"][*idx]["id"].get(); nlohmann::json profile; if (auto loaded = loadProfileFromDisk(id)) profile = *loaded; else { profile = LayoutProfile::make(state["layouts"][*idx]["name"].get(), LayoutSchema::defaultLayoutObject(), nlohmann::json::array()); profile["id"] = id; if (state["layouts"][*idx].contains("created_at")) profile["created_at"] = state["layouts"][*idx]["created_at"]; } if (!profile.contains("layout") || !profile["layout"].is_object()) profile["layout"] = LayoutSchema::defaultLayoutObject(); if (!profile.contains("lidars") || !profile["lidars"].is_array()) profile["lidars"] = nlohmann::json::array(); if (!profile.contains("imus") || !profile["imus"].is_array()) profile["imus"] = nlohmann::json::array(); LayoutSchema::ensure(profile["layout"]); state["layout"] = profile["layout"]; state["lidars"] = profile["lidars"]; state["imus"] = profile["imus"]; } bool StateRepository::persistActiveProfile() { nlohmann::json& state = app_.state; const auto idx = LayoutProfile::findActiveIndex(state); if (!idx) return false; auto& entry = state["layouts"][*idx]; nlohmann::json profile; profile["id"] = entry["id"]; profile["name"] = entry.contains("name") ? entry["name"] : nlohmann::json("Layout"); profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601()); profile["updated_at"] = IdUtil::nowIso8601(); profile["layout"] = state["layout"]; profile["lidars"] = state["lidars"]; profile["imus"] = state.contains("imus") && state["imus"].is_array() ? state["imus"] : nlohmann::json::array(); LayoutSchema::ensure(profile["layout"]); if (!saveProfileToDisk(profile)) return false; entry = LayoutProfile::catalogEntryFromProfile(profile); return true; } nlohmann::json StateRepository::globalStateForDisk(const nlohmann::json& state) const { nlohmann::json out = nlohmann::json::object(); out["version"] = 3; if (state.contains("active_layout_id")) out["active_layout_id"] = state["active_layout_id"]; out["layouts"] = nlohmann::json::array(); if (state.contains("layouts") && state["layouts"].is_array()) { for (const auto& entry : state["layouts"]) { if (!entry.is_object() || !entry.contains("id") || !entry.contains("name")) continue; if (entry.contains("layout")) out["layouts"].push_back(LayoutProfile::catalogEntryFromProfile(entry)); else out["layouts"].push_back(entry); } } return out; } void StateRepository::stripInlineProfiles(nlohmann::json& state) const { if (!state.contains("layouts") || !state["layouts"].is_array()) return; nlohmann::json catalog = nlohmann::json::array(); for (const auto& entry : state["layouts"]) { if (!entry.is_object() || !entry.contains("id")) continue; if (entry.contains("layout")) catalog.push_back(LayoutProfile::catalogEntryFromProfile(entry)); else catalog.push_back(entry); } state["layouts"] = catalog; } void StateRepository::migrateStorage() { nlohmann::json& s = app_.state; if (!s.is_object()) s = nlohmann::json::object(); const int version = s.contains("version") && s["version"].is_number_integer() ? s["version"].get() : 1; if (!s.contains("layouts") || !s["layouts"].is_array() || s["layouts"].empty()) { nlohmann::json layout = s.contains("layout") && s["layout"].is_object() ? s["layout"] : LayoutSchema::defaultLayoutObject(); nlohmann::json lidars = s.contains("lidars") && s["lidars"].is_array() ? s["lidars"] : nlohmann::json::array(); nlohmann::json profile = LayoutProfile::make("Mặc định", layout, lidars); LayoutSchema::ensure(profile["layout"]); saveProfileToDisk(profile); s["layouts"] = nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)}); s["active_layout_id"] = profile["id"].get(); } else if (version < 3) { if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() || !LayoutProfile::findIndex(s, s["active_layout_id"].get())) { s["active_layout_id"] = s["layouts"][0]["id"].get(); } nlohmann::json catalog = nlohmann::json::array(); for (auto& entry : s["layouts"]) { if (!entry.is_object() || !entry.contains("id")) continue; nlohmann::json profile; if (entry.contains("layout")) { profile = entry; if (!profile.contains("name")) profile["name"] = "Layout"; if (!profile.contains("lidars") || !profile["lidars"].is_array()) profile["lidars"] = nlohmann::json::array(); if (!profile.contains("created_at")) profile["created_at"] = IdUtil::nowIso8601(); LayoutProfile::touch(profile); LayoutSchema::ensure(profile["layout"]); saveProfileToDisk(profile); catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile)); } else { const std::string id = entry["id"].get(); if (auto loaded = loadProfileFromDisk(id)) { catalog.push_back(LayoutProfile::catalogEntryFromProfile(*loaded)); } else { profile = LayoutProfile::make(entry["name"].get(), LayoutSchema::defaultLayoutObject(), nlohmann::json::array()); profile["id"] = id; profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601()); LayoutProfile::touch(profile); saveProfileToDisk(profile); catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile)); } } } s["layouts"] = catalog; } else { stripInlineProfiles(s); if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() || !LayoutProfile::findIndex(s, s["active_layout_id"].get())) { s["active_layout_id"] = s["layouts"][0]["id"].get(); } } s["version"] = 3; s.erase("layout"); s.erase("lidars"); loadActiveCache(); } void StateRepository::bootstrapDefaultState() { const nlohmann::json layout = LayoutSchema::defaultLayoutObject(); nlohmann::json profile = LayoutProfile::make("Mặc định", layout, nlohmann::json::array()); LayoutSchema::ensure(profile["layout"]); saveProfileToDisk(profile); app_.state = nlohmann::json{{"version", 3}, {"active_layout_id", profile["id"]}, {"layouts", nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)})}}; app_.state["layout"] = profile["layout"]; app_.state["lidars"] = profile["lidars"]; app_.state["imus"] = profile.contains("imus") ? profile["imus"] : nlohmann::json::array(); } StateRepository::StateRepository(std::filesystem::path data_path) { app_.data_path = std::move(data_path); } bool StateRepository::load() { const auto raw = FileUtil::readBinary(app_.data_path); if (raw.empty()) { bootstrapDefaultState(); save(); return true; } try { app_.state = nlohmann::json::parse(raw); ensureSchema(); save(); return true; } catch (...) { bootstrapDefaultState(); save(); return false; } } void StateRepository::ensureSchema() { migrateStorage(); } bool StateRepository::saveProfile(const nlohmann::json& profile) { return saveProfileToDisk(profile); } void StateRepository::reloadActiveCache() { loadActiveCache(); } bool StateRepository::deleteProfile(const std::string& id) { return deleteProfileFile(id); } std::optional StateRepository::loadProfileById(const std::string& id) const { return loadProfileFromDisk(id); } bool StateRepository::saveAppState() { if (!persistActiveProfile()) return false; return save(); } bool StateRepository::save() const { try { const nlohmann::json disk = globalStateForDisk(app_.state); auto raw = disk.dump(2); raw.push_back('\n'); return FileUtil::writeBinaryAtomic(app_.data_path, raw); } catch (...) { return false; } } } // namespace lm