You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
5.5 KiB
225 lines
5.5 KiB
package web
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
"track-gopher/derby"
|
|
)
|
|
|
|
//go:embed templates static
|
|
var content embed.FS
|
|
|
|
// Server represents the web server for the derby clock
|
|
type Server struct {
|
|
router *chi.Mux
|
|
clock *derby.DerbyClock
|
|
templates *template.Template
|
|
raceEvents chan derby.Event
|
|
clients map[chan string]bool
|
|
port int
|
|
}
|
|
|
|
// NewServer creates a new web server
|
|
func NewServer(clock *derby.DerbyClock, port int) (*Server, error) {
|
|
// Parse templates
|
|
templates, err := template.ParseFS(content, "templates/*.html", "templates/**/*.html")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse templates: %w", err)
|
|
}
|
|
|
|
// Create server
|
|
s := &Server{
|
|
router: chi.NewRouter(),
|
|
clock: clock,
|
|
templates: templates,
|
|
raceEvents: make(chan derby.Event, 10),
|
|
clients: make(map[chan string]bool),
|
|
port: port,
|
|
}
|
|
|
|
// Set up routes
|
|
s.routes()
|
|
|
|
// Start event forwarder
|
|
go s.forwardEvents()
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// routes sets up the HTTP routes
|
|
func (s *Server) routes() {
|
|
// Middleware
|
|
s.router.Use(middleware.Logger)
|
|
s.router.Use(middleware.Recoverer)
|
|
s.router.Use(middleware.Timeout(10 * time.Second))
|
|
|
|
// Static files
|
|
staticFS, _ := fs.Sub(content, "static")
|
|
s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
|
|
|
// Pages
|
|
s.router.Get("/", s.handleIndex())
|
|
|
|
// API endpoints
|
|
s.router.Post("/api/reset", s.handleReset())
|
|
s.router.Post("/api/force-end", s.handleForceEnd())
|
|
s.router.Get("/api/status", s.handleStatus())
|
|
s.router.Get("/api/events", s.handleEvents())
|
|
}
|
|
|
|
// Start starts the web server
|
|
func (s *Server) Start() error {
|
|
fmt.Printf("Starting web server on port %d...\n", s.port)
|
|
return http.ListenAndServe(fmt.Sprintf(":%d", s.port), s.router)
|
|
}
|
|
|
|
// forwardEvents forwards derby clock events to the web clients
|
|
func (s *Server) forwardEvents() {
|
|
for event := range s.clock.Events() {
|
|
// Store the event for new clients
|
|
s.raceEvents <- event
|
|
|
|
// Create the SSE message based on the event type
|
|
var message string
|
|
switch event.Type {
|
|
case derby.EventRaceStart:
|
|
message = "event: race-start\ndata: {\"status\": \"running\"}\n\n"
|
|
|
|
case derby.EventLaneFinish:
|
|
result := event.Result
|
|
message = fmt.Sprintf("event: lane-finish\ndata: {\"lane\": %d, \"place\": %d, \"time\": %.4f}\n\n",
|
|
result.Lane, result.FinishPlace, result.Time)
|
|
|
|
case derby.EventRaceComplete:
|
|
message = "event: race-complete\ndata: {\"status\": \"finished\"}\n\n"
|
|
}
|
|
|
|
// Send to all connected clients
|
|
for clientChan := range s.clients {
|
|
select {
|
|
case clientChan <- message:
|
|
// Message sent successfully
|
|
default:
|
|
// Client is not receiving, remove it
|
|
delete(s.clients, clientChan)
|
|
close(clientChan)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleIndex handles the index page
|
|
func (s *Server) handleIndex() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
s.templates.ExecuteTemplate(w, "index.html", nil)
|
|
}
|
|
}
|
|
|
|
// handleReset handles the reset API endpoint
|
|
func (s *Server) handleReset() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.clock.Reset(); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to reset clock: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status": "reset"}`))
|
|
}
|
|
}
|
|
|
|
// handleForceEnd handles the force end API endpoint
|
|
func (s *Server) handleForceEnd() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.clock.ForceEnd(); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to force end race: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status": "forced"}`))
|
|
}
|
|
}
|
|
|
|
// handleStatus handles the status API endpoint
|
|
func (s *Server) handleStatus() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
status := s.clock.Status()
|
|
|
|
var statusStr string
|
|
switch status {
|
|
case derby.StatusIdle:
|
|
statusStr = "idle"
|
|
case derby.StatusRunning:
|
|
statusStr = "running"
|
|
case derby.StatusFinished:
|
|
statusStr = "finished"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(fmt.Sprintf(`{"status": "%s"}`, statusStr)))
|
|
}
|
|
}
|
|
|
|
// handleEvents handles the SSE events endpoint
|
|
func (s *Server) handleEvents() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Set headers for SSE
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
// Create a channel for this client
|
|
clientChan := make(chan string, 10)
|
|
s.clients[clientChan] = true
|
|
|
|
// Clean up when the client disconnects
|
|
defer func() {
|
|
delete(s.clients, clientChan)
|
|
close(clientChan)
|
|
}()
|
|
|
|
// Send initial status
|
|
status := s.clock.Status()
|
|
var statusStr string
|
|
switch status {
|
|
case derby.StatusIdle:
|
|
statusStr = "idle"
|
|
case derby.StatusRunning:
|
|
statusStr = "running"
|
|
case derby.StatusFinished:
|
|
statusStr = "finished"
|
|
}
|
|
|
|
fmt.Fprintf(w, "event: status\ndata: {\"status\": \"%s\"}\n\n", statusStr)
|
|
w.(http.Flusher).Flush()
|
|
|
|
// Keep the connection open
|
|
for {
|
|
select {
|
|
case message, ok := <-clientChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Send the message to the client
|
|
fmt.Fprint(w, message)
|
|
w.(http.Flusher).Flush()
|
|
|
|
case <-r.Context().Done():
|
|
// Client disconnected
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|