From 64cb99734e5a032b436f4c7f8bfa3f3796067b1d Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 18:53:09 +0200 Subject: [PATCH 01/16] Cleanup unneded task completion code --- .../lib/providers/note_editor_provider.dart | 79 ++----------------- .../lib/providers/tasks_provider.dart | 7 +- src/frontend/lib/services/block_service.dart | 26 ------ src/frontend/lib/utils/document_builder.dart | 2 +- .../lib/viewmodel/note_editor_viewmodel.dart | 4 - .../lib/viewmodel/tasks_viewmodel.dart | 2 +- 6 files changed, 10 insertions(+), 110 deletions(-) diff --git a/src/frontend/lib/providers/note_editor_provider.dart b/src/frontend/lib/providers/note_editor_provider.dart index 7fb45666..6588443e 100644 --- a/src/frontend/lib/providers/note_editor_provider.dart +++ b/src/frontend/lib/providers/note_editor_provider.dart @@ -449,76 +449,9 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { // DocumentChangeListener implementation for content changes void _documentChangeListener(dynamic _) { if (_updatingDocument) return; - - // Check if this change is a DocumentChangeLog which might contain TaskNode changes - if (_ is DocumentChangeLog) { - DocumentChangeLog changeLog = _; - - // Check if this change includes a TaskNode's isComplete property change - bool hasTaskStateChange = false; - String? taskNodeId; - bool? newCompletionState; - - for (final change in changeLog.changes) { - if (change is NodeChangeEvent) { - final node = _documentBuilder.document.getNodeById(change.nodeId); - if (node is TaskNode) { - taskNodeId = change.nodeId; - newCompletionState = node.isComplete; - hasTaskStateChange = true; - break; - } - } - } - - // If a task state changed, handle it immediately - if (hasTaskStateChange && taskNodeId != null) { - _handleTaskNodeStateChange(taskNodeId, newCompletionState!); - return; - } - } - - // Regular change handling for typing/editing _handleDocumentChange(); } - // Handle task completion state change - void _handleTaskNodeStateChange(String nodeId, bool isComplete) { - // Find the block ID for this node - final blockId = _documentBuilder.nodeToBlockMap[nodeId]; - if (blockId == null) return; - - // Get the block to check against - final block = getBlock(blockId); - if (block == null || block.type != 'task') return; - - // Compare with current state to see if it actually changed - final bool wasComplete = block.metadata != null && - block.metadata!['is_completed'] == true; - - if (wasComplete != isComplete) { - _logger.info('Task completion changed: nodeId=$nodeId, blockId=$blockId, isComplete=$isComplete'); - - // Content ONLY contains text - final content = {'text': block.getTextContent()}; - - // Metadata contains everything else - final metadata = Map.from(block.metadata ?? {}); - metadata['_sync_source'] = 'block'; - metadata['is_completed'] = isComplete; - metadata['block_id'] = blockId; - - // Create properly structured payload - final payload = { - 'content': content, - 'metadata': metadata - }; - - // Send immediate update to server with standardized format - updateBlock(blockId, payload); - } - } - // Track which block is being edited and schedule updates void _handleDocumentChange() { // TODO: improve change listener using a similar approach @@ -579,7 +512,7 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } // Send content update with formats included - updateBlock(blockId, extractedData, type: blockType); + _updateBlock(blockId, extractedData, type: blockType); } // WebSocket event handlers @@ -727,7 +660,6 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } // Implementation of NoteEditorViewModel methods - @override Future> fetchBlocksForNote(String noteId, { int page = 1, @@ -897,8 +829,7 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } } - @override - void updateBlock(String id, Map content, { + void _updateBlock(String id, Map content, { String? type, double? order, }) { @@ -925,12 +856,14 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { // Track this as a user modification _documentBuilder.markBlockAsModified(id); + // Update block without waiting for server response + _sendBlockUpdate(id, contentMap, metadata: metadataMap, type: type, order: order); + // Notify listeners for UI responsiveness notifyListeners(); - _updateBlock(id, contentMap, metadata: metadataMap, type: type, order: order); } - Future _updateBlock(String id, + Future _sendBlockUpdate(String id, Map content, { Map? metadata, String? type, diff --git a/src/frontend/lib/providers/tasks_provider.dart b/src/frontend/lib/providers/tasks_provider.dart index d811551f..9a3d7876 100644 --- a/src/frontend/lib/providers/tasks_provider.dart +++ b/src/frontend/lib/providers/tasks_provider.dart @@ -237,7 +237,7 @@ class TasksProvider with ChangeNotifier implements TasksViewModel { // Fetch tasks with proper user filtering @override - Future fetchTasks({String? completed, String? noteId}) async { + Future fetchTasks({String? noteId}) async { if (!_isActive) return; // Don't fetch if not active // Get current user ID for filtering @@ -251,10 +251,7 @@ class TasksProvider with ChangeNotifier implements TasksViewModel { notifyListeners(); try { - final tasksList = await _taskService.fetchTasks( - completed: completed, - noteId: noteId, - ); + final tasksList = await _taskService.fetchTasks(noteId: noteId); // Convert list to map _tasksMap.clear(); diff --git a/src/frontend/lib/services/block_service.dart b/src/frontend/lib/services/block_service.dart index 2f310bf7..c54b500e 100644 --- a/src/frontend/lib/services/block_service.dart +++ b/src/frontend/lib/services/block_service.dart @@ -139,32 +139,6 @@ class BlockService extends BaseService { } } - Future updateTaskCompletion(String blockId, bool isCompleted) async { - try { - // Get existing block - Block block = await getBlock(blockId); - - // Manually create update payload - final updatedMetadata = block.metadata != null ? - Map.from(block.metadata!) : {}; - - updatedMetadata['_sync_source'] = 'block'; - updatedMetadata['block_id'] = block.id; - updatedMetadata['is_completed'] = isCompleted; - updatedMetadata['last_synced'] = DateTime.now().toIso8601String(); - - final payload = { - 'content': block.content, - 'metadata': updatedMetadata, - }; - - return await updateBlock(blockId, payload); - } catch (e) { - _logger.error('Error updating task completion', e); - rethrow; - } -} - Future getBlock(String id) async { try { final response = await authenticatedGet('/api/v1/blocks/$id'); diff --git a/src/frontend/lib/utils/document_builder.dart b/src/frontend/lib/utils/document_builder.dart index ea55b63a..56f1c14d 100644 --- a/src/frontend/lib/utils/document_builder.dart +++ b/src/frontend/lib/utils/document_builder.dart @@ -839,7 +839,7 @@ class DocumentBuilder { // Get completion status from metadata bool isCompleted = false; if (metadata != null && metadata.containsKey('is_completed')) { - isCompleted = metadata['is_completed'] == true; + isCompleted = metadata['is_completed']; } return [ diff --git a/src/frontend/lib/viewmodel/note_editor_viewmodel.dart b/src/frontend/lib/viewmodel/note_editor_viewmodel.dart index f2721320..47849ab1 100644 --- a/src/frontend/lib/viewmodel/note_editor_viewmodel.dart +++ b/src/frontend/lib/viewmodel/note_editor_viewmodel.dart @@ -29,10 +29,6 @@ abstract class NoteEditorViewModel extends BaseViewModel { }); Future fetchBlockById(String blockId); Future deleteBlock(String blockId); - void updateBlock(String id, Map content, { - String? type, - double? order, - }); // Document builder access DocumentBuilder get documentBuilder; diff --git a/src/frontend/lib/viewmodel/tasks_viewmodel.dart b/src/frontend/lib/viewmodel/tasks_viewmodel.dart index 5768ddcd..57444d79 100644 --- a/src/frontend/lib/viewmodel/tasks_viewmodel.dart +++ b/src/frontend/lib/viewmodel/tasks_viewmodel.dart @@ -14,7 +14,7 @@ abstract class TasksViewModel extends BaseViewModel { List get availableNotes; /// Fetch tasks with filtering - Future fetchTasks({String? completed, String? noteId}); + Future fetchTasks({String? noteId}); /// Load available notes for task creation Future loadAvailableNotes(); From 7eae4730d99cced459883bdb566e15991f7f0bcc Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 19:14:48 +0200 Subject: [PATCH 02/16] Fix block removal on task deletion --- src/backend/services/sync_handler.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 0b9652b9..24aaf194 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -685,10 +685,6 @@ func (s *SyncHandlerService) handleTaskDeleted(payload map[string]interface{}) e metadataMap["task_id"] = taskIDStr metadataMap["deleted_at"] = time.Now().Format(time.RFC3339) - blockData := map[string]interface{}{ - "metadata": metadataMap, - } - // Get user_id from payload or from block userIDStr := "" if userID, ok := payload["user_id"].(string); ok { @@ -703,6 +699,6 @@ func (s *SyncHandlerService) handleTaskDeleted(payload map[string]interface{}) e } // Update the block to reflect task deletion - _, err := s.blockService.UpdateBlock(s.db, blockIDStr, blockData, params) + err := s.blockService.DeleteBlock(s.db, blockIDStr, params) return err } From d9294eb0dc0c7f0fc2138b2cb7f903e4cdbd55ca Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 20:00:56 +0200 Subject: [PATCH 03/16] Fix task deletion when block is deleted in the editor --- src/backend/services/sync_handler.go | 83 +++++++++------------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 24aaf194..102a1fd7 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "log" + "maps" "time" "owlistic-notes/owlistic/broker" @@ -84,13 +85,6 @@ func (s *SyncHandlerService) handleSyncEvent(eventType string, data []byte) erro return errors.New("empty payload in event message") } - // Check if this is a sync event to prevent infinite loops - syncSource, isSync := message.Payload["_sync_source"].(string) - if isSync { - log.Printf("Skipping %s event from sync source: %s", message.Type, syncSource) - return nil - } - switch eventType { case string(broker.BlockCreated): return s.handleBlockCreated(message.Payload) @@ -159,6 +153,15 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) return nil } + + // Check if sync source exists - this is a key issue that's causing loops + if syncSource, exists := block.Metadata["_sync_source"]; exists { + if syncSource == "task" { + log.Printf("Block %s was created by task sync, skipping task creation", blockIDStr) + return nil // Skip creating a taks since this block was created from a task + } + } + taskData := map[string]interface{}{ "user_id": userIDStr, "note_id": noteIDStr, @@ -207,6 +210,12 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) updatedType, hasType := payload["block_type"].(string) typeChanged := hasType && string(block.Type) != updatedType + + // Only continue if this is a task block (either unchanged or still a task block after update) + if block.Type != models.TaskBlock || hasType && updatedType != string(models.TaskBlock) { + return nil // Not a task block, nothing to do + } + // If block type changed from task to another type, delete the associated task if typeChanged && updatedType != string(models.TaskBlock) { log.Printf("Block type changed from TaskBlock to %s, deleting associated task", updatedType) @@ -231,19 +240,12 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) log.Printf("Block type changed to TaskBlock, creating a new task") // Create a new payload with the updated type newPayload := make(map[string]interface{}) - for k, v := range payload { - newPayload[k] = v - } + maps.Copy(newPayload, payload) // Call handleBlockCreated to create the task return s.handleBlockCreated(newPayload) } - // Only continue if this is a task block (either unchanged or still a task block after update) - if !hasType && block.Type != models.TaskBlock || hasType && updatedType != string(models.TaskBlock) { - return nil // Not a task block, nothing to do - } - // Find the task associated with this block var task models.Task if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).First(&task).Error; err != nil { @@ -290,32 +292,23 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) // Create a copy of metadata updateData.Metadata = models.TaskMetadata{} - for k, v := range task.Metadata { - updateData.Metadata[k] = v - } - updateData.Metadata["_sync_source"] = "block" - - // Check for completion status in metadata - isCompleted := task.IsCompleted // Default to current value + maps.Copy(updateData.Metadata, task.Metadata) // Get completed status from block metadata if block.Metadata != nil { if completed, exists := block.Metadata["is_completed"].(bool); exists { - isCompleted = completed + updateData.IsCompleted = completed } } - updateData.IsCompleted = isCompleted - // Always include the sync timestamp + updateData.Metadata["_sync_source"] = "block" updateData.Metadata["last_synced"] = time.Now().Format(time.RFC3339) // Only update if there are changes to apply - if updateData.Title != "" || updateData.IsCompleted != task.IsCompleted || len(updateData.Metadata) > 0 { - _, err := s.taskService.UpdateTask(s.db, task.ID.String(), updateData) - if err != nil { - return err - } + _, err := s.taskService.UpdateTask(s.db, task.ID.String(), updateData) + if err != nil { + return err } return nil @@ -531,28 +524,6 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e return s.createBlockForTask(task) } - // Check if block type matches - if not, update it to task block - if block.Type != models.TaskBlock { - blockData := map[string]interface{}{ - "type": string(models.TaskBlock), - "content": models.BlockContent{ - "text": task.Title, - }, - "metadata": models.BlockMetadata{ - "is_completed": task.IsCompleted, - "task_id": task.ID.String(), - "_sync_source": "task", - }, - } - - params := map[string]interface{}{ - "user_id": task.UserID.String(), - } - - _, err := s.blockService.UpdateBlock(s.db, blockIDStr, blockData, params) - return err - } - // Skip if this task was just updated by block sync if syncSource, exists := task.Metadata["_sync_source"]; exists && syncSource == "block" { // Get last sync timestamp @@ -599,9 +570,7 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e // Copy existing metadata if block.Metadata != nil { - for k, v := range block.Metadata { - metadataMap[k] = v - } + maps.Copy(metadataMap, block.Metadata) } metadataMap["is_completed"] = isCompleted @@ -615,9 +584,7 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e // Copy existing metadata if block.Metadata != nil { - for k, v := range block.Metadata { - metadataMap[k] = v - } + maps.Copy(metadataMap, block.Metadata) } metadataMap["_sync_source"] = "task" From f9429513293e956f1b4f9fa9d7d3e94872c3eb02 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 20:01:09 +0200 Subject: [PATCH 04/16] Cleanup _sync_source in frontend api calls --- src/frontend/lib/providers/note_editor_provider.dart | 1 - src/frontend/lib/services/block_service.dart | 1 - src/frontend/lib/services/task_service.dart | 8 ++------ src/frontend/lib/utils/document_builder.dart | 3 +-- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/frontend/lib/providers/note_editor_provider.dart b/src/frontend/lib/providers/note_editor_provider.dart index 6588443e..33494e9d 100644 --- a/src/frontend/lib/providers/note_editor_provider.dart +++ b/src/frontend/lib/providers/note_editor_provider.dart @@ -892,7 +892,6 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { // Setup metadata Map metadataMap = metadata ?? {}; - metadataMap['_sync_source'] = 'block'; metadataMap['block_id'] = id; // Add metadata to payload diff --git a/src/frontend/lib/services/block_service.dart b/src/frontend/lib/services/block_service.dart index c54b500e..3ba2c83f 100644 --- a/src/frontend/lib/services/block_service.dart +++ b/src/frontend/lib/services/block_service.dart @@ -87,7 +87,6 @@ class BlockService extends BaseService { // Ensure metadata structure is correct with proper sync timestamps Map metadataMap = { - '_sync_source': 'block', 'block_id': blockId, 'last_synced': DateTime.now().toIso8601String() // Add current timestamp }; diff --git a/src/frontend/lib/services/task_service.dart b/src/frontend/lib/services/task_service.dart index 8c38c847..0b903562 100644 --- a/src/frontend/lib/services/task_service.dart +++ b/src/frontend/lib/services/task_service.dart @@ -42,9 +42,7 @@ class TaskService extends BaseService { Future createTask(String title, String noteId, {String? blockId}) async { try { // Create metadata with sync source - final metadata = { - '_sync_source': 'task', - }; + final metadata = {}; // Add block_id to metadata if provided if (blockId != null && blockId.isNotEmpty) { @@ -105,9 +103,7 @@ class TaskService extends BaseService { } // Create metadata with task_id and keep existing metadata - final metadata = { - '_sync_source': 'task', - }; + final metadata = {}; // Copy existing metadata if (existingTask.metadata != null) { diff --git a/src/frontend/lib/utils/document_builder.dart b/src/frontend/lib/utils/document_builder.dart index 56f1c14d..f89e1c02 100644 --- a/src/frontend/lib/utils/document_builder.dart +++ b/src/frontend/lib/utils/document_builder.dart @@ -689,7 +689,6 @@ class DocumentBuilder { // Initialize with strict format Map content = {'text': ''}; Map metadata = { - '_sync_source': 'block', 'block_id': blockId }; @@ -761,7 +760,7 @@ class DocumentBuilder { DocumentNode node, {Block? originalBlock}) { // Initialize with a base structure Map content = {'text': ''}; - Map metadata = {'_sync_source': 'block'}; + Map metadata = {}; // Preserve original metadata values if available if (originalBlock != null && originalBlock.metadata != null) { From b0417ecbc4f250126e9c13fefa34798f672b4942 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 20:31:21 +0200 Subject: [PATCH 05/16] Remove unneded complexity for task/block relation (always 1-to-1) --- src/backend/services/sync_handler.go | 64 +++++++++++----------------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 102a1fd7..6c274f64 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -153,7 +153,6 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) return nil } - // Check if sync source exists - this is a key issue that's causing loops if syncSource, exists := block.Metadata["_sync_source"]; exists { if syncSource == "task" { @@ -196,7 +195,6 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) error { blockIDStr, ok := payload["block_id"].(string) if !ok { - log.Printf("Payload: %v", payload) return errors.New("missing block_id in block event payload") } @@ -208,48 +206,40 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) // Check if the block type has changed updatedType, hasType := payload["block_type"].(string) - typeChanged := hasType && string(block.Type) != updatedType - - - // Only continue if this is a task block (either unchanged or still a task block after update) - if block.Type != models.TaskBlock || hasType && updatedType != string(models.TaskBlock) { - return nil // Not a task block, nothing to do + if !hasType { + return errors.New("missing block_type in block event payload") } + typeChanged := hasType && string(block.Type) != updatedType + // If block type changed from task to another type, delete the associated task - if typeChanged && updatedType != string(models.TaskBlock) { - log.Printf("Block type changed from TaskBlock to %s, deleting associated task", updatedType) - // Find any tasks associated with this block and delete them - var tasks []models.Task - if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).Find(&tasks).Error; err != nil { - return nil // No tasks found or error, nothing to delete - } + if typeChanged { + if updatedType != string(models.TaskBlock) { + log.Printf("Block type changed from TaskBlock to %s, deleting associated task", updatedType) + // Find any tasks associated with this block and delete them + var task models.Task + if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).Find(&task).Error; err != nil { + return nil // No tasks found or error, nothing to delete + } - // Delete each task associated with this block since it's no longer a task block - for _, task := range tasks { + // Delete task associated with this block since it's no longer a task block if err := s.taskService.DeleteTask(s.db, task.ID.String()); err != nil { return err } + } else { + log.Printf("Block type changed to TaskBlock, creating a new task") + // Create a new payload with the updated type + newPayload := make(map[string]interface{}) + maps.Copy(newPayload, payload) + + // Call handleBlockCreated to create the task + return s.handleBlockCreated(newPayload) } - - return nil // No need to process content updates since it's not a task block anymore - } - - // If block type changed to task block, create a new task for it - if typeChanged && updatedType == string(models.TaskBlock) { - log.Printf("Block type changed to TaskBlock, creating a new task") - // Create a new payload with the updated type - newPayload := make(map[string]interface{}) - maps.Copy(newPayload, payload) - - // Call handleBlockCreated to create the task - return s.handleBlockCreated(newPayload) } // Find the task associated with this block var task models.Task if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).First(&task).Error; err != nil { - // No task found for this block but it's a task block - create one return s.handleBlockCreated(payload) } @@ -322,16 +312,14 @@ func (s *SyncHandlerService) handleBlockDeleted(payload map[string]interface{}) } // Find all tasks associated with this block - var tasks []models.Task - if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).Find(&tasks).Error; err != nil { + var task models.Task + if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).Find(&task).Error; err != nil { return nil // No tasks found or error, nothing to delete } - // Delete each task - for _, task := range tasks { - if err := s.taskService.DeleteTask(s.db, task.ID.String()); err != nil { - return err - } + // Delete task + if err := s.taskService.DeleteTask(s.db, task.ID.String()); err != nil { + return err } return nil From c954c12e61fdc5a79a4a0646c510ae3d4004897c Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 20:31:45 +0200 Subject: [PATCH 06/16] Fix task_id parsing in tasks_screen (not updating on delete) --- src/frontend/lib/providers/tasks_provider.dart | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/frontend/lib/providers/tasks_provider.dart b/src/frontend/lib/providers/tasks_provider.dart index 9a3d7876..26709e76 100644 --- a/src/frontend/lib/providers/tasks_provider.dart +++ b/src/frontend/lib/providers/tasks_provider.dart @@ -171,8 +171,8 @@ class TasksProvider with ChangeNotifier implements TasksViewModel { try { // Use the standardized parser final parsedMessage = WebSocketMessage.fromJson(message); - final String? taskId = WebSocketModelExtractor.extractTaskId(parsedMessage); // Using block extractor as tasks don't have a specific extractor - final String? noteId = WebSocketModelExtractor.extractNoteId(parsedMessage); // Using block extractor as tasks don't have a specific extractor + final String? taskId = WebSocketModelExtractor.extractTaskId(parsedMessage); + final String? noteId = WebSocketModelExtractor.extractNoteId(parsedMessage); if (taskId != null && taskId.isNotEmpty) { // Only fetch if we have tasks for this note already or if we're showing all tasks @@ -193,17 +193,9 @@ class TasksProvider with ChangeNotifier implements TasksViewModel { try { // Use the standardized parser final parsedMessage = WebSocketMessage.fromJson(message); - final payload = parsedMessage.payload; - final data = payload['data']; - - String taskId = ''; - - // Extract task ID - tasks don't have a specific extractor yet - if (data != null && data is Map) { - taskId = data['id']?.toString() ?? data['task_id']?.toString() ?? ''; - } + final String? taskId = WebSocketModelExtractor.extractTaskId(parsedMessage); - if (taskId.isNotEmpty) { + if (taskId != null && taskId.isNotEmpty) { // Remove task from local state if it exists _tasksMap.remove(taskId); notifyListeners(); From 9bcef05a6774b5923797302d605f758e71c383bf Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 21:09:46 +0200 Subject: [PATCH 07/16] FIx block_type key in block.updated events --- src/backend/services/block_service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/services/block_service.go b/src/backend/services/block_service.go index 6bfb3521..bf234feb 100644 --- a/src/backend/services/block_service.go +++ b/src/backend/services/block_service.go @@ -256,7 +256,7 @@ func (s *BlockService) UpdateBlock(db *database.Database, id string, blockData m } if blockType, ok := blockData["type"].(string); ok { - eventData["type"] = blockType + eventData["block_type"] = blockType } // Create the event @@ -344,6 +344,7 @@ func (s *BlockService) DeleteBlock(db *database.Database, id string, params map[ "block_id": block.ID.String(), "note_id": block.NoteID.String(), "user_id": block.UserID.String(), + "block_type": string(block.Type), }, ) From c6a3d88c026d4b833b7743cfdf6440fad1817a01 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 22:27:08 +0200 Subject: [PATCH 08/16] Replace _sync_source with last_sync+updated_at --- src/backend/services/block_service.go | 1 - src/backend/services/sync_handler.go | 104 +++++++++++--------------- src/backend/services/task_service.go | 1 - 3 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/backend/services/block_service.go b/src/backend/services/block_service.go index bf234feb..c27b0ff1 100644 --- a/src/backend/services/block_service.go +++ b/src/backend/services/block_service.go @@ -118,7 +118,6 @@ func (s *BlockService) CreateBlock(db *database.Database, blockData map[string]i metadata := models.BlockMetadata{} if metaData, ok := blockData["metadata"].(map[string]interface{}); ok { metadata = models.BlockMetadata(metaData) - metadata["_sync_source"] = "block" metadata["block_id"] = blockID } diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 6c274f64..7a4c8d7c 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -153,11 +153,16 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) return nil } - // Check if sync source exists - this is a key issue that's causing loops - if syncSource, exists := block.Metadata["_sync_source"]; exists { - if syncSource == "task" { - log.Printf("Block %s was created by task sync, skipping task creation", blockIDStr) - return nil // Skip creating a taks since this block was created from a task + // Check if this update originated from a task sync + if lastSyncStr, exists := block.Metadata["last_synced"].(string); exists { + lastSync, err := time.Parse(time.RFC3339, lastSyncStr) + if err == nil { + // Compare actual timestamps instead of using arbitrary time window + if block.UpdatedAt.Compare(lastSync) <= 0 { + log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", + blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) + return nil + } } } @@ -166,9 +171,8 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) "note_id": noteIDStr, "title": textContent, "metadata": models.TaskMetadata{ - "_sync_source": "block", - "block_id": blockIDStr, - "last_synced": time.Now().Format(time.RFC3339), + "block_id": blockIDStr, + "last_synced": time.Now(), }, } @@ -210,7 +214,7 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) return errors.New("missing block_type in block event payload") } - typeChanged := hasType && string(block.Type) != updatedType + typeChanged := string(block.Type) != updatedType // If block type changed from task to another type, delete the associated task if typeChanged { @@ -228,33 +232,25 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) } } else { log.Printf("Block type changed to TaskBlock, creating a new task") - // Create a new payload with the updated type - newPayload := make(map[string]interface{}) - maps.Copy(newPayload, payload) - - // Call handleBlockCreated to create the task - return s.handleBlockCreated(newPayload) + return s.handleBlockCreated(payload) } } // Find the task associated with this block var task models.Task - if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).First(&task).Error; err != nil { + if err := s.db.DB.Where("metadata->>'block_id' = ?", blockIDStr).Find(&task).Error; err != nil { return s.handleBlockCreated(payload) } // Check if this update originated from a task sync - if syncSource, exists := block.Metadata["_sync_source"]; exists && syncSource == "task" { - // Get last sync timestamp - if lastSyncStr, exists := block.Metadata["last_synced"].(string); exists { - lastSync, err := time.Parse(time.RFC3339, lastSyncStr) - if err == nil { - // Compare actual timestamps instead of using arbitrary time window - if block.UpdatedAt.Compare(lastSync) <= 0 { - log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", - blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) - return nil - } + if lastSyncStr, exists := block.Metadata["last_synced"].(string); exists { + lastSync, err := time.Parse(time.RFC3339, lastSyncStr) + if err == nil { + // Compare actual timestamps instead of using arbitrary time window + if block.UpdatedAt.Compare(lastSync) <= 0 { + log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", + blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) + return nil } } } @@ -292,8 +288,7 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) } // Always include the sync timestamp - updateData.Metadata["_sync_source"] = "block" - updateData.Metadata["last_synced"] = time.Now().Format(time.RFC3339) + updateData.Metadata["last_synced"] = time.Now() // Only update if there are changes to apply _, err := s.taskService.UpdateTask(s.db, task.ID.String(), updateData) @@ -339,10 +334,15 @@ func (s *SyncHandlerService) handleTaskCreated(payload map[string]interface{}) e } // Check if sync source exists - this is a key issue that's causing loops - if syncSource, exists := task.Metadata["_sync_source"]; exists { - if syncSource == "block" { - log.Printf("Task %s was created by block sync, skipping block creation", taskIDStr) - return nil // Skip creating a block since this task was created from a block + if lastSyncStr, exists := task.Metadata["last_synced"].(string); exists { + lastSync, err := time.Parse(time.RFC3339, lastSyncStr) + if err == nil { + // Compare actual timestamps instead of using arbitrary time window + if task.UpdatedAt.Compare(lastSync) <= 0 { + log.Printf("Task %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", + taskIDStr, task.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) + return nil + } } } @@ -377,7 +377,6 @@ func (s *SyncHandlerService) handleTaskCreated(payload map[string]interface{}) e "metadata": models.BlockMetadata{ "is_completed": task.IsCompleted, "task_id": task.ID.String(), - "_sync_source": "task", }, } @@ -457,7 +456,6 @@ func (s *SyncHandlerService) createBlockForTask(task models.Task) error { "metadata": models.BlockMetadata{ "is_completed": task.IsCompleted, "task_id": task.ID.String(), // Add task ID reference - "_sync_source": "task", }, "user_id": task.UserID.String(), } @@ -476,8 +474,7 @@ func (s *SyncHandlerService) createBlockForTask(task models.Task) error { updateData := models.Task{ NoteID: noteID, Metadata: models.TaskMetadata{ - "_sync_source": "task", - "block_id": block.ID.String(), + "block_id": block.ID.String(), }, } @@ -513,17 +510,14 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e } // Skip if this task was just updated by block sync - if syncSource, exists := task.Metadata["_sync_source"]; exists && syncSource == "block" { - // Get last sync timestamp - if lastSyncStr, exists := task.Metadata["last_synced"].(string); exists { - lastSync, err := time.Parse(time.RFC3339, lastSyncStr) - if err == nil { - // Compare actual timestamps instead of using arbitrary time window - if block.UpdatedAt.Compare(lastSync) <= 0 { - log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", - blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) - return nil - } + if lastSyncStr, exists := task.Metadata["last_synced"].(string); exists { + lastSync, err := time.Parse(time.RFC3339, lastSyncStr) + if err == nil { + // Compare actual timestamps instead of using arbitrary time window + if block.UpdatedAt.Compare(lastSync) <= 0 { + log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", + blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) + return nil } } } @@ -562,7 +556,6 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e } metadataMap["is_completed"] = isCompleted - metadataMap["_sync_source"] = "task" blockData["metadata"] = metadataMap needsUpdate = true @@ -575,8 +568,7 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e maps.Copy(metadataMap, block.Metadata) } - metadataMap["_sync_source"] = "task" - metadataMap["last_synced"] = time.Now().Format(time.RFC3339) + metadataMap["last_synced"] = time.Now() blockData["metadata"] = metadataMap } @@ -627,18 +619,12 @@ func (s *SyncHandlerService) handleTaskDeleted(payload map[string]interface{}) e // Copy existing metadata if block.Metadata != nil { - for k, v := range block.Metadata { - if k != "_sync_source" && k != "task_deleted" && k != "deleted_at" { - metadataMap[k] = v - } - } + maps.Copy(metadataMap, block.Metadata) } // Add task deletion markers - metadataMap["task_deleted"] = true - metadataMap["_sync_source"] = "task" metadataMap["task_id"] = taskIDStr - metadataMap["deleted_at"] = time.Now().Format(time.RFC3339) + metadataMap["deleted_at"] = time.Now() // Get user_id from payload or from block userIDStr := "" diff --git a/src/backend/services/task_service.go b/src/backend/services/task_service.go index 39317b94..b246ae10 100644 --- a/src/backend/services/task_service.go +++ b/src/backend/services/task_service.go @@ -123,7 +123,6 @@ func (s *TaskService) CreateTask(db *database.Database, taskData map[string]inte Metadata: models.BlockMetadata{ "is_completed": task.IsCompleted, "task_id": taskID.String(), - "_sync_source": "task", }, } From a78f1570a01ecd75361fd9b1b9846c083a0aa060 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 22:57:04 +0200 Subject: [PATCH 09/16] Fix task content update sync --- src/backend/services/sync_handler.go | 72 ++++++++-------------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 7a4c8d7c..c41ba71a 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -522,69 +522,39 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e } } - // Prepare block update - needsUpdate := false - blockData := map[string]interface{}{} - - // Check if title was updated - title, titleUpdated := payload["title"].(string) - var currentText string - if textVal, exists := block.Content["text"]; exists { - if textStr, ok := textVal.(string); ok { - currentText = textStr - } - } + updatedContent := map[string]interface{}{} + updatedMetadata := map[string]interface{}{} - if titleUpdated && title != currentText { - blockData["content"] = models.BlockContent{ - "text": title, - } - needsUpdate = true + // Update title if block content has changed + if task.Title != payload["title"] && payload["title"] != "" { + updatedContent["text"] = payload["title"] } - // Check if completion status was updated - isCompleted, statusUpdated := payload["is_completed"].(bool) - - // If completion status has changed, update metadata - if statusUpdated { - // Start with existing metadata - metadataMap := models.BlockMetadata{} - - // Copy existing metadata - if block.Metadata != nil { - maps.Copy(metadataMap, block.Metadata) - } - - metadataMap["is_completed"] = isCompleted + // Create a copy of metadata + updatedMetadata = models.BlockMetadata(task.Metadata) - blockData["metadata"] = metadataMap - needsUpdate = true - } else if needsUpdate { - // If we're updating content but not metadata, still add sync marker - metadataMap := models.BlockMetadata{} + // Get completed status from block metadata + updatedMetadata["is_completed"] = task.IsCompleted - // Copy existing metadata - if block.Metadata != nil { - maps.Copy(metadataMap, block.Metadata) - } + // Always include the sync timestamp + updatedMetadata["last_synced"] = time.Now() - metadataMap["last_synced"] = time.Now() - blockData["metadata"] = metadataMap + // Update task with block data + updateData := map[string]interface{}{ + "content": updatedContent, + "metadata": updatedMetadata, } - // Only update if something changed - if !needsUpdate { - return nil + params := map[string]interface{}{ + "user_id": task.UserID, } - // Get user_id for permissions check - params := map[string]interface{}{ - "user_id": task.UserID.String(), + _, err := s.blockService.UpdateBlock(s.db, task.ID.String(), updateData, params) + if err != nil { + return err } - // Update block with task data - _, err := s.blockService.UpdateBlock(s.db, blockIDStr, blockData, params) - return err + return nil } // handleTaskDeleted handles cleanup when a task is deleted From 0b8a2589e63197caba44f20fb76797a2ef01323e Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 22:57:25 +0200 Subject: [PATCH 10/16] Fix wrong block_id instead of task_id --- src/backend/services/sync_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index c41ba71a..75dc7ecc 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -549,7 +549,7 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e "user_id": task.UserID, } - _, err := s.blockService.UpdateBlock(s.db, task.ID.String(), updateData, params) + _, err := s.blockService.UpdateBlock(s.db, blockIDStr, updateData, params) if err != nil { return err } From 79ac9d34430b3238509bd250f459445531368870 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 23:14:34 +0200 Subject: [PATCH 11/16] Align timestamp format --- src/backend/models/websocket_message.go | 2 +- src/backend/routes/debug.go | 6 +++--- src/backend/services/block_service.go | 6 +++--- src/backend/services/event_handler_service.go | 3 +-- src/backend/services/note_service.go | 4 ++-- src/backend/services/notebook_service.go | 2 +- src/backend/services/sync_handler.go | 21 +++++++------------ src/backend/services/task_service.go | 14 ++++++------- src/backend/services/websocket_service.go | 12 +++++------ src/backend/testutils/event_utils.go | 2 +- src/backend/utils/token/token.go | 8 +++---- 11 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/backend/models/websocket_message.go b/src/backend/models/websocket_message.go index 95ef8c85..5c5d9720 100644 --- a/src/backend/models/websocket_message.go +++ b/src/backend/models/websocket_message.go @@ -31,7 +31,7 @@ func NewStandardMessage(msgType string, event string, payload map[string]interfa ID: uuid.New().String(), Type: msgType, Event: event, - Timestamp: time.Now(), + Timestamp: time.Now().UTC(), Payload: payload, } } diff --git a/src/backend/routes/debug.go b/src/backend/routes/debug.go index 3edf9ca1..f7181a6d 100644 --- a/src/backend/routes/debug.go +++ b/src/backend/routes/debug.go @@ -25,7 +25,7 @@ func SetupDebugRoutes(router *gin.Engine, db *database.Database) { c.JSON(http.StatusOK, gin.H{ "exists": false, "error": result.Error.Error(), - "time": time.Now(), + "time": time.Now().UTC(), }) return } @@ -35,7 +35,7 @@ func SetupDebugRoutes(router *gin.Engine, db *database.Database) { "id": note.ID, "title": note.Title, "notebook_id": note.NotebookID, - "time": time.Now(), + "time": time.Now().UTC(), }) }) @@ -47,7 +47,7 @@ func SetupDebugRoutes(router *gin.Engine, db *database.Database) { c.JSON(http.StatusOK, gin.H{ "pending_events": len(events), "events": events, - "time": time.Now(), + "time": time.Now().UTC(), }) }) } diff --git a/src/backend/services/block_service.go b/src/backend/services/block_service.go index c27b0ff1..23e8e020 100644 --- a/src/backend/services/block_service.go +++ b/src/backend/services/block_service.go @@ -340,9 +340,9 @@ func (s *BlockService) DeleteBlock(db *database.Database, id string, params map[ string(broker.BlockDeleted), // Use standard event type "block", map[string]interface{}{ - "block_id": block.ID.String(), - "note_id": block.NoteID.String(), - "user_id": block.UserID.String(), + "block_id": block.ID.String(), + "note_id": block.NoteID.String(), + "user_id": block.UserID.String(), "block_type": string(block.Type), }, ) diff --git a/src/backend/services/event_handler_service.go b/src/backend/services/event_handler_service.go index e0ab9a51..9ac9aa98 100644 --- a/src/backend/services/event_handler_service.go +++ b/src/backend/services/event_handler_service.go @@ -154,11 +154,10 @@ func (s *EventHandlerService) dispatchEvent(event models.Event) error { } // Mark the event as dispatched in the database - now := time.Now() return s.db.DB.Model(&event).Updates(map[string]interface{}{ "dispatched": true, - "dispatched_at": now, "status": "completed", + "dispatched_at": time.Now().UTC(), }).Error } diff --git a/src/backend/services/note_service.go b/src/backend/services/note_service.go index b8b7ba73..63059d9f 100644 --- a/src/backend/services/note_service.go +++ b/src/backend/services/note_service.go @@ -251,7 +251,7 @@ func (s *NoteService) UpdateNote(db *database.Database, id string, noteData map[ note.NotebookID = notebookID } - note.UpdatedAt = time.Now() + note.UpdatedAt = time.Now().UTC() if err := tx.Save(¬e).Error; err != nil { tx.Rollback() @@ -327,7 +327,7 @@ func (s *NoteService) DeleteNote(db *database.Database, id string, params map[st tx.Rollback() return errors.New("not authorized to delete this note") } - + if err := tx.Exec("UPDATE blocks SET deleted_at = NOW() WHERE note_id = ?", id).Error; err != nil { tx.Rollback() return err diff --git a/src/backend/services/notebook_service.go b/src/backend/services/notebook_service.go index 66eb924e..b3432243 100644 --- a/src/backend/services/notebook_service.go +++ b/src/backend/services/notebook_service.go @@ -180,7 +180,7 @@ func (s *NotebookService) UpdateNotebook(db *database.Database, id string, noteb notebook.Description = description } - notebook.UpdatedAt = time.Now() + notebook.UpdatedAt = time.Now().UTC() if err := tx.Save(¬ebook).Error; err != nil { tx.Rollback() diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index 75dc7ecc..f932c095 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "log" - "maps" "time" "owlistic-notes/owlistic/broker" @@ -172,7 +171,7 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) "title": textContent, "metadata": models.TaskMetadata{ "block_id": blockIDStr, - "last_synced": time.Now(), + "last_synced": time.Now().UTC(), }, } @@ -277,8 +276,7 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) } // Create a copy of metadata - updateData.Metadata = models.TaskMetadata{} - maps.Copy(updateData.Metadata, task.Metadata) + updateData.Metadata = models.TaskMetadata(task.Metadata) // Get completed status from block metadata if block.Metadata != nil { @@ -288,7 +286,7 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) } // Always include the sync timestamp - updateData.Metadata["last_synced"] = time.Now() + updateData.Metadata["last_synced"] = time.Now().UTC() // Only update if there are changes to apply _, err := s.taskService.UpdateTask(s.db, task.ID.String(), updateData) @@ -537,11 +535,11 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e updatedMetadata["is_completed"] = task.IsCompleted // Always include the sync timestamp - updatedMetadata["last_synced"] = time.Now() + updatedMetadata["last_synced"] = time.Now().UTC() // Update task with block data updateData := map[string]interface{}{ - "content": updatedContent, + "content": updatedContent, "metadata": updatedMetadata, } @@ -585,16 +583,11 @@ func (s *SyncHandlerService) handleTaskDeleted(payload map[string]interface{}) e } // Update block metadata to reflect task deletion - metadataMap := models.BlockMetadata{} - - // Copy existing metadata - if block.Metadata != nil { - maps.Copy(metadataMap, block.Metadata) - } + metadataMap := models.BlockMetadata(block.Metadata) // Add task deletion markers metadataMap["task_id"] = taskIDStr - metadataMap["deleted_at"] = time.Now() + metadataMap["deleted_at"] = time.Now().UTC() // Get user_id from payload or from block userIDStr := "" diff --git a/src/backend/services/task_service.go b/src/backend/services/task_service.go index b246ae10..3891c519 100644 --- a/src/backend/services/task_service.go +++ b/src/backend/services/task_service.go @@ -139,12 +139,12 @@ func (s *TaskService) CreateTask(db *database.Database, taskData map[string]inte string(broker.BlockCreated), "block", map[string]interface{}{ - "block_id": block.ID.String(), - "note_id": noteID.String(), - "user_id": userID.String(), - "type": string(models.TaskBlock), - "content": block.Content, - "metadata": block.Metadata, + "block_id": block.ID.String(), + "note_id": noteID.String(), + "user_id": userID.String(), + "type": string(models.TaskBlock), + "content": block.Content, + "metadata": block.Metadata, }, ) if err != nil { @@ -326,7 +326,7 @@ func (s *TaskService) DeleteTask(db *database.Database, id string) error { // Delete roles for this task if err := tx.Model(&models.Role{}). Where("resource_id = ? AND resource_type = ?", taskID, models.TaskResource). - Update("deleted_at", time.Now()). + Update("deleted_at", time.Now().UTC()). Error; err != nil { tx.Rollback() return err diff --git a/src/backend/services/websocket_service.go b/src/backend/services/websocket_service.go index 718556c9..4213ffc7 100644 --- a/src/backend/services/websocket_service.go +++ b/src/backend/services/websocket_service.go @@ -149,7 +149,7 @@ func (s *WebSocketService) HandleConnection(c *gin.Context) { conn: conn, userID: userID, send: make(chan []byte, 256), - createdAt: time.Now(), + createdAt: time.Now().UTC(), } // Register the connection @@ -192,10 +192,10 @@ func (s *WebSocketService) readPump(connID string, wsConn *websocketConnection) log.Printf("WebSocket connection closed: %s", connID) }() - wsConn.conn.SetReadLimit(1024) // Increase read limit to handle larger messages - wsConn.conn.SetReadDeadline(time.Now().Add(120 * time.Second)) // Increase timeout + wsConn.conn.SetReadLimit(1024) // Increase read limit to handle larger messages + wsConn.conn.SetReadDeadline(time.Now().UTC().Add(120 * time.Second)) // Increase timeout wsConn.conn.SetPongHandler(func(string) error { - wsConn.conn.SetReadDeadline(time.Now().Add(120 * time.Second)) + wsConn.conn.SetReadDeadline(time.Now().UTC().Add(120 * time.Second)) return nil }) @@ -388,7 +388,7 @@ func (s *WebSocketService) writePump(connID string, wsConn *websocketConnection) for { select { case message, ok := <-wsConn.send: - wsConn.conn.SetWriteDeadline(time.Now().Add(15 * time.Second)) // Longer deadline + wsConn.conn.SetWriteDeadline(time.Now().UTC().Add(15 * time.Second)) // Longer deadline if !ok { // Channel was closed wsConn.conn.WriteMessage(websocket.CloseMessage, []byte{}) @@ -422,7 +422,7 @@ func (s *WebSocketService) writePump(connID string, wsConn *websocketConnection) } case <-ticker.C: - wsConn.conn.SetWriteDeadline(time.Now().Add(15 * time.Second)) + wsConn.conn.SetWriteDeadline(time.Now().UTC().Add(15 * time.Second)) if err := wsConn.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { log.Printf("Error sending ping to conn %s: %v", connID, err) return diff --git a/src/backend/testutils/event_utils.go b/src/backend/testutils/event_utils.go index d7e4705d..43cea4d5 100644 --- a/src/backend/testutils/event_utils.go +++ b/src/backend/testutils/event_utils.go @@ -24,7 +24,7 @@ func MockEventRows(events []models.Event) *sqlmock.Rows { event.ID = uuid.New() } if event.Timestamp.IsZero() { - event.Timestamp = time.Now() + event.Timestamp = time.Now().UTC() } if event.Data == nil { event.Data = json.RawMessage(`{}`) diff --git a/src/backend/utils/token/token.go b/src/backend/utils/token/token.go index bd327428..a19559c6 100644 --- a/src/backend/utils/token/token.go +++ b/src/backend/utils/token/token.go @@ -51,9 +51,9 @@ func GenerateToken(userID uuid.UUID, email string, secret []byte, expiration tim UserID: userID, Email: email, RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)), - IssuedAt: jwt.NewNumericDate(time.Now()), - NotBefore: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiration)), + IssuedAt: jwt.NewNumericDate(time.Now().UTC()), + NotBefore: jwt.NewNumericDate(time.Now().UTC()), }, } @@ -95,6 +95,6 @@ func ExtractAndValidateToken(c *gin.Context, secret []byte) (*JWTClaims, error) if err != nil { return nil, err } - + return ValidateToken(tokenString, secret) } From 9724fbd1d2d2feb23f00444f84749d8f88ff7a64 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Thu, 22 May 2025 23:22:35 +0200 Subject: [PATCH 12/16] Remove client-side last_synced --- src/frontend/lib/services/block_service.dart | 1 - src/frontend/lib/services/task_service.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/src/frontend/lib/services/block_service.dart b/src/frontend/lib/services/block_service.dart index 3ba2c83f..060a847e 100644 --- a/src/frontend/lib/services/block_service.dart +++ b/src/frontend/lib/services/block_service.dart @@ -88,7 +88,6 @@ class BlockService extends BaseService { // Ensure metadata structure is correct with proper sync timestamps Map metadataMap = { 'block_id': blockId, - 'last_synced': DateTime.now().toIso8601String() // Add current timestamp }; // If there's existing metadata, merge it but preserve our sync fields diff --git a/src/frontend/lib/services/task_service.dart b/src/frontend/lib/services/task_service.dart index 0b903562..36eac20e 100644 --- a/src/frontend/lib/services/task_service.dart +++ b/src/frontend/lib/services/task_service.dart @@ -111,7 +111,6 @@ class TaskService extends BaseService { } metadata['task_id'] = id; - metadata['last_synced'] = DateTime.now().toIso8601String(); if (isCompleted != null) metadata['is_completed'] = isCompleted; final updates = { From 70a294849fb798b8e2cc06886de7ce2338527710 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Fri, 23 May 2025 18:28:23 +0200 Subject: [PATCH 13/16] Fix typo in sync handler logs --- src/backend/services/sync_handler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index f932c095..a992eb42 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -373,8 +373,8 @@ func (s *SyncHandlerService) handleTaskCreated(payload map[string]interface{}) e "text": task.Title, }, "metadata": models.BlockMetadata{ - "is_completed": task.IsCompleted, "task_id": task.ID.String(), + "is_completed": task.IsCompleted, }, } @@ -452,8 +452,8 @@ func (s *SyncHandlerService) createBlockForTask(task models.Task) error { "text": task.Title, }, "metadata": models.BlockMetadata{ + "task_id": task.ID.String(), "is_completed": task.IsCompleted, - "task_id": task.ID.String(), // Add task ID reference }, "user_id": task.UserID.String(), } @@ -513,7 +513,7 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e if err == nil { // Compare actual timestamps instead of using arbitrary time window if block.UpdatedAt.Compare(lastSync) <= 0 { - log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", + log.Printf("Task %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) return nil } From 07cfd4e38d49eacfe0a67f9387ac19323a79cb9d Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Fri, 23 May 2025 18:29:22 +0200 Subject: [PATCH 14/16] Restore changelog listener for task completion changes --- .../lib/providers/note_editor_provider.dart | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/frontend/lib/providers/note_editor_provider.dart b/src/frontend/lib/providers/note_editor_provider.dart index 33494e9d..89b89744 100644 --- a/src/frontend/lib/providers/note_editor_provider.dart +++ b/src/frontend/lib/providers/note_editor_provider.dart @@ -447,11 +447,75 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } // DocumentChangeListener implementation for content changes - void _documentChangeListener(dynamic _) { + void _documentChangeListener(dynamic changelog) { if (_updatingDocument) return; + // Check if this change is a DocumentChangeLog which might contain TaskNode changes + if (changelog is DocumentChangeLog) { + DocumentChangeLog changeLog = changelog; + + // Check if this change includes a TaskNode's isComplete property change + bool hasTaskStateChange = false; + String? taskNodeId; + bool? newCompletionState; + + for (final change in changeLog.changes) { + if (change is NodeChangeEvent) { + final node = _documentBuilder.document.getNodeById(change.nodeId); + if (node is TaskNode) { + taskNodeId = change.nodeId; + newCompletionState = node.isComplete; + hasTaskStateChange = true; + break; + } + } + } + + // If a task state changed, handle it immediately + if (hasTaskStateChange && taskNodeId != null) { + _handleTaskNodeStateChange(taskNodeId, newCompletionState!); + return; + } + } _handleDocumentChange(); } + // Handle task completion state change + void _handleTaskNodeStateChange(String nodeId, bool isComplete) { + // Find the block ID for this node + final blockId = _documentBuilder.nodeToBlockMap[nodeId]; + if (blockId == null) return; + + // Get the block to check against + final block = getBlock(blockId); + if (block == null || block.type != 'task') return; + + // Compare with current state to see if it actually changed + final bool wasComplete = block.metadata != null && + block.metadata!['is_completed'] == true; + + if (wasComplete != isComplete) { + _logger.info('Task completion changed: nodeId=$nodeId, blockId=$blockId, isComplete=$isComplete'); + + // Content ONLY contains text + final content = {'text': block.getTextContent()}; + + // Metadata contains everything else + final metadata = Map.from(block.metadata ?? {}); + metadata['_sync_source'] = 'block'; + metadata['is_completed'] = isComplete; + metadata['block_id'] = blockId; + + // Create properly structured payload + final payload = { + 'content': content, + 'metadata': metadata + }; + + // Send immediate update to server with standardized format + _updateBlock(blockId, payload); + } + } + // Track which block is being edited and schedule updates void _handleDocumentChange() { // TODO: improve change listener using a similar approach From 32d9ceedd0e6d9aed37c05a0c909ceb8c222ea50 Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Fri, 23 May 2025 18:53:20 +0200 Subject: [PATCH 15/16] Fix is_complete updates from task service --- src/frontend/lib/services/task_service.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/frontend/lib/services/task_service.dart b/src/frontend/lib/services/task_service.dart index 36eac20e..e5ea9432 100644 --- a/src/frontend/lib/services/task_service.dart +++ b/src/frontend/lib/services/task_service.dart @@ -52,7 +52,7 @@ class TaskService extends BaseService { final taskData = { 'title': title, 'is_completed': false, - 'note_id': noteId, // Direct note_id field + 'note_id': noteId, 'metadata': metadata, }; @@ -110,16 +110,14 @@ class TaskService extends BaseService { metadata.addAll(existingTask.metadata!); } - metadata['task_id'] = id; - if (isCompleted != null) metadata['is_completed'] = isCompleted; - final updates = { 'metadata': metadata, }; // Add basic task properties - if (title != null) updates['title'] = title; - if (noteId != null) updates['note_id'] = noteId; + updates['title'] = (title != null) ? title : existingTask.title; + updates['note_id'] = (noteId != null) ? noteId : existingTask.noteId; + updates['is_completed'] = (isCompleted != null) ? isCompleted : existingTask.isCompleted; final response = await authenticatedPut('/api/v1/tasks/$id', updates); From 6cbbec02e47c24bcc965d1a5a000b95b4aacbb2c Mon Sep 17 00:00:00 2001 From: Davide Rutigliano Date: Fri, 23 May 2025 18:53:34 +0200 Subject: [PATCH 16/16] Fix last_sync checks --- src/backend/services/sync_handler.go | 92 ++++++---------------------- 1 file changed, 20 insertions(+), 72 deletions(-) diff --git a/src/backend/services/sync_handler.go b/src/backend/services/sync_handler.go index a992eb42..a3ad4a82 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -157,7 +157,7 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) lastSync, err := time.Parse(time.RFC3339, lastSyncStr) if err == nil { // Compare actual timestamps instead of using arbitrary time window - if block.UpdatedAt.Compare(lastSync) <= 0 { + if block.UpdatedAt.Compare(lastSync) < 0 { log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) return nil @@ -246,7 +246,7 @@ func (s *SyncHandlerService) handleBlockUpdated(payload map[string]interface{}) lastSync, err := time.Parse(time.RFC3339, lastSyncStr) if err == nil { // Compare actual timestamps instead of using arbitrary time window - if block.UpdatedAt.Compare(lastSync) <= 0 { + if block.UpdatedAt.Compare(lastSync) < 0 { log.Printf("Block %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) return nil @@ -331,12 +331,12 @@ func (s *SyncHandlerService) handleTaskCreated(payload map[string]interface{}) e return err } - // Check if sync source exists - this is a key issue that's causing loops + // Skip if this task was just updated by block sync if lastSyncStr, exists := task.Metadata["last_synced"].(string); exists { lastSync, err := time.Parse(time.RFC3339, lastSyncStr) if err == nil { // Compare actual timestamps instead of using arbitrary time window - if task.UpdatedAt.Compare(lastSync) <= 0 { + if task.UpdatedAt.Compare(lastSync) < 0 { log.Printf("Task %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", taskIDStr, task.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) return nil @@ -403,47 +403,12 @@ func (s *SyncHandlerService) createBlockForTask(task models.Task) error { // Verify the note exists var note models.Note if err := s.db.DB.First(¬e, "id = ?", id).Error; err == nil { - // Note found, we can use it - } else { - // Note not found, reset noteID to find another note - noteID = uuid.Nil + noteID = note.ID } } } } - // If no valid noteId in metadata, find a suitable note - if noteID == uuid.Nil { - // Find the user's primary note, or any note - var notes []models.Note - if err := s.db.DB.Where("user_id = ? AND is_primary = ?", task.UserID, true).Limit(1).Find(¬es).Error; err != nil { - return err - } - - if len(notes) > 0 { - noteID = notes[0].ID - } else { - // No primary note found, try to find any note - if err := s.db.DB.Where("user_id = ?", task.UserID).Limit(1).Find(¬es).Error; err != nil { - return err - } - if len(notes) == 0 { - // No notes found, create a new one - newNote := models.Note{ - ID: uuid.New(), - UserID: task.UserID, - Title: "Tasks", - } - if err := s.db.DB.Create(&newNote).Error; err != nil { - return err - } - noteID = newNote.ID - } else { - noteID = notes[0].ID - } - } - } - // Create a block for this task blockData := map[string]interface{}{ "note_id": noteID.String(), @@ -463,20 +428,10 @@ func (s *SyncHandlerService) createBlockForTask(task models.Task) error { "user_id": task.UserID.String(), } - block, err := s.blockService.CreateBlock(s.db, blockData, params) + _, err := s.blockService.CreateBlock(s.db, blockData, params) if err != nil { return err } - - // Update task with block_id and store note_id in metadata - updateData := models.Task{ - NoteID: noteID, - Metadata: models.TaskMetadata{ - "block_id": block.ID.String(), - }, - } - - _, err = s.taskService.UpdateTask(s.db, task.ID.String(), updateData) return err } @@ -512,35 +467,28 @@ func (s *SyncHandlerService) handleTaskUpdated(payload map[string]interface{}) e lastSync, err := time.Parse(time.RFC3339, lastSyncStr) if err == nil { // Compare actual timestamps instead of using arbitrary time window - if block.UpdatedAt.Compare(lastSync) <= 0 { + if task.UpdatedAt.Compare(lastSync) < 0 { log.Printf("Task %s was already synced (UpdatedAt=%v, lastSync=%v), skipping update", - blockIDStr, block.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) + taskIDStr, task.UpdatedAt.Format(time.RFC3339), lastSync.Format(time.RFC3339)) return nil } } } - updatedContent := map[string]interface{}{} - updatedMetadata := map[string]interface{}{} - - // Update title if block content has changed - if task.Title != payload["title"] && payload["title"] != "" { - updatedContent["text"] = payload["title"] - } - - // Create a copy of metadata - updatedMetadata = models.BlockMetadata(task.Metadata) - - // Get completed status from block metadata - updatedMetadata["is_completed"] = task.IsCompleted - - // Always include the sync timestamp - updatedMetadata["last_synced"] = time.Now().UTC() - // Update task with block data updateData := map[string]interface{}{ - "content": updatedContent, - "metadata": updatedMetadata, + "note_id": block.NoteID.String(), + "type": string(models.TaskBlock), + "content": models.BlockContent{ + "text": task.Title, + }, + "metadata": models.BlockMetadata{ + "task_id": task.ID.String(), + "last_synced": time.Now().Format(time.RFC3339), + "is_completed": task.IsCompleted, + "emmmnnagiaa": true, + }, + "user_id": task.UserID.String(), } params := map[string]interface{}{