Skip to content

Commit ded6bc7

Browse files
authored
feat: Add Developer Logs/Debugging feature (#141) (#145)
* feat: Add Developer Logs/Debugging feature (#141) * chore: Run formatting (gofmt & prettier)
1 parent 2902655 commit ded6bc7

12 files changed

Lines changed: 430 additions & 6 deletions

File tree

backend/controllers/add_task.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,19 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) {
5656
http.Error(w, "Due Date is required, and cannot be empty!", http.StatusBadRequest)
5757
return
5858
}
59+
60+
logStore := models.GetLogStore()
5961
job := Job{
6062
Name: "Add Task",
6163
Execute: func() error {
62-
return tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, tags)
64+
logStore.AddLog("INFO", fmt.Sprintf("Adding task: %s", description), uuid, "Add Task")
65+
err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, tags)
66+
if err != nil {
67+
logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), uuid, "Add Task")
68+
return err
69+
}
70+
logStore.AddLog("INFO", fmt.Sprintf("Successfully added task: %s", description), uuid, "Add Task")
71+
return nil
6372
},
6473
}
6574
GlobalJobQueue.AddJob(job)

backend/controllers/complete_task.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,18 @@ func CompleteTaskHandler(w http.ResponseWriter, r *http.Request) {
5353
// http.Error(w, err.Error(), http.StatusInternalServerError)
5454
// return
5555
// }
56+
logStore := models.GetLogStore()
5657
job := Job{
5758
Name: "Complete Task",
5859
Execute: func() error {
59-
return tw.CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid)
60+
logStore.AddLog("INFO", fmt.Sprintf("Completing task UUID: %s", taskuuid), uuid, "Complete Task")
61+
err := tw.CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid)
62+
if err != nil {
63+
logStore.AddLog("ERROR", fmt.Sprintf("Failed to complete task UUID %s: %v", taskuuid, err), uuid, "Complete Task")
64+
return err
65+
}
66+
logStore.AddLog("INFO", fmt.Sprintf("Successfully completed task UUID: %s", taskuuid), uuid, "Complete Task")
67+
return nil
6068
},
6169
}
6270
GlobalJobQueue.AddJob(job)

backend/controllers/delete_task.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ func DeleteTaskHandler(w http.ResponseWriter, r *http.Request) {
5151
// http.Error(w, err.Error(), http.StatusInternalServerError)
5252
// return
5353
// }
54+
logStore := models.GetLogStore()
5455
job := Job{
5556
Name: "Delete Task",
5657
Execute: func() error {
57-
return tw.DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid)
58+
logStore.AddLog("INFO", fmt.Sprintf("Deleting task UUID: %s", taskuuid), uuid, "Delete Task")
59+
err := tw.DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid)
60+
if err != nil {
61+
logStore.AddLog("ERROR", fmt.Sprintf("Failed to delete task UUID %s: %v", taskuuid, err), uuid, "Delete Task")
62+
return err
63+
}
64+
logStore.AddLog("INFO", fmt.Sprintf("Successfully deleted task UUID: %s", taskuuid), uuid, "Delete Task")
65+
return nil
5866
},
5967
}
6068
GlobalJobQueue.AddJob(job)

backend/controllers/edit_task.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,18 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) {
5656
// return
5757
// }
5858

59+
logStore := models.GetLogStore()
5960
job := Job{
6061
Name: "Edit Task",
6162
Execute: func() error {
62-
return tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags)
63+
logStore.AddLog("INFO", fmt.Sprintf("Editing task ID: %s", taskID), uuid, "Edit Task")
64+
err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags)
65+
if err != nil {
66+
logStore.AddLog("ERROR", fmt.Sprintf("Failed to edit task ID %s: %v", taskID, err), uuid, "Edit Task")
67+
return err
68+
}
69+
logStore.AddLog("INFO", fmt.Sprintf("Successfully edited task ID: %s", taskID), uuid, "Edit Task")
70+
return nil
6371
},
6472
}
6573
GlobalJobQueue.AddJob(job)

