fix: Remove Number() conversions that corrupt uint64 IDs

JavaScript's Number type cannot accurately represent uint64 values exceeding
Number.MAX_SAFE_INTEGER (2^53-1). Converting these IDs to numbers causes
precision loss and API errors.

Root cause found:
- match/[id]/+layout.ts: `Number(params.id)` corrupted match IDs
- player/[id]/+page.ts: `Number(params.id)` corrupted player IDs

Example of the bug:
- URL param: "3638078243082338615" (correct)
- After Number(): 3638078243082339000 (rounded!)
- API response: "Match 3638078243082339000 not found"

Changes:
- Remove Number() conversions in route loaders
- Keep params.id as string throughout the application
- Update API functions to only accept string (not string | number)
- Update MatchesQueryParams.player_id type to string
- Add comprehensive transformers for legacy API responses
- Transform player stats: duo→mk_2, triple→mk_3, steamid64→id
- Build full Steam avatar URLs
- Make share_code optional (not always present)

This ensures uint64 IDs maintain full precision from URL → API → response.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 23:38:37 +01:00
parent 43c50084c6
commit f583ff54a9
11 changed files with 158 additions and 40 deletions

View File

@@ -3,7 +3,7 @@
* Converts legacy CSGO:WTF API responses to the new CS2.WTF format
*/
import type { MatchListItem, MatchesListResponse } from '$lib/types';
import type { MatchListItem, MatchesListResponse, Match, MatchPlayer } from '$lib/types';
/**
* Legacy API match format (from api.csgow.tf)
@@ -21,12 +21,71 @@ export interface LegacyMatchListItem {
game_ban: boolean;
}
/**
* Legacy API match detail format
*/
export interface LegacyMatchDetail {
match_id: string;
share_code?: string;
map: string;
date: number; // Unix timestamp
score: [number, number]; // [team_a, team_b]
duration: number;
match_result: number;
max_rounds: number;
parsed: boolean;
vac: boolean;
game_ban: boolean;
stats?: LegacyPlayerStats[];
}
/**
* Legacy player stats format
*/
export interface LegacyPlayerStats {
team_id: number;
kills: number;
deaths: number;
assists: number;
headshot: number;
mvp: number;
score: number;
player: {
steamid64: string;
name: string;
avatar: string;
vac: boolean;
game_ban: boolean;
vanity_url?: string;
};
rank: Record<string, unknown>;
multi_kills?: {
duo?: number;
triple?: number;
quad?: number;
ace?: number;
};
dmg?: Record<string, unknown>;
flash?: {
duration?: {
self?: number;
team?: number;
enemy?: number;
};
total?: {
self?: number;
team?: number;
enemy?: number;
};
};
}
/**
* Transform legacy match list item to new format
*/
export function transformMatchListItem(legacy: LegacyMatchListItem): MatchListItem {
return {
match_id: Number(legacy.match_id),
match_id: legacy.match_id, // Keep as string to preserve uint64 precision
map: legacy.map || 'unknown', // Handle empty map names
date: new Date(legacy.date * 1000).toISOString(), // Convert Unix timestamp to ISO string
score_team_a: legacy.score[0],
@@ -49,3 +108,56 @@ export function transformMatchesListResponse(
next_page_time: undefined
};
}
/**
* Transform legacy player stats to new format
*/
export function transformPlayerStats(legacy: LegacyPlayerStats): MatchPlayer {
return {
id: legacy.player.steamid64,
name: legacy.player.name,
avatar: `https://avatars.steamstatic.com/${legacy.player.avatar}_full.jpg`,
team_id: legacy.team_id,
kills: legacy.kills,
deaths: legacy.deaths,
assists: legacy.assists,
headshot: legacy.headshot,
mvp: legacy.mvp,
score: legacy.score,
kast: 0, // Not provided by legacy API
// Multi-kills: map legacy names to new format
mk_2: legacy.multi_kills?.duo,
mk_3: legacy.multi_kills?.triple,
mk_4: legacy.multi_kills?.quad,
mk_5: legacy.multi_kills?.ace,
// Flash stats
flash_duration_self: legacy.flash?.duration?.self,
flash_duration_team: legacy.flash?.duration?.team,
flash_duration_enemy: legacy.flash?.duration?.enemy,
flash_total_self: legacy.flash?.total?.self,
flash_total_team: legacy.flash?.total?.team,
flash_total_enemy: legacy.flash?.total?.enemy
};
}
/**
* Transform legacy match detail to new format
*/
export function transformMatchDetail(legacy: LegacyMatchDetail): Match {
return {
match_id: legacy.match_id,
share_code: legacy.share_code || undefined,
map: legacy.map || 'unknown',
date: new Date(legacy.date * 1000).toISOString(),
score_team_a: legacy.score[0],
score_team_b: legacy.score[1],
duration: legacy.duration,
match_result: legacy.match_result,
max_rounds: legacy.max_rounds,
demo_parsed: legacy.parsed,
vac_present: legacy.vac,
gameban_present: legacy.game_ban,
tick_rate: 64, // Default to 64, not provided by API
players: legacy.stats?.map(transformPlayerStats)
};
}