package handlers import ( "encoding/json" "fmt" "log" "net/http" "strings" "sync" "time" "gocheck/database" "gocheck/models" "gocheck/sse" ) var ( itemLocks = make(map[int]*models.ItemLock) // item ID → lock itemLocksMutex sync.Mutex ) // GetItemLocks returns the current item locks func GetItemLocks() map[int]*models.ItemLock { return itemLocks } // StartLockExpiryDaemon starts the lock expiry daemon func StartLockExpiryDaemon() { go lockExpiryDaemon() } func lockExpiryDaemon() { for { time.Sleep(1 * time.Second) now := time.Now() // Collect expired locks first var expiredLocks []struct { id int uuid string } itemLocksMutex.Lock() for id, lock := range itemLocks { if lock.Expires.Before(now) { expiredLocks = append(expiredLocks, struct { id int uuid string }{id: id, uuid: lock.ChecklistUUID}) delete(itemLocks, id) } } itemLocksMutex.Unlock() // Broadcast unlock events after releasing the mutex for _, expired := range expiredLocks { sse.Broadcast(expired.uuid, map[string]interface{}{ "type": "item_unlocked", "id": expired.id, }) } } } func HandleGetItems(w http.ResponseWriter, r *http.Request) { uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid = uuid[:36] // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } itemLocksMutex.Lock() items, err := database.LoadChecklistItems(uuid, itemLocks) itemLocksMutex.Unlock() if err != nil { http.Error(w, "Failed to load items", 500) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Items loaded successfully", "items": items, }) } func HandleCreateChecklist(w http.ResponseWriter, r *http.Request) { type Req struct { Name string `json:"name"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Name) == "" { http.Error(w, "Missing name", 400) return } uuid, err := database.AddChecklist(req.Name) if err != nil { http.Error(w, "Failed to create checklist", 500) return } w.Header().Set("Content-Type", "application/json") resp := map[string]interface{}{ "success": true, "message": "Checklist created successfully", "uuid": uuid, } json.NewEncoder(w).Encode(resp) } func HandleAddItem(w http.ResponseWriter, r *http.Request) { uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid = uuid[:36] // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } type Req struct { Content string `json:"content"` ParentID *int `json:"parent_id"` NotBefore *time.Time `json:"not_before"` NotAfter *time.Time `json:"not_after"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Content) == "" { http.Error(w, "Missing content", 400) return } item, err := database.AddItem(uuid, req.Content, req.ParentID, req.NotBefore, req.NotAfter) if err != nil { http.Error(w, "Failed to add item", 500) return } // broadcast sse.Broadcast(uuid, map[string]interface{}{"type": "item_added", "item": item}) w.Header().Set("Content-Type", "application/json") w.WriteHeader(201) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Item added successfully", "item": item, }) } func HandleUpdateItem(w http.ResponseWriter, r *http.Request) { log.Printf("handleUpdateItem called with path: %s", r.URL.Path) parts := strings.Split(r.URL.Path, "/") if len(parts) < 6 { http.Error(w, "Invalid path", 400) return } uuid := parts[3] id := 0 fmt.Sscanf(parts[5], "%d", &id) // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } type Req struct { Content *string `json:"content"` Checked *bool `json:"checked"` ParentID *int `json:"parent_id"` Dependencies *[]int `json:"dependencies"` NotBefore *time.Time `json:"not_before"` NotAfter *time.Time `json:"not_after"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad body", 400) return } item, err := database.UpdateItem(uuid, id, req.Content, req.Checked, req.ParentID, req.Dependencies, req.NotBefore, req.NotAfter) if err != nil { if strings.Contains(err.Error(), "cannot complete item:") { http.Error(w, err.Error(), 400) } else { http.Error(w, "Not found", 404) } return } sse.Broadcast(uuid, map[string]interface{}{"type": "item_updated", "item": item}) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Item updated successfully", "item": item, }) } func HandleDeleteItem(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") uuid := parts[3] id := 0 fmt.Sscanf(parts[5], "%d", &id) // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } if err := database.DeleteItem(uuid, id); err != nil { http.Error(w, "Delete failed", 500) return } sse.Broadcast(uuid, map[string]interface{}{"type": "item_deleted", "id": id}) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Item deleted successfully", "id": id, }) } func HandleLockItem(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") uuid := parts[3] id := 0 fmt.Sscanf(parts[5], "%d", &id) // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } type Req struct { User string `json:"user"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.User) == "" { http.Error(w, "Missing user", 400) return } expiry := time.Now().Add(15 * time.Second) // e.g. 30 sec lock itemLocksMutex.Lock() itemLocks[id] = &models.ItemLock{LockedBy: req.User, Expires: expiry, ChecklistUUID: uuid} itemLocksMutex.Unlock() // Broadcast lock sse.Broadcast(uuid, map[string]interface{}{"type": "item_locked", "id": id, "locked_by": req.User, "expires": expiry}) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Item locked successfully", "id": id, "locked_by": req.User, "expires": expiry, }) } func HandleUpdateChecklistName(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") uuid := parts[3] log.Printf("handleUpdateChecklistName called for uuid: %s", uuid) // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { log.Printf("Failed to ensure checklist exists: %v", err) http.Error(w, "Failed to ensure checklist exists", 500) return } type Req struct { Name string `json:"name"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Name) == "" { log.Printf("Invalid request body: %v", err) http.Error(w, "Missing or empty name", 400) return } log.Printf("Updating checklist name to: %s", req.Name) if err := database.UpdateChecklistName(uuid, req.Name); err != nil { log.Printf("Failed to update checklist name: %v", err) http.Error(w, "Failed to update checklist name", 500) return } log.Printf("Checklist name updated successfully, broadcasting...") // Broadcast name update sse.Broadcast(uuid, map[string]interface{}{"type": "checklist_name_updated", "name": req.Name}) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Checklist name updated successfully", "name": req.Name, }) } func HandleSSE(w http.ResponseWriter, r *http.Request) { uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid = uuid[:36] // Ensure checklist exists if err := database.EnsureChecklistExists(uuid); err != nil { http.Error(w, "Failed to ensure checklist exists", 500) return } flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") ch := make(chan string, 20) // Register client sse.RegisterClient(uuid, ch) defer func() { sse.UnregisterClient(uuid, ch) close(ch) }() // Send full state on connect itemLocksMutex.Lock() items, err := database.LoadChecklistItems(uuid, itemLocks) itemLocksMutex.Unlock() name, err2 := database.LoadChecklistName(uuid) if err == nil && err2 == nil { msg, _ := json.Marshal(map[string]interface{}{ "type": "full_state", "items": items, }) fmt.Fprintf(w, "data: %s\n\n", msg) flusher.Flush() msg, _ = json.Marshal(map[string]interface{}{ "type": "checklist_name", "name": name, }) fmt.Fprintf(w, "data: %s\n\n", msg) flusher.Flush() } // Forward events for { select { case ev := <-ch: fmt.Fprintf(w, "data: %s\n\n", ev) flusher.Flush() case <-r.Context().Done(): return } } }