From 4097a33691c1fe5359032977c17499fb7e031f10 Mon Sep 17 00:00:00 2001 From: Fayupable <90789180+Fayupable@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:19:29 +0300 Subject: [PATCH] feat: enhance API request handling and improve client IP extraction logic --- chessfut-be/adapter/http/middleware.go | 21 ++++++++++++++++--- .../application/service/card_engine.go | 21 ++++++++++++------- .../panels/ScoutingMetricsPanel.tsx | 10 +++++++++ chessfut-fe/src/lib/api.ts | 10 ++++++++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/chessfut-be/adapter/http/middleware.go b/chessfut-be/adapter/http/middleware.go index bd9eecf..7adff4f 100644 --- a/chessfut-be/adapter/http/middleware.go +++ b/chessfut-be/adapter/http/middleware.go @@ -3,6 +3,7 @@ package http import ( "crypto/subtle" "log/slog" + "net" "net/http" "runtime" "strings" @@ -75,8 +76,22 @@ func writeErrorMessage(w http.ResponseWriter, status int, publicMessage string) } func clientIP(r *http.Request) string { - if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { - return strings.Split(fwd, ",")[0] + fwd := r.Header.Get("X-Forwarded-For") + if fwd == "" { + return r.RemoteAddr } - return r.RemoteAddr + + parts := strings.Split(fwd, ",") + for i := len(parts) - 1; i >= 0; i-- { + candidate := strings.TrimSpace(parts[i]) + ip := net.ParseIP(candidate) + if ip != nil && !isPrivateOrInternalIP(ip) { + return candidate + } + } + return strings.TrimSpace(parts[0]) +} + +func isPrivateOrInternalIP(ip net.IP) bool { + return ip.IsPrivate() || ip.IsLoopback() } diff --git a/chessfut-be/application/service/card_engine.go b/chessfut-be/application/service/card_engine.go index 7b9e630..c1d2084 100644 --- a/chessfut-be/application/service/card_engine.go +++ b/chessfut-be/application/service/card_engine.go @@ -36,6 +36,8 @@ const ( longGameMoves = 80.0 maxAnchorDeviation = 0.45 + + inactivityFactor = 0.65 ) const maxPlausibleFideRating = 2900 @@ -219,20 +221,25 @@ func effectiveFideRating(title domain.Title, fideRating int) int { // everything else (attribute shaping, OVR) is built around. FIDE dominates // when present (or assumed from title) since it's independently verified and // tightly banded; chess.com activity contributes a smaller nudge on top. +// A player with zero games in every chess.com format has no platform-verified +// performance to blend in — their FIDE-derived score is discounted by +// inactivityFactor instead of granted in full, so an inactive titled account +// never outranks someone who's actually proven their strength on chess.com. // Without FIDE or a title, chess.com rating alone drives it — no hard -// ceiling, a genuinely strong untitled player can still land high. A player -// with zero recorded rating in every format (an unused/inactive account) is -// floored well below the chesscom sigmoid's ~30 base, so a blank account can -// never outrank someone with real game history. +// ceiling, a genuinely strong untitled player can still land high. func anchorScore(title domain.Title, stats domain.PlayerStats) float64 { + fide := effectiveFideRating(title, stats.FideRating) peak := peakRating(stats) + if peak <= 0 { - return statFloor + if fide <= 0 { + return statFloor + } + fideScore := fideBase + fideRange/(1+math.Exp(-fideK*(float64(fide)-fideX0))) + return math.Max(fideScore*inactivityFactor, statFloor) } chesscomScore := chesscomBase + chesscomRange/(1+math.Exp(-chesscomK*(peak-chesscomX0))) - - fide := effectiveFideRating(title, stats.FideRating) if fide <= 0 { return chesscomScore } diff --git a/chessfut-fe/src/components/panels/ScoutingMetricsPanel.tsx b/chessfut-fe/src/components/panels/ScoutingMetricsPanel.tsx index 4748c97..5d92ec6 100644 --- a/chessfut-fe/src/components/panels/ScoutingMetricsPanel.tsx +++ b/chessfut-fe/src/components/panels/ScoutingMetricsPanel.tsx @@ -20,6 +20,10 @@ function fideRatingLabel(card: Card): string | null { return null; } +function hasNoRecordedGames(card: Card): boolean { + return card.bullet.rating === 0 && card.blitz.rating === 0 && card.rapid.rating === 0; +} + export function ScoutingMetricsPanel({ card }: { card: Card }) { const fideLabel = fideRatingLabel(card); const hasUnreliableRating = @@ -79,6 +83,12 @@ export function ScoutingMetricsPanel({ card }: { card: Card }) { FIDE rating isn't linked on Chess.com — the {card.title} title's minimum norm rating was assumed instead.
)} + {card.title && hasNoRecordedGames(card) && ( ++ No recorded Chess.com games in any format — this OVR is based on FIDE strength alone, discounted since + it's unproven on this platform. +
+ )} {hasUnreliableRating && (
Some ratings are based on limited recent games and may shift as more are played.
diff --git a/chessfut-fe/src/lib/api.ts b/chessfut-fe/src/lib/api.ts
index 6bfe235..9f13219 100644
--- a/chessfut-fe/src/lib/api.ts
+++ b/chessfut-fe/src/lib/api.ts
@@ -3,7 +3,15 @@ import type { Card, LeaderboardResponse } from "@/types/card.types";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
async function fetchJSON