diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a75dee4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+Spool Putaway
+
+
+
+
+
+
+
+
diff --git a/main.go b/main.go
index 4714f57..85f23ae 100644
--- a/main.go
+++ b/main.go
@@ -52,15 +52,12 @@ const spoolURLPrefix = "https://spooldb.com/s/"
func main() {
verbose := flag.Bool("v", false, "verbose: log each event's start/end with timings to stderr")
dryRun := flag.Bool("n", false, "dry run: read and report, but do not write weight changes back to the site")
+ serveAddr := flag.String("serve", "", "run the mobile putaway web server at this address (e.g. :8080)")
flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "usage: %s [-v] [-n] ...\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, "usage: %s [-v] [-n] [-serve addr] ...\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
- if flag.NArg() < 1 {
- flag.Usage()
- os.Exit(2)
- }
level := slog.LevelInfo
if *verbose {
@@ -77,6 +74,17 @@ func main() {
sp := &spoolSync{}
defer sp.close()
+ if *serveAddr != "" {
+ if err := serve(*serveAddr, auth, sp, *dryRun); err != nil {
+ fail("serve: %v", err)
+ }
+ return
+ }
+
+ if flag.NArg() < 1 {
+ flag.Usage()
+ os.Exit(2)
+ }
results := make([]result, 0, flag.NArg())
for _, path := range flag.Args() {
results = append(results, processImage(path, auth, sp, *dryRun))
@@ -163,14 +171,18 @@ func (s *spoolSync) close() {
// processImage reads one photo, capturing any failure in the result's Error
// field so a single bad image doesn't abort the whole batch.
func processImage(path string, auth claude.Auth, sp *spoolSync, dryRun bool) result {
- defer trace("image " + path)()
- r := result{Image: path}
-
img, err := loadImage(path)
if err != nil {
- r.Error = fmt.Sprintf("load image: %v", err)
- return r
+ return result{Image: path, Error: fmt.Sprintf("load image: %v", err)}
}
+ return processImg(img, path, auth, sp, dryRun)
+}
+
+// processImg runs the QR + weight + spooldb logic on a decoded image. name is
+// just a label for the output (a file path or upload filename).
+func processImg(img image.Image, name string, auth claude.Auth, sp *spoolSync, dryRun bool) result {
+ defer trace("image " + name)()
+ r := result{Image: name}
url, err := decodeQR(img)
if err != nil {
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..f110057
--- /dev/null
+++ b/server.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ _ "embed"
+ "encoding/json"
+ "image"
+ "log/slog"
+ "net/http"
+ "sync"
+ "time"
+
+ "spoolweight/claude"
+)
+
+//go:embed index.html
+var indexHTML []byte
+
+// serve runs the mobile putaway web server. Requests are serialized because the
+// shared spooldb browser session is single-threaded.
+func serve(addr string, auth claude.Auth, sp *spoolSync, dryRun bool) error {
+ var mu sync.Mutex
+
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write(indexHTML)
+ })
+
+ mux.HandleFunc("POST /process", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ if err := r.ParseMultipartForm(64 << 20); err != nil {
+ writeJSON(w, result{Error: "upload too large or malformed"})
+ return
+ }
+ file, hdr, err := r.FormFile("photo")
+ if err != nil {
+ writeJSON(w, result{Error: "no photo in request"})
+ return
+ }
+ defer file.Close()
+
+ img, _, err := image.Decode(file)
+ if err != nil {
+ writeJSON(w, result{Image: hdr.Filename, Error: "could not read image (try choosing from your library)"})
+ return
+ }
+
+ start := time.Now()
+ mu.Lock()
+ res := processImg(img, hdr.Filename, auth, sp, dryRun)
+ mu.Unlock()
+ slog.Info("processed upload", "file", hdr.Filename, "spool", res.SpoolID, "dur", time.Since(start))
+
+ writeJSON(w, res)
+ })
+
+ slog.Info("putaway server listening", "addr", addr, "dry_run", dryRun)
+ return http.ListenAndServe(addr, mux)
+}
+
+func writeJSON(w http.ResponseWriter, v any) {
+ json.NewEncoder(w).Encode(v)
+}