cheekylist/backend/handlers/handlers.go
2025-07-28 19:54:33 +02:00

363 lines
No EOL
9.4 KiB
Go

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
}
}
}