363 lines
No EOL
9.4 KiB
Go
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
|
|
}
|
|
}
|
|
} |