2026-06-18 01:40:12 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
|
|
"maisym.com/hsa/internal/classify"
|
|
|
|
|
"maisym.com/hsa/internal/config"
|
|
|
|
|
"maisym.com/hsa/internal/storage"
|
|
|
|
|
"maisym.com/hsa/internal/web"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
// Best-effort local dev convenience: load .env if present.
|
|
|
|
|
if err := config.LoadDotEnv(".env"); err != nil {
|
|
|
|
|
log.Fatalf("loading .env: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("config: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(cfg.DBPath), 0o700); err != nil {
|
|
|
|
|
log.Fatalf("create db dir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
store, err := storage.Open(cfg.DBPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("open store: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer store.Close()
|
|
|
|
|
|
|
|
|
|
// Load the people/categories catalog and seed it into the DB (insert-if-absent).
|
|
|
|
|
catalog, err := config.LoadCatalog(cfg.ConfigPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("config catalog: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := store.Seed(catalog.CategoryNames(), catalog.PersonLabels()); err != nil {
|
|
|
|
|
log.Fatalf("seed catalog: %v", err)
|
|
|
|
|
}
|
2026-06-19 02:04:42 +00:00
|
|
|
// Collapse any stray partial-name people (e.g. "Jude" → "Jude Tremblay") left
|
|
|
|
|
// over from earlier data, reassigning their receipts to the canonical person.
|
|
|
|
|
if merged, err := store.ReconcilePeople(catalog.PersonLabels()); err != nil {
|
|
|
|
|
log.Fatalf("reconcile people: %v", err)
|
|
|
|
|
} else if merged > 0 {
|
|
|
|
|
log.Printf("reconciled %d stray person entr(ies) into canonical labels", merged)
|
|
|
|
|
}
|
2026-06-18 01:40:12 +00:00
|
|
|
|
|
|
|
|
// Receipt auto-classification is best-effort: only enabled when an API key is set.
|
|
|
|
|
var classifier *classify.Classifier
|
|
|
|
|
if cfg.ClassifyAPIKey != "" {
|
|
|
|
|
classifier = classify.New(cfg.ClassifyAPIKey, cfg.ClassifyModel, catalog.Persons, catalog.Categories)
|
|
|
|
|
log.Printf("receipt classification enabled (model %s)", cfg.ClassifyModel)
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("receipt classification disabled (CLAUDE_API_KEY not set)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv, err := web.NewServer(context.Background(), cfg, store, classifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("server init: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("HSA listening on %s (issuer %s)", cfg.ListenAddr, cfg.IssuerURL)
|
|
|
|
|
if err := http.ListenAndServe(cfg.ListenAddr, srv.Routes()); err != nil {
|
|
|
|
|
log.Fatalf("listen: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|