include-notes.go (12246B)
1 package main 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "os" 11 "os/exec" 12 "sort" 13 "strconv" 14 "strings" 15 ) 16 17 // GetNotes retrieves the notes content from database 18 func GetNotes(db *sql.DB) (string, error) { 19 var content string 20 err := db.QueryRow("SELECT content FROM notes WHERE id = 1").Scan(&content) 21 if err == sql.ErrNoRows { 22 return "", nil 23 } 24 return content, err 25 } 26 27 // SaveNotes saves the notes content to database with sorting and deduplication 28 func SaveNotes(db *sql.DB, content string) error { 29 // Process: deduplicate and sort 30 processed := ProcessNotes(content) 31 32 _, err := db.Exec(` 33 INSERT INTO notes (id, content, updated_at) 34 VALUES (1, ?, datetime('now')) 35 ON CONFLICT(id) DO UPDATE SET 36 content = excluded.content, 37 updated_at = excluded.updated_at 38 `, processed) 39 40 return err 41 } 42 43 // ProcessNotes deduplicates and sorts lines alphabetically 44 func ProcessNotes(content string) string { 45 lines := strings.Split(content, "\n") 46 47 // Deduplicate using map 48 seen := make(map[string]bool) 49 var unique []string 50 51 for _, line := range lines { 52 trimmed := strings.TrimSpace(line) 53 if trimmed == "" { 54 continue // Skip empty lines 55 } 56 if !seen[trimmed] { 57 seen[trimmed] = true 58 unique = append(unique, trimmed) 59 } 60 } 61 62 // Sort alphabetically (case-insensitive) 63 sort.Slice(unique, func(i, j int) bool { 64 return strings.ToLower(unique[i]) < strings.ToLower(unique[j]) 65 }) 66 67 return strings.Join(unique, "\n") 68 } 69 70 // ApplySedRule applies a sed command to content 71 func ApplySedRule(content, sedCmd string) (string, error) { 72 // Create temp file for input 73 tmpIn, err := os.CreateTemp("", "notes-in-*.txt") 74 if err != nil { 75 return "", fmt.Errorf("failed to create temp file: %v", err) 76 } 77 defer os.Remove(tmpIn.Name()) 78 defer tmpIn.Close() 79 80 if _, err := tmpIn.WriteString(content); err != nil { 81 return "", fmt.Errorf("failed to write temp file: %v", err) 82 } 83 tmpIn.Close() 84 85 // Run sed command 86 cmd := exec.Command("sed", sedCmd, tmpIn.Name()) 87 88 // Capture both stdout and stderr 89 var stdout, stderr strings.Builder 90 cmd.Stdout = &stdout 91 cmd.Stderr = &stderr 92 93 err = cmd.Run() 94 if err != nil { 95 // Include stderr in error message for debugging 96 errMsg := stderr.String() 97 if errMsg != "" { 98 return "", fmt.Errorf("sed failed: %s (command: sed %s)", errMsg, sedCmd) 99 } 100 return "", fmt.Errorf("sed failed: %v (command: sed %s)", err, sedCmd) 101 } 102 103 return stdout.String(), nil 104 } 105 106 // ParseNote parses a line into category and value 107 func ParseNote(line string) Note { 108 parts := strings.SplitN(line, ">", 2) 109 110 note := Note{Original: line} 111 112 if len(parts) == 2 { 113 note.Category = strings.TrimSpace(parts[0]) 114 note.Value = strings.TrimSpace(parts[1]) 115 } else { 116 note.Value = strings.TrimSpace(line) 117 } 118 119 return note 120 } 121 122 // FilterNotes filters notes by search term 123 func FilterNotes(content, searchTerm string) string { 124 if searchTerm == "" { 125 return content 126 } 127 128 lines := strings.Split(content, "\n") 129 var filtered []string 130 131 searchLower := strings.ToLower(searchTerm) 132 133 for _, line := range lines { 134 if strings.Contains(strings.ToLower(line), searchLower) { 135 filtered = append(filtered, line) 136 } 137 } 138 139 return strings.Join(filtered, "\n") 140 } 141 142 // FilterByCategory filters notes by category 143 func FilterByCategory(content, category string) string { 144 if category == "" { 145 return content 146 } 147 148 lines := strings.Split(content, "\n") 149 var filtered []string 150 151 for _, line := range lines { 152 note := ParseNote(line) 153 if note.Category == category { 154 filtered = append(filtered, line) 155 } 156 } 157 158 return strings.Join(filtered, "\n") 159 } 160 161 // GetCategories returns a sorted list of unique categories 162 func GetCategories(content string) []string { 163 lines := strings.Split(content, "\n") 164 categoryMap := make(map[string]bool) 165 166 for _, line := range lines { 167 trimmed := strings.TrimSpace(line) 168 if trimmed == "" { 169 continue 170 } 171 172 note := ParseNote(trimmed) 173 if note.Category != "" { 174 categoryMap[note.Category] = true 175 } 176 } 177 178 categories := make([]string, 0, len(categoryMap)) 179 for cat := range categoryMap { 180 categories = append(categories, cat) 181 } 182 183 sort.Strings(categories) 184 return categories 185 } 186 187 // GetNoteStats returns statistics about the notes 188 func GetNoteStats(content string) map[string]int { 189 lines := strings.Split(content, "\n") 190 191 totalLines := 0 192 categorizedLines := 0 193 categories := make(map[string]bool) 194 195 for _, line := range lines { 196 trimmed := strings.TrimSpace(line) 197 if trimmed == "" { 198 continue 199 } 200 201 totalLines++ 202 note := ParseNote(trimmed) 203 204 if note.Category != "" { 205 categorizedLines++ 206 categories[note.Category] = true 207 } 208 } 209 210 return map[string]int{ 211 "total_lines": totalLines, 212 "categorized_lines": categorizedLines, 213 "uncategorized": totalLines - categorizedLines, 214 "unique_categories": len(categories), 215 } 216 } 217 218 // CountLines returns the number of non-empty lines 219 func CountLines(content string) int { 220 lines := strings.Split(content, "\n") 221 count := 0 222 for _, line := range lines { 223 if strings.TrimSpace(line) != "" { 224 count++ 225 } 226 } 227 return count 228 } 229 230 // notesViewHandler displays the notes editor page 231 func notesViewHandler(w http.ResponseWriter, r *http.Request) { 232 content, err := GetNotes(db) 233 if err != nil { 234 log.Printf("Error: notesViewHandler: failed to load notes: %v", err) 235 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 236 return 237 } 238 239 stats := GetNoteStats(content) 240 categories := GetCategories(content) 241 242 notesData := struct { 243 Content string 244 Stats map[string]int 245 Categories []string 246 SedRules []SedRule 247 }{ 248 Content: content, 249 Stats: stats, 250 Categories: categories, 251 SedRules: config.SedRules, 252 } 253 254 pageData := buildPageData("Notes", notesData) 255 256 if err := tmpl.ExecuteTemplate(w, "notes.html", pageData); err != nil { 257 log.Printf("Error: notesViewHandler: template error: %v", err) 258 http.Error(w, "Template error", http.StatusInternalServerError) 259 } 260 } 261 262 // notesSaveHandler saves the notes content 263 func notesSaveHandler(w http.ResponseWriter, r *http.Request) { 264 if r.Method != http.MethodPost { 265 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 266 return 267 } 268 269 content := r.FormValue("content") 270 271 // Process (deduplicate and sort) before saving 272 if err := SaveNotes(db, content); err != nil { 273 log.Printf("Error: notesSaveHandler: failed to save notes: %v", err) 274 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 275 return 276 } 277 278 // Return success response 279 w.Header().Set("Content-Type", "application/json") 280 json.NewEncoder(w).Encode(map[string]interface{}{ 281 "success": true, 282 "message": "Notes saved successfully", 283 }) 284 } 285 286 // notesFilterHandler filters notes by search term or category 287 func notesFilterHandler(w http.ResponseWriter, r *http.Request) { 288 content, err := GetNotes(db) 289 if err != nil { 290 log.Printf("Error: notesFilterHandler: failed to load notes: %v", err) 291 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 292 return 293 } 294 295 searchTerm := r.URL.Query().Get("search") 296 category := r.URL.Query().Get("category") 297 298 // Filter by search term 299 if searchTerm != "" { 300 content = FilterNotes(content, searchTerm) 301 } 302 303 // Filter by category 304 if category != "" { 305 content = FilterByCategory(content, category) 306 } 307 308 w.Header().Set("Content-Type", "text/plain") 309 w.Write([]byte(content)) 310 } 311 312 // notesStatsHandler returns statistics about the notes 313 func notesStatsHandler(w http.ResponseWriter, r *http.Request) { 314 content, err := GetNotes(db) 315 if err != nil { 316 log.Printf("Error: notesStatsHandler: failed to load notes: %v", err) 317 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 318 return 319 } 320 321 stats := GetNoteStats(content) 322 323 w.Header().Set("Content-Type", "application/json") 324 json.NewEncoder(w).Encode(stats) 325 } 326 327 // notesApplySedHandler applies a sed rule to the notes 328 func notesApplySedHandler(w http.ResponseWriter, r *http.Request) { 329 log.Printf("Info: notesApplySedHandler called - Method: %s, Path: %s", r.Method, r.URL.Path) 330 331 if r.Method != http.MethodPost { 332 log.Printf("Warning: notesApplySedHandler: wrong method: %s", r.Method) 333 w.Header().Set("Content-Type", "application/json") 334 w.WriteHeader(http.StatusMethodNotAllowed) 335 json.NewEncoder(w).Encode(map[string]interface{}{ 336 "success": false, 337 "error": "Method not allowed", 338 }) 339 return 340 } 341 342 content := r.FormValue("content") 343 ruleIndexStr := r.FormValue("rule_index") 344 log.Printf("Info: notesApplySedHandler: content length=%d, rule_index=%s", len(content), ruleIndexStr) 345 346 ruleIndex, err := strconv.Atoi(ruleIndexStr) 347 if err != nil || ruleIndex < 0 || ruleIndex >= len(config.SedRules) { 348 log.Printf("Warning: notesApplySedHandler: invalid rule index: %s (error: %v, len(rules)=%d)", ruleIndexStr, err, len(config.SedRules)) 349 w.Header().Set("Content-Type", "application/json") 350 w.WriteHeader(http.StatusBadRequest) 351 json.NewEncoder(w).Encode(map[string]interface{}{ 352 "success": false, 353 "error": "Invalid rule index", 354 }) 355 return 356 } 357 358 rule := config.SedRules[ruleIndex] 359 log.Printf("Info: notesApplySedHandler: applying rule: %s (command: %s)", rule.Name, rule.Command) 360 result, err := ApplySedRule(content, rule.Command) 361 if err != nil { 362 log.Printf("Error: notesApplySedHandler: sed rule error: %v", err) 363 w.Header().Set("Content-Type", "application/json") 364 w.WriteHeader(http.StatusInternalServerError) 365 json.NewEncoder(w).Encode(map[string]interface{}{ 366 "success": false, 367 "error": err.Error(), 368 }) 369 return 370 } 371 372 // Return the processed content 373 w.Header().Set("Content-Type", "application/json") 374 response := map[string]interface{}{ 375 "success": true, 376 "content": result, 377 "stats": GetNoteStats(result), 378 } 379 log.Printf("Info: notesApplySedHandler: sed rule success, returning %d bytes", len(result)) 380 json.NewEncoder(w).Encode(response) 381 } 382 383 // notesPreviewHandler previews an operation without saving 384 func notesPreviewHandler(w http.ResponseWriter, r *http.Request) { 385 if r.Method != http.MethodPost { 386 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 387 return 388 } 389 390 content := r.FormValue("content") 391 392 // Process (deduplicate and sort) 393 processed := ProcessNotes(content) 394 395 w.Header().Set("Content-Type", "application/json") 396 json.NewEncoder(w).Encode(map[string]interface{}{ 397 "success": true, 398 "content": processed, 399 "stats": GetNoteStats(processed), 400 "lineCount": CountLines(processed), 401 }) 402 } 403 404 // notesExportHandler exports notes as plain text file 405 func notesExportHandler(w http.ResponseWriter, r *http.Request) { 406 content, err := GetNotes(db) 407 if err != nil { 408 log.Printf("Error: notesExportHandler: failed to load notes: %v", err) 409 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 410 return 411 } 412 413 w.Header().Set("Content-Type", "text/plain") 414 w.Header().Set("Content-Disposition", "attachment; filename=notes.txt") 415 w.Write([]byte(content)) 416 } 417 418 // notesImportHandler imports notes from uploaded file 419 func notesImportHandler(w http.ResponseWriter, r *http.Request) { 420 if r.Method != http.MethodPost { 421 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 422 return 423 } 424 425 file, _, err := r.FormFile("file") 426 if err != nil { 427 log.Printf("Warning: notesImportHandler: failed to read uploaded file: %v", err) 428 http.Error(w, "Failed to read file", http.StatusBadRequest) 429 return 430 } 431 defer file.Close() 432 433 // Read file content 434 buf := new(strings.Builder) 435 if _, err := io.Copy(buf, file); err != nil { 436 log.Printf("Error: notesImportHandler: failed to read file content: %v", err) 437 http.Error(w, "Failed to read file", http.StatusInternalServerError) 438 return 439 } 440 441 content := buf.String() 442 443 // Option to merge or replace 444 mergeMode := r.FormValue("merge") == "true" 445 446 if mergeMode { 447 // Merge with existing content 448 existingContent, err := GetNotes(db) 449 if err != nil { 450 log.Printf("Warning: notesImportHandler: failed to load existing notes for merge: %v", err) 451 } 452 content = existingContent + "\n" + content 453 } 454 455 // Save (will auto-process) 456 if err := SaveNotes(db, content); err != nil { 457 log.Printf("Error: notesImportHandler: failed to save imported notes: %v", err) 458 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 459 return 460 } 461 462 http.Redirect(w, r, "/notes", http.StatusSeeOther) 463 }