This commit is contained in:
lubiana 2025-07-25 15:20:23 +02:00
parent 91d308485b
commit 0535cd1aad
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk

159
main.go
View file

@ -46,7 +46,6 @@ type ItemLock struct {
// ==== Globals ==== // ==== Globals ====
var ( var (
db *sql.DB
sseClients = make(map[string]map[chan string]bool) // checklist uuid → set of client channels sseClients = make(map[string]map[chan string]bool) // checklist uuid → set of client channels
sseClientsMutex sync.Mutex sseClientsMutex sync.Mutex
itemLocks = make(map[int]*ItemLock) // item ID → lock itemLocks = make(map[int]*ItemLock) // item ID → lock
@ -55,14 +54,21 @@ var (
// ==== Database ==== // ==== Database ====
func setupDatabase() error { func getChecklistDB(uuid string) (*sql.DB, error) {
var err error // Ensure data directory exists
db, err = sql.Open("sqlite3", "data/checklists.db") if err := os.MkdirAll("data", 0755); err != nil {
if err != nil { return nil, err
return err
} }
dbPath := fmt.Sprintf("data/%s.db", uuid)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
}
// Setup schema for this checklist
queries := []string{ queries := []string{
`CREATE TABLE IF NOT EXISTS checklists ( `CREATE TABLE IF NOT EXISTS checklist_info (
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY,
name TEXT NOT NULL name TEXT NOT NULL
);`, );`,
@ -71,23 +77,52 @@ func setupDatabase() error {
content TEXT NOT NULL, content TEXT NOT NULL,
checked INTEGER NOT NULL, checked INTEGER NOT NULL,
parent_id INTEGER, parent_id INTEGER,
checklist_uuid TEXT NOT NULL, FOREIGN KEY(parent_id) REFERENCES items(id)
FOREIGN KEY(parent_id) REFERENCES items(id),
FOREIGN KEY(checklist_uuid) REFERENCES checklists(uuid)
);`, );`,
} }
for _, q := range queries { for _, q := range queries {
if _, err := db.Exec(q); err != nil { if _, err := db.Exec(q); err != nil {
db.Close()
return nil, err
}
}
return db, nil
}
// ensureChecklistExists creates a checklist if it doesn't exist
func ensureChecklistExists(uuid string) error {
db, err := getChecklistDB(uuid)
if err != nil {
return err
}
defer db.Close()
// Check if checklist_info table has any data
var count int
err = db.QueryRow(`SELECT COUNT(*) FROM checklist_info`).Scan(&count)
if err != nil {
return err
}
// If no checklist exists, create one with a default name
if count == 0 {
_, err = db.Exec(`INSERT INTO checklist_info (uuid, name) VALUES (?, ?)`, uuid, "Untitled Checklist")
if err != nil {
return err return err
} }
} }
return nil return nil
} }
func loadChecklistName(uuid string) (string, error) { func loadChecklistName(uuid string) (string, error) {
rows, err := db.Query( db, err := getChecklistDB(uuid)
`SELECT name FROM checklists WHERE uuid = ?`, if err != nil {
uuid) return "", err
}
defer db.Close()
rows, err := db.Query(`SELECT name FROM checklist_info WHERE uuid = ?`, uuid)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -104,9 +139,13 @@ func loadChecklistName(uuid string) (string, error) {
} }
func loadChecklistItems(uuid string) ([]ChecklistItem, error) { func loadChecklistItems(uuid string) ([]ChecklistItem, error) {
rows, err := db.Query( db, err := getChecklistDB(uuid)
`SELECT id, content, checked, parent_id FROM items WHERE checklist_uuid = ?`, if err != nil {
uuid) return nil, err
}
defer db.Close()
rows, err := db.Query(`SELECT id, content, checked, parent_id FROM items`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -143,13 +182,25 @@ func loadChecklistItems(uuid string) ([]ChecklistItem, error) {
func addChecklist(name string) (string, error) { func addChecklist(name string) (string, error) {
uuidStr := uuid.New().String() uuidStr := uuid.New().String()
_, err := db.Exec(`INSERT INTO checklists (uuid, name) VALUES (?, ?)`, uuidStr, name) db, err := getChecklistDB(uuidStr)
if err != nil {
return "", err
}
defer db.Close()
_, err = db.Exec(`INSERT INTO checklist_info (uuid, name) VALUES (?, ?)`, uuidStr, name)
return uuidStr, err return uuidStr, err
} }
func addItem(uuid, content string, parentID *int) (ChecklistItem, error) { func addItem(uuid, content string, parentID *int) (ChecklistItem, error) {
res, err := db.Exec(`INSERT INTO items (content, checked, parent_id, checklist_uuid) VALUES (?, 0, ?, ?)`, db, err := getChecklistDB(uuid)
content, parentID, uuid) if err != nil {
return ChecklistItem{}, err
}
defer db.Close()
res, err := db.Exec(`INSERT INTO items (content, checked, parent_id) VALUES (?, 0, ?)`,
content, parentID)
if err != nil { if err != nil {
return ChecklistItem{}, err return ChecklistItem{}, err
} }
@ -164,6 +215,12 @@ func addItem(uuid, content string, parentID *int) (ChecklistItem, error) {
} }
func updateItem(uuid string, id int, content *string, checked *bool, parentID *int) (ChecklistItem, error) { func updateItem(uuid string, id int, content *string, checked *bool, parentID *int) (ChecklistItem, error) {
db, err := getChecklistDB(uuid)
if err != nil {
return ChecklistItem{}, err
}
defer db.Close()
q := "UPDATE items SET " q := "UPDATE items SET "
args := []interface{}{} args := []interface{}{}
set := []string{} set := []string{}
@ -183,15 +240,14 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i
set = append(set, "parent_id = ?") set = append(set, "parent_id = ?")
args = append(args, *parentID) args = append(args, *parentID)
} }
q += strings.Join(set, ", ") + " WHERE id = ? AND checklist_uuid = ?" q += strings.Join(set, ", ") + " WHERE id = ?"
args = append(args, id, uuid) args = append(args, id)
_, err := db.Exec(q, args...) _, err = db.Exec(q, args...)
if err != nil { if err != nil {
return ChecklistItem{}, err return ChecklistItem{}, err
} }
// Return updated item // Return updated item
rows, err := db.Query( rows, err := db.Query(`SELECT id, content, checked, parent_id FROM items WHERE id = ?`, id)
`SELECT id, content, checked, parent_id FROM items WHERE id = ? AND checklist_uuid = ?`, id, uuid)
if err != nil { if err != nil {
return ChecklistItem{}, err return ChecklistItem{}, err
} }
@ -216,7 +272,13 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i
} }
func deleteItem(uuid string, id int) error { func deleteItem(uuid string, id int) error {
_, err := db.Exec(`DELETE FROM items WHERE id = ? AND checklist_uuid = ?`, id, uuid) db, err := getChecklistDB(uuid)
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(`DELETE FROM items WHERE id = ?`, id)
return err return err
} }
@ -240,6 +302,13 @@ func broadcast(uuid string, msg interface{}) {
func handleGetItems(w http.ResponseWriter, r *http.Request) { func handleGetItems(w http.ResponseWriter, r *http.Request) {
uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/")
uuid = uuid[:36] uuid = uuid[:36]
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
items, err := loadChecklistItems(uuid) items, err := loadChecklistItems(uuid)
if err != nil { if err != nil {
http.Error(w, "Failed to load items", 500) http.Error(w, "Failed to load items", 500)
@ -279,6 +348,13 @@ func handleCreateChecklist(w http.ResponseWriter, r *http.Request) {
func handleAddItem(w http.ResponseWriter, r *http.Request) { func handleAddItem(w http.ResponseWriter, r *http.Request) {
uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/")
uuid = uuid[:36] uuid = uuid[:36]
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
type Req struct { type Req struct {
Content string `json:"content"` Content string `json:"content"`
ParentID *int `json:"parent_id"` ParentID *int `json:"parent_id"`
@ -309,6 +385,13 @@ func handleUpdateItem(w http.ResponseWriter, r *http.Request) {
uuid := parts[3] uuid := parts[3]
id := 0 id := 0
fmt.Sscanf(parts[5], "%d", &id) fmt.Sscanf(parts[5], "%d", &id)
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
type Req struct { type Req struct {
Content *string `json:"content"` Content *string `json:"content"`
Checked *bool `json:"checked"` Checked *bool `json:"checked"`
@ -338,6 +421,13 @@ func handleDeleteItem(w http.ResponseWriter, r *http.Request) {
uuid := parts[3] uuid := parts[3]
id := 0 id := 0
fmt.Sscanf(parts[5], "%d", &id) fmt.Sscanf(parts[5], "%d", &id)
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
if err := deleteItem(uuid, id); err != nil { if err := deleteItem(uuid, id); err != nil {
http.Error(w, "Delete failed", 500) http.Error(w, "Delete failed", 500)
return return
@ -356,6 +446,13 @@ func handleLockItem(w http.ResponseWriter, r *http.Request) {
uuid := parts[3] uuid := parts[3]
id := 0 id := 0
fmt.Sscanf(parts[5], "%d", &id) fmt.Sscanf(parts[5], "%d", &id)
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
type Req struct { type Req struct {
User string `json:"user"` User string `json:"user"`
} }
@ -419,6 +516,12 @@ func handleSSE(w http.ResponseWriter, r *http.Request) {
uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/") uuid := strings.TrimPrefix(r.URL.Path, "/api/checklists/")
uuid = uuid[:36] uuid = uuid[:36]
// Ensure checklist exists
if err := ensureChecklistExists(uuid); err != nil {
http.Error(w, "Failed to ensure checklist exists", 500)
return
}
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError) http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
@ -477,9 +580,11 @@ func handleSSE(w http.ResponseWriter, r *http.Request) {
// ==== Main + Routing ==== // ==== Main + Routing ====
func main() { func main() {
if err := setupDatabase(); err != nil { // Ensure data directory exists
log.Fatalf("DB setup: %v", err) if err := os.MkdirAll("data", 0755); err != nil {
log.Fatalf("Failed to create data directory: %v", err)
} }
go lockExpiryDaemon() go lockExpiryDaemon()
// Serve static files from embedded filesystem // Serve static files from embedded filesystem