tangled
alpha
login
or
join now
dunkirk.sh
/
battleship-arena
1
fork
atom
a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1
fork
atom
overview
issues
pulls
pipelines
feat: use chi
dunkirk.sh
3 months ago
c00a0be2
36145a07
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+104
-102
5 changed files
expand all
collapse all
unified
split
go.mod
go.sum
main.go
sse.go
web.go
+2
-4
go.mod
···
3
3
go 1.25.4
4
4
5
5
require (
6
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
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
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
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
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
48
-
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
49
47
)
+4
-17
go.sum
···
1
1
+
github.com/alexandrevicenzi/go-sse v1.6.0 h1:3KvOzpuY7UrbqZgAtOEmub9/V5ykr7Myudw+PA+H1Ik=
2
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
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
43
+
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
44
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
70
-
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
71
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
75
-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
76
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
79
-
github.com/tmaxmax/go-sse v0.11.0 h1:nogmJM6rJUoOLoAwEKeQe5XlVpt9l7N82SS1jI7lWFg=
80
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
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
88
-
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
89
89
-
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
90
90
-
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
91
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
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
101
-
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
102
102
-
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
103
103
-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
104
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
20
+
"github.com/alexandrevicenzi/go-sse"
21
21
+
"github.com/go-chi/chi/v5"
22
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
25
-
webPort = "8080"
26
26
-
ssePort = "8081"
28
28
+
webPort = "8081"
27
29
uploadDir = "./submissions"
28
30
resultsDB = "./results.db"
29
31
)
···
34
36
log.Fatal(err)
35
37
}
36
38
37
37
-
// Initialize SSE server
38
38
-
initSSE()
39
39
-
40
40
-
// Start SSE server on separate port
41
41
-
go startSSEServer()
39
39
+
// Initialize SSE server EXACTLY like test
40
40
+
s := sse.NewServer(nil)
41
41
+
defer s.Shutdown()
42
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
48
-
// Start web server
49
49
-
go startWebServer()
50
50
-
51
51
-
// Start SSH server with TUI, SCP, and SFTP
49
49
+
// Start SSH server in background
52
50
toClient, fromClient := newSCPHandlers()
53
53
-
s, err := wish.NewServer(
51
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
74
-
if err := s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
72
72
+
if err := sshServer.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
75
73
log.Fatal(err)
76
74
}
77
75
}()
78
76
79
79
-
<-done
80
80
-
log.Println("Shutting down servers...")
81
81
-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
82
82
-
defer cancel()
83
83
-
if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
84
84
-
log.Fatal(err)
85
85
-
}
77
77
+
// Graceful shutdown handler
78
78
+
go func() {
79
79
+
<-done
80
80
+
log.Println("Shutting down servers...")
81
81
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
82
82
+
defer cancel()
83
83
+
if err := sshServer.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
84
84
+
log.Fatal(err)
85
85
+
}
86
86
+
os.Exit(0)
87
87
+
}()
88
88
+
89
89
+
// Start web server EXACTLY like test
90
90
+
r := chi.NewRouter()
91
91
+
92
92
+
// Middleware
93
93
+
r.Use(middleware.Logger)
94
94
+
r.Use(middleware.Recoverer)
95
95
+
96
96
+
// SSE endpoint - mounted directly to router
97
97
+
r.Mount("/events/", s)
98
98
+
99
99
+
// API routes
100
100
+
r.Get("/api/leaderboard", handleAPILeaderboard)
101
101
+
r.Get("/api/rating-history/{player}", handleRatingHistory)
102
102
+
103
103
+
// Player pages
104
104
+
r.Get("/player/{player}", handlePlayerPage)
105
105
+
106
106
+
// Home page
107
107
+
r.Get("/", handleLeaderboard)
108
108
+
109
109
+
// Static files
110
110
+
fs := http.FileServer(http.Dir("./static"))
111
111
+
r.Handle("/static/*", http.StripPrefix("/static/", fs))
112
112
+
113
113
+
log.Println("Server running at http://localhost:" + webPort)
114
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
149
+
150
150
+
// SSE endpoint with explicit logging
151
151
+
mux.HandleFunc("/events/", func(w http.ResponseWriter, r *http.Request) {
152
152
+
log.Printf("SSE request received: %s", r.URL.Path)
153
153
+
154
154
+
// Try to manually write headers and flush BEFORE SSE library
155
155
+
w.Header().Set("Content-Type", "text/event-stream")
156
156
+
w.Header().Set("Cache-Control", "no-cache")
157
157
+
w.Header().Set("Connection", "keep-alive")
158
158
+
w.Header().Set("X-Accel-Buffering", "no")
159
159
+
160
160
+
log.Printf("Headers set, writing header...")
161
161
+
w.WriteHeader(http.StatusOK)
162
162
+
163
163
+
if flusher, ok := w.(http.Flusher); ok {
164
164
+
log.Printf("Flushing headers manually...")
165
165
+
flusher.Flush()
166
166
+
log.Printf("Headers flushed!")
167
167
+
} else {
168
168
+
log.Printf("NO FLUSHER!")
169
169
+
}
170
170
+
171
171
+
log.Printf("Calling SSE ServeHTTP...")
172
172
+
sseServer.ServeHTTP(w, r)
173
173
+
log.Printf("SSE ServeHTTP returned")
174
174
+
})
175
175
+
176
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
125
-
// Serve static files
182
182
+
// Static files
126
183
fs := http.FileServer(http.Dir("./static"))
127
184
mux.Handle("/static/", http.StripPrefix("/static/", fs))
128
185
129
129
-
server := &http.Server{
130
130
-
Addr: ":" + webPort,
131
131
-
Handler: mux,
132
132
-
ReadTimeout: 0, // No timeout for SSE
133
133
-
WriteTimeout: 0, // No timeout for SSE
134
134
-
MaxHeaderBytes: 1 << 20,
135
135
-
}
136
136
-
137
186
log.Printf("Web server starting on :%s", webPort)
138
138
-
if err := server.ListenAndServe(); err != nil {
139
139
-
log.Fatal(err)
140
140
-
}
187
187
+
http.ListenAndServe(":"+webPort, mux)
141
188
}
142
189
143
143
-
func startSSEServer() {
144
144
-
// Wrap SSE server with CORS middleware
145
145
-
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146
146
-
w.Header().Set("Access-Control-Allow-Origin", "*")
147
147
-
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
148
148
-
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
149
149
-
150
150
-
if r.Method == "OPTIONS" {
151
151
-
w.WriteHeader(http.StatusOK)
152
152
-
return
153
153
-
}
154
154
-
155
155
-
sseServer.ServeHTTP(w, r)
156
156
-
})
157
157
-
158
158
-
log.Printf("SSE server starting on :%s", ssePort)
159
159
-
if err := http.ListenAndServe(":"+ssePort, handler); err != nil {
160
160
-
log.Fatal(err)
161
161
-
}
162
162
-
}
190
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
7
-
"net/http"
8
7
"time"
9
8
10
10
-
"github.com/tmaxmax/go-sse"
9
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
27
-
sseServer = &sse.Server{}
26
26
+
sseServer = sse.NewServer(&sse.Options{
27
27
+
Logger: log.New(log.Writer(), "go-sse: ", log.Ldate|log.Ltime),
28
28
+
})
28
29
}
29
30
30
30
-
func handleSSE(w http.ResponseWriter, r *http.Request) {
31
31
-
sseServer.ServeHTTP(w, r)
32
32
-
}
31
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
48
-
msg := &sse.Message{}
49
49
-
msg.AppendData(string(data))
50
50
-
51
51
-
if err := sseServer.Publish(msg); err != nil {
52
52
-
log.Printf("SSE: publish failed: %v", err)
53
53
-
}
47
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
93
-
msg := &sse.Message{}
94
94
-
msg.AppendData(string(data))
95
95
-
// Don't set Type - just send as regular message
96
96
-
97
97
-
if err := sseServer.Publish(msg); err != nil {
98
98
-
log.Printf("SSE: progress publish failed: %v", err)
99
99
-
}
87
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
130
-
msg := &sse.Message{}
131
131
-
msg.AppendData(string(data))
132
132
-
// Don't set Type - just send as regular message
133
133
-
134
134
-
sseServer.Publish(msg)
118
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
8
+
9
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
382
-
eventSource = new EventSource('http://localhost:8081');
384
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
392
+
console.log('SSE raw event:', event);
393
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
409
+
} else {
410
410
+
console.log('Unknown message type:', data);
405
411
}
406
412
} catch (error) {
407
407
-
console.error('Failed to parse SSE data:', error);
413
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
696
-
// Extract username from URL path /api/rating-history/{username}
697
697
-
username := r.URL.Path[len("/api/rating-history/"):]
702
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
727
-
username := r.URL.Path[len("/player/"):]
732
732
+
username := chi.URLParam(r, "player")
728
733
if username == "" {
729
734
http.Redirect(w, r, "/", http.StatusSeeOther)
730
735
return