Skip to content
Merged
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
81 changes: 81 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.

use CompleteTasksInTaskwarrior signature that returns a failed tasks map. logs individual failures and final success/failure counts to DevLogs for debugging visibility.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 {
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Starting %d tasks", len(taskUUIDs)), uuid, "Bulk Complete Task")

failedTasks, err := tw.CompleteTasksInTaskwarrior(email, encryptionSecret, uuid, taskUUIDs)

for taskUUID, errMsg := range failedTasks {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Complete] Failed: %s (%s)", taskUUID, errMsg), uuid, "Bulk Complete Task")
}

if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Complete] Sync error: %v", err), uuid, "Bulk Complete Task")
return err
}

successCount := len(taskUUIDs) - len(failedTasks)
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Finished: %d succeeded, %d failed", successCount, len(failedTasks)), uuid, "Bulk Complete Task")

return nil
},
}

GlobalJobQueue.AddJob(job)
w.WriteHeader(http.StatusAccepted)
}
80 changes: 80 additions & 0 deletions backend/controllers/delete_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.

use DeleteTasksInTaskwarrior signature that returns a failed tasks map. Logs individual failures and final success/failure counts to DevLogs for debugging visibility.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 {
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Starting %d tasks", len(taskUUIDs)), uuid, "Bulk Delete Task")

failedTasks, err := tw.DeleteTasksInTaskwarrior(email, encryptionSecret, uuid, taskUUIDs)

for taskUUID, errMsg := range failedTasks {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Delete] Failed: %s (%s)", taskUUID, errMsg), uuid, "Bulk Delete Task")
}

if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Delete] Sync error: %v", err), uuid, "Bulk Delete Task")
return err
}

successCount := len(taskUUIDs) - len(failedTasks)
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Finished: %d succeeded, %d failed", successCount, len(failedTasks)), uuid, "Bulk Delete Task")

return nil
},
}

GlobalJobQueue.AddJob(job)
w.WriteHeader(http.StatusAccepted)
}
2 changes: 2 additions & 0 deletions backend/main.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.

new routes added

Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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("/health", controllers.HealthCheckHandler)

Expand Down
12 changes: 12 additions & 0 deletions backend/models/request_body.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.

added bulk request body structs, it is using array for taskUUIDs to accept multiple tasks identifiers in one request.

Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,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"`
}
45 changes: 45 additions & 0 deletions backend/utils/tw/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.

returns map[string]string (uuid → error) to collect individual task failures and optimized from N syncs to single sync cycle pattern (SetConfig -> InitialSync -> Loop -> FinalSync)

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tw

import (
"ccsync_backend/utils"
"fmt"
"os"
)

func CompleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs []string) (map[string]string, error) {
failedTasks := make(map[string]string)

if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return nil, fmt.Errorf("error deleting Taskwarrior data: %v", err)
}

tempDir, err := os.MkdirTemp("", "taskwarrior-"+email)

if err != nil {
return nil, fmt.Errorf("failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir)

origin := os.Getenv("CONTAINER_ORIGIN")
if err := SetTaskwarriorConfig(tempDir, encryptionSecret, origin, uuid); err != nil {
return nil, err
}

if err := SyncTaskwarrior(tempDir); err != nil {
return nil, err
}

for _, taskuuid := range taskUUIDs {
if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "done", "rc.confirmation=off"); err != nil {
failedTasks[taskuuid] = err.Error()
continue
}
}

// Sync Taskwarrior again
if err := SyncTaskwarrior(tempDir); err != nil {
return failedTasks, err
}

return failedTasks, nil
}
45 changes: 45 additions & 0 deletions backend/utils/tw/delete_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.

returns map[string]string (uuid → error) to collect individual task failures and optimized from N syncs to single sync cycle pattern (SetConfig -> InitialSync -> Loop -> FinalSync)

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tw

import (
"ccsync_backend/utils"
"fmt"
"os"
)

func DeleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs []string) (map[string]string, error) {
failedTasks := make(map[string]string)

if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return nil, fmt.Errorf("error deleting Taskwarrior data: %v", err)
}

tempDir, err := os.MkdirTemp("", "taskwarrior-"+email)

if err != nil {
return nil, fmt.Errorf("failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir)

origin := os.Getenv("CONTAINER_ORIGIN")
if err := SetTaskwarriorConfig(tempDir, encryptionSecret, origin, uuid); err != nil {
return nil, err
}

if err := SyncTaskwarrior(tempDir); err != nil {
return nil, err
}

for _, taskuuid := range taskUUIDs {
if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "delete", "rc.confirmation=off"); err != nil {
failedTasks[taskuuid] = err.Error()
continue
}
}

// Sync Taskwarrior again
if err := SyncTaskwarrior(tempDir); err != nil {
return failedTasks, err
}

return failedTasks, nil
}
14 changes: 14 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

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

added checkbox column to task rows and also deleted tasks have disabled checkboxes since they can't be bulk actioned

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 @@ -100,6 +102,18 @@ 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