feat: Integrate player meta stats, match metadata, and sitemap proxies

- Add player meta stats API endpoint with teammates, weapons, and map data
- Display avg_rank badge on match cards and match detail page
- Add tick rate and demo download improvements to match layout
- Create sitemap proxy routes to backend for SEO
- Document backend data limitations in transformers (rounds/weapons)
- Fix 400 error: backend limits meta stats to max 10 items

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-07 18:42:44 +01:00
parent 95b385c471
commit e27e9e8821
13 changed files with 480 additions and 64 deletions

View File

@@ -1,6 +1,6 @@
import { apiClient } from './client';
import { parsePlayer } from '$lib/schemas';
import type { Player, PlayerMeta, TrackPlayerResponse } from '$lib/types';
import type { Player, PlayerMeta, PlayerMetaStats, TrackPlayerResponse } from '$lib/types';
import { transformPlayerProfile, type LegacyPlayerProfile } from './transformers';
/**
@@ -85,6 +85,18 @@ export const playersAPI = {
return playerMeta;
},
/**
* Get player aggregated meta stats from backend
* Uses pre-computed stats (cached 30 days) including teammates, weapons, maps
* @param steamId - Steam ID (uint64 as string to preserve precision)
* @param limit - Number of items per category (max 10, default: 4)
* @returns Player meta stats with teammates, weapons, map performance
*/
async getPlayerMetaStats(steamId: string, limit = 4): Promise<PlayerMetaStats> {
const url = `/player/${steamId}/meta/${limit}`;
return apiClient.get<PlayerMetaStats>(url);
},
/**
* Add player to tracking system
* @param steamId - Steam ID (uint64 as string to preserve precision)

View File

@@ -55,6 +55,9 @@ export interface LegacyMatchDetail {
vac: boolean; // NOT vac_present
game_ban: boolean; // NOT gameban_present
stats?: LegacyPlayerStats[]; // Player stats array
tick_rate?: number; // Server tick rate (64 or 128)
avg_rank?: number; // Average Premier rating (backend computed)
replay_url?: string; // Demo download URL (if < 30 days old)
}
/**
@@ -219,6 +222,9 @@ export function transformMatchDetail(legacy: LegacyMatchDetail): Match {
demo_parsed: legacy.parsed,
vac_present: legacy.vac,
gameban_present: legacy.game_ban,
tick_rate: legacy.tick_rate,
avg_rank: legacy.avg_rank,
replay_url: legacy.replay_url,
players: legacy.stats?.map(transformPlayerStats)
};
}

View File

@@ -3,10 +3,15 @@ import type { MatchRoundsResponse, RoundDetail, RoundStats, Match } from '$lib/t
/**
* Transform raw rounds API response into structured format
* @param rawData - Raw API response
*
* NOTE: The backend API only provides economy data (bank/equipment/spent) per round.
* Round winners and win reasons are not currently stored in the database.
* To add this data would require backend changes to the demo parser.
*
* @param rawData - Raw API response containing economy data per round
* @param matchId - Match ID
* @param match - Match data with player information
* @returns Structured rounds data
* @param match - Match data with player information and final score
* @returns Structured rounds data with economy info (winner/win_reason unavailable)
*/
export function transformRoundsResponse(
rawData: RoundsAPIResponse,
@@ -15,7 +20,7 @@ export function transformRoundsResponse(
): MatchRoundsResponse {
const rounds: RoundDetail[] = [];
// Create player ID to team mapping
// Create player ID to team mapping for potential future use
const playerTeamMap = new Map<string, number>();
if (match?.players) {
for (const player of match.players) {
@@ -47,8 +52,10 @@ export function transformRoundsResponse(
rounds.push({
round: roundNum + 1,
winner: 0, // TODO: Determine winner from data if available
win_reason: '', // TODO: Determine win reason if available
// Round winner data not available from backend API
// Would require demo parser changes to store RoundEnd event winners
winner: 0,
win_reason: '',
players
});
}

View File

@@ -3,10 +3,15 @@ import type { MatchWeaponsResponse, PlayerWeaponStats, WeaponStats, Match } from
/**
* Transform raw weapons API response into structured format
* @param rawData - Raw API response
*
* NOTE: The backend API provides hit/damage data per weapon but not kill counts.
* Kill tracking would require the demo parser to correlate damage events with
* player death events. Currently only aggregated damage and hit group data is available.
*
* @param rawData - Raw API response containing damage/hit data per weapon
* @param matchId - Match ID
* @param match - Match data with player information
* @returns Structured weapons data
* @returns Structured weapons data with damage stats (kills unavailable)
*/
export function transformWeaponsResponse(
rawData: WeaponsAPIResponse,
@@ -77,7 +82,9 @@ export function transformWeaponsResponse(
weapon_stats.push({
eq_type: eqType,
weapon_name: rawData.equipment_map[String(eqType)] || `Weapon ${eqType}`,
kills: 0, // TODO: Calculate kills if needed
// Kill data not available - API only provides hit/damage events
// Would require backend changes to correlate damage with death events
kills: 0,
damage: stats.damage,
hits: stats.hits,
hit_groups: hitGroupCounts,

View File

@@ -3,6 +3,7 @@
import type { MatchListItem } from '$lib/types';
import { storeMatchesState, type FilterState } from '$lib/utils/navigation';
import { getMapBackground, formatMapName } from '$lib/utils/mapAssets';
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
interface Props {
match: MatchListItem;
@@ -87,6 +88,11 @@
</div>
<!-- Status badges - horizontal layout -->
<div class="flex items-center gap-1">
{#if match.avg_rank && match.avg_rank > 0}
<div class="backdrop-blur-sm" title="Average player rating">
<PremierRatingBadge rating={match.avg_rank} size="sm" showTier={false} />
</div>
{/if}
{#if match.player_count}
<span
class="rounded-full border border-neon-blue/30 bg-neon-blue/20 px-1.5 py-0.5 text-[10px] text-neon-blue backdrop-blur-sm"

View File

@@ -42,6 +42,12 @@ export interface Match {
/** Server tick rate (64 or 128) - optional, not always provided by API */
tick_rate?: number;
/** Average Premier rating of all players in the match - optional, backend computed */
avg_rank?: number;
/** Demo replay download URL (only available for matches < 30 days old) */
replay_url?: string;
/**
* Game mode: 'premier' | 'competitive' | 'wingman'
* - Premier: Uses CS Rating (numerical ELO, 0-30,000+)
@@ -67,6 +73,8 @@ export interface MatchListItem {
duration: number;
demo_parsed: boolean;
player_count?: number;
/** Average Premier rating of all players (backend computed, optional) */
avg_rank?: number;
}
/**

View File

@@ -119,3 +119,79 @@ export interface PlayerProfile extends Player {
/** Peak CS2 Premier rating */
peak_rating?: number;
}
/**
* Teammate statistics from backend meta endpoint
*/
export interface TeammateStats {
/** Player profile */
player: {
steamid64: string;
name?: string;
avatar?: string;
vac?: boolean;
game_ban?: boolean;
};
/** Win rate when playing together (0-1) */
win_rate?: number;
/** Tie rate when playing together (0-1) */
tie_rate?: number;
/** Total matches played together */
total?: number;
}
/**
* Weapon damage statistics from backend meta endpoint
*/
export interface WeaponDamageStats {
/** Equipment type ID */
eq: number;
/** Total damage dealt with this weapon */
dmg: number;
}
/**
* Map statistics from backend meta endpoint
*/
export interface MapStats {
/** Map name (e.g., "de_inferno") */
map: string;
/** Win rate on this map (0-1) */
win_rate: number;
/** Tie rate on this map (0-1) */
tie_rate: number;
/** Total matches on this map */
total: number;
}
/**
* Full player meta statistics from /player/:id/meta endpoint
* Contains pre-aggregated stats from the backend (cached for 30 days)
*/
export interface PlayerMetaStats {
/** Basic player info */
player: {
steamid64: string;
name?: string;
avatar?: string;
vac?: boolean;
vac_date?: number;
game_ban?: boolean;
game_ban_date?: number;
tracked?: boolean;
};
/** Best teammates sorted by win rate */
best_mates?: TeammateStats[];
/** Most played teammates sorted by total games */
most_mates?: TeammateStats[];
/** Equipment ID to name mapping */
eq_map?: Record<number, string>;
/** Weapon damage stats sorted by damage */
weapon_dmg?: WeaponDamageStats[];
/** Win rate per map (map name -> rate 0-1) */
win_maps?: Record<string, number>;
/** Tie rate per map (map name -> rate 0-1) */
tie_maps?: Record<string, number>;
/** Total matches per map (map name -> count) */
total_maps?: Record<string, number>;
}

View File

@@ -11,7 +11,16 @@ export type { ChatAPIResponse } from './api/ChatAPIResponse';
export type { Match, MatchListItem, MatchPlayer, MatchWithPlayers } from './Match';
// Player types
export type { Player, PlayerMatch, PlayerMeta, PlayerProfile } from './Player';
export type {
Player,
PlayerMatch,
PlayerMeta,
PlayerProfile,
PlayerMetaStats,
TeammateStats,
WeaponDamageStats,
MapStats
} from './Player';
// Round statistics types
export type {