diff --git a/README.md b/README.md index d8a0365..f1f6471 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ persistence. - Real-time collaboration with Server-Sent Events - Item locking mechanism to prevent conflicts - Hierarchical items (parent-child relationships) +- **Item dependencies: Define prerequisites for completing items** - SQLite database for data persistence - **Modern web frontend with React and TypeScript** - **Real-time updates across multiple browser tabs/windows** @@ -293,7 +294,8 @@ All API endpoints return JSON responses with a consistent format: "content": "New item", "checked": false, "parent_id": null, - "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000" + "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", + "dependencies": [] } } ``` @@ -309,7 +311,8 @@ All API endpoints return JSON responses with a consistent format: "content": "Updated content", "checked": true, "parent_id": null, - "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000" + "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", + "dependencies": [2, 3] } } ``` @@ -348,7 +351,16 @@ All API endpoints return JSON responses with a consistent format: "content": "Item 1", "checked": false, "parent_id": null, - "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000" + "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", + "dependencies": [] + }, + { + "id": 2, + "content": "Item 2", + "checked": true, + "parent_id": null, + "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", + "dependencies": [1] } ] } @@ -363,8 +375,9 @@ All API endpoints return JSON responses with a consistent format: The application uses SQLite with the following schema: -- `checklists` table: Stores checklist metadata +- `checklist_info` table: Stores checklist metadata (uuid, name) - `items` table: Stores checklist items with hierarchical relationships +- `dependencies` table: Stores item dependencies (item_id, dependency_id) ## Real-time Features @@ -374,6 +387,41 @@ The application uses SQLite with the following schema: - **Broadcast updates to all connected clients** - **Immediate UI updates**: Changes appear instantly across all browsers +## Item Dependencies + +The application supports item dependencies, allowing you to define prerequisites +that must be completed before an item can be marked as done. + +### How Dependencies Work + +- **Prerequisites**: Items can depend on other items being completed first +- **Visual Indicators**: Items with unmet dependencies show warning indicators +- **Prevention**: Items cannot be completed until all dependencies are satisfied +- **Circular Prevention**: The system prevents circular dependencies +- **Real-time Updates**: Dependency status updates in real-time across all + clients + +### Managing Dependencies + +1. **Add Dependencies**: Click the dependency icon (⚡) on any item to open the + dependency manager +2. **Select Prerequisites**: Choose which items must be completed first +3. **Visual Feedback**: Items show dependency status with color-coded + indicators: + - 🟠 Orange: Dependencies not met (shows count) + - 🟢 Green: All dependencies met (shows "Ready") +4. **Completion Prevention**: Items with unmet dependencies cannot be checked + off + +### Dependency Features + +- **Flexible Dependencies**: Items can depend on any number of other items +- **Hierarchical Support**: Dependencies work with parent-child relationships +- **Real-time Validation**: Dependency status updates immediately when items are + completed +- **Clear Feedback**: Users see exactly which items need to be completed first +- **Error Prevention**: System prevents circular dependencies automatically + ## Development ### Local Development @@ -383,7 +431,8 @@ To run in development mode with automatic reloading, you can use tools like #### Using the Development Script -The project includes a development script that runs both backend and frontend with hot reloading: +The project includes a development script that runs both backend and frontend +with hot reloading: ```bash # Start development environment @@ -391,6 +440,7 @@ The project includes a development script that runs both backend and frontend wi ``` This will: + - Start the Go backend with Air for hot reloading - Start the React frontend with Vite for hot reloading - Proxy API requests from frontend to backend @@ -413,6 +463,7 @@ For a consistent development environment using Docker with hot reloading: ``` This will: + - Build a development container with all dependencies - Mount your source code for hot reloading - Start both backend (Air) and frontend (Vite) with hot reloading @@ -440,8 +491,10 @@ docker-compose -f docker-compose.dev.yml up --build --force-recreate The Docker development environment includes: -- **Hot Reloading**: Both Go backend and React frontend reload automatically on code changes -- **Volume Mounts**: Your local code is mounted into the container for live editing +- **Hot Reloading**: Both Go backend and React frontend reload automatically on + code changes +- **Volume Mounts**: Your local code is mounted into the container for live + editing - **Dependency Management**: All dependencies are pre-installed in the container - **Consistent Environment**: Same environment across all developers - **Easy Cleanup**: Simple commands to start/stop the development environment @@ -449,6 +502,7 @@ The Docker development environment includes: #### File Watching The development container uses: + - **Air** for Go backend hot reloading (watches `.go` files) - **Vite** for React frontend hot reloading (watches all frontend files) - **inotify-tools** for efficient file system watching @@ -459,5 +513,7 @@ If you encounter issues: 1. **Port conflicts**: Ensure ports 8080 and 5173 are not in use 2. **Permission issues**: The container runs with appropriate permissions -3. **Hot reload not working**: Check that your files are being watched by the respective tools -4. **Container won't start**: Check Docker logs with `docker-compose -f docker-compose.dev.yml logs` +3. **Hot reload not working**: Check that your files are being watched by the + respective tools +4. **Container won't start**: Check Docker logs with + `docker-compose -f docker-compose.dev.yml logs` diff --git a/frontend/src/components/ChecklistItem.tsx b/frontend/src/components/ChecklistItem.tsx index 5433e70..3c57f5e 100644 --- a/frontend/src/components/ChecklistItem.tsx +++ b/frontend/src/components/ChecklistItem.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect } from 'react' import type { ChecklistItem as ChecklistItemType } from '../types' +import DependencyManager from './DependencyManager' interface ChecklistItemProps { item: ChecklistItemType @@ -8,6 +9,7 @@ interface ChecklistItemProps { onLock: (id: number, user: string) => Promise depth?: number children?: ChecklistItemType[] + allItems?: ChecklistItemType[] } export default function ChecklistItem({ @@ -16,16 +18,32 @@ export default function ChecklistItem({ onDelete, onLock, depth = 0, - children = [] + children = [], + allItems = [] }: ChecklistItemProps) { const [isEditing, setIsEditing] = useState(false) const [isDeleting, setIsDeleting] = useState(false) + const [isDependencyModalOpen, setIsDependencyModalOpen] = useState(false) const [userName, setUserName] = useState('') const contentRef = useRef(null) const isLocked = item.locked_by && item.lock_until && new Date(item.lock_until) > new Date() const isLockedByMe = isLocked && item.locked_by === userName + // Check if all dependencies are completed + const dependenciesCompleted = item.dependencies?.every(depId => { + const depItem = allItems.find(i => i.id === depId) + return depItem?.checked + }) ?? true + + // Check if item can be completed (all dependencies met) + const canComplete = dependenciesCompleted || item.checked + + // Get dependency items for display + const dependencyItems = item.dependencies?.map(depId => + allItems.find(i => i.id === depId) + ).filter(Boolean) ?? [] + useEffect(() => { // Generate a random user name if not set if (!userName) { @@ -97,10 +115,29 @@ export default function ChecklistItem({ } const handleToggleCheck = async () => { + // Don't allow unchecking if already checked + if (item.checked) { + try { + await onUpdate(item.id, { checked: false }) + } catch (error) { + console.error('Failed to uncheck item:', error) + } + return + } + + // Check if dependencies are met before allowing completion + if (!dependenciesCompleted) { + alert(`Cannot complete this item. The following dependencies must be completed first:\n\n${dependencyItems.map(dep => `• ${dep?.content}`).join('\n')}`) + return + } + try { - await onUpdate(item.id, { checked: !item.checked }) + await onUpdate(item.id, { checked: true }) } catch (error) { console.error('Failed to toggle item:', error) + if (error instanceof Error && error.message.includes('dependency')) { + alert(error.message) + } } } @@ -132,8 +169,11 @@ export default function ChecklistItem({ type="checkbox" checked={item.checked} onChange={handleToggleCheck} - disabled={Boolean(isLocked && !isLockedByMe)} - className="w-5 h-5 text-blue-600 bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500" + disabled={Boolean(isLocked && !isLockedByMe) || (!item.checked && !canComplete)} + className={`w-5 h-5 text-blue-600 bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500 ${ + !item.checked && !canComplete ? 'cursor-not-allowed' : '' + }`} + title={!item.checked && !canComplete ? 'Complete dependencies first' : ''} /> @@ -151,6 +191,8 @@ export default function ChecklistItem({ item.checked ? 'line-through text-gray-500 dark:text-gray-500 opacity-75' : 'text-gray-900 dark:text-gray-100' + } ${ + !item.checked && !canComplete ? 'opacity-60' : '' }`} onClick={!isEditing ? handleEdit : undefined} onKeyDown={isEditing ? handleKeyDown : undefined} @@ -158,10 +200,33 @@ export default function ChecklistItem({ > {item.content} + + {/* Dependency warning */} + {!item.checked && !canComplete && dependencyItems.length > 0 && ( +
+ ⚠️ Depends on: {dependencyItems.map(dep => dep?.content).join(', ')} +
+ )} {/* Actions */}
+ {/* Dependency indicators */} + {item.dependencies && item.dependencies.length > 0 && ( +
+ + + + + {dependenciesCompleted ? 'Ready' : `${item.dependencies.length} deps`} + +
+ )} + {isLocked && !isLockedByMe && ( @@ -172,16 +237,27 @@ export default function ChecklistItem({ )} {!isEditing && ( - + <> + + + )}
@@ -197,10 +273,20 @@ export default function ChecklistItem({ onDelete={onDelete} onLock={onLock} depth={depth + 1} + allItems={allItems} /> ))} )} + + {/* Dependency Manager Modal */} + setIsDependencyModalOpen(false)} + isOpen={isDependencyModalOpen} + /> ) } \ No newline at end of file diff --git a/frontend/src/components/DependencyManager.tsx b/frontend/src/components/DependencyManager.tsx new file mode 100644 index 0000000..b5af463 --- /dev/null +++ b/frontend/src/components/DependencyManager.tsx @@ -0,0 +1,134 @@ +import { useState, useEffect } from 'react' +import type { ChecklistItem } from '../types' + +interface DependencyManagerProps { + item: ChecklistItem + allItems: ChecklistItem[] + onUpdate: (id: number, updates: Partial) => Promise + onClose: () => void + isOpen: boolean +} + +export default function DependencyManager({ + item, + allItems, + onUpdate, + onClose, + isOpen +}: DependencyManagerProps) { + const [selectedDependencies, setSelectedDependencies] = useState([]) + const [isUpdating, setIsUpdating] = useState(false) + + useEffect(() => { + if (isOpen) { + setSelectedDependencies(item.dependencies || []) + } + }, [isOpen, item.dependencies]) + + const handleSave = async () => { + setIsUpdating(true) + try { + await onUpdate(item.id, { dependencies: selectedDependencies }) + onClose() + } catch (error) { + console.error('Failed to update dependencies:', error) + } finally { + setIsUpdating(false) + } + } + + const toggleDependency = (depId: number) => { + setSelectedDependencies(prev => + prev.includes(depId) + ? prev.filter(id => id !== depId) + : [...prev, depId] + ) + } + + const availableItems = allItems.filter(otherItem => + otherItem.id !== item.id && + !otherItem.dependencies?.includes(item.id) // Prevent circular dependencies + ) + + if (!isOpen) return null + + return ( +
+
+
+
+

+ Manage Dependencies +

+ +
+ +
+

+ Select items that must be completed before "{item.content}" can be completed: +

+
+ +
+ {availableItems.length === 0 ? ( +
+ No available items to depend on +
+ ) : ( +
+ {availableItems.map(otherItem => ( + + ))} +
+ )} +
+ +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/Checklist.tsx b/frontend/src/pages/Checklist.tsx index 147dac5..1fa0a19 100644 --- a/frontend/src/pages/Checklist.tsx +++ b/frontend/src/pages/Checklist.tsx @@ -133,8 +133,8 @@ export default function Checklist() { } } - const renderItems = (items: ChecklistItemType[]) => { - return items.map(item => ( + const renderItems = (treeItems: ChecklistItemType[], allItems: ChecklistItemType[]) => { + return treeItems.map(item => ( )) } @@ -236,7 +237,7 @@ export default function Checklist() { ) : (
    - {renderItems(itemTree)} + {renderItems(itemTree, items)}
)} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e2e60e5..d0f1a0d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -7,6 +7,7 @@ export interface ChecklistItem { lock_until?: string checklist_uuid: string children?: ChecklistItem[] + dependencies?: number[] } export interface SavedChecklist { diff --git a/main.go b/main.go index 311b93c..d11eb35 100644 --- a/main.go +++ b/main.go @@ -29,13 +29,14 @@ type Checklist struct { } type ChecklistItem struct { - ID int `json:"id"` - Content string `json:"content"` - Checked bool `json:"checked"` - ParentID *int `json:"parent_id"` - LockedBy *string `json:"locked_by,omitempty"` - LockUntil *time.Time `json:"lock_until,omitempty"` - Checklist string `json:"checklist_uuid"` + ID int `json:"id"` + Content string `json:"content"` + Checked bool `json:"checked"` + ParentID *int `json:"parent_id"` + LockedBy *string `json:"locked_by,omitempty"` + LockUntil *time.Time `json:"lock_until,omitempty"` + Checklist string `json:"checklist_uuid"` + Dependencies []int `json:"dependencies,omitempty"` } type ItemLock struct { @@ -79,6 +80,14 @@ func getChecklistDB(uuid string) (*sql.DB, error) { parent_id INTEGER, FOREIGN KEY(parent_id) REFERENCES items(id) );`, + `CREATE TABLE IF NOT EXISTS dependencies ( + item_id INTEGER NOT NULL, + dependency_id INTEGER NOT NULL, + PRIMARY KEY (item_id, dependency_id), + FOREIGN KEY(item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY(dependency_id) REFERENCES items(id) ON DELETE CASCADE, + CHECK(item_id != dependency_id) + );`, } for _, q := range queries { if _, err := db.Exec(q); err != nil { @@ -89,6 +98,25 @@ func getChecklistDB(uuid string) (*sql.DB, error) { return db, nil } +func loadItemDependencies(db *sql.DB, itemID int) ([]int, error) { + rows, err := db.Query(`SELECT dependency_id FROM dependencies WHERE item_id = ?`, itemID) + if err != nil { + return nil, err + } + defer rows.Close() + + var deps []int + for rows.Next() { + var depID int + err = rows.Scan(&depID) + if err != nil { + return nil, err + } + deps = append(deps, depID) + } + return deps, nil +} + // ensureChecklistExists creates a checklist if it doesn't exist func ensureChecklistExists(uuid string) error { db, err := getChecklistDB(uuid) @@ -166,6 +194,13 @@ func loadChecklistItems(uuid string) ([]ChecklistItem, error) { } it.Checklist = uuid + // Load dependencies for this item + deps, err := loadItemDependencies(db, it.ID) + if err != nil { + return nil, err + } + it.Dependencies = deps + // Attach lock info if present itemLocksMutex.Lock() if lock, ok := itemLocks[it.ID]; ok && lock.Expires.After(time.Now()) { @@ -214,16 +249,72 @@ func addItem(uuid, content string, parentID *int) (ChecklistItem, error) { }, nil } -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, dependencies *[]int) (ChecklistItem, error) { + log.Printf("updateItem called with uuid: %s, id: %d", uuid, id) + log.Printf("Parameters: content=%v, checked=%v, parentID=%v, dependencies=%v", content, checked, parentID, dependencies) + db, err := getChecklistDB(uuid) if err != nil { + log.Printf("Failed to get database: %v", err) return ChecklistItem{}, err } defer db.Close() - q := "UPDATE items SET " - args := []interface{}{} + log.Printf("Database connection successful for uuid: %s", uuid) + + // If trying to check an item, validate dependencies first + if checked != nil && *checked { + log.Printf("Validating dependencies for item %d", id) + deps, err := loadItemDependencies(db, id) + if err != nil { + log.Printf("Failed to load dependencies: %v", err) + return ChecklistItem{}, err + } + + // Check if all dependencies are completed + for _, depID := range deps { + var depChecked int + err = db.QueryRow(`SELECT checked FROM items WHERE id = ?`, depID).Scan(&depChecked) + if err != nil { + log.Printf("Failed to check dependency %d: %v", depID, err) + return ChecklistItem{}, err + } + if depChecked == 0 { + return ChecklistItem{}, fmt.Errorf("cannot complete item: dependency %d is not completed", depID) + } + } + } else { + log.Printf("Skipping dependency validation - checked is %v", checked) + } + + log.Printf("About to check dependencies parameter") + // Update dependencies if provided + if dependencies != nil { + log.Printf("Updating dependencies for item %d: %v", id, *dependencies) + // Delete existing dependencies + _, err = db.Exec(`DELETE FROM dependencies WHERE item_id = ?`, id) + if err != nil { + log.Printf("Failed to delete existing dependencies: %v", err) + return ChecklistItem{}, err + } + + // Add new dependencies + for _, depID := range *dependencies { + log.Printf("Adding dependency %d for item %d", depID, id) + _, err = db.Exec(`INSERT INTO dependencies (item_id, dependency_id) VALUES (?, ?)`, id, depID) + if err != nil { + log.Printf("Failed to add dependency %d: %v", depID, err) + return ChecklistItem{}, err + } + } + log.Printf("Dependencies updated successfully") + } else { + log.Printf("No dependencies to update") + } + + log.Printf("About to build SQL update query") set := []string{} + args := []interface{}{} if content != nil { set = append(set, "content = ?") args = append(args, *content) @@ -240,24 +331,36 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i set = append(set, "parent_id = ?") args = append(args, *parentID) } - q += strings.Join(set, ", ") + " WHERE id = ?" - args = append(args, id) - _, err = db.Exec(q, args...) - if err != nil { - return ChecklistItem{}, err + if len(set) > 0 { + q := "UPDATE items SET " + strings.Join(set, ", ") + " WHERE id = ?" + args = append(args, id) + log.Printf("SQL query: %s with args: %v", q, args) + _, err = db.Exec(q, args...) + if err != nil { + log.Printf("Failed to execute SQL update: %v", err) + return ChecklistItem{}, err + } + log.Printf("SQL update executed successfully") + } else { + log.Printf("No fields to update in SQL, skipping update query") } + // Return updated item + log.Printf("Querying updated item %d", id) rows, err := db.Query(`SELECT id, content, checked, parent_id FROM items WHERE id = ?`, id) if err != nil { + log.Printf("Failed to query updated item: %v", err) return ChecklistItem{}, err } defer rows.Close() if rows.Next() { + log.Printf("Found item %d in database", id) var it ChecklistItem var checkedInt int var parentID sql.NullInt64 err = rows.Scan(&it.ID, &it.Content, &checkedInt, &parentID) if err != nil { + log.Printf("Failed to scan updated item: %v", err) return ChecklistItem{}, err } it.Checked = checkedInt != 0 @@ -266,8 +369,19 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i it.ParentID = &v } it.Checklist = uuid + + // Load dependencies + deps, err := loadItemDependencies(db, it.ID) + if err != nil { + log.Printf("Failed to load dependencies for return: %v", err) + return ChecklistItem{}, err + } + it.Dependencies = deps + + log.Printf("Successfully updated item %d", id) return it, nil } + log.Printf("Item %d not found in database", id) return ChecklistItem{}, fmt.Errorf("not found") } @@ -278,6 +392,13 @@ func deleteItem(uuid string, id int) error { } defer db.Close() + // Delete dependencies that reference this item + _, err = db.Exec(`DELETE FROM dependencies WHERE dependency_id = ?`, id) + if err != nil { + return err + } + + // Delete the item itself _, err = db.Exec(`DELETE FROM items WHERE id = ?`, id) return err } @@ -381,7 +502,14 @@ func handleAddItem(w http.ResponseWriter, r *http.Request) { } 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) @@ -393,18 +521,23 @@ func handleUpdateItem(w http.ResponseWriter, r *http.Request) { } type Req struct { - Content *string `json:"content"` - Checked *bool `json:"checked"` - ParentID *int `json:"parent_id"` + Content *string `json:"content"` + Checked *bool `json:"checked"` + ParentID *int `json:"parent_id"` + Dependencies *[]int `json:"dependencies"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad body", 400) return } - item, err := updateItem(uuid, id, req.Content, req.Checked, req.ParentID) + item, err := updateItem(uuid, id, req.Content, req.Checked, req.ParentID, req.Dependencies) if err != nil { - http.Error(w, "Not found", 404) + if strings.Contains(err.Error(), "cannot complete item: dependency") { + http.Error(w, err.Error(), 400) + } else { + http.Error(w, "Not found", 404) + } return } broadcast(uuid, map[string]interface{}{"type": "item_updated", "item": item}) @@ -587,7 +720,45 @@ func main() { go lockExpiryDaemon() - // Serve static files from embedded filesystem + // Register API handlers first + http.HandleFunc("/api/checklists", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + handleCreateChecklist(w, r) + } else { + http.NotFound(w, r) + } + }) + + http.HandleFunc("/api/checklists/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + log.Printf("API request: %s %s", r.Method, path) + + switch { + case strings.HasSuffix(path, "/items") && r.Method == "GET": + log.Printf("Handling GET items") + handleGetItems(w, r) + case strings.HasSuffix(path, "/items") && r.Method == "POST": + log.Printf("Handling POST items") + handleAddItem(w, r) + case strings.Contains(path, "/items/") && strings.HasSuffix(path, "/lock") && r.Method == "POST": + log.Printf("Handling lock item") + handleLockItem(w, r) + case strings.Contains(path, "/items/") && r.Method == "PATCH": + log.Printf("Handling PATCH item") + handleUpdateItem(w, r) + case strings.Contains(path, "/items/") && r.Method == "DELETE": + log.Printf("Handling DELETE item") + handleDeleteItem(w, r) + case strings.HasSuffix(path, "/sse") && r.Method == "GET": + log.Printf("Handling SSE") + handleSSE(w, r) + default: + log.Printf("No handler found for %s %s", r.Method, path) + http.NotFound(w, r) + } + }) + + // Serve static files from embedded filesystem (register last) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Check if this is a UUID path (starts with / followed by 36 characters) isUUIDPath := len(r.URL.Path) == 37 && r.URL.Path[0] == '/' && @@ -613,34 +784,6 @@ func main() { } }) - http.HandleFunc("/api/checklists", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - handleCreateChecklist(w, r) - } else { - http.NotFound(w, r) - } - }) - - http.HandleFunc("/api/checklists/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - switch { - case strings.HasSuffix(path, "/items") && r.Method == "GET": - handleGetItems(w, r) - case strings.HasSuffix(path, "/items") && r.Method == "POST": - handleAddItem(w, r) - case strings.Contains(path, "/items/") && strings.HasSuffix(path, "/lock") && r.Method == "POST": - handleLockItem(w, r) - case strings.Contains(path, "/items/") && r.Method == "PATCH": - handleUpdateItem(w, r) - case strings.Contains(path, "/items/") && r.Method == "DELETE": - handleDeleteItem(w, r) - case strings.HasSuffix(path, "/sse") && r.Method == "GET": - handleSSE(w, r) - default: - http.NotFound(w, r) - } - }) - port := strings.TrimSpace(os.Getenv("PORT")) if port == "" { port = "8080"