Files
csgowtf/docs/MATCHES_API.md
vikingowl 8f3b652740 feat: Implement Phase 1 critical features and fix API integration
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>
2025-11-12 19:31:18 +01:00

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 seconds
  • match_result: 0 = tie, 1 = team_a win, 2 = team_b win
  • max_rounds: Maximum rounds (24 for MR12, 30 for MR15)
  • parsed: Whether the demo has been parsed
  • vac: Whether any player has a VAC ban
  • game_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 date field 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 limit parameter
  • Example: If you request limit=20 and 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 successful
  • 400 Bad Request: Invalid parameters
  • 404 Not Found: Resource not found
  • 500 Internal Server Error: Server error

Implementation Notes

Pagination

The matches API implements cursor-based pagination using timestamps:

  1. Initial request to /matches returns a plain array of matches (sorted newest first)
  2. Extract the date field from the last match in the array
  3. Request /matches/next/{timestamp} to get older matches
  4. Continue until the response returns fewer matches than your limit parameter
  5. The API does not provide has_more or next_page_time fields - 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: parseddemo_parsed, vacvac_present, game_bangameban_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();