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