include-notes.go (11415B)
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 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 235 log.Printf("Error loading notes: %v", err) 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 http.Error(w, "Template error", http.StatusInternalServerError) 258 log.Printf("Template error: %v", err) 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 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 274 log.Printf("Error saving notes: %v", err) 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 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 291 return 292 } 293 294 searchTerm := r.URL.Query().Get("search") 295 category := r.URL.Query().Get("category") 296 297 // Filter by search term 298 if searchTerm != "" { 299 content = FilterNotes(content, searchTerm) 300 } 301 302 // Filter by category 303 if category != "" { 304 content = FilterByCategory(content, category) 305 } 306 307 w.Header().Set("Content-Type", "text/plain") 308 w.Write([]byte(content)) 309 } 310 311 // notesStatsHandler returns statistics about the notes 312 func notesStatsHandler(w http.ResponseWriter, r *http.Request) { 313 content, err := GetNotes(db) 314 if err != nil { 315 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 316 return 317 } 318 319 stats := GetNoteStats(content) 320 321 w.Header().Set("Content-Type", "application/json") 322 json.NewEncoder(w).Encode(stats) 323 } 324 325 // notesApplySedHandler applies a sed rule to the notes 326 func notesApplySedHandler(w http.ResponseWriter, r *http.Request) { 327 log.Printf("notesApplySedHandler called - Method: %s, Path: %s", r.Method, r.URL.Path) 328 329 if r.Method != http.MethodPost { 330 log.Printf("Wrong method: %s", r.Method) 331 w.Header().Set("Content-Type", "application/json") 332 w.WriteHeader(http.StatusMethodNotAllowed) 333 json.NewEncoder(w).Encode(map[string]interface{}{ 334 "success": false, 335 "error": "Method not allowed", 336 }) 337 return 338 } 339 340 content := r.FormValue("content") 341 ruleIndexStr := r.FormValue("rule_index") 342 log.Printf("Received: content length=%d, rule_index=%s", len(content), ruleIndexStr) 343 344 ruleIndex, err := strconv.Atoi(ruleIndexStr) 345 if err != nil || ruleIndex < 0 || ruleIndex >= len(config.SedRules) { 346 log.Printf("Invalid rule index: %s (error: %v, len(rules)=%d)", ruleIndexStr, err, len(config.SedRules)) 347 w.Header().Set("Content-Type", "application/json") 348 w.WriteHeader(http.StatusBadRequest) 349 json.NewEncoder(w).Encode(map[string]interface{}{ 350 "success": false, 351 "error": "Invalid rule index", 352 }) 353 return 354 } 355 356 rule := config.SedRules[ruleIndex] 357 log.Printf("Applying rule: %s (command: %s)", rule.Name, rule.Command) 358 result, err := ApplySedRule(content, rule.Command) 359 if err != nil { 360 log.Printf("Sed rule error: %v", err) 361 w.Header().Set("Content-Type", "application/json") 362 w.WriteHeader(http.StatusInternalServerError) 363 json.NewEncoder(w).Encode(map[string]interface{}{ 364 "success": false, 365 "error": err.Error(), 366 }) 367 return 368 } 369 370 // Return the processed content 371 w.Header().Set("Content-Type", "application/json") 372 response := map[string]interface{}{ 373 "success": true, 374 "content": result, 375 "stats": GetNoteStats(result), 376 } 377 log.Printf("Sed rule success, returning %d bytes", len(result)) 378 json.NewEncoder(w).Encode(response) 379 } 380 381 // notesPreviewHandler previews an operation without saving 382 func notesPreviewHandler(w http.ResponseWriter, r *http.Request) { 383 if r.Method != http.MethodPost { 384 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 385 return 386 } 387 388 content := r.FormValue("content") 389 390 // Process (deduplicate and sort) 391 processed := ProcessNotes(content) 392 393 w.Header().Set("Content-Type", "application/json") 394 json.NewEncoder(w).Encode(map[string]interface{}{ 395 "success": true, 396 "content": processed, 397 "stats": GetNoteStats(processed), 398 "lineCount": CountLines(processed), 399 }) 400 } 401 402 // notesExportHandler exports notes as plain text file 403 func notesExportHandler(w http.ResponseWriter, r *http.Request) { 404 content, err := GetNotes(db) 405 if err != nil { 406 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 407 return 408 } 409 410 w.Header().Set("Content-Type", "text/plain") 411 w.Header().Set("Content-Disposition", "attachment; filename=notes.txt") 412 w.Write([]byte(content)) 413 } 414 415 // notesImportHandler imports notes from uploaded file 416 func notesImportHandler(w http.ResponseWriter, r *http.Request) { 417 if r.Method != http.MethodPost { 418 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 419 return 420 } 421 422 file, _, err := r.FormFile("file") 423 if err != nil { 424 http.Error(w, "Failed to read file", http.StatusBadRequest) 425 return 426 } 427 defer file.Close() 428 429 // Read file content 430 buf := new(strings.Builder) 431 if _, err := io.Copy(buf, file); err != nil { 432 http.Error(w, "Failed to read file", http.StatusInternalServerError) 433 return 434 } 435 436 content := buf.String() 437 438 // Option to merge or replace 439 mergeMode := r.FormValue("merge") == "true" 440 441 if mergeMode { 442 // Merge with existing content 443 existingContent, _ := GetNotes(db) 444 content = existingContent + "\n" + content 445 } 446 447 // Save (will auto-process) 448 if err := SaveNotes(db, content); err != nil { 449 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 450 return 451 } 452 453 http.Redirect(w, r, "/notes", http.StatusSeeOther) 454 }