tagliatelle

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

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 }