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 6bfb3521..23e8e020 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 } @@ -256,7 +255,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 @@ -341,9 +340,10 @@ 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 0b9652b9..a3ad4a82 100644 --- a/src/backend/services/sync_handler.go +++ b/src/backend/services/sync_handler.go @@ -84,13 +84,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,14 +152,26 @@ func (s *SyncHandlerService) handleBlockCreated(payload map[string]interface{}) return nil } + // 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 + } + } + } + taskData := map[string]interface{}{ "user_id": userIDStr, "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().UTC(), }, } @@ -193,7 +198,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") } @@ -205,64 +209,47 @@ 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 + if !hasType { + return errors.New("missing block_type in block event payload") + } + + typeChanged := 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") + return s.handleBlockCreated(payload) } - - 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{}) - for k, v := range payload { - newPayload[k] = v - } - - // 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 { - // No task found for this block but it's a task block - create one + 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 } } } @@ -289,33 +276,22 @@ 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 + updateData.Metadata = models.TaskMetadata(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["last_synced"] = time.Now().Format(time.RFC3339) + updateData.Metadata["last_synced"] = time.Now().UTC() // 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 @@ -329,16 +305,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 @@ -357,11 +331,16 @@ 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 - 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 + // 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 { + 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 + } } } @@ -394,9 +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(), - "_sync_source": "task", + "is_completed": task.IsCompleted, }, } @@ -425,43 +403,8 @@ 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 - } - } - } - } - - // 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 = note.ID } - noteID = newNote.ID - } else { - noteID = notes[0].ID } } } @@ -474,9 +417,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 - "_sync_source": "task", }, "user_id": task.UserID.String(), } @@ -486,21 +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{ - "_sync_source": "task", - "block_id": block.ID.String(), - }, - } - - _, err = s.taskService.UpdateTask(s.db, task.ID.String(), updateData) return err } @@ -531,113 +462,45 @@ 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 - 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 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 } } } - // 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 - } - } - - if titleUpdated && title != currentText { - blockData["content"] = models.BlockContent{ - "text": title, - } - needsUpdate = true - } - - // 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 { - for k, v := range block.Metadata { - metadataMap[k] = v - } - } - - metadataMap["is_completed"] = isCompleted - metadataMap["_sync_source"] = "task" - - blockData["metadata"] = metadataMap - needsUpdate = true - } else if needsUpdate { - // If we're updating content but not metadata, still add sync marker - metadataMap := models.BlockMetadata{} - - // Copy existing metadata - if block.Metadata != nil { - for k, v := range block.Metadata { - metadataMap[k] = v - } - } - - metadataMap["_sync_source"] = "task" - metadataMap["last_synced"] = time.Now().Format(time.RFC3339) - blockData["metadata"] = metadataMap + // Update task with block data + updateData := map[string]interface{}{ + "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(), } - // 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, blockIDStr, 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 @@ -668,26 +531,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 { - for k, v := range block.Metadata { - if k != "_sync_source" && k != "task_deleted" && k != "deleted_at" { - metadataMap[k] = v - } - } - } + metadataMap := models.BlockMetadata(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) - - blockData := map[string]interface{}{ - "metadata": metadataMap, - } + metadataMap["deleted_at"] = time.Now().UTC() // Get user_id from payload or from block userIDStr := "" @@ -703,6 +551,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 } diff --git a/src/backend/services/task_service.go b/src/backend/services/task_service.go index 39317b94..3891c519 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", }, } @@ -140,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 { @@ -327,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) } diff --git a/src/frontend/lib/providers/note_editor_provider.dart b/src/frontend/lib/providers/note_editor_provider.dart index 7fb45666..89b89744 100644 --- a/src/frontend/lib/providers/note_editor_provider.dart +++ b/src/frontend/lib/providers/note_editor_provider.dart @@ -447,12 +447,11 @@ 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 (_ is DocumentChangeLog) { - DocumentChangeLog changeLog = _; + if (changelog is DocumentChangeLog) { + DocumentChangeLog changeLog = changelog; // Check if this change includes a TaskNode's isComplete property change bool hasTaskStateChange = false; @@ -477,8 +476,6 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { return; } } - - // Regular change handling for typing/editing _handleDocumentChange(); } @@ -515,7 +512,7 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { }; // Send immediate update to server with standardized format - updateBlock(blockId, payload); + _updateBlock(blockId, payload); } } @@ -579,7 +576,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 +724,6 @@ class NoteEditorProvider with ChangeNotifier implements NoteEditorViewModel { } // Implementation of NoteEditorViewModel methods - @override Future> fetchBlocksForNote(String noteId, { int page = 1, @@ -897,8 +893,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 +920,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, @@ -959,7 +956,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/providers/tasks_provider.dart b/src/frontend/lib/providers/tasks_provider.dart index d811551f..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(); @@ -237,7 +229,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 +243,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..060a847e 100644 --- a/src/frontend/lib/services/block_service.dart +++ b/src/frontend/lib/services/block_service.dart @@ -87,9 +87,7 @@ 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 }; // If there's existing metadata, merge it but preserve our sync fields @@ -139,32 +137,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/services/task_service.dart b/src/frontend/lib/services/task_service.dart index 8c38c847..e5ea9432 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) { @@ -54,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, }; @@ -105,26 +103,21 @@ 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) { metadata.addAll(existingTask.metadata!); } - metadata['task_id'] = id; - metadata['last_synced'] = DateTime.now().toIso8601String(); - 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); diff --git a/src/frontend/lib/utils/document_builder.dart b/src/frontend/lib/utils/document_builder.dart index ea55b63a..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) { @@ -839,7 +838,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();