|
|
|
|
@ -7,8 +7,10 @@ import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"log"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
@ -33,10 +35,16 @@ type Server struct {
|
|
|
|
|
port int
|
|
|
|
|
server *http.Server
|
|
|
|
|
shutdown chan struct{}
|
|
|
|
|
logger *slog.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewServer creates a new web server
|
|
|
|
|
func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*Server, error) {
|
|
|
|
|
// Create logger
|
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
|
|
|
Level: slog.LevelDebug,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Create server
|
|
|
|
|
s := &Server{
|
|
|
|
|
router: chi.NewRouter(),
|
|
|
|
|
@ -46,6 +54,7 @@ func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*S
|
|
|
|
|
clientsMux: sync.Mutex{},
|
|
|
|
|
port: port,
|
|
|
|
|
shutdown: make(chan struct{}),
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up routes
|
|
|
|
|
@ -115,8 +124,9 @@ func (s *Server) Start() error {
|
|
|
|
|
|
|
|
|
|
// Start server in a goroutine
|
|
|
|
|
go func() {
|
|
|
|
|
s.logger.Info("Web server starting", "port", s.port)
|
|
|
|
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
|
|
fmt.Printf("HTTP server error: %v\n", err)
|
|
|
|
|
s.logger.Error("HTTP server error", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
@ -140,6 +150,8 @@ func (s *Server) Stop() error {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
s.logger.Info("Shutting down web server")
|
|
|
|
|
|
|
|
|
|
// Shutdown the HTTP server
|
|
|
|
|
if s.server != nil {
|
|
|
|
|
return s.server.Shutdown(ctx)
|
|
|
|
|
@ -170,6 +182,7 @@ func (s *Server) broadcastEvent(event derby.Event) {
|
|
|
|
|
|
|
|
|
|
switch event.Type {
|
|
|
|
|
case derby.EventRaceStart:
|
|
|
|
|
s.logger.Info("Broadcasting race start event")
|
|
|
|
|
statusMsg := struct {
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
}{
|
|
|
|
|
@ -179,6 +192,11 @@ func (s *Server) broadcastEvent(event derby.Event) {
|
|
|
|
|
message = fmt.Sprintf("event: status\ndata: %s", statusJSON)
|
|
|
|
|
|
|
|
|
|
case derby.EventLaneFinish:
|
|
|
|
|
s.logger.Info("Broadcasting lane finish event",
|
|
|
|
|
"lane", event.Result.Lane,
|
|
|
|
|
"time", event.Result.Time,
|
|
|
|
|
"place", event.Result.FinishPlace)
|
|
|
|
|
|
|
|
|
|
// Create a message for lane finish
|
|
|
|
|
laneData := struct {
|
|
|
|
|
Lane int `json:"lane"`
|
|
|
|
|
@ -193,6 +211,7 @@ func (s *Server) broadcastEvent(event derby.Event) {
|
|
|
|
|
message = fmt.Sprintf("event: lane-finish\ndata: %s", laneJSON)
|
|
|
|
|
|
|
|
|
|
case derby.EventRaceComplete:
|
|
|
|
|
s.logger.Info("Broadcasting race complete event")
|
|
|
|
|
statusMsg := struct {
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
}{
|
|
|
|
|
@ -208,13 +227,20 @@ func (s *Server) broadcastEvent(event derby.Event) {
|
|
|
|
|
|
|
|
|
|
// Send to all clients
|
|
|
|
|
s.clientsMux.Lock()
|
|
|
|
|
clientCount := len(s.clients)
|
|
|
|
|
sentCount := 0
|
|
|
|
|
for clientChan := range s.clients {
|
|
|
|
|
select {
|
|
|
|
|
case clientChan <- message:
|
|
|
|
|
sentCount++
|
|
|
|
|
default:
|
|
|
|
|
// Client channel is full, could log this
|
|
|
|
|
s.logger.Warn("Client channel is full, event not sent")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.logger.Info("Event broadcast complete",
|
|
|
|
|
"sentCount", sentCount,
|
|
|
|
|
"totalClients", clientCount,
|
|
|
|
|
"eventType", event.Type)
|
|
|
|
|
s.clientsMux.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -280,51 +306,55 @@ func (s *Server) handleEvents() http.HandlerFunc {
|
|
|
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
|
|
|
|
|
|
// Debug message to confirm connection
|
|
|
|
|
fmt.Fprintf(w, "event: debug\ndata: {\"message\":\"SSE connection established\"}\n\n")
|
|
|
|
|
|
|
|
|
|
// Flush headers to ensure they're sent to the client
|
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
|
|
|
flusher.Flush()
|
|
|
|
|
} else {
|
|
|
|
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a channel for this client
|
|
|
|
|
clientChan := make(chan string, 10)
|
|
|
|
|
|
|
|
|
|
// Add client to map with mutex protection
|
|
|
|
|
s.clientsMux.Lock()
|
|
|
|
|
s.clients[clientChan] = true
|
|
|
|
|
clientCount := len(s.clients)
|
|
|
|
|
s.clientsMux.Unlock()
|
|
|
|
|
|
|
|
|
|
s.logger.Info("New client connected",
|
|
|
|
|
"clientIP", r.RemoteAddr,
|
|
|
|
|
"totalClients", clientCount)
|
|
|
|
|
|
|
|
|
|
// Remove client when connection is closed
|
|
|
|
|
defer func() {
|
|
|
|
|
s.clientsMux.Lock()
|
|
|
|
|
delete(s.clients, clientChan)
|
|
|
|
|
remainingClients := len(s.clients)
|
|
|
|
|
s.clientsMux.Unlock()
|
|
|
|
|
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()
|
|
|
|
|
s.logger.Info("Client disconnected",
|
|
|
|
|
"clientIP", r.RemoteAddr,
|
|
|
|
|
"remainingClients", remainingClients)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Keep the connection open
|
|
|
|
|
// Keep connection open and send events as they arrive
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case message, ok := <-clientChan:
|
|
|
|
|
case msg, ok := <-clientChan:
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send the message to the client
|
|
|
|
|
fmt.Fprint(w, message)
|
|
|
|
|
w.(http.Flusher).Flush()
|
|
|
|
|
|
|
|
|
|
fmt.Fprintf(w, "%s\n\n", msg)
|
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
|
|
|
flusher.Flush()
|
|
|
|
|
}
|
|
|
|
|
case <-r.Context().Done():
|
|
|
|
|
// Client disconnected
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|