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>
This commit is contained in:
460
docs/MATCHES_API.md
Normal file
460
docs/MATCHES_API.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# 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.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
|
||||
```json
|
||||
{
|
||||
"match_id": "3589487716842078322",
|
||||
"status": "parsing",
|
||||
"message": "Demo download and parsing initiated",
|
||||
"estimated_time": 120
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### Match
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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: `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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/matches?map=de_inferno&limit=20');
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
### Filtering Matches by Player
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/matches?player_id=765611980123456&limit=20');
|
||||
const data = await response.json();
|
||||
```
|
||||
Reference in New Issue
Block a user