1//go:build dev23package main45import (6 "fmt"7 "io"8 "io/fs"9 "log/slog"10 "net"11 "net/http"12 "os"13 "path/filepath"14 "time"1516 "github.com/FelineStateMachine/atlas/internal/bundle"17)1819// runHeadless serves the whole application over plain HTTP and never opens a20// window, when ATLAS_HEADLESS asks for it. The parity harness drives the app21// through a browser either way; a desktop window popping over the reader's22// work was the one part of the sweep that was never needed. The listener's23// address lands in inspector.url exactly where the framework would put it,24// so the harness finds a headless run and a windowed one the same way.25func runHeadless(assets fs.FS) bool {26 if os.Getenv("ATLAS_HEADLESS") == "" {27 return false28 }29 dir := os.Getenv("ATLAS_BUNDLES_DIR")30 if dir == "" {31 base, err := os.UserConfigDir()32 if err != nil {33 fmt.Fprintln(os.Stderr, "atlas: headless without a config dir:", err)34 os.Exit(1)35 }36 dir = filepath.Join(base, "dev.felinestatemachine.atlas", "bundles")37 }38 registry := bundle.NewRegistry(dir)39 if err := registry.Rescan(); err != nil {40 slog.Warn("atlas: scanning bundles", "error", err)41 }42 handler := routes(assets, registry)43 // The framework serves the shell's own files under /static; headless44 // answers for them from the same embedded tree.45 mux := http.NewServeMux()46 mux.Handle("/", handler)47 for name, kind := range map[string]string{"app.css": "text/css", "app.js": "text/javascript"} {48 mux.HandleFunc("GET /static/"+name, func(w http.ResponseWriter, r *http.Request) {49 data, err := fs.ReadFile(assets, "assets/"+filepath.Base(r.URL.Path))50 if err != nil {51 http.NotFound(w, r)52 return53 }54 w.Header().Set("Content-Type", kind+"; charset=utf-8")55 _, _ = w.Write(data)56 })57 }58 listener, err := net.Listen("tcp", "127.0.0.1:0")59 if err != nil {60 fmt.Fprintln(os.Stderr, "atlas: headless listener:", err)61 os.Exit(1)62 }63 base, err := os.UserConfigDir()64 if err == nil {65 address := "http://" + listener.Addr().String()66 path := filepath.Join(base, "dev.felinestatemachine.atlas", "inspector.url")67 if err := os.MkdirAll(filepath.Dir(path), 0o755); err == nil {68 _ = os.WriteFile(path, []byte(address+"\n"), 0o644)69 }70 fmt.Println("atlas: headless at", address)71 }72 if err := http.Serve(listener, mux); err != nil {73 fmt.Fprintln(os.Stderr, "atlas: headless server:", err)74 os.Exit(1)75 }76 return true77}7879// parityDir returns the directory that receives behavior-parity tour results.80// It sits beside the app's own data so the harness can find results without81// threading an environment variable through `open`.82func parityDir() (string, error) {83 base, err := os.UserConfigDir()84 if err != nil {85 return "", err86 }87 dir := filepath.Join(base, "dev.felinestatemachine.atlas", "parity")88 return dir, os.MkdirAll(dir, 0o755)89}9091// registerParityRoutes accepts tour results from the frontend's parity92// harness in development builds. The frontend runs the tour inside the real93// WKWebView and posts its snapshot log here; comparing two such logs across a94// refactor is the behavior-parity gate.95func registerParityRoutes(mux *http.ServeMux) {96 mux.HandleFunc("POST /parity/result", func(w http.ResponseWriter, r *http.Request) {97 dir, err := parityDir()98 if err != nil {99 http.Error(w, err.Error(), http.StatusInternalServerError)100 return101 }102 body, err := io.ReadAll(io.LimitReader(r.Body, 64<<20))103 if err != nil {104 http.Error(w, err.Error(), http.StatusBadRequest)105 return106 }107 path := filepath.Join(dir, fmt.Sprintf("tour-%d.json", time.Now().UnixMilli()))108 if err := os.WriteFile(path, body, 0o644); err != nil {109 http.Error(w, err.Error(), http.StatusInternalServerError)110 return111 }112 w.Header().Set("Content-Type", "text/plain; charset=utf-8")113 fmt.Fprintln(w, path)114 })115}