tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 }