This commit completes the first phase of feature parity implementation and resolves all API integration issues to match the backend API format. ## API Integration Fixes - Remove all hardcoded default values from transformers (tick_rate, kast, player_count, steam_updated) - Update TypeScript types to make fields optional where backend doesn't guarantee them - Update Zod schemas to validate optional fields correctly - Fix mock data to match real API response format (plain arrays, not wrapped objects) - Update UI components to handle undefined values with proper fallbacks - Add comprehensive API documentation for Match and Player endpoints ## Phase 1 Features Implemented (3/6) ### 1. Player Tracking System ✅ - Created TrackPlayerModal.svelte with auth code input - Integrated track/untrack player API endpoints - Added UI for providing optional share code - Displays tracked status on player profiles - Full validation and error handling ### 2. Share Code Parsing ✅ - Created ShareCodeInput.svelte component - Added to matches page for easy match submission - Real-time validation of share code format - Parse status feedback with loading states - Auto-redirect to match page on success ### 3. VAC/Game Ban Status ✅ - Added VAC and game ban count/date fields to Player type - Display status badges on player profile pages - Show ban count and date when available - Visual indicators using DaisyUI badge components ## Component Improvements - Modal.svelte: Added Svelte 5 Snippet types, actions slot support - ThemeToggle.svelte: Removed deprecated svelte:component usage - Tooltip.svelte: Fixed type safety with Snippet type - All new components follow Svelte 5 runes pattern ($state, $derived, $bindable) ## Type Safety & Linting - Fixed all ESLint errors (any types → proper types) - Fixed form label accessibility issues - Replaced error: any with error: unknown + proper type guards - Added Snippet type imports where needed - Updated all catch blocks to use instanceof Error checks ## Static Assets - Migrated all files from public/ to static/ directory per SvelteKit best practices - Moved 200+ map icons, screenshots, and other assets - Updated all import paths to use /images/ (served from static/) ## Documentation - Created IMPLEMENTATION_STATUS.md tracking all 15 missing features - Updated API.md with optional field annotations - Created MATCHES_API.md with comprehensive endpoint documentation - Added inline comments marking optional vs required fields ## Testing - Updated mock fixtures to remove default values - Fixed mock handlers to return plain arrays like real API - Ensured all components handle undefined gracefully ## Remaining Phase 1 Tasks - [ ] Add VAC status column to match scoreboard - [ ] Create weapons statistics tab for matches - [ ] Implement recently visited players on home page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
Matches API Endpoint Documentation
This document provides detailed information about the matches API endpoints used by CS2.WTF to retrieve match data from the backend CSGOWTFD service.
Overview
The matches API provides access to Counter-Strike 2 match data including match listings, detailed match statistics, and related match information such as weapons, rounds, and chat data.
Base URL
All endpoints are relative to the API base URL: https://api.csgow.tf
During development, requests are proxied through /api to avoid CORS issues.
Authentication
No authentication is required for read operations. All match data is publicly accessible.
Rate Limiting
The API does not currently enforce rate limiting, but clients should implement reasonable request throttling to avoid overwhelming the service.
Endpoints
1. Get Matches List
Retrieves a paginated list of matches.
Endpoint: GET /matches
Alternative: GET /matches/next/:time
Parameters:
time(path, optional): Unix timestamp for pagination (use with/matches/next/:time)- Query parameters:
limit(optional): Number of matches to return (default: 50, max: 100)map(optional): Filter by map name (e.g.,de_inferno)player_id(optional): Filter by player Steam ID
Response (200 OK):
IMPORTANT: This endpoint returns a plain array, not an object with properties.
[
{
"match_id": "3589487716842078322",
"map": "de_inferno",
"date": 1730487900,
"score": [13, 10],
"duration": 2456,
"match_result": 1,
"max_rounds": 24,
"parsed": true,
"vac": false,
"game_ban": false
}
]
Field Descriptions:
match_id: Unique match identifier (uint64 as string)map: Map name (can be empty string if not parsed)date: Unix timestamp (seconds since epoch)score: Array with two elements[team_a_score, team_b_score]duration: Match duration in secondsmatch_result: 0 = tie, 1 = team_a win, 2 = team_b winmax_rounds: Maximum rounds (24 for MR12, 30 for MR15)parsed: Whether the demo has been parsedvac: Whether any player has a VAC bangame_ban: Whether any player has a game ban
Pagination:
- The API returns a plain array of matches, sorted by date (newest first)
- To get the next page, use the
datefield from the last match in the array - Request
/matches/next/{timestamp}where{timestamp}is the Unix timestamp - Continue until the response returns fewer matches than your
limitparameter - Example: If you request
limit=20and get back 15 matches, you've reached the end
2. Get Match Details
Retrieves detailed information about a specific match including player statistics.
Endpoint: GET /match/{match_id}
Parameters:
match_id(path): The unique match identifier (uint64 as string)
Response (200 OK):
{
"match_id": "3589487716842078322",
"share_code": "CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
"map": "de_inferno",
"date": "2024-11-01T18:45:00Z",
"score_team_a": 13,
"score_team_b": 10,
"duration": 2456,
"match_result": 1,
"max_rounds": 24,
"demo_parsed": true,
"vac_present": false,
"gameban_present": false,
"tick_rate": 64.0, // Optional: not always provided by API
"players": [
{
"id": "765611980123456",
"name": "Player1",
"avatar": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/fe/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg",
"team_id": 2,
"kills": 24,
"deaths": 18,
"assists": 6,
"headshot": 12,
"mvp": 3,
"score": 56,
"kast": 78, // Optional: not always provided by API
"rank_old": 18500,
"rank_new": 18650,
"dmg_enemy": 2450,
"dmg_team": 120,
"flash_assists": 4,
"flash_duration_enemy": 15.6,
"flash_total_enemy": 8,
"ud_he": 450,
"ud_flames": 230,
"ud_flash": 5,
"ud_smoke": 3,
"avg_ping": 25.5,
"color": "yellow"
}
]
}
3. Get Match Weapons
Retrieves weapon statistics for all players in a match.
Endpoint: GET /match/{match_id}/weapons
Parameters:
match_id(path): The unique match identifier
Response (200 OK):
{
"match_id": 3589487716842078322,
"weapons": [
{
"player_id": 765611980123456,
"weapon_stats": [
{
"eq_type": 17,
"weapon_name": "AK-47",
"kills": 12,
"damage": 1450,
"hits": 48,
"hit_groups": {
"head": 8,
"chest": 25,
"stomach": 8,
"left_arm": 3,
"right_arm": 2,
"left_leg": 1,
"right_leg": 1
},
"headshot_pct": 16.7
}
]
}
]
}
4. Get Match Rounds
Retrieves round-by-round statistics for a match.
Endpoint: GET /match/{match_id}/rounds
Parameters:
match_id(path): The unique match identifier
Response (200 OK):
{
"match_id": 3589487716842078322,
"rounds": [
{
"round": 1,
"winner": 2,
"win_reason": "elimination",
"players": [
{
"round": 1,
"player_id": 765611980123456,
"bank": 800,
"equipment": 650,
"spent": 650,
"kills_in_round": 2,
"damage_in_round": 120
}
]
}
]
}
5. Get Match Chat
Retrieves chat messages from a match.
Endpoint: GET /match/{match_id}/chat
Parameters:
match_id(path): The unique match identifier
Response (200 OK):
{
"match_id": 3589487716842078322,
"messages": [
{
"player_id": 765611980123456,
"player_name": "Player1",
"message": "nice shot!",
"tick": 15840,
"round": 8,
"all_chat": true,
"timestamp": "2024-11-01T19:12:34Z"
}
]
}
6. Parse Match from Share Code
Initiates parsing of a match from a CS:GO/CS2 share code.
Endpoint: GET /match/parse/{sharecode}
Parameters:
sharecode(path): The CS:GO/CS2 match share code
Response (200 OK):
{
"match_id": "3589487716842078322",
"status": "parsing",
"message": "Demo download and parsing initiated",
"estimated_time": 120
}
Data Models
Match
interface Match {
match_id: string; // Unique match identifier (uint64 as string)
share_code?: string; // CS:GO/CS2 share code (optional)
map: string; // Map name (e.g., "de_inferno")
date: string; // Match date and time (ISO 8601)
score_team_a: number; // Final score for team A
score_team_b: number; // Final score for team B
duration: number; // Match duration in seconds
match_result: number; // Match result: 0 = tie, 1 = team_a win, 2 = team_b win
max_rounds: number; // Maximum rounds (24 for MR12, 30 for MR15)
demo_parsed: boolean; // Whether the demo has been successfully parsed
vac_present: boolean; // Whether any player has a VAC ban
gameban_present: boolean; // Whether any player has a game ban
tick_rate?: number; // Server tick rate (64 or 128) - optional, not always provided by API
players?: MatchPlayer[]; // Array of player statistics (optional)
}
MatchPlayer
interface MatchPlayer {
id: string; // Player Steam ID (uint64 as string)
name: string; // Player display name
avatar: string; // Steam avatar URL
team_id: number; // Team ID: 2 = T side, 3 = CT side
kills: number; // Kills
deaths: number; // Deaths
assists: number; // Assists
headshot: number; // Headshot kills
mvp: number; // MVP stars earned
score: number; // In-game score
kast?: number; // KAST percentage (0-100) - optional, not always provided by API
rank_old?: number; // Premier rating before match (0-30000)
rank_new?: number; // Premier rating after match (0-30000)
dmg_enemy?: number; // Damage to enemies
dmg_team?: number; // Damage to teammates
flash_assists?: number; // Flash assist count
flash_duration_enemy?: number; // Total enemy blind time
flash_total_enemy?: number; // Enemies flashed count
ud_he?: number; // HE grenade damage
ud_flames?: number; // Molotov/Incendiary damage
ud_flash?: number; // Flash grenades used
ud_smoke?: number; // Smoke grenades used
avg_ping?: number; // Average ping
color?: string; // Player color
}
MatchListItem
interface MatchListItem {
match_id: string; // Unique match identifier (uint64 as string)
map: string; // Map name
date: string; // Match date and time (ISO 8601)
score_team_a: number; // Final score for team A
score_team_b: number; // Final score for team B
duration: number; // Match duration in seconds
demo_parsed: boolean; // Whether the demo has been successfully parsed
player_count?: number; // Number of players in the match - optional, not provided by API
}
Error Handling
All API errors follow a consistent format:
{
"error": "Error message",
"code": 404,
"details": {
"match_id": "3589487716842078322"
}
}
Common HTTP Status Codes
200 OK: Request successful400 Bad Request: Invalid parameters404 Not Found: Resource not found500 Internal Server Error: Server error
Implementation Notes
Pagination
The matches API implements cursor-based pagination using timestamps:
- Initial request to
/matchesreturns a plain array of matches (sorted newest first) - Extract the
datefield from the last match in the array - Request
/matches/next/{timestamp}to get older matches - Continue until the response returns fewer matches than your
limitparameter - The API does not provide
has_moreornext_page_timefields - you must calculate these yourself
Data Transformation
The frontend application transforms legacy API responses to a modern schema-validated format:
- Unix timestamps are converted to ISO strings
- Avatar hashes are converted to full URLs (if provided)
- Team IDs are normalized (1/2 → 2/3 if needed)
- Score arrays
[team_a, team_b]are split into separate fields - Field names are mapped:
parsed→demo_parsed,vac→vac_present,game_ban→gameban_present - Missing fields are provided with defaults (e.g.,
tick_rate: 64)
Steam ID Handling
All Steam IDs and Match IDs are handled as strings to preserve uint64 precision. Never convert these to numbers as it causes precision loss.
Examples
Fetching Matches with Pagination
// Initial request - API returns a plain array
const matches = await fetch('/api/matches?limit=20').then((r) => r.json());
// matches is an array: [{ match_id, map, date, ... }, ...]
console.log(`Loaded ${matches.length} matches`);
// Get the timestamp of the last match for pagination
if (matches.length > 0) {
const lastMatch = matches[matches.length - 1];
const lastTimestamp = lastMatch.date; // Unix timestamp
// Fetch next page using the timestamp
const moreMatches = await fetch(`/api/matches/next/${lastTimestamp}?limit=20`).then((r) =>
r.json()
);
console.log(`Loaded ${moreMatches.length} more matches`);
// Check if we've reached the end
if (moreMatches.length < 20) {
console.log('Reached the end of matches');
}
}
Complete Pagination Loop
async function loadAllMatches(limit = 50) {
let allMatches = [];
let hasMore = true;
let lastTimestamp = null;
while (hasMore) {
// Build URL based on whether we have a timestamp
const url = lastTimestamp
? `/api/matches/next/${lastTimestamp}?limit=${limit}`
: `/api/matches?limit=${limit}`;
// Fetch matches
const matches = await fetch(url).then((r) => r.json());
// Add to collection
allMatches.push(...matches);
// Check if there are more
if (matches.length < limit) {
hasMore = false;
} else {
// Get timestamp of last match for next iteration
lastTimestamp = matches[matches.length - 1].date;
}
}
return allMatches;
}
Filtering Matches by Map
const response = await fetch('/api/matches?map=de_inferno&limit=20');
const data = await response.json();
Filtering Matches by Player
const response = await fetch('/api/matches?player_id=765611980123456&limit=20');
const data = await response.json();