Add Export Functionality to Match History
complete
George Jabbour
Add the ability to export match history data from the Matches page in CSV and JSON formats. This feature should mirror the existing analytics export functionality but be scoped to the match history list view with current filters applied.
Export Types:
- CSV (Free + Team tier)
- JSON (Team tier only)
Key Features:
- Respect current filter state (player, deck, date range, etc.)
- Respect subscription tier limits
- Proper file download with Content-Disposition header
- Loading states and error handling
See comments below for detailed technical context, acceptance criteria, and implementation notes.
George Jabbour
marked this post as
complete
George Jabbour
marked this post as
in progress
George Jabbour
marked this post as
under review
George Jabbour
## Part 5: Questions for Stakeholder
- Include game details in export?Should each row include individual game data (on_play, turn_count, etc.) or just match-level summary?
- Include nested games in JSON?Should JSON export include the full games array for each match, or just match summaries?
- Export limit?Should there be a max row limit (e.g., 10,000 matches) to prevent server timeouts?
- PDF export?Should this feature include PDF export (Team tier), or start with CSV/JSON only?
- Filename format?Suggested:matches_export_2024-01-15.csv- is this acceptable?
---
## Dependencies/Blockers
None - all infrastructure exists
---
## Estimated Complexity
T-shirt Size: S-M (Small to Medium)
Justification:
- Backend: ~2-3 hours (export action + file response + tests)
- Frontend: ~1-2 hours (add ExportButton + wire up filters)
- Most logic already exists in analytics export - can reference/reuse
- No new models or migrations required
- Well-defined patterns to follow
George Jabbour
## Part 4: Export Format Specifications
### CSV Export Format
Suggested columns (human-readable headers):
```csv
Date,Player,My Deck,My Archetype,Opponent,Opponent Deck/Archetype,Format,Venue,Result,Wins,Losses,Notes
2024-01-15,John Doe,Mono Red Burn,,Jane Smith,Azorius Control,Modern,Local Game Store,2-1,2,1,"Close match, sideboard was key"
### JSON Export Format
```json
[
{
"id": "uuid",
"date": "2024-01-15",
"player": { "id": "uuid", "name": "John Doe" },
"deck": { "id": "uuid", "name": "Mono Red Burn" },
"opponent_name": "Jane Smith",
"opponent_archetype": { "id": "uuid", "name": "Azorius Control" },
"format": { "id": "uuid", "name": "Modern" },
"venue": { "id": "uuid", "name": "Local Game Store" },
"result": "2-1",
"wins": 2,
"losses": 1,
"notes": "Close match, sideboard was key",
"games": [
{ "game_number": 1, "result": "win", "on_play": true },
{ "game_number": 2, "result": "loss", "on_play": false },
{ "game_number": 3, "result": "win", "on_play": true }
]
}
]
George Jabbour
## Part 3: Acceptance Criteria
### Backend
- [ ] New GET /api/v1/matches/export/endpoint that returns match data
- [ ] Endpoint accepts ?format=csvor?format=jsonquery parameter
- [ ] Export respects all existing match filters (player, deck, date range, etc.)
- [ ] Export respects subscription tier (Free=CSV only, Team=CSV+JSON)
- [ ] Export respects retention limits (subscription tier data retention)
- [ ] Returns proper Content-Disposition header for file download
### Frontend
- [ ] ExportButton added to Matches page header/toolbar
- [ ] Export uses current filter state
- [ ] Shows loading state during export
- [ ] Shows lock icon + tooltip for tier-gated formats
- [ ] Toast notification on success/failure
### Testing & Documentation
- [ ] Backend tests for export endpoint with various filters
- [ ] Backend tests for subscription tier gating
- [ ] OpenAPI schema updated (run pnpm api:generate)
George Jabbour
## Part 2: Suggested Implementation Approach
### Option A: Add Export Action to MatchViewSet (Recommended)
Add a new
export
action to the existing MatchViewSet
that reuses the filter logic:```python
apps/backend/apps/matches/views/match.py
@extend_schema(
description="Export filtered matches as CSV or JSON",
parameters=[
OpenApiParameter(name="format", enum=["csv", "json"], default="csv")
],
)
@action(detail=False, methods=["get"], url_path="export")
def export(self, request: Request) -> Response:
"""Export filtered match data."""
export_format = request.query_params.get("format", "csv")
Check entitlements
team = get_team_from_request(request)
entitlements = get_entitlements(team)
can_export, error = entitlements.can_export(export_format)
if not can_export:
return Response({"detail": error}, status=403)
Apply existing filters
queryset = self.filter_queryset(self.get_queryset())
Generate export (similar to ExportService.export_matches)
Return file response with appropriate content type
```
### Option B: Reuse ExportService from Analytics
Extend
ExportService
to accept pre-filtered querysets or add a standalone match export service.---
### Affected Files/Areas
Backend:
- apps/backend/apps/matches/views/match.py- Add export action
- apps/backend/apps/matches/serializers/match.py- May need export-specific serializer
Frontend:
- apps/frontend/src/app/(app)/matches/matches-page-client.tsx- Add ExportButton to page actions
- apps/frontend/src/components/matches/filters/matches-filters.tsx- Pass filters to export
- apps/frontend/src/lib/api/generated/- Regenerate types after backend changes
George Jabbour
## Part 1: Technical Context
### Existing Export Infrastructure
The codebase already has a robust export system in the analytics app:
- Backend Export Service:apps/backend/apps/analytics/services/export.py-ExportServiceclass withexport_matches()andexport_games()methods
- Frontend Export Button:apps/frontend/src/components/analytics/export-button.tsx- ReusableExportButtoncomponent with CSV/JSON dropdown
- Entitlements Checking:useEntitlements()hook withcanExport("csv")andcanExport("json")methods
- Subscription Gating: Free tier gets CSV only, Team tier gets CSV + JSON + PDF
### Key Files to Reference
| File | Purpose |
|------|---------|
|
apps/backend/apps/matches/views/match.py
| MatchViewSet with existing list/filter actions ||
apps/backend/apps/matches/serializers/match.py
| Match serializers ||
apps/backend/apps/analytics/services/export.py
| Existing export service to reference/reuse ||
apps/frontend/src/components/analytics/export-button.tsx
| Existing export button component ||
apps/backend/apps/subscriptions/tier_limits.py
| Export permission definitions ||
apps/frontend/src/hooks/useEntitlements.ts
| Frontend entitlement checking |### Data Models Involved
Match Model
(apps/backend/apps/matches/models/match.py
):- id,date,result,wins,losses,draws
- player(FK to User)
- deckorarchetype(XOR - one required)
- opponent_name,opponent_team_member,opponent_deck,opponent_archetype
- format(FK),venue(FK, optional),tournament(FK, optional)
- notes
Game Model
(apps/backend/apps/matches/models/game.py
):- match(FK),game_number(1-5),result(win/loss/draw)
- on_play(boolean),opening_hand_size(1-7)
- turn_count,final_life_total,opponent_final_life,win_condition,notes
### Current Match List Filters
The MatchViewSet already supports these filters via
MatchFilter
:- player,deck,format_id,opponent_archetype,venue
- result(choice filter)
- date_from,date_to(date range)
- is_win(boolean)
- game(TCG game via format__game_id)