a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
at main 15 kB view raw
1package server 2 3import ( 4 "fmt" 5 "html/template" 6 "log" 7 "net/http" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 gossh "golang.org/x/crypto/ssh" 12 13 "battleship-arena/internal/storage" 14) 15 16func HandleUserProfile(w http.ResponseWriter, r *http.Request) { 17 username := chi.URLParam(r, "username") 18 if username == "" { 19 http.Redirect(w, r, "/", http.StatusSeeOther) 20 return 21 } 22 23 user, err := storage.GetUserByUsername(username) 24 if err != nil { 25 http.Error(w, "Error loading user", http.StatusInternalServerError) 26 return 27 } 28 if user == nil { 29 http.Error(w, "User not found", http.StatusNotFound) 30 return 31 } 32 33 // Get user's submission stats 34 entries, _ := storage.GetLeaderboard(100) 35 var userEntry *storage.LeaderboardEntry 36 for _, e := range entries { 37 if e.Username == username { 38 userEntry = &e 39 break 40 } 41 } 42 43 // Get user's submissions with stats 44 submissions, err := storage.GetUserSubmissionsWithStats(username) 45 if err != nil { 46 log.Printf("Error getting submissions for %s: %v", username, err) 47 submissions = []storage.SubmissionWithStats{} 48 } 49 if submissions == nil { 50 submissions = []storage.SubmissionWithStats{} 51 } 52 log.Printf("Found %d submissions for %s", len(submissions), username) 53 54 // Parse public key for display 55 publicKeyDisplay := formatPublicKey(user.PublicKey) 56 57 tmpl := template.Must(template.New("user").Parse(userProfileHTML)) 58 data := struct { 59 User *storage.User 60 Entry *storage.LeaderboardEntry 61 Submissions []storage.SubmissionWithStats 62 PublicKeyDisplay string 63 }{ 64 User: user, 65 Entry: userEntry, 66 Submissions: submissions, 67 PublicKeyDisplay: publicKeyDisplay, 68 } 69 tmpl.Execute(w, data) 70} 71 72func HandleUsers(w http.ResponseWriter, r *http.Request) { 73 users, err := storage.GetAllUsers() 74 if err != nil { 75 http.Error(w, "Error loading users", http.StatusInternalServerError) 76 return 77 } 78 79 tmpl := template.Must(template.New("users").Parse(usersListHTML)) 80 tmpl.Execute(w, users) 81} 82 83func formatPublicKey(key string) string { 84 key = strings.TrimSpace(key) 85 parts := strings.Fields(key) 86 if len(parts) < 2 { 87 return key 88 } 89 90 // Parse the key to get fingerprint 91 pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key)) 92 if err != nil { 93 return key 94 } 95 96 fingerprint := gossh.FingerprintSHA256(pubKey) 97 return fmt.Sprintf("%s %s", parts[0], fingerprint) 98} 99 100const userProfileHTML = ` 101<!DOCTYPE html> 102<html lang="en"> 103<head> 104 <title>{{.User.Name}} (@{{.User.Username}}) - Battleship Arena</title> 105 <meta charset="UTF-8"> 106 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 107 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚓</text></svg>"> 108 <style> 109 * { 110 margin: 0; 111 padding: 0; 112 box-sizing: border-box; 113 } 114 115 body { 116 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 117 background: #0f172a; 118 color: #e2e8f0; 119 min-height: 100vh; 120 padding: 2rem 1rem; 121 } 122 123 .container { 124 max-width: 900px; 125 margin: 0 auto; 126 } 127 128 .back-link { 129 display: inline-block; 130 margin-bottom: 2rem; 131 color: #60a5fa; 132 text-decoration: none; 133 font-size: 0.9rem; 134 } 135 136 .back-link:hover { 137 text-decoration: underline; 138 } 139 140 .profile-header { 141 background: #1e293b; 142 border: 1px solid #334155; 143 border-radius: 12px; 144 padding: 2rem; 145 margin-bottom: 2rem; 146 } 147 148 .username { 149 font-size: 2rem; 150 font-weight: 700; 151 color: #e2e8f0; 152 margin-bottom: 0.5rem; 153 } 154 155 .handle { 156 font-size: 1.2rem; 157 color: #94a3b8; 158 margin-bottom: 1rem; 159 } 160 161 .bio { 162 color: #cbd5e1; 163 margin-bottom: 1rem; 164 line-height: 1.6; 165 } 166 167 .link { 168 color: #60a5fa; 169 text-decoration: none; 170 } 171 172 .link:hover { 173 text-decoration: underline; 174 } 175 176 .stats-grid { 177 display: grid; 178 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 179 gap: 1rem; 180 margin-bottom: 2rem; 181 } 182 183 .stat-card { 184 background: #1e293b; 185 border: 1px solid #334155; 186 border-radius: 12px; 187 padding: 1.5rem; 188 } 189 190 .stat-label { 191 font-size: 0.875rem; 192 color: #94a3b8; 193 margin-bottom: 0.5rem; 194 } 195 196 .stat-value { 197 font-size: 2rem; 198 font-weight: 700; 199 color: #60a5fa; 200 } 201 202 .key-section { 203 background: #1e293b; 204 border: 1px solid #334155; 205 border-radius: 12px; 206 padding: 2rem; 207 } 208 209 .section-title { 210 font-size: 1.25rem; 211 font-weight: 600; 212 margin-bottom: 1rem; 213 color: #e2e8f0; 214 } 215 216 .key-display { 217 background: #0f172a; 218 padding: 1rem; 219 border-radius: 8px; 220 font-family: 'Monaco', 'Courier New', monospace; 221 font-size: 0.875rem; 222 color: #94a3b8; 223 word-break: break-all; 224 } 225 226 .metadata { 227 display: grid; 228 grid-template-columns: repeat(2, 1fr); 229 gap: 1rem; 230 margin-top: 1rem; 231 font-size: 0.875rem; 232 color: #64748b; 233 } 234 </style> 235</head> 236<body> 237 <div class="container"> 238 <a href="/" class="back-link">← Back to Leaderboard</a> 239 240 <div class="profile-header"> 241 <div class="username">{{.User.Name}}</div> 242 <div class="handle">@{{.User.Username}}</div> 243 {{if .User.Bio}} 244 <div class="bio">{{.User.Bio}}</div> 245 {{end}} 246 {{if .User.Link}} 247 <a href="{{.User.Link}}" class="link" target="_blank">🔗 {{.User.Link}}</a> 248 {{end}} 249 </div> 250 251 {{if .Entry}} 252 <div class="stats-grid"> 253 <div class="stat-card"> 254 <div class="stat-label">Rating</div> 255 <div class="stat-value">{{.Entry.Rating}}</div> 256 </div> 257 <div class="stat-card"> 258 <div class="stat-label">Wins</div> 259 <div class="stat-value">{{.Entry.Wins}}</div> 260 </div> 261 <div class="stat-card"> 262 <div class="stat-label">Losses</div> 263 <div class="stat-value">{{.Entry.Losses}}</div> 264 </div> 265 <div class="stat-card"> 266 <div class="stat-label">Win Rate</div> 267 <div class="stat-value">{{printf "%.1f" .Entry.WinPct}}%</div> 268 </div> 269 </div> 270 {{end}} 271 272 {{if .Submissions}} 273 <div class="key-section" style="margin-bottom: 2rem;"> 274 <h2 class="section-title">📤 Submissions</h2> 275 <div style="overflow-x: auto;"> 276 <table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;"> 277 <thead> 278 <tr style="border-bottom: 1px solid #334155;"> 279 <th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Filename</th> 280 <th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Uploaded</th> 281 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Rating</th> 282 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Wins</th> 283 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Losses</th> 284 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Win Rate</th> 285 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Avg Moves</th> 286 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Status</th> 287 <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Active</th> 288 </tr> 289 </thead> 290 <tbody> 291 {{range .Submissions}} 292 <tr style="border-bottom: 1px solid #334155;"> 293 <td style="padding: 0.75rem 0.5rem; font-family: Monaco, monospace;">{{.Filename}}</td> 294 <td style="padding: 0.75rem 0.5rem; color: #94a3b8;">{{.UploadTime.Format "Jan 2, 3:04 PM"}}</td> 295 <td style="padding: 0.75rem 0.5rem; text-align: center;"> 296 {{if .HasMatches}}{{.Rating}} <span style="color: #94a3b8; font-size: 0.8em;">±{{.RD}}</span>{{else}}-{{end}} 297 </td> 298 <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Wins}}{{else}}-{{end}}</td> 299 <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Losses}}{{else}}-{{end}}</td> 300 <td style="padding: 0.75rem 0.5rem; text-align: center;"> 301 {{if .HasMatches}} 302 {{if ge .WinPct 60.0}}<span style="color: #10b981;">{{printf "%.1f" .WinPct}}%</span>{{end}} 303 {{if and (lt .WinPct 60.0) (ge .WinPct 40.0)}}<span style="color: #f59e0b;">{{printf "%.1f" .WinPct}}%</span>{{end}} 304 {{if lt .WinPct 40.0}}<span style="color: #ef4444;">{{printf "%.1f" .WinPct}}%</span>{{end}} 305 {{else}}-{{end}} 306 </td> 307 <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{printf "%.1f" .AvgMoves}}{{else}}-{{end}}</td> 308 <td style="padding: 0.75rem 0.5rem; text-align: center;"> 309 {{if eq .Status "completed"}}<span style="color: #10b981;">✓</span>{{end}} 310 {{if eq .Status "pending"}}<span style="color: #fbbf24;">⏳</span>{{end}} 311 {{if eq .Status "testing"}}<span style="color: #3b82f6;">⚙️</span>{{end}} 312 {{if eq .Status "failed"}}<span style="color: #ef4444;">✗</span>{{end}} 313 </td> 314 <td style="padding: 0.75rem 0.5rem; text-align: center;"> 315 {{if .IsActive}}<span style="color: #10b981;">●</span>{{else}}<span style="color: #64748b;">○</span>{{end}} 316 </td> 317 </tr> 318 {{end}} 319 </tbody> 320 </table> 321 </div> 322 </div> 323 {{end}} 324 325 <div class="key-section"> 326 <h2 class="section-title">SSH Public Key</h2> 327 <div class="key-display">{{.PublicKeyDisplay}}</div> 328 <div class="metadata"> 329 <div>Member since: {{.User.CreatedAt.Format "Jan 2, 2006"}}</div> 330 <div>Last login: {{.User.LastLoginAt.Format "Jan 2, 3:04 PM"}}</div> 331 </div> 332 </div> 333 </div> 334</body> 335</html> 336` 337 338const usersListHTML = ` 339<!DOCTYPE html> 340<html lang="en"> 341<head> 342 <title>Users - Battleship Arena</title> 343 <meta charset="UTF-8"> 344 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 345 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚓</text></svg>"> 346 <style> 347 * { 348 margin: 0; 349 padding: 0; 350 box-sizing: border-box; 351 } 352 353 body { 354 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 355 background: #0f172a; 356 color: #e2e8f0; 357 min-height: 100vh; 358 padding: 2rem 1rem; 359 } 360 361 .container { 362 max-width: 1200px; 363 margin: 0 auto; 364 } 365 366 h1 { 367 font-size: 2.5rem; 368 font-weight: 700; 369 margin-bottom: 0.5rem; 370 background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); 371 -webkit-background-clip: text; 372 -webkit-text-fill-color: transparent; 373 } 374 375 .back-link { 376 display: inline-block; 377 margin-bottom: 2rem; 378 color: #60a5fa; 379 text-decoration: none; 380 font-size: 0.9rem; 381 } 382 383 .back-link:hover { 384 text-decoration: underline; 385 } 386 387 .users-grid { 388 display: grid; 389 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 390 gap: 1.5rem; 391 } 392 393 .user-card { 394 background: #1e293b; 395 border: 1px solid #334155; 396 border-radius: 12px; 397 padding: 1.5rem; 398 transition: transform 0.2s, border-color 0.2s; 399 text-decoration: none; 400 color: inherit; 401 display: block; 402 } 403 404 .user-card:hover { 405 transform: translateY(-2px); 406 border-color: #60a5fa; 407 } 408 409 .user-name { 410 font-size: 1.25rem; 411 font-weight: 600; 412 color: #e2e8f0; 413 margin-bottom: 0.25rem; 414 } 415 416 .user-handle { 417 font-size: 0.9rem; 418 color: #94a3b8; 419 margin-bottom: 0.75rem; 420 } 421 422 .user-bio { 423 font-size: 0.875rem; 424 color: #cbd5e1; 425 line-height: 1.5; 426 } 427 </style> 428</head> 429<body> 430 <div class="container"> 431 <a href="/" class="back-link">← Back to Leaderboard</a> 432 <h1>Players</h1> 433 <p style="color: #94a3b8; margin-bottom: 2rem;">{{len .}} registered users</p> 434 435 <div class="users-grid"> 436 {{range .}} 437 <a href="/user/{{.Username}}" class="user-card"> 438 <div class="user-name">{{.Name}}</div> 439 <div class="user-handle">@{{.Username}}</div> 440 {{if .Bio}} 441 <div class="user-bio">{{.Bio}}</div> 442 {{end}} 443 </a> 444 {{end}} 445 </div> 446 </div> 447</body> 448</html> 449`