33#include < vxcore/vxcore_types.h>
44
55#include < algorithm>
6+ #include < unordered_map>
67
78#include " db/sqlite_metadata_store.h"
89#include " folder_manager.h"
@@ -45,7 +46,8 @@ nlohmann::json TagNode::ToJson() const {
4546NotebookConfig::NotebookConfig ()
4647 : assets_folder(" vx_assets" ),
4748 attachments_folder (" vx_attachments" ),
48- metadata(nlohmann::json::object()) {}
49+ metadata(nlohmann::json::object()),
50+ tags_modified_utc(0 ) {}
4951
5052NotebookConfig NotebookConfig::FromJson (const nlohmann::json &json) {
5153 NotebookConfig config;
@@ -72,6 +74,9 @@ NotebookConfig NotebookConfig::FromJson(const nlohmann::json &json) {
7274 config.tags .push_back (TagNode::FromJson (tag_json));
7375 }
7476 }
77+ if (json.contains (" tagsModifiedUtc" ) && json[" tagsModifiedUtc" ].is_number_integer ()) {
78+ config.tags_modified_utc = json[" tagsModifiedUtc" ].get <int64_t >();
79+ }
7580 return config;
7681}
7782
@@ -88,6 +93,7 @@ nlohmann::json NotebookConfig::ToJson() const {
8893 tags_array.push_back (tag.ToJson ());
8994 }
9095 json[" tags" ] = std::move (tags_array);
96+ json[" tagsModifiedUtc" ] = tags_modified_utc;
9197 return json;
9298}
9399
@@ -155,6 +161,136 @@ VxCoreError Notebook::InitMetadataStore() {
155161 return VXCORE_OK;
156162}
157163
164+ VxCoreError Notebook::SyncTagsToMetadataStore () {
165+ if (!metadata_store_ || !metadata_store_->IsOpen ()) {
166+ return VXCORE_ERR_INVALID_STATE;
167+ }
168+
169+ // 1. Get tags_synced_utc from DB
170+ int64_t tags_synced_utc = 0 ;
171+ auto synced_str = metadata_store_->GetNotebookMetadata (" tags_synced_utc" );
172+ if (synced_str.has_value ()) {
173+ try {
174+ tags_synced_utc = std::stoll (synced_str.value ());
175+ } catch (...) {
176+ tags_synced_utc = 0 ;
177+ }
178+ }
179+
180+ // 2. Check if sync needed
181+ if (config_.tags_modified_utc > 0 && config_.tags_modified_utc <= tags_synced_utc) {
182+ VXCORE_LOG_DEBUG (" Tags already synced: config=%lld, db=%lld" , config_.tags_modified_utc ,
183+ tags_synced_utc);
184+ return VXCORE_OK; // No sync needed
185+ }
186+
187+ VXCORE_LOG_INFO (" Syncing tags to MetadataStore: config=%lld, db=%lld" , config_.tags_modified_utc ,
188+ tags_synced_utc);
189+
190+ // 3. Begin transaction
191+ if (!metadata_store_->BeginTransaction ()) {
192+ VXCORE_LOG_ERROR (" Failed to begin transaction for tag sync" );
193+ return VXCORE_ERR_UNKNOWN;
194+ }
195+
196+ bool success = true ;
197+
198+ // 4. Sort tags by depth (root tags first, then children)
199+ // Build a map of tag name -> depth
200+ std::unordered_map<std::string, int > tag_depth;
201+ for (const auto &tag : config_.tags ) {
202+ if (tag.parent .empty ()) {
203+ tag_depth[tag.name ] = 0 ;
204+ }
205+ }
206+
207+ // Iteratively compute depths for tags with parents
208+ bool changed = true ;
209+ while (changed) {
210+ changed = false ;
211+ for (const auto &tag : config_.tags ) {
212+ if (tag_depth.find (tag.name ) != tag_depth.end ()) {
213+ continue ;
214+ }
215+ if (!tag.parent .empty () && tag_depth.find (tag.parent ) != tag_depth.end ()) {
216+ tag_depth[tag.name ] = tag_depth[tag.parent ] + 1 ;
217+ changed = true ;
218+ }
219+ }
220+ }
221+
222+ // Sort tags by depth
223+ std::vector<const TagNode *> sorted_tags;
224+ sorted_tags.reserve (config_.tags .size ());
225+ for (const auto &tag : config_.tags ) {
226+ sorted_tags.push_back (&tag);
227+ }
228+ std::sort (sorted_tags.begin (), sorted_tags.end (),
229+ [&tag_depth](const TagNode *a, const TagNode *b) {
230+ int da = tag_depth.count (a->name ) ? tag_depth[a->name ] : 999 ;
231+ int db = tag_depth.count (b->name ) ? tag_depth[b->name ] : 999 ;
232+ return da < db;
233+ });
234+
235+ // 5. Create/update tags in depth order
236+ for (const TagNode *tag : sorted_tags) {
237+ StoreTagRecord store_tag;
238+ store_tag.name = tag->name ;
239+ store_tag.parent_name = tag->parent ;
240+ store_tag.metadata = tag->metadata .dump ();
241+
242+ if (!metadata_store_->CreateOrUpdateTag (store_tag)) {
243+ VXCORE_LOG_ERROR (" Failed to sync tag: %s" , tag->name .c_str ());
244+ success = false ;
245+ break ;
246+ }
247+ }
248+
249+ // 6. Delete orphan tags (tags in DB but not in config)
250+ if (success) {
251+ auto db_tags = metadata_store_->ListTags ();
252+ for (const auto &db_tag : db_tags) {
253+ if (tag_depth.find (db_tag.name ) == tag_depth.end ()) {
254+ VXCORE_LOG_INFO (" Deleting orphan tag: %s" , db_tag.name .c_str ());
255+ if (!metadata_store_->DeleteTag (db_tag.name )) {
256+ VXCORE_LOG_WARN (" Failed to delete orphan tag: %s" , db_tag.name .c_str ());
257+ // Continue anyway - orphan cleanup is best-effort
258+ }
259+ }
260+ }
261+ }
262+
263+ // 7. Commit or rollback
264+ if (success) {
265+ if (!metadata_store_->CommitTransaction ()) {
266+ VXCORE_LOG_ERROR (" Failed to commit tag sync transaction" );
267+ return VXCORE_ERR_UNKNOWN;
268+ }
269+ } else {
270+ metadata_store_->RollbackTransaction ();
271+ return VXCORE_ERR_UNKNOWN;
272+ }
273+
274+ // 8. Update tags_synced_utc in DB
275+ int64_t sync_time =
276+ config_.tags_modified_utc > 0 ? config_.tags_modified_utc : GetCurrentTimestampMillis ();
277+ metadata_store_->SetNotebookMetadata (" tags_synced_utc" , std::to_string (sync_time));
278+
279+ // 9. If config had tags_modified_utc=0, set it to current time and save
280+ if (config_.tags_modified_utc == 0 && !config_.tags .empty ()) {
281+ config_.tags_modified_utc = sync_time;
282+ // Note: UpdateConfig is virtual, will save to appropriate location
283+ auto err = UpdateConfig (config_);
284+ if (err != VXCORE_OK) {
285+ VXCORE_LOG_WARN (" Failed to update config after tag sync: error=%d" , err);
286+ // Don't fail - sync itself succeeded
287+ }
288+ }
289+
290+ VXCORE_LOG_INFO (" Tag sync completed successfully" );
291+ return VXCORE_OK;
292+ }
293+
158294void Notebook::Close () {
159295 VXCORE_LOG_INFO (" Closing notebook: id=%s" , config_.id .c_str ());
160296
@@ -191,6 +327,7 @@ VxCoreError Notebook::CreateTag(const std::string &tag_name, const std::string &
191327 }
192328
193329 config_.tags .emplace_back (tag_name, parent_tag);
330+ config_.tags_modified_utc = GetCurrentTimestampMillis ();
194331
195332 auto err = UpdateConfig (config_);
196333 if (err != VXCORE_OK) {
@@ -323,6 +460,7 @@ VxCoreError Notebook::DeleteTag(const std::string &tag_name) {
323460 }
324461 }
325462
463+ config_.tags_modified_utc = GetCurrentTimestampMillis ();
326464 auto err = UpdateConfig (config_);
327465 if (err != VXCORE_OK) {
328466 VXCORE_LOG_ERROR (
@@ -367,6 +505,7 @@ VxCoreError Notebook::MoveTag(const std::string &tag_name, const std::string &pa
367505
368506 tag->parent = parent_tag;
369507
508+ config_.tags_modified_utc = GetCurrentTimestampMillis ();
370509 auto err = UpdateConfig (config_);
371510 if (err != VXCORE_OK) {
372511 VXCORE_LOG_ERROR (
0 commit comments