diff --git a/main.go b/main.go index 7c546d3..ff42597 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,20 @@ package main import ( + "context" "embed" "encoding/json" "flag" "fmt" "io/fs" "log" - "math/rand/v2" "net/http" + "os" + "os/signal" "runtime/debug" "strconv" + "sync" + "syscall" "time" "git.chandlerswift.com/chandlerswift/nau-sidewalks/sidewalk" @@ -19,18 +23,32 @@ import ( //go:embed static var embeddedFiles embed.FS -//go:embed data/sidewalks.json -var sidewalk_data []byte - var buildInfoJSON []byte -const DEVEL = false // serve files from dev dir, not embedded, so I don't have to rebuild the app every time - func main() { + devel := flag.Bool("devel", false, "Serve from ./static instead of embedded") port := flag.Int("port", 8000, "Port to listen on") + dataFile := flag.String("datafile", "", "File to read sidewalk data from (required)") flag.Parse() + if *dataFile == "" { + panic("datafile arg is required") + } - if DEVEL { + sidewalkData, err := os.ReadFile(*dataFile) + if err != nil { + panic(err) + } + + // Decode the JSON data + var sidewalks []sidewalk.Sidewalk + var sidewalksMutex sync.RWMutex + if err := json.Unmarshal(sidewalkData, &sidewalks); err != nil { + log.Fatalf("Error decoding JSON: %v", err) + } + sidewalksLen := len(sidewalks) + + // Serve static content + if *devel { http.Handle("GET /", http.FileServer(http.Dir("./static"))) } else { frontend, err := fs.Sub(embeddedFiles, "static") @@ -40,16 +58,6 @@ func main() { http.Handle("GET /", http.FileServer(http.FS(frontend))) } - // Decode the JSON data - var sidewalks []sidewalk.Sidewalk - if err := json.Unmarshal(sidewalk_data, &sidewalks); err != nil { - log.Fatalf("Error decoding JSON: %v", err) - } - - for i := range sidewalks { - sidewalks[i].Condition = sidewalk.Condition(rand.IntN(4)) - } - initializeBuildInfoJSON() http.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -58,30 +66,70 @@ func main() { http.HandleFunc("GET /api/sidewalks", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + sidewalksMutex.RLock() json.NewEncoder(w).Encode(sidewalks) + sidewalksMutex.RUnlock() }) http.HandleFunc("POST /api/sidewalks/{id}", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Could not parse form", http.StatusInternalServerError) + return } sidewalkID, err := strconv.Atoi(r.PathValue("id")) - if err != nil { + if err != nil || sidewalkID < 0 || sidewalkID >= sidewalksLen { http.Error(w, "Invalid id", http.StatusBadRequest) return } condition, err := strconv.Atoi(r.Form.Get("condition")) if err != nil { http.Error(w, "Invalid condition", http.StatusBadRequest) + return } + sidewalksMutex.Lock() sidewalks[sidewalkID].Condition = sidewalk.Condition(condition) sidewalks[sidewalkID].LastUpdated = time.Now() + sidewalksMutex.Unlock() http.Redirect(w, r, "/", http.StatusSeeOther) }) - fmt.Printf("Serving on :%v\n", *port) - panic(http.ListenAndServe(fmt.Sprintf(":%v", *port), nil)) + server := &http.Server{Addr: fmt.Sprintf(":%v", *port)} + + go func() { + fmt.Printf("Serving %v sidewalks on :%v\n", sidewalksLen, *port) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + // Listen for OS termination signals + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + <-stop // Block until signal is received + + fmt.Println("\nShutting down server...") + + // write out JSON + sidewalksMutex.RLock() + f, err := os.Create(*dataFile) + if err != nil { + panic(err) + } + defer f.Close() + err = json.NewEncoder(f).Encode(sidewalks) + if err != nil { + panic(err) + } + sidewalksMutex.RUnlock() + + // graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + } func initializeBuildInfoJSON() { diff --git a/static/index.html b/static/index.html index e09b26e..5041587 100644 --- a/static/index.html +++ b/static/index.html @@ -92,19 +92,6 @@ polyline.bindPopup(tagsContent+"
"+buttons); - // // Listen for popup open to attach event listeners - // polyline.on('popupopen', function(e) { - // document.querySelectorAll(".popup-btn").forEach(button => { - // button.addEventListener("click", async function(event) { - // const value = event.target.getAttribute("data-value"); - - // await fetch("/api/foo", { - // method: "POST", - // body: value - // }); - // }); - // }); - // }); } }) .catch(error => { @@ -114,11 +101,6 @@ const control = new L.Control.SimpleLocate({ position: "topleft", className: "button-locate", - afterClick: (result) => { - console.log("afterClick", result); - if (!result.geolocation) console.log("Geolocation Error"); - if (!result.orientation) console.log("Orientation Error"); - }, }).addTo(map);