include-notes.go (11644B)
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 func analyzeNotes(content string) notesAnalysis { 162 lines := strings.Split(content, "\n") 163 164 totalLines := 0 165 categorizedLines := 0 166 categoryMap := make(map[string]bool) 167 168 for _, line := range lines { 169 trimmed := strings.TrimSpace(line) 170 if trimmed == "" { 171 continue 172 } 173 totalLines++ 174 note := ParseNote(trimmed) 175 if note.Category != "" { 176 categorizedLines++ 177 categoryMap[note.Category] = true 178 } 179 } 180 181 categories := make([]string, 0, len(categoryMap)) 182 for cat := range categoryMap { 183 categories = append(categories, cat) 184 } 185 sort.Strings(categories) 186 187 return notesAnalysis{ 188 Stats: map[string]int{ 189 "total_lines": totalLines, 190 "categorized_lines": categorizedLines, 191 "uncategorized": totalLines - categorizedLines, 192 "unique_categories": len(categories), 193 }, 194 Categories: categories, 195 LineCount: totalLines, 196 } 197 } 198 199 // notesViewHandler displays the notes editor page 200 func notesViewHandler(w http.ResponseWriter, r *http.Request) { 201 content, err := GetNotes(db) 202 if err != nil { 203 log.Printf("Error: notesViewHandler: failed to load notes: %v", err) 204 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 205 return 206 } 207 208 analysis := analyzeNotes(content) 209 210 notesData := struct { 211 Content string 212 Stats map[string]int 213 Categories []string 214 SedRules []SedRule 215 }{ 216 Content: content, 217 Stats: analysis.Stats, 218 Categories: analysis.Categories, 219 SedRules: config.SedRules, 220 } 221 222 pageData := buildPageData("Notes", notesData) 223 224 if err := tmpl.ExecuteTemplate(w, "notes.html", pageData); err != nil { 225 log.Printf("Error: notesViewHandler: template error: %v", err) 226 http.Error(w, "Template error", http.StatusInternalServerError) 227 } 228 } 229 230 // notesSaveHandler saves the notes content 231 func notesSaveHandler(w http.ResponseWriter, r *http.Request) { 232 if r.Method != http.MethodPost { 233 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 234 return 235 } 236 237 content := r.FormValue("content") 238 239 // Process (deduplicate and sort) before saving 240 if err := SaveNotes(db, content); err != nil { 241 log.Printf("Error: notesSaveHandler: failed to save notes: %v", err) 242 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 243 return 244 } 245 246 // Return success response 247 w.Header().Set("Content-Type", "application/json") 248 json.NewEncoder(w).Encode(map[string]interface{}{ 249 "success": true, 250 "message": "Notes saved successfully", 251 }) 252 } 253 254 // notesFilterHandler filters notes by search term or category 255 func notesFilterHandler(w http.ResponseWriter, r *http.Request) { 256 content, err := GetNotes(db) 257 if err != nil { 258 log.Printf("Error: notesFilterHandler: failed to load notes: %v", err) 259 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 260 return 261 } 262 263 searchTerm := r.URL.Query().Get("search") 264 category := r.URL.Query().Get("category") 265 266 // Filter by search term 267 if searchTerm != "" { 268 content = FilterNotes(content, searchTerm) 269 } 270 271 // Filter by category 272 if category != "" { 273 content = FilterByCategory(content, category) 274 } 275 276 w.Header().Set("Content-Type", "text/plain") 277 w.Write([]byte(content)) 278 } 279 280 // notesStatsHandler returns statistics about the notes 281 func notesStatsHandler(w http.ResponseWriter, r *http.Request) { 282 content, err := GetNotes(db) 283 if err != nil { 284 log.Printf("Error: notesStatsHandler: failed to load notes: %v", err) 285 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 286 return 287 } 288 289 stats := analyzeNotes(content).Stats 290 291 w.Header().Set("Content-Type", "application/json") 292 json.NewEncoder(w).Encode(stats) 293 } 294 295 // notesApplySedHandler applies a sed rule to the notes 296 func notesApplySedHandler(w http.ResponseWriter, r *http.Request) { 297 log.Printf("Info: notesApplySedHandler called - Method: %s, Path: %s", r.Method, r.URL.Path) 298 299 if r.Method != http.MethodPost { 300 log.Printf("Warning: notesApplySedHandler: wrong method: %s", r.Method) 301 w.Header().Set("Content-Type", "application/json") 302 w.WriteHeader(http.StatusMethodNotAllowed) 303 json.NewEncoder(w).Encode(map[string]interface{}{ 304 "success": false, 305 "error": "Method not allowed", 306 }) 307 return 308 } 309 310 content := r.FormValue("content") 311 ruleIndexStr := r.FormValue("rule_index") 312 log.Printf("Info: notesApplySedHandler: content length=%d, rule_index=%s", len(content), ruleIndexStr) 313 314 ruleIndex, err := strconv.Atoi(ruleIndexStr) 315 if err != nil || ruleIndex < 0 || ruleIndex >= len(config.SedRules) { 316 log.Printf("Warning: notesApplySedHandler: invalid rule index: %s (error: %v, len(rules)=%d)", ruleIndexStr, err, len(config.SedRules)) 317 w.Header().Set("Content-Type", "application/json") 318 w.WriteHeader(http.StatusBadRequest) 319 json.NewEncoder(w).Encode(map[string]interface{}{ 320 "success": false, 321 "error": "Invalid rule index", 322 }) 323 return 324 } 325 326 rule := config.SedRules[ruleIndex] 327 log.Printf("Info: notesApplySedHandler: applying rule: %s (command: %s)", rule.Name, rule.Command) 328 result, err := ApplySedRule(content, rule.Command) 329 if err != nil { 330 log.Printf("Error: notesApplySedHandler: sed rule error: %v", err) 331 w.Header().Set("Content-Type", "application/json") 332 w.WriteHeader(http.StatusInternalServerError) 333 json.NewEncoder(w).Encode(map[string]interface{}{ 334 "success": false, 335 "error": err.Error(), 336 }) 337 return 338 } 339 340 // Return the processed content 341 w.Header().Set("Content-Type", "application/json") 342 response := map[string]interface{}{ 343 "success": true, 344 "content": result, 345 "stats": analyzeNotes(result).Stats, 346 } 347 log.Printf("Info: notesApplySedHandler: sed rule success, returning %d bytes", len(result)) 348 json.NewEncoder(w).Encode(response) 349 } 350 351 // notesPreviewHandler previews an operation without saving 352 func notesPreviewHandler(w http.ResponseWriter, r *http.Request) { 353 if r.Method != http.MethodPost { 354 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 355 return 356 } 357 358 content := r.FormValue("content") 359 360 // Process (deduplicate and sort) 361 processed := ProcessNotes(content) 362 analysis := analyzeNotes(processed) 363 364 w.Header().Set("Content-Type", "application/json") 365 json.NewEncoder(w).Encode(map[string]interface{}{ 366 "success": true, 367 "content": processed, 368 "stats": analysis.Stats, 369 "lineCount": analysis.LineCount, 370 }) 371 } 372 373 // notesExportHandler exports notes as plain text file 374 func notesExportHandler(w http.ResponseWriter, r *http.Request) { 375 content, err := GetNotes(db) 376 if err != nil { 377 log.Printf("Error: notesExportHandler: failed to load notes: %v", err) 378 http.Error(w, "Failed to load notes", http.StatusInternalServerError) 379 return 380 } 381 382 w.Header().Set("Content-Type", "text/plain") 383 w.Header().Set("Content-Disposition", "attachment; filename=notes.txt") 384 w.Write([]byte(content)) 385 } 386 387 // notesImportHandler imports notes from uploaded file 388 func notesImportHandler(w http.ResponseWriter, r *http.Request) { 389 if r.Method != http.MethodPost { 390 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 391 return 392 } 393 394 file, _, err := r.FormFile("file") 395 if err != nil { 396 log.Printf("Warning: notesImportHandler: failed to read uploaded file: %v", err) 397 http.Error(w, "Failed to read file", http.StatusBadRequest) 398 return 399 } 400 defer file.Close() 401 402 // Read file content 403 buf := new(strings.Builder) 404 if _, err := io.Copy(buf, file); err != nil { 405 log.Printf("Error: notesImportHandler: failed to read file content: %v", err) 406 http.Error(w, "Failed to read file", http.StatusInternalServerError) 407 return 408 } 409 410 content := buf.String() 411 412 // Option to merge or replace 413 mergeMode := r.FormValue("merge") == "true" 414 415 if mergeMode { 416 // Merge with existing content 417 existingContent, err := GetNotes(db) 418 if err != nil { 419 log.Printf("Warning: notesImportHandler: failed to load existing notes for merge: %v", err) 420 } 421 content = existingContent + "\n" + content 422 } 423 424 // Save (will auto-process) 425 if err := SaveNotes(db, content); err != nil { 426 log.Printf("Error: notesImportHandler: failed to save imported notes: %v", err) 427 http.Error(w, "Failed to save notes", http.StatusInternalServerError) 428 return 429 } 430 431 http.Redirect(w, r, "/notes", http.StatusSeeOther) 432 }