tagliatelle

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

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 }