tagliatelle

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

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 }