Skip to content

Implement Phase 2: Player search, stats, matches, charts, mode breakdown#26

Merged
grovecj merged 1 commit intomainfrom
feature/phase-2-player-stats
Feb 8, 2026
Merged

Implement Phase 2: Player search, stats, matches, charts, mode breakdown#26
grovecj merged 1 commit intomainfrom
feature/phase-2-player-stats

Conversation

@grovecj
Copy link
Owner

@grovecj grovecj commented Feb 8, 2026

Summary

Bugfixes (commit 2)

  • Fix CoD API base URL (my.callofduty.comwww.callofduty.com redirects to store)
  • Add NoRedirectPolicy + doRequest helper to detect 3xx as expired tokens
  • Parse 200 OK responses with {"status":"error"} JSON bodies into sentinel errors
  • Fix profileResponse.Level/Prestige type mismatch (API returns float, struct expected int)
  • Fix nullable activision_id column scan crash (string*string)
  • Add error logging in writeAPIError default case

Multi-title support (commit 3)

  • Add title and mode query params to all API endpoints (defaults: mw/wz)
  • Support Warzone 1 (mw/wz), Warzone 2 (mw2/wz2), MW MP (mw/mp), MW2 MP (mw2/mp)
  • Use id lookup type for uno (Activision) platform per API spec
  • Add game selector dropdown to HomeView
  • Thread title/mode through cache, services, handlers, store, and PlayerView

Test plan

  • go build ./... and go vet ./... pass
  • cd web && npm run build-only && npm run type-check pass
  • cd web && npx oxlint . && npx eslint . --cache pass
  • Start dev server with valid COD_SSO_TOKEN + PostgreSQL
  • Search for a known gamertag on HomeView — verify no 500 errors
  • Test game selector: switch between WZ, WZ2, MW MP, MW2 MP
  • Verify redirect to PlayerView with stats cards rendering
  • Verify match history table populates
  • Switch mode tabs and verify stats update
  • Check charts render with correct data

🤖 Generated with Claude Code

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Phase 2 of the player experience: search → navigate to player page → load lifetime stats, charts, match history, and per-mode breakdown, backed by new Go API services/handlers and a CoD API client reliability pass.

Changes:

  • Frontend: adds Home search flow w/ recent searches, Player view with mode tabs, stat cards/grids, ECharts visualizations, and match history table.
  • Backend: implements /api/v1/players/search, /stats, and /matches with services + persistence (players, match snapshots, stats snapshots).
  • CoD API client: fixes redirect/token-expiry handling and enriches stats parsing with per-mode breakdown.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/src/views/PlayerView.vue Player page data loading, mode tabs, stats/charts rendering, error display, skeletons.