backend/controllers/get_logs.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package controllers
2+
3+
import (
4+
"ccsync_backend/models"
5+
"encoding/json"
6+
"net/http"
7+
"strconv"
8+
)
9+
10+
// SyncLogsHandler godoc
11+
// @Summary Get sync logs
12+
// @Description Fetch the latest sync operation logs
13+
// @Tags Logs
14+
// @Accept json
15+
// @Produce json
16+
// @Param last query int false "Number of latest log entries to return (default: 100)"
17+
// @Success 200 {array} models.LogEntry "List of log entries"
18+
// @Failure 400 {string} string "Invalid last parameter"
19+
// @Router /sync/logs [get]
20+
func SyncLogsHandler(w http.ResponseWriter, r *http.Request) {
21+
if r.Method != http.MethodGet {
22+
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
23+
return
24+
}
25+
26+
// Get the 'last' query parameter, default to 100
27+
lastParam := r.URL.Query().Get("last")
28+
last := 100
29+
if lastParam != "" {
30+
parsedLast, err := strconv.Atoi(lastParam)
31+
if err != nil || parsedLast < 0 {
32+
http.Error(w, "Invalid 'last' parameter", http.StatusBadRequest)
33+
return
34+
}
35+
last = parsedLast
36+
}
37+
38+
// Get the log store and retrieve logs
39+
logStore := models.GetLogStore()
40+
logs := logStore.GetLogs(last)
41+
42+
// Return logs as JSON
43+
w.Header().Set("Content-Type", "application/json")
44+
if err := json.NewEncoder(w).Encode(logs); err != nil {
45+
http.Error(w, "Failed to encode logs", http.StatusInternalServerError)
46+
return
47+
}
48+
}

backend/controllers/modify_task.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,18 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) {
6363
// return
6464
// }
6565

66+
logStore := models.GetLogStore()
6667
job := Job{
6768
Name: "Modify Task",
6869
Execute: func() error {
69-
return tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID, tags)
70+
logStore.AddLog("INFO", fmt.Sprintf("Modifying task ID: %s", taskID), uuid, "Modify Task")
71+
err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID, tags)
72+
if err != nil {
73+
logStore.AddLog("ERROR", fmt.Sprintf("Failed to modify task ID %s: %v", taskID, err), uuid, "Modify Task")
74+
return err
75+
}
76+
logStore.AddLog("INFO", fmt.Sprintf("Successfully modified task ID: %s", taskID), uuid, "Modify Task")
77+
return nil
7078
},
7179
}
7280
GlobalJobQueue.AddJob(job)

backend/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func main() {
9191
mux.Handle("/modify-task", rateLimitedHandler(http.HandlerFunc(controllers.ModifyTaskHandler)))
9292
mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler)))
9393
mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler)))
94+
mux.Handle("/sync/logs", rateLimitedHandler(http.HandlerFunc(controllers.SyncLogsHandler)))
9495

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

backend/models/logs.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package models
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
// LogEntry represents a single log entry for sync operations
9+
type LogEntry struct {
10+
Timestamp string `json:"timestamp"`
11+
Level string `json:"level"` // INFO, WARN, ERROR
12+
Message string `json:"message"`
13+
SyncID string `json:"syncId,omitempty"`
14+
Operation string `json:"operation,omitempty"`
15+
}
16+
17+
// LogStore manages the in-memory log storage with a max of 100 entries
18+
type LogStore struct {
19+
mu sync.RWMutex
20+
entries []LogEntry
21+
maxSize int
22+
}
23+
24+
var (
25+
// GlobalLogStore is the global instance of the log store
26+
GlobalLogStore *LogStore
27+
once sync.Once
28+
)
29+
30+
// GetLogStore returns the singleton instance of LogStore
31+
func GetLogStore() *LogStore {
32+
once.Do(func() {
33+
GlobalLogStore = &LogStore{
34+
entries: make([]LogEntry, 0, 100),
35+
maxSize: 100,
36+
}
37+
})
38+
return GlobalLogStore
39+
}
40+
41+
// AddLog adds a new log entry to the store
42+
func (ls *LogStore) AddLog(level, message, syncID, operation string) {
43+
ls.mu.Lock()
44+
defer ls.mu.Unlock()
45+
46+
entry := LogEntry{
47+
Timestamp: time.Now().Format(time.RFC3339),
48+
Level: level,
49+
Message: message,
50+
SyncID: syncID,
51+
Operation: operation,
52+
}
53+
54+
// Add to the end
55+
ls.entries = append(ls.entries, entry)
56+
57+
// Keep only the last maxSize entries
58+
if len(ls.entries) > ls.maxSize {
59+
ls.entries = ls.entries[len(ls.entries)-ls.maxSize:]
60+
}
61+
}
62+
63+
// GetLogs returns the last N log entries (or all if N > total)
64+
func (ls *LogStore) GetLogs(last int) []LogEntry {
65+
ls.mu.RLock()
66+
defer ls.mu.RUnlock()
67+
68+
if last <= 0 || last > len(ls.entries) {
69+
// Return all entries in reverse order (newest first)
70+
result := make([]LogEntry, len(ls.entries))
71+
for i, entry := range ls.entries {
72+
result[len(ls.entries)-1-i] = entry
73+
}
74+
return result
75+
}
76+
77+
// Return last N entries in reverse order (newest first)
78+
result := make([]LogEntry, last)
79+
for i := 0; i < last; i++ {
80+
result[i] = ls.entries[len(ls.entries)-1-i]
81+
}
82+
return result
83+
}

0 commit comments

Comments
 (0)