Go app for capturing and archiving HSA-eligible receipts: OIDC/PKCE auth against Authelia, SQLite storage with dual-write (filesystem + DB blob), mobile-first upload, and DB export. Adds AI receipt classification: a config.json catalog of people and categories (seeded into the DB on startup), a prompt builder that derives name-order/initial variants from the data (with same-surname ambiguity handling), and an Anthropic tool-use client behind POST /classify. Tests run against a mock endpoint; a live integration test is env-gated to the cheapest model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
2.2 KiB
Go
85 lines
2.2 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"maisym.com/hsa/internal/storage"
|
|
)
|
|
|
|
type manageView struct {
|
|
Categories []storage.Lookup
|
|
People []storage.Lookup
|
|
Error string
|
|
}
|
|
|
|
func (s *Server) handleManage(w http.ResponseWriter, r *http.Request) {
|
|
cats, people, err := s.lookups()
|
|
if err != nil {
|
|
s.serverError(w, "load lookups", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = managePage.ExecuteTemplate(w, "base", manageView{
|
|
Categories: cats,
|
|
People: people,
|
|
Error: r.URL.Query().Get("error"),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAddCategory(w http.ResponseWriter, r *http.Request) {
|
|
s.addLookup(w, r, s.store.AddCategory)
|
|
}
|
|
|
|
func (s *Server) handleAddPerson(w http.ResponseWriter, r *http.Request) {
|
|
s.addLookup(w, r, s.store.AddPerson)
|
|
}
|
|
|
|
func (s *Server) handleRenameCategory(w http.ResponseWriter, r *http.Request) {
|
|
s.renameLookup(w, r, s.store.RenameCategory)
|
|
}
|
|
|
|
func (s *Server) handleRenamePerson(w http.ResponseWriter, r *http.Request) {
|
|
s.renameLookup(w, r, s.store.RenamePerson)
|
|
}
|
|
|
|
func (s *Server) addLookup(w http.ResponseWriter, r *http.Request, add func(string) (int64, error)) {
|
|
label := strings.TrimSpace(r.FormValue("label"))
|
|
if label == "" {
|
|
redirectManage(w, r, "Label cannot be empty.")
|
|
return
|
|
}
|
|
if _, err := add(label); err != nil {
|
|
redirectManage(w, r, "Could not add — maybe that label already exists.")
|
|
return
|
|
}
|
|
redirectManage(w, r, "")
|
|
}
|
|
|
|
func (s *Server) renameLookup(w http.ResponseWriter, r *http.Request, rename func(int64, string) error) {
|
|
id, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("id")), 10, 64)
|
|
if err != nil {
|
|
redirectManage(w, r, "Invalid id.")
|
|
return
|
|
}
|
|
label := strings.TrimSpace(r.FormValue("label"))
|
|
if label == "" {
|
|
redirectManage(w, r, "Label cannot be empty.")
|
|
return
|
|
}
|
|
if err := rename(id, label); err != nil {
|
|
redirectManage(w, r, "Could not rename — maybe that label already exists.")
|
|
return
|
|
}
|
|
redirectManage(w, r, "")
|
|
}
|
|
|
|
func redirectManage(w http.ResponseWriter, r *http.Request, errMsg string) {
|
|
target := "/manage"
|
|
if errMsg != "" {
|
|
target += "?error=" + url.QueryEscape(errMsg)
|
|
}
|
|
http.Redirect(w, r, target, http.StatusSeeOther)
|
|
}
|