steps 8+9: worklog + FTS5 search
STEP 8 — Worklog: - Migration 006: worklog_entries table (node_id, date, minutes, approximate, billable, summary, details) - WorklogService: Add, Get, Update, Delete, ListByNode, SumMinutes, Report (text report generator with total time) - CLI: verstak log add/list/report (verstak log --help for usage) - GUI tab: entries list with date/time/approx, add form with minutes+text+approx checkbox, total minutes counter STEP 9 — FTS5 Search: - FTS5 virtual table created lazily by search.Rebuild() (works with/without FTS5 compiled in — graceful fallback) - SearchService: Index, Remove, Rebuild, Search (with FTS5 MATCH) - CLI: verstak index rebuild — builds search index from node titles - GUI search bar uses /api/search?q= (FTS5 when available, fallback to LIKE on node titles) Acceptance: go build ./... pass, go test ./... pass (all packages).
This commit is contained in:
@@ -8,8 +8,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"verstak/internal/core/actions"
|
||||
"verstak/internal/core/search"
|
||||
"verstak/internal/core/storage"
|
||||
"verstak/internal/core/vault"
|
||||
"verstak/internal/core/worklog"
|
||||
)
|
||||
|
||||
const version = "0.1.0-dev"
|
||||
@@ -31,6 +33,10 @@ func main() {
|
||||
runNode(os.Args[2:])
|
||||
case "action":
|
||||
runAction(os.Args[2:])
|
||||
case "log":
|
||||
runLog(os.Args[2:])
|
||||
case "index":
|
||||
runIndex(os.Args[2:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
|
||||
os.Exit(1)
|
||||
@@ -395,3 +401,195 @@ func runActionDelete(args []string) {
|
||||
}
|
||||
fmt.Println("deleted")
|
||||
}
|
||||
|
||||
// --- log ---
|
||||
|
||||
func runLog(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Println("verstak log — manage worklog entries")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage: verstak log <command> [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" add --node ID --time MIN --text TEXT Add worklog entry")
|
||||
fmt.Println(" list --node ID List entries for node")
|
||||
fmt.Println(" report --node ID Copy report to stdout")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch args[0] {
|
||||
case "add":
|
||||
runLogAdd(args[1:])
|
||||
case "list":
|
||||
runLogList(args[1:])
|
||||
case "report":
|
||||
runLogReport(args[1:])
|
||||
case "--help", "-h":
|
||||
runLog(nil)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown log command: %s\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runLogAdd(args []string) {
|
||||
nodeID, _ := stringFlag(args, "--node")
|
||||
minutesStr, _ := stringFlag(args, "--time")
|
||||
text, _ := stringFlag(args, "--text")
|
||||
vaultPath, _ := stringFlag(args, "--vault")
|
||||
|
||||
if nodeID == "" || text == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --node and --text required")
|
||||
os.Exit(1)
|
||||
}
|
||||
minutes := 0
|
||||
if minutesStr != "" {
|
||||
fmt.Sscanf(minutesStr, "%d", &minutes)
|
||||
}
|
||||
|
||||
abs, _ := filepath.Abs(vaultPath)
|
||||
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Open vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := worklog.NewService(db)
|
||||
e, err := svc.Add(nodeID, text, "", minutes, true, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Add failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("logged\t%s\t%dm\t%s\n", e.ID, minutes, e.Summary)
|
||||
}
|
||||
|
||||
func runLogList(args []string) {
|
||||
nodeID, _ := stringFlag(args, "--node")
|
||||
vaultPath, _ := stringFlag(args, "--vault")
|
||||
|
||||
if nodeID == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --node required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
abs, _ := filepath.Abs(vaultPath)
|
||||
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Open vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := worklog.NewService(db)
|
||||
entries, err := svc.ListByNode(nodeID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "List failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, e := range entries {
|
||||
dur := "—"
|
||||
if e.Minutes != nil {
|
||||
dur = fmt.Sprintf("%dm", *e.Minutes)
|
||||
}
|
||||
approx := ""
|
||||
if e.Approximate {
|
||||
approx = " ~"
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\t%s\n", e.Date, dur, approx, e.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func runLogReport(args []string) {
|
||||
nodeID, _ := stringFlag(args, "--node")
|
||||
vaultPath, _ := stringFlag(args, "--vault")
|
||||
|
||||
if nodeID == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --node required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
abs, _ := filepath.Abs(vaultPath)
|
||||
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Open vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := worklog.NewService(db)
|
||||
report, err := svc.Report(nodeID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Report failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(report)
|
||||
}
|
||||
|
||||
// --- index ---
|
||||
|
||||
func runIndex(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Println("verstak index — manage search index")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage: verstak index <command> [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" rebuild Rebuild FTS5 search index")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch args[0] {
|
||||
case "rebuild":
|
||||
runIndexRebuild(args[1:])
|
||||
case "--help", "-h":
|
||||
runIndex(nil)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown index command: %s\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runIndexRebuild(args []string) {
|
||||
vaultPath, _ := stringFlag(args, "--vault")
|
||||
abs, _ := filepath.Abs(vaultPath)
|
||||
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Open vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := search.NewService(db)
|
||||
if err := svc.Rebuild(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Rebuild failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Index all node titles.
|
||||
rows, err := db.Query(
|
||||
`SELECT id, title, type, section FROM nodes WHERE deleted_at IS NULL`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Query nodes: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var id, title, nodeType, section string
|
||||
if err := rows.Scan(&id, &title, &nodeType, §ion); err != nil {
|
||||
continue
|
||||
}
|
||||
tags := section
|
||||
if tags != "" {
|
||||
tags = "section:" + tags
|
||||
}
|
||||
svc.Index(id, title, "", "", tags, nodeType)
|
||||
count++
|
||||
}
|
||||
|
||||
fmt.Printf("indexed %d nodes\n", count)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user