web/src/views/HomeView.vue Search form wired to backend search + recent searches via localStorage.
web/src/utils/modes.ts Defines top-level modes + mode label mapping helper for UI.
web/src/utils/format.ts Shared number/KD/percent/time formatting utilities.
web/src/utils/echarts.ts Tree-shaken ECharts setup + shared dark theme/colors export.
web/src/types/index.ts Expands types for mode breakdown, search result, paginated matches.
web/src/stores/index.ts Pinia player store: search/stats/matches fetching w/ separate loading/error state.
web/src/components/charts/RadarChart.vue Radar chart visualization for key derived stats.
web/src/components/charts/PlacementChart.vue Donut chart for placement distribution.
web/src/components/charts/PerformanceTrend.vue Line chart computing average K/D trend from matches.
web/src/components/charts/ModeComparisonChart.vue Bar chart comparing kills/deaths across mode breakdown entries.
web/src/components/charts/KDGauge.vue Gauge chart for K/D ratio.
web/src/components/StatsHero.vue Hero stat cards (K/D, wins, kills).
web/src/components/StatsGrid.vue Secondary stats grid (12 cards) with responsive layout.
web/src/components/StatCard.vue Reusable stat card component with size + K/D color classes.
web/src/components/RankedBadge.vue Small badge/tag for ranked division/SR display.
web/src/components/ModeSummaryRow.vue Horizontal scrollable per-mode summary cards emitting mode selection.
web/src/components/MatchHistory.vue Naive UI data table for matches with sorting/filtering/win highlighting.
web/src/components/ChartsSection.vue Layout wrapper for chart cards + empty states.
internal/service/player.go Player search + stats service logic w/ upsert + stats snapshot save.
internal/service/match.go Match fetching/persistence + paginated response and DB fallback behavior.
internal/router/router.go Wires new player/match handlers into /api/v1/players/* routes.
internal/repository/match_repo.go Adds match count query used for pagination metadata.
internal/model/player.go Makes ActivisionID nullable pointer to match DB nullable column.
internal/handler/player.go Implements HTTP handlers for search + stats endpoints.
internal/handler/match.go Implements HTTP handler for matches endpoint w/ limit/offset parsing.
internal/handler/errors.go Centralizes sentinel error → HTTP JSON mapping with logging.
internal/codclient/types.go Adds modeBreakdown types + fixes Level/Prestige float decoding.
internal/codclient/client.go Adds redirect/token-expiry detection + parses 200/{"status":"error"} responses and mode breakdown.
docker-compose.yml Changes Postgres host port mapping to 5435.
cmd/server/main.go Wires repos/services/handlers into server initialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to +20
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatDateTime uses Date.prototype.toLocaleDateString while passing hour/minute options. In most runtimes those time options are ignored for toLocaleDateString, so the column may show only the date and drop the time. Use toLocaleString (or an Intl.DateTimeFormat) when you need both date and time.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +48
filterOptions: computed(() => {
const modes = [...new Set(props.matches.map((m) => m.mode))]
return modes.map((m) => ({ label: getModeLabel(m), value: m }))
}).value,
},
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterOptions is computed from props.matches but you immediately read .value while defining the columns array, which freezes the options at setup time. When matches load/update, the filter options won't refresh. Make columns (or just filterOptions) reactive so it recomputes when props.matches changes.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +80
if upsertErr := s.matchRepo.UpsertBatch(ctx, player.ID, modelMatches); upsertErr != nil {
slog.Warn("failed to persist matches", "error", upsertErr)
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matches.match_id is globally UNIQUE in the current schema (migrations/000001_init.up.sql), but UpsertBatch uses ON CONFLICT (match_id) DO NOTHING. If two different players share a match (same match_id) and you search them both, the second player's rows will not be inserted, and the DB query (filtered by player_id) will return empty/partial results even though the API call succeeded. Consider changing uniqueness to (match_id, player_id) (or introducing a join table) and updating the upsert conflict target accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines 4 to 6
ports:
- "5432:5432"
- "5435:5432"
environment:
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker-compose maps Postgres to host port 5435 now, but .env.example (and likely local dev docs) still reference localhost:5432. This will cause connection failures for anyone using the example env as-is; update the example/config or keep the default port mapping consistent.

Copilot uses AI. Check for mistakes.
@grovecj grovecj force-pushed the feature/phase-2-player-stats branch from 6818187 to 65747a3 Compare February 8, 2026 14:39
…ode breakdown (#9-#13)

Backend:
- PlayerService/Handler for search and stats endpoints
- MatchService/Handler for match history with pagination
- Multi-title support (mw/wz, mw2/wz2, mw/mp, mw2/mp) via title/mode query params
- CoD API error handling: redirect detection, JSON error body parsing, HTML detection
- ModeBreakdown parsing from lifetime stats response
- Fix profileResponse Level/Prestige types (float64), nullable ActivisionID (*string)
- Base URL changed to www.callofduty.com (my.callofduty.com redirects to store)

Frontend:
- Search-then-navigate flow with error alerts and recent searches (localStorage)
- Game selector (Warzone, WZ2, MW MP, MW2 MP) and platform selector
- StatCard, StatsHero, StatsGrid components with responsive layouts
- 5 ECharts visualizations: Radar, K/D Gauge, Mode Comparison, Placement, Trend
- MatchHistory with NDataTable sorting, mode filter, win highlighting, gulag badges
- Mode tabs (NTabs segment) with ModeSummaryRow click-to-switch
- Split loading/error state (statsLoading/matchesLoading)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@grovecj grovecj force-pushed the feature/phase-2-player-stats branch from 65747a3 to 198cdbd Compare February 8, 2026 14:42
@grovecj
Copy link
Owner Author

grovecj commented Feb 8, 2026

Addressed all 4 Copilot review comments in the latest force push (198cdbd):

  1. MatchHistory.vue:20 — Changed toLocaleDateStringtoLocaleString so hour/minute options are respected.
  2. MatchHistory.vue:48 — Made columns a computed() ref and extracted filterOptions as a separate computed, so mode filter options update reactively when matches load.
  3. match.go / migration — Changed UNIQUE(match_id) to UNIQUE(match_id, player_id) and updated ON CONFLICT target so multiple players sharing a match are stored correctly.
  4. docker-compose.yml — Reverted port to standard 5432:5432 for consistency with .env and docs.

Copy link
Owner Author

@grovecj grovecj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed all 4 Copilot review items:

  1. toLocaleDateString → toLocaleString — Fixed so hour/minute render correctly
  2. filterOptions reactivitycolumns and filterOptions are now both computed() refs
  3. match_id uniqueness — Changed to composite UNIQUE(match_id, player_id) in migration + repo
  4. docker-compose port — Reverted to standard 5432:5432 (no diff since it matches the base branch now)

month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed toLocaleDateString to toLocaleString so the hour/minute options are properly rendered.

},
filterOptions: filterOptions.value,
},
{
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — extracted filterOptions as a standalone computed() and made columns itself a computed() ref, so both recompute reactively when props.matches changes.

damage_dealt, damage_taken, gulag_result, match_time, raw_data)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (match_id) DO NOTHING
ON CONFLICT (match_id, player_id) DO NOTHING
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed the migration from match_id UNIQUE to a composite UNIQUE(match_id, player_id) index, and updated the ON CONFLICT target here to (match_id, player_id). Two players sharing a match now get separate rows.

@grovecj grovecj merged commit 5f3a12e into main Feb 8, 2026
2 checks passed
@grovecj grovecj deleted the feature/phase-2-player-stats branch February 8, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants