a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh

feat: use chi

dunkirk.sh c00a0be2 36145a07

verified
+104 -102
+2 -4
go.mod
··· 3 3 go 1.25.4 4 4 5 5 require ( 6 + github.com/alexandrevicenzi/go-sse v1.6.0 6 7 github.com/charmbracelet/bubbletea v1.3.10 7 8 github.com/charmbracelet/lipgloss v1.1.0 8 9 github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 9 10 github.com/charmbracelet/wish v1.4.7 11 + github.com/go-chi/chi/v5 v5.2.3 10 12 github.com/mattn/go-sqlite3 v1.14.32 11 13 github.com/pkg/sftp v1.13.10 12 - github.com/r3labs/sse/v2 v2.10.0 13 14 ) 14 15 15 16 require ( ··· 38 39 github.com/muesli/cancelreader v0.2.2 // indirect 39 40 github.com/muesli/termenv v0.16.0 // indirect 40 41 github.com/rivo/uniseg v0.4.7 // indirect 41 - github.com/tmaxmax/go-sse v0.11.0 // indirect 42 42 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 43 43 golang.org/x/crypto v0.41.0 // indirect 44 44 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 45 - golang.org/x/net v0.42.0 // indirect 46 45 golang.org/x/sys v0.36.0 // indirect 47 46 golang.org/x/text v0.28.0 // indirect 48 - gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect 49 47 )
+4 -17
go.sum
··· 1 + github.com/alexandrevicenzi/go-sse v1.6.0 h1:3KvOzpuY7UrbqZgAtOEmub9/V5ykr7Myudw+PA+H1Ik= 2 + github.com/alexandrevicenzi/go-sse v1.6.0/go.mod h1:jdrNAhMgVqP7OfcUuM8eJx0sOY17wc+girs5utpFZUU= 1 3 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 2 4 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 3 5 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= ··· 34 36 github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= 35 37 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 36 38 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 37 - github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 39 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 39 40 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 41 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 41 42 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 43 + github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= 44 + github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 42 45 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 43 46 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 44 47 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= ··· 67 70 github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= 68 71 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 72 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 - github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= 71 - github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= 72 73 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 73 74 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 74 75 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 75 - github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 - github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 76 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 78 77 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 - github.com/tmaxmax/go-sse v0.11.0 h1:nogmJM6rJUoOLoAwEKeQe5XlVpt9l7N82SS1jI7lWFg= 80 - github.com/tmaxmax/go-sse v0.11.0/go.mod h1:u/2kZQR1tyngo1lKaNCj1mJmhXGZWS1Zs5yiSOD+Eg8= 81 78 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 82 79 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 83 - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 84 80 golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 85 81 golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 86 82 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 87 83 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 88 - golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 - golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 90 - golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 91 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 84 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 85 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 86 golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 95 87 golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 96 88 golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 97 89 golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 98 - golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 90 golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 100 91 golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 101 - gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= 102 - gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 103 - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 92 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 106 93 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+80 -52
main.go
··· 17 17 "github.com/charmbracelet/wish/bubbletea" 18 18 "github.com/charmbracelet/wish/logging" 19 19 "github.com/charmbracelet/wish/scp" 20 + "github.com/alexandrevicenzi/go-sse" 21 + "github.com/go-chi/chi/v5" 22 + "github.com/go-chi/chi/v5/middleware" 20 23 ) 21 24 22 25 const ( 23 26 host = "0.0.0.0" 24 27 sshPort = "2222" 25 - webPort = "8080" 26 - ssePort = "8081" 28 + webPort = "8081" 27 29 uploadDir = "./submissions" 28 30 resultsDB = "./results.db" 29 31 ) ··· 34 36 log.Fatal(err) 35 37 } 36 38 37 - // Initialize SSE server 38 - initSSE() 39 - 40 - // Start SSE server on separate port 41 - go startSSEServer() 39 + // Initialize SSE server EXACTLY like test 40 + s := sse.NewServer(nil) 41 + defer s.Shutdown() 42 + sseServer = s 42 43 43 44 // Start background worker 44 45 workerCtx, workerCancel := context.WithCancel(context.Background()) 45 46 defer workerCancel() 46 47 go startWorker(workerCtx) 47 48 48 - // Start web server 49 - go startWebServer() 50 - 51 - // Start SSH server with TUI, SCP, and SFTP 49 + // Start SSH server in background 52 50 toClient, fromClient := newSCPHandlers() 53 - s, err := wish.NewServer( 51 + sshServer, err := wish.NewServer( 54 52 wish.WithAddress(host + ":" + sshPort), 55 53 wish.WithHostKeyPath(".ssh/battleship_arena"), 56 54 wish.WithSubsystem("sftp", sftpHandler), ··· 71 69 log.Printf("Web leaderboard at http://%s:%s", host, webPort) 72 70 73 71 go func() { 74 - if err := s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 72 + if err := sshServer.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 75 73 log.Fatal(err) 76 74 } 77 75 }() 78 76 79 - <-done 80 - log.Println("Shutting down servers...") 81 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 82 - defer cancel() 83 - if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 84 - log.Fatal(err) 85 - } 77 + // Graceful shutdown handler 78 + go func() { 79 + <-done 80 + log.Println("Shutting down servers...") 81 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 82 + defer cancel() 83 + if err := sshServer.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 84 + log.Fatal(err) 85 + } 86 + os.Exit(0) 87 + }() 88 + 89 + // Start web server EXACTLY like test 90 + r := chi.NewRouter() 91 + 92 + // Middleware 93 + r.Use(middleware.Logger) 94 + r.Use(middleware.Recoverer) 95 + 96 + // SSE endpoint - mounted directly to router 97 + r.Mount("/events/", s) 98 + 99 + // API routes 100 + r.Get("/api/leaderboard", handleAPILeaderboard) 101 + r.Get("/api/rating-history/{player}", handleRatingHistory) 102 + 103 + // Player pages 104 + r.Get("/player/{player}", handlePlayerPage) 105 + 106 + // Home page 107 + r.Get("/", handleLeaderboard) 108 + 109 + // Static files 110 + fs := http.FileServer(http.Dir("./static")) 111 + r.Handle("/static/*", http.StripPrefix("/static/", fs)) 112 + 113 + log.Println("Server running at http://localhost:" + webPort) 114 + http.ListenAndServe(":"+webPort, r) 86 115 } 87 116 88 117 func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { ··· 117 146 118 147 func startWebServer() { 119 148 mux := http.NewServeMux() 149 + 150 + // SSE endpoint with explicit logging 151 + mux.HandleFunc("/events/", func(w http.ResponseWriter, r *http.Request) { 152 + log.Printf("SSE request received: %s", r.URL.Path) 153 + 154 + // Try to manually write headers and flush BEFORE SSE library 155 + w.Header().Set("Content-Type", "text/event-stream") 156 + w.Header().Set("Cache-Control", "no-cache") 157 + w.Header().Set("Connection", "keep-alive") 158 + w.Header().Set("X-Accel-Buffering", "no") 159 + 160 + log.Printf("Headers set, writing header...") 161 + w.WriteHeader(http.StatusOK) 162 + 163 + if flusher, ok := w.(http.Flusher); ok { 164 + log.Printf("Flushing headers manually...") 165 + flusher.Flush() 166 + log.Printf("Headers flushed!") 167 + } else { 168 + log.Printf("NO FLUSHER!") 169 + } 170 + 171 + log.Printf("Calling SSE ServeHTTP...") 172 + sseServer.ServeHTTP(w, r) 173 + log.Printf("SSE ServeHTTP returned") 174 + }) 175 + 176 + // Web routes (no Chi) 120 177 mux.HandleFunc("/", handleLeaderboard) 121 178 mux.HandleFunc("/api/leaderboard", handleAPILeaderboard) 122 179 mux.HandleFunc("/api/rating-history/", handleRatingHistory) 123 180 mux.HandleFunc("/player/", handlePlayerPage) 124 181 125 - // Serve static files 182 + // Static files 126 183 fs := http.FileServer(http.Dir("./static")) 127 184 mux.Handle("/static/", http.StripPrefix("/static/", fs)) 128 185 129 - server := &http.Server{ 130 - Addr: ":" + webPort, 131 - Handler: mux, 132 - ReadTimeout: 0, // No timeout for SSE 133 - WriteTimeout: 0, // No timeout for SSE 134 - MaxHeaderBytes: 1 << 20, 135 - } 136 - 137 186 log.Printf("Web server starting on :%s", webPort) 138 - if err := server.ListenAndServe(); err != nil { 139 - log.Fatal(err) 140 - } 187 + http.ListenAndServe(":"+webPort, mux) 141 188 } 142 189 143 - func startSSEServer() { 144 - // Wrap SSE server with CORS middleware 145 - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 146 - w.Header().Set("Access-Control-Allow-Origin", "*") 147 - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") 148 - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 149 - 150 - if r.Method == "OPTIONS" { 151 - w.WriteHeader(http.StatusOK) 152 - return 153 - } 154 - 155 - sseServer.ServeHTTP(w, r) 156 - }) 157 - 158 - log.Printf("SSE server starting on :%s", ssePort) 159 - if err := http.ListenAndServe(":"+ssePort, handler); err != nil { 160 - log.Fatal(err) 161 - } 162 - } 190 + 163 191 164 192 var titleStyle = lipgloss.NewStyle(). 165 193 Bold(true).
+8 -24
sse.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "log" 7 - "net/http" 8 7 "time" 9 8 10 - "github.com/tmaxmax/go-sse" 9 + "github.com/alexandrevicenzi/go-sse" 11 10 ) 12 11 13 12 var sseServer *sse.Server ··· 24 23 } 25 24 26 25 func initSSE() { 27 - sseServer = &sse.Server{} 26 + sseServer = sse.NewServer(&sse.Options{ 27 + Logger: log.New(log.Writer(), "go-sse: ", log.Ldate|log.Ltime), 28 + }) 28 29 } 29 30 30 - func handleSSE(w http.ResponseWriter, r *http.Request) { 31 - sseServer.ServeHTTP(w, r) 32 - } 31 + 33 32 34 33 // NotifyLeaderboardUpdate sends updated leaderboard to all connected clients 35 34 func NotifyLeaderboardUpdate() { ··· 45 44 return 46 45 } 47 46 48 - msg := &sse.Message{} 49 - msg.AppendData(string(data)) 50 - 51 - if err := sseServer.Publish(msg); err != nil { 52 - log.Printf("SSE: publish failed: %v", err) 53 - } 47 + sseServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 54 48 } 55 49 56 50 func broadcastProgress(player string, currentMatch, totalMatches int, startTime time.Time, queuedPlayers []string) { ··· 90 84 91 85 log.Printf("Broadcasting progress: %s [%d/%d] %.1f%% (queue: %d)", player, currentMatch, totalMatches, percentComplete, len(filteredQueue)) 92 86 93 - msg := &sse.Message{} 94 - msg.AppendData(string(data)) 95 - // Don't set Type - just send as regular message 96 - 97 - if err := sseServer.Publish(msg); err != nil { 98 - log.Printf("SSE: progress publish failed: %v", err) 99 - } 87 + sseServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 100 88 } 101 89 102 90 func formatDuration(d time.Duration) string { ··· 127 115 128 116 log.Printf("Broadcasting progress complete") 129 117 130 - msg := &sse.Message{} 131 - msg.AppendData(string(data)) 132 - // Don't set Type - just send as regular message 133 - 134 - sseServer.Publish(msg) 118 + sseServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 135 119 }
+10 -5
web.go
··· 5 5 "fmt" 6 6 "html/template" 7 7 "net/http" 8 + 9 + "github.com/go-chi/chi/v5" 8 10 ) 9 11 10 12 const leaderboardHTML = ` ··· 379 381 380 382 function connectSSE() { 381 383 console.log('Connecting to SSE...'); 382 - eventSource = new EventSource('http://localhost:8081'); 384 + eventSource = new EventSource('/events/updates'); 383 385 384 386 eventSource.onopen = () => { 385 387 console.log('SSE connection established'); ··· 387 389 }; 388 390 389 391 eventSource.onmessage = (event) => { 392 + console.log('SSE raw event:', event); 393 + console.log('SSE event.data:', event.data); 390 394 try { 391 395 const data = JSON.parse(event.data); 392 396 console.log('SSE message received:', data); ··· 402 406 // Leaderboard update 403 407 console.log('Updating leaderboard with', data.length, 'entries'); 404 408 updateLeaderboard(data); 409 + } else { 410 + console.log('Unknown message type:', data); 405 411 } 406 412 } catch (error) { 407 - console.error('Failed to parse SSE data:', error); 413 + console.error('Failed to parse SSE data:', error, 'Raw data:', event.data); 408 414 } 409 415 }; 410 416 ··· 693 699 } 694 700 695 701 func handleRatingHistory(w http.ResponseWriter, r *http.Request) { 696 - // Extract username from URL path /api/rating-history/{username} 697 - username := r.URL.Path[len("/api/rating-history/"):] 702 + username := chi.URLParam(r, "player") 698 703 if username == "" { 699 704 http.Error(w, "Username required", http.StatusBadRequest) 700 705 return ··· 724 729 } 725 730 726 731 func handlePlayerPage(w http.ResponseWriter, r *http.Request) { 727 - username := r.URL.Path[len("/player/"):] 732 + username := chi.URLParam(r, "player") 728 733 if username == "" { 729 734 http.Redirect(w, r, "/", http.StatusSeeOther) 730 735 return