Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions backend/controllers/complete_tasks.go
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created new endpoint for bulk task completion. uses a single job for all UUIDs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a O(n) operation with lots of Config->Operate->Sync tasks, preferably what shall be done could be that- add another CMD operation for this called CompleteTasksInTaskwarrior, and then operate that loop there, instead of this API that initiates the Job. So they would have a single SetConfig->MultipleOperations->SingleSync as the final workflow. Similar shall be done for delete so as to reduce server overheads

Copy link
Contributor Author

@ShivaGupta-14 ShivaGupta-14 Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@its-me-abhishek
Thanks for the suggestion! I understand, I'll refactor to move the loop into a single CompleteTasksInTaskwarrior function with the flow: SetConfig -> Loop operations -> SingleSync.

Quick question: for tracking which individual tasks fail within the batch, should I return a map[string]string (uuid -> error) or define a struct like FailedTask{UUID, Error}? Both work, just wondering if you have a preference.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package controllers

import (
"ccsync_backend/models"
"ccsync_backend/utils/tw"
"encoding/json"
"fmt"
"io"
"net/http"
)

// BulkCompleteTaskHandler godoc
// @Summary Bulk complete tasks
// @Description Mark multiple tasks as completed in Taskwarrior
// @Tags Tasks
// @Accept json
// @Produce json
// @Param task body models.BulkCompleteTaskRequestBody true "Bulk task completion details"
// @Success 202 {string} string "Bulk task completion accepted for processing"
// @Failure 400 {string} string "Invalid request - missing or empty taskuuids"
// @Failure 405 {string} string "Method not allowed"
// @Router /complete-tasks [post]
func BulkCompleteTaskHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusBadRequest)
return
}
defer r.Body.Close()

var requestBody models.BulkCompleteTaskRequestBody

if err := json.Unmarshal(body, &requestBody); err != nil {
http.Error(w, fmt.Sprintf("error decoding request body: %v", err), http.StatusBadRequest)
return
}

email := requestBody.Email
encryptionSecret := requestBody.EncryptionSecret
uuid := requestBody.UUID
taskUUIDs := requestBody.TaskUUIDs

if len(taskUUIDs) == 0 {
http.Error(w, "taskuuids is required and cannot be empty", http.StatusBadRequest)
return
}

logStore := models.GetLogStore()

// Create a *single* job for all UUIDs
job := Job{
Name: "Bulk Complete Tasks",
Execute: func() error {
for _, tu := range taskUUIDs {
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Starting: %s", tu), uuid, "Bulk Complete Task")

err := tw.CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, tu)
if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Complete] Failed: %s (%v)", tu, err), uuid, "Bulk Complete Task")
continue
}

logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Completed: %s", tu), uuid, "Bulk Complete Task")
}
return nil
},
}

GlobalJobQueue.AddJob(job)
w.WriteHeader(http.StatusAccepted)
}
75 changes: 75 additions & 0 deletions backend/controllers/delete_tasks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package controllers

import (
"ccsync_backend/models"
"ccsync_backend/utils/tw"
"encoding/json"
"fmt"
"io"
"net/http"
)

// BulkDeleteTaskHandler godoc
// @Summary Bulk delete tasks
// @Description Delete multiple tasks in Taskwarrior
// @Tags Tasks
// @Accept json
// @Produce json
// @Param task body models.BulkDeleteTaskRequestBody true "Bulk task deletion details"
// @Success 202 {string} string "Bulk task deletion accepted for processing"
// @Failure 400 {string} string "Invalid request - missing or empty taskuuids"
// @Failure 405 {string} string "Method not allowed"
// @Router /delete-tasks [post]
func BulkDeleteTaskHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusBadRequest)
return
}
defer r.Body.Close()

var requestBody models.BulkDeleteTaskRequestBody

if err := json.Unmarshal(body, &requestBody); err != nil {
http.Error(w, fmt.Sprintf("error decoding request body: %v", err), http.StatusBadRequest)
return
}

email := requestBody.Email
encryptionSecret := requestBody.EncryptionSecret
uuid := requestBody.UUID
taskUUIDs := requestBody.TaskUUIDs

if len(taskUUIDs) == 0 {
http.Error(w, "taskuuids is required and cannot be empty", http.StatusBadRequest)
return
}

logStore := models.GetLogStore()

job := Job{
Name: "Bulk Delete Tasks",
Execute: func() error {
for _, tu := range taskUUIDs {
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Starting: %s", tu), uuid, "Bulk Delete Task")

err := tw.DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, tu)
if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Delete] Failed: %s (%v)", tu, err), uuid, "Bulk Delete Task")
continue
}

logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Deleted: %s", tu), uuid, "Bulk Delete Task")
}
return nil
},
}

GlobalJobQueue.AddJob(job)
w.WriteHeader(http.StatusAccepted)
}
2 changes: 2 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func main() {
mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler)))
mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler)))
mux.Handle("/sync/logs", rateLimitedHandler(http.HandlerFunc(controllers.SyncLogsHandler)))
mux.Handle("/complete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler)))
mux.Handle("/delete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler)))

mux.HandleFunc("/ws", controllers.WebSocketHandler)

Expand Down
12 changes: 12 additions & 0 deletions backend/models/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ type DeleteTaskRequestBody struct {
UUID string `json:"UUID"`
TaskUUID string `json:"taskuuid"`
}
type BulkCompleteTaskRequestBody struct {
Email string `json:"email"`
EncryptionSecret string `json:"encryptionSecret"`
UUID string `json:"UUID"`
TaskUUIDs []string `json:"taskuuids"`
}
type BulkDeleteTaskRequestBody struct {
Email string `json:"email"`
EncryptionSecret string `json:"encryptionSecret"`
UUID string `json:"UUID"`
TaskUUIDs []string `json:"taskuuids"`
}
13 changes: 13 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const TaskDialog = ({
selectedIndex,
onOpenChange,
onSelectTask,
selectedTaskUUIDs,
onCheckboxChange,
editState,
onUpdateState,
allTasks,
Expand Down Expand Up @@ -98,6 +100,17 @@ export const TaskDialog = ({
onSelectTask(task, index);
}}
>
<TableCell className="py-2" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedTaskUUIDs.includes(task.uuid)}
disabled={task.status === 'deleted'}
onChange={(e) => {
e.stopPropagation();
onCheckboxChange(task.uuid, e.target.checked);
}}
/>
</TableCell>
{/* Display task details */}
<TableCell className="py-2">
<span
Expand Down
Loading
Loading