CLAUDE: Add comprehensive CLAUDE.md documentation files for AI agent guidance

Adding 17 CLAUDE.md files across the project to provide detailed context
and implementation guidelines for AI development agents:

Root Documentation:
- CLAUDE.md - Main project guide with Git workflow requirements

Component Documentation:
- commands/CLAUDE.md - Command architecture and patterns
- models/CLAUDE.md - Pydantic models and validation
- services/CLAUDE.md - Service layer and API interactions
- tasks/CLAUDE.md - Background tasks and automation
- tests/CLAUDE.md - Testing strategies and patterns
- utils/CLAUDE.md - Utility functions and decorators
- views/CLAUDE.md - Discord UI components and embeds

Command Package Documentation:
- commands/help/CLAUDE.md - Help system implementation
- commands/injuries/CLAUDE.md - Injury management commands
- commands/league/CLAUDE.md - League-wide commands
- commands/players/CLAUDE.md - Player information commands
- commands/profile/CLAUDE.md - User profile commands
- commands/teams/CLAUDE.md - Team information commands
- commands/transactions/CLAUDE.md - Transaction management
- commands/utilities/CLAUDE.md - Utility commands
- commands/voice/CLAUDE.md - Voice channel management

Key Updates:
- Updated .gitignore to track CLAUDE.md files in version control
- Added Git Workflow section requiring branch-based development
- Documented all architectural patterns and best practices
- Included comprehensive command/service implementation guides

These files provide essential context for AI agents working on the codebase,
ensuring consistent patterns, proper error handling, and maintainable code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-10-20 20:30:07 -05:00
parent 64e60232dd
commit ca325142d8
18 changed files with 7752 additions and 1 deletions

2
.gitignore vendored
View File

@ -13,7 +13,7 @@ logs/
# Claude files & directories # Claude files & directories
.claude/ .claude/
CLAUDE.md # CLAUDE.md
.env* .env*
# Distribution / packaging # Distribution / packaging

665
CLAUDE.md Normal file
View File

@ -0,0 +1,665 @@
# 🚨 CRITICAL: @ MENTION HANDLING 🚨
When ANY file is mentioned with @ syntax, you MUST IMMEDIATELY call Read tool on that file BEFORE responding.
You will see automatic loads of any @ mentioned filed, this is NOT ENOUGH, it only loads the file contents.
You MUST perform Read tool calls on the files directly, even if they were @ included.
This is NOT optional - it loads required CLAUDE.md context. along the file path.
See @./.claude/force-claude-reads.md for details.
---
# CLAUDE.md - Discord Bot v2.0
This file provides comprehensive guidance to Claude Code (claude.ai/code) when working with the Discord Bot v2.0 codebase.
**🔍 IMPORTANT:** Always check CLAUDE.md files in the current and parent directories for specific reference information and implementation details before making changes or additions to the codebase.
## 📚 Documentation References
### Core Documentation Files
- **[commands/CLAUDE.md](commands/CLAUDE.md)** - Command architecture, patterns, and implementation guidelines
- **[services/CLAUDE.md](services/CLAUDE.md)** - Service layer architecture, BaseService patterns, and API interactions
- **[models/CLAUDE.md](models/CLAUDE.md)** - Pydantic models, validation patterns, and data structures
- **[views/CLAUDE.md](views/CLAUDE.md)** - Discord UI components, embeds, modals, and interactive elements
- **[tasks/CLAUDE.md](tasks/CLAUDE.md)** - Background tasks, automated cleanup, and scheduled operations
- **[tests/CLAUDE.md](tests/CLAUDE.md)** - Testing strategies, patterns, and lessons learned
- **[utils/CLAUDE.md](utils/CLAUDE.md)** - Utility functions, logging system, caching, and decorators
### Command-Specific Documentation
- **[commands/league/CLAUDE.md](commands/league/CLAUDE.md)** - League-wide commands (/league, /standings, /schedule)
- **[commands/players/CLAUDE.md](commands/players/CLAUDE.md)** - Player information commands (/player)
- **[commands/teams/CLAUDE.md](commands/teams/CLAUDE.md)** - Team information and roster commands (/team, /teams, /roster)
- **[commands/transactions/CLAUDE.md](commands/transactions/CLAUDE.md)** - Transaction management commands (/mymoves, /legal)
- **[commands/voice/CLAUDE.md](commands/voice/CLAUDE.md)** - Voice channel management commands (/voice-channel)
- **[commands/help/CLAUDE.md](commands/help/CLAUDE.md)** - Help system commands (/help, /help-create, /help-edit, /help-delete, /help-list)
## 🏗️ Project Overview
**Discord Bot v2.0** is a comprehensive Discord bot for managing a Strat-o-Matic Baseball Association (SBA) fantasy league. Built with discord.py and modern async Python patterns.
### Core Architecture
- **Command System**: Modular, package-based command organization
- **Logging**: Structured logging with Discord context integration
- **Services**: Clean service layer for API interactions
- **Caching**: Optional Redis caching for performance
- **Error Handling**: Comprehensive error handling with user-friendly messages
## 🚀 Development Commands
### Local Development
```bash
# Start the bot
python bot.py
# Install dependencies
pip install -r requirements.txt
# Run tests
python -m pytest --tb=short -q
# Run specific test files
python -m pytest tests/test_commands_transactions.py -v --tb=short
python -m pytest tests/test_services.py -v
```
### Testing Commands
```bash
# Full test suite (should pass all tests)
python -m pytest --tb=short -q
# Test specific components
python -m pytest tests/test_utils_decorators.py -v
python -m pytest tests/test_models.py -v
python -m pytest tests/test_api_client_with_aioresponses.py -v
```
## 📁 Project Structure
```
discord-app-v2/
├── CLAUDE.md files (check these first!)
├── bot.py # Main bot entry point
├── commands/ # Discord slash commands
│ ├── CLAUDE.md # 🔍 Command architecture guide
│ ├── league/ # League-wide commands
│ │ ├── CLAUDE.md # 🔍 League command reference
│ │ └── info.py # /league command
│ ├── players/ # Player information commands
│ │ ├── CLAUDE.md # 🔍 Player command reference
│ │ └── info.py # /player command
│ ├── teams/ # Team information commands
│ │ ├── CLAUDE.md # 🔍 Team command reference
│ │ ├── info.py # /team, /teams commands
│ │ └── roster.py # /roster command
│ ├── transactions/ # Transaction management
│ │ ├── CLAUDE.md # 🔍 Transaction command reference
│ │ └── management.py # /mymoves, /legal commands
│ └── voice/ # Voice channel management
│ ├── CLAUDE.md # 🔍 Voice command reference
│ ├── channels.py # /voice-channel commands
│ ├── cleanup_service.py # Automatic channel cleanup
│ └── tracker.py # JSON-based channel tracking
├── services/ # API service layer
│ └── CLAUDE.md # 🔍 Service layer documentation
├── models/ # Pydantic data models
│ └── CLAUDE.md # 🔍 Model patterns and validation
├── tasks/ # Background automated tasks
│ └── CLAUDE.md # 🔍 Task system documentation
├── utils/ # Utility functions and helpers
│ ├── CLAUDE.md # 🔍 Utils documentation
│ ├── logging.py # Structured logging system
│ ├── decorators.py # Command and caching decorators
│ └── cache.py # Redis caching system
├── views/ # Discord UI components
│ └── CLAUDE.md # 🔍 UI components and embeds
├── tests/ # Test suite
│ └── CLAUDE.md # 🔍 Testing guide and patterns
└── logs/ # Log files
```
## 🔧 Key Development Patterns
### 1. Command Implementation with @logged_command Decorator
**✅ Recommended Pattern:**
```python
from utils.decorators import logged_command
from utils.logging import get_contextual_logger
class YourCommandCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.YourCommandCog') # Required
@discord.app_commands.command(name="example")
@logged_command("/example") # Add this decorator
async def your_command(self, interaction, param: str):
# Only business logic - no try/catch/finally boilerplate needed
# Decorator handles all logging, timing, and error handling
result = await some_service.get_data(param)
embed = create_embed(result)
await interaction.followup.send(embed=embed)
```
**Benefits:**
- Eliminates ~15-20 lines of boilerplate per command
- Automatic trace ID generation and request correlation
- Consistent error handling and operation timing
- Standardized logging patterns
### 2. Service Layer with Caching
**✅ Service Implementation:**
```python
from utils.decorators import cached_api_call
from services.base_service import BaseService
class PlayerService(BaseService[Player]):
@cached_api_call(ttl=600) # Cache for 10 minutes
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
```
### 3. Structured Logging
**✅ Logging Pattern:**
```python
from utils.logging import get_contextual_logger, set_discord_context
# In command handlers
set_discord_context(interaction=interaction, command="/example", param_value=param)
trace_id = self.logger.start_operation("operation_name")
self.logger.info("Operation started", context_param=value)
# ... business logic ...
self.logger.end_operation(trace_id, "completed")
```
### 4. Autocomplete Pattern (REQUIRED)
**✅ Recommended Pattern - Standalone Function:**
```python
# Define autocomplete function OUTSIDE the class
async def entity_name_autocomplete(
interaction: discord.Interaction,
current: str,
) -> List[app_commands.Choice[str]]:
"""Autocomplete for entity names."""
try:
# Get matching entities from service
entities = await service.get_entities_for_autocomplete(current, limit=25)
return [
app_commands.Choice(name=entity.display_name, value=entity.name)
for entity in entities
]
except Exception:
# Return empty list on error to avoid breaking autocomplete
return []
class YourCommandCog(commands.Cog):
@app_commands.command(name="command")
@app_commands.autocomplete(parameter_name=entity_name_autocomplete) # Reference function
@logged_command("/command")
async def your_command(self, interaction, parameter_name: str):
# ... command implementation ...
```
**Benefits:**
- Consistent pattern across all commands
- Reusable autocomplete functions
- Cleaner code organization
- Easier testing and maintenance
**❌ Avoid Method-Based Autocomplete:**
```python
# DON'T USE THIS PATTERN
class YourCommandCog(commands.Cog):
@app_commands.command(name="command")
async def your_command(self, interaction, parameter: str):
pass
@your_command.autocomplete('parameter') # Avoid this pattern
async def parameter_autocomplete(self, interaction, current: str):
pass
```
**Reference Implementation:**
- See `commands/players/info.py:20-67` for the canonical example
- See `commands/help/main.py:30-48` for help topic autocomplete
### 5. Discord Embed Usage - Emoji Best Practices
**CRITICAL:** Avoid double emoji in Discord embeds by following these guidelines:
#### Template Method Emoji Prefixes
The following `EmbedTemplate` methods **automatically add emoji prefixes** to titles:
- `EmbedTemplate.success()` → adds `✅` prefix
- `EmbedTemplate.error()` → adds `❌` prefix
- `EmbedTemplate.warning()` → adds `⚠️` prefix
- `EmbedTemplate.info()` → adds `` prefix
- `EmbedTemplate.loading()` → adds `⏳` prefix
#### ✅ CORRECT Usage Patterns
```python
# ✅ CORRECT - Using template method without emoji in title
embed = EmbedTemplate.success(
title="Operation Completed",
description="Your action was successful"
)
# Result: "✅ Operation Completed"
# ✅ CORRECT - Custom emoji with create_base_embed
embed = EmbedTemplate.create_base_embed(
title="🎉 Special Success Message",
description="Using a different emoji",
color=EmbedColors.SUCCESS
)
# Result: "🎉 Special Success Message"
```
#### ❌ WRONG Usage Patterns
```python
# ❌ WRONG - Double emoji
embed = EmbedTemplate.success(
title="✅ Operation Completed", # Will result in "✅ ✅ Operation Completed"
description="Your action was successful"
)
# ❌ WRONG - Emoji in title with auto-prefix method
embed = EmbedTemplate.error(
title="❌ Failed", # Will result in "❌ ❌ Failed"
description="Something went wrong"
)
```
#### Rules for Embed Creation
1. **When using template methods** (`success()`, `error()`, `warning()`, `info()`, `loading()`):
- ❌ **DON'T** include emojis in the title parameter
- ✅ **DO** use plain text titles
- The template method will add the appropriate emoji automatically
2. **When you want custom emojis**:
- ✅ **DO** use `EmbedTemplate.create_base_embed()`
- ✅ **DO** specify the appropriate color parameter
- You have full control over the title including custom emojis
3. **Code review checklist**:
- Check all `EmbedTemplate.success/error/warning/info/loading()` calls
- Verify titles don't contain emojis
- Ensure `create_base_embed()` is used for custom emoji titles
#### Common Mistakes to Avoid
```python
# ❌ BAD: Double warning emoji
embed = EmbedTemplate.warning(
title="⚠️ Delete Command" # Results in "⚠️ ⚠️ Delete Command"
)
# ✅ GOOD: Template adds emoji automatically
embed = EmbedTemplate.warning(
title="Delete Command" # Results in "⚠️ Delete Command"
)
# ❌ BAD: Different emoji with auto-prefix method
embed = EmbedTemplate.error(
title="🗑️ Deleted" # Results in "❌ 🗑️ Deleted"
)
# ✅ GOOD: Use create_base_embed for custom emoji
embed = EmbedTemplate.create_base_embed(
title="🗑️ Deleted", # Results in "🗑️ Deleted"
color=EmbedColors.ERROR
)
```
**Reference Files**:
- `views/embeds.py` - EmbedTemplate implementation
- `DOUBLE_EMOJI_AUDIT.md` - Complete audit of double emoji issues (January 2025)
- Fixed files: `tasks/custom_command_cleanup.py`, `views/help_commands.py`, `views/custom_commands.py`
## 🎯 Development Guidelines
### Git Workflow - CRITICAL
**🚨 NEVER make code changes directly to the `main` branch. Always work in a separate branch.**
#### Branch Workflow
1. **Create a new branch** for all code changes:
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description
```
2. **Make your changes** in the feature branch
3. **Commit your changes** following the commit message format:
```bash
git commit -m "CLAUDE: Your commit message
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
4. **Push your branch** to the remote repository:
```bash
git push -u origin feature/your-feature-name
```
5. **Create a Pull Request** to merge into `main` when ready
**Why This Matters:**
- Protects the `main` branch from broken code
- Allows code review before merging
- Makes it easy to revert changes if needed
- Enables parallel development on multiple features
- Maintains a clean, stable main branch
**Exception:** Only emergency hotfixes or critical production issues may warrant direct commits to `main`, and only with explicit authorization.
### Before Making Changes
1. **Check CLAUDE.md files** in current and parent directories
2. **Review existing patterns** in similar commands/services
3. **Follow the @logged_command decorator pattern** for new commands
4. **Use structured logging** with contextual information
5. **Add appropriate caching** for expensive operations
### Command Development
- **Always use `@logged_command` decorator** for Discord commands
- **Ensure command class has `self.logger`** in `__init__`
- **Focus on business logic** - decorator handles boilerplate
- **Use EmbedTemplate** for consistent Discord embed styling
- **CRITICAL: Follow emoji best practices** - see "Discord Embed Usage - Emoji Best Practices" section
- **Follow error handling patterns** from existing commands
- **Use standalone functions for autocomplete** - see Autocomplete Pattern below
### Testing Requirements
- **All tests should pass**: Run `python -m pytest --tb=short -q`
- **Use aioresponses** for HTTP client testing (see `tests/CLAUDE.md`)
- **Provide complete model data** that satisfies Pydantic validation
- **Follow testing patterns** documented in `tests/CLAUDE.md`
### Model Requirements
- **Database entities require `id` fields** since they're always fetched from database
- **Use explicit None checks** (`if obj is None:`) for better type safety
- **Required fields eliminate Optional type issues**
## 🔍 Key Files to Reference
### When Adding New Commands
1. **Read `commands/CLAUDE.md`** - Command architecture and patterns
2. **Check existing command files** in relevant package (league/players/teams/transactions)
3. **Review package-specific CLAUDE.md** files for implementation details
4. **Follow `@logged_command` decorator patterns**
### When Working with Services
1. **Check `services/` directory** for existing service patterns
2. **Use `BaseService[T]` inheritance** for consistent API interactions
3. **Add caching decorators** for expensive operations
4. **Follow error handling patterns**
### When Writing Tests
1. **Read `tests/CLAUDE.md`** thoroughly - contains crucial testing patterns
2. **Use aioresponses** for HTTP mocking, not manual AsyncMock
3. **Provide complete model data** with helper functions
4. **Test both success and error scenarios**
### When Working with Utilities
1. **Read `utils/CLAUDE.md`** for logging, caching, and decorators
2. **Use structured logging** with `get_contextual_logger()`
3. **Leverage caching decorators** for performance optimization
4. **Follow established patterns** for Discord context setting
## 🚨 Critical Reminders
### Decorator Migration (Completed)
- **All commands use `@logged_command`** - eliminates boilerplate logging code
- **Standardizes error handling** and operation timing across all commands
- **See existing commands** for implementation patterns
### Model Breaking Changes (Implemented)
- **Database entities require `id` fields** - test cases must provide ID values
- **Use `Player(id=123, ...)` and `Team(id=456, ...)` in tests**
- **No more Optional[int] warnings** - improved PyLance type safety
### Redis Caching (Available)
- **Optional caching infrastructure** - graceful fallback without Redis
- **Use `@cached_api_call()` and `@cached_single_item()` decorators**
- **Zero breaking changes** - all existing functionality preserved
### Code Quality Requirements
- **Pylance warnings are disabled for optional values** - always either return a value or raise an Exception
- **Use "Raise or Return" pattern** - do not return optional values unless specifically required
- **Favor Python dataclasses** over standard classes unless there's a technical limitation
## 🧪 Testing Strategy
### Current Test Coverage
- **44 comprehensive tests** covering core functionality
- **HTTP testing with aioresponses** - reliable async HTTP mocking
- **Service layer testing** - complete model validation
- **All tests should pass** - run `python -m pytest --tb=short -q`
### Test Files by Component
- `test_utils_decorators.py` - Decorator functionality
- `test_models.py` - Pydantic model validation
- `test_services.py` - Service layer operations (25 tests)
- `test_api_client_with_aioresponses.py` - HTTP client operations (19 tests)
## 🔧 Environment Variables
### Required
```bash
BOT_TOKEN=your_discord_bot_token
API_TOKEN=your_database_api_token
DB_URL=http://localhost:8000
GUILD_ID=your_discord_server_id
LOG_LEVEL=INFO
```
### Optional (Caching)
```bash
REDIS_URL=redis://localhost:6379 # Empty disables caching
REDIS_CACHE_TTL=300 # Default TTL in seconds
```
## 📊 Monitoring and Logs
### Log Files
- **`logs/discord_bot_v2.log`** - Human-readable logs
- **`logs/discord_bot_v2.json`** - Structured JSON logs for analysis
### Log Analysis Examples
```bash
# Find all errors
jq 'select(.level == "ERROR")' logs/discord_bot_v2.json
# Track specific request
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json
# Find slow operations
jq 'select(.extra.duration_ms > 5000)' logs/discord_bot_v2.json
```
## 🎯 Future AI Agent Instructions
### Always Start Here
1. **Read this CLAUDE.md file completely**
2. **Check CLAUDE.md files** in current and parent directories
3. **Review relevant package-specific documentation**
4. **Understand existing patterns** before implementing changes
### Documentation Priority
1. **Current directory CLAUDE.md** - Most specific context
2. **Parent directory CLAUDE.md** - Package-level context
3. **Root project CLAUDE.md** - Overall project guidance
4. **Component-specific CLAUDE.md** files - Detailed implementation guides
### Implementation Checklist
- [ ] Reviewed relevant CLAUDE.md files
- [ ] Followed existing command/service patterns
- [ ] Used `@logged_command` decorator for new commands
- [ ] Added structured logging with context
- [ ] Included appropriate error handling
- [ ] **Followed Discord Embed emoji best practices** (no double emojis)
- [ ] Added tests following established patterns
- [ ] Verified all tests pass
## 🔄 Recent Major Enhancements (January 2025)
### Custom Help Commands System (January 2025)
**Comprehensive admin-managed help system for league documentation**:
- **Full CRUD Operations**: Create, read, update, and delete help topics via Discord commands
- **Permission System**: Administrators and "Help Editor" role can manage content
- **Rich Features**:
- Markdown-formatted content (up to 4000 characters)
- Category organization (rules, guides, resources, info, faq)
- View tracking and analytics
- Soft delete with restore capability
- Full audit trail (creator, editor, timestamps)
- Autocomplete for topic discovery
- Paginated list views
- **Interactive UI**: Modals for creation/editing, confirmation dialogs for deletion
- **Replaces**: Planned `/links` command with more flexible solution
- **Commands**: `/help`, `/help-create`, `/help-edit`, `/help-delete`, `/help-list`
- **Documentation**: See `commands/help/CLAUDE.md` for complete reference
- **Database**: Requires `help_commands` table migration (see `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`)
**Key Components**:
- **Model**: `models/help_command.py` - Pydantic models with validation
- **Service**: `services/help_commands_service.py` - Business logic and API integration
- **Views**: `views/help_commands.py` - Interactive modals and list views
- **Commands**: `commands/help/main.py` - Command handlers with @logged_command decorator
### Enhanced Transaction Builder with sWAR Tracking
**Major upgrade to transaction management system**:
- **Comprehensive sWAR Calculations**: Now tracks Major League and Minor League sWAR projections
- **Pre-existing Transaction Support**: Accounts for scheduled moves when validating new transactions
- **Organizational Team Matching**: Proper handling of PORMIL, PORIL transactions for POR teams
- **Enhanced User Interface**: Transaction embeds show complete context including pre-existing moves
### Team Model Organizational Affiliates
**New Team model methods for organizational relationships**:
- **`team.major_league_affiliate()`**: Get ML team via API
- **`team.minor_league_affiliate()`**: Get MiL team via API
- **`team.injured_list_affiliate()`**: Get IL team via API
- **`team.is_same_organization()`**: Check if teams belong to same organization
### Key Benefits for Users
- **Complete Transaction Context**: Users see impact of both current and scheduled moves
- **Accurate sWAR Projections**: Better strategic decision-making with team strength data
- **Improved Validation**: Proper roster type detection for all transaction types
- **Enhanced UX**: Clean, informative displays with contextual information
### Technical Improvements
- **API Efficiency**: Cached data to avoid repeated calls
- **Backwards Compatibility**: All existing functionality preserved
- **Error Handling**: Graceful fallbacks and proper exception handling
- **Testing**: Comprehensive test coverage for new functionality
### Critical Bug Fixes (January 2025)
#### 1. Trade Channel Creation Permission Error (Fixed)
**Issue**: Trade channels failed to create with Discord API error 50013 "Missing Permissions"
**Root Cause**: The bot was attempting to grant itself `manage_channels` and `manage_permissions` in channel-specific permission overwrites during channel creation. Discord prohibits bots from self-granting elevated permissions in channel overwrites as a security measure.
**Fix**: Removed `manage_channels` and `manage_permissions` from bot's channel-specific overwrites in `commands/transactions/trade_channels.py:74-77`. The bot's server-level permissions are sufficient for all channel management operations.
**Impact**: Trade discussion channels now create successfully. All channel management features (create, delete, permission updates) work correctly with server-level permissions only.
**Files Changed**:
- `commands/transactions/trade_channels.py` - Simplified bot permission overwrites
- `commands/transactions/CLAUDE.md` - Documented permission requirements and fix
#### 2. TeamService Method Name AttributeError (Fixed)
**Issue**: Bot crashed with `AttributeError: 'TeamService' object has no attribute 'get_team_by_id'` when adding players to trades
**Root Cause**: Code was calling non-existent method `team_service.get_team_by_id()`. The correct method name is `team_service.get_team()`.
**Fix**: Updated method call in `services/trade_builder.py:201` and all corresponding test mocks in `tests/test_services_trade_builder.py`.
**Impact**: Adding players to trades now works correctly. All 18 trade builder tests pass.
**Files Changed**:
- `services/trade_builder.py` - Changed `get_team_by_id()` to `get_team()`
- `tests/test_services_trade_builder.py` - Updated all test mocks
- `services/CLAUDE.md` - Documented correct TeamService method names
**Prevention**: Added clear documentation in `services/CLAUDE.md` showing correct TeamService method signatures to prevent future confusion.
### Critical Bug Fixes (October 2025)
#### 1. Custom Command Execution Validation Error (Fixed)
**Issue**: Custom commands failed to execute with Pydantic validation error: `creator_id Field required`
**Root Cause**: The API's `/custom_commands/by_name/{name}/execute` endpoint returns command data **without** the `creator_id` field, but the `CustomCommand` model required this field. This caused validation to fail when parsing the execute endpoint response.
**Fix**: Made `creator_id` field optional in the `CustomCommand` model:
```python
# Before
creator_id: int = Field(..., description="ID of the creator")
# After
creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
```
**Impact**: Custom commands (`/cc`) now execute successfully. The execute endpoint only needs to return command content, not creator information. Permission checks (edit/delete) use different endpoints that include full creator data.
**Files Changed**:
- `models/custom_command.py:30` - Made `creator_id` optional
- `models/CLAUDE.md` - Documented Custom Command Model changes
**Why This Design Works**:
- Execute endpoint: Returns minimal data (name, content, use_count) for fast execution
- Get/Create/Update/Delete endpoints: Return complete data including creator information
- Permission checks always use endpoints with full creator data
#### 2. Admin Commands Type Safety Issues (Fixed)
**Issue**: Multiple Pylance type errors in `commands/admin/management.py` causing potential runtime errors
**Root Causes**:
1. Accessing `guild_permissions` on `User` type (only exists on `Member`)
2. Passing `Optional[int]` to `discord.Object(id=...)` which requires `int`
3. Calling `purge()` on channel types that don't support it
4. Calling `delete()` on potentially `None` message
5. Passing `None` to `content` parameter that requires `str`
**Fixes**:
1. Added `isinstance(interaction.user, discord.Member)` check before accessing `guild_permissions`
2. Added explicit None check: `if not interaction.guild_id: raise ValueError(...)`
3. Added channel type validation: `isinstance(interaction.channel, (discord.TextChannel, discord.Thread, ...))`
4. Added None check: `if message:` before calling `message.delete()`
5. Changed from conditional variable to explicit if/else branches for `content` parameter
6. Removed unused imports (`Optional`, `Union`)
**Impact**: All admin commands now have proper type safety. No more Pylance warnings, and code is more robust against edge cases.
**Files Changed**:
- `commands/admin/management.py:26-42` - Guild permissions and member checks
- `commands/admin/management.py:246-253` - Guild ID validation
- `commands/admin/management.py:361-366` - Channel type validation
- `commands/admin/management.py:389-393` - Message None check
- `commands/admin/management.py:436-439` - Content parameter handling
- `commands/admin/management.py:6` - Removed unused imports
**Prevention**: Follow Discord.py type patterns - always check for `Member` vs `User`, validate optional IDs before use, and verify channel types support the operations you're calling.
---
**Last Updated:** October 2025
**Maintenance:** Keep this file synchronized with CLAUDE.md files when making significant architectural changes
**Next Review:** When major new features or patterns are added
This CLAUDE.md file serves as the central reference point for AI development agents. Always consult the referenced CLAUDE.md files for the most detailed and current information about specific components and implementation patterns.

699
commands/CLAUDE.md Normal file
View File

@ -0,0 +1,699 @@
# Commands Package Documentation
**Discord Bot v2.0 - Scalable Command Architecture**
This document outlines the command architecture, patterns, and best practices established for the SBA Discord Bot v2.0.
## 📁 Architecture Overview
### **Package Structure**
```
commands/
├── README.md # This documentation
├── __init__.py # Future: Global command utilities
└── players/ # Player-related commands
├── __init__.py # Package setup with resilient loading
└── info.py # Player information commands
```
### **Current Implementation Status (October 2025)**
#### **Implemented Packages**
```
commands/
├── README.md # This documentation
├── __init__.py
├── players/ # ✅ COMPLETED - Player information
│ ├── README.md # Player commands documentation
│ ├── __init__.py
│ └── info.py # /player command
├── teams/ # ✅ COMPLETED - Team information
│ ├── README.md # Team commands documentation
│ ├── __init__.py
│ ├── info.py # /team, /teams commands
│ └── roster.py # /roster command
├── league/ # ✅ COMPLETED - League-wide features
│ ├── README.md # League commands documentation
│ ├── __init__.py
│ ├── info.py # /league command
│ ├── standings.py # /standings command
│ ├── schedule.py # /schedule command
│ └── submit_scorecard.py # /submit-scorecard command
├── transactions/ # ✅ COMPLETED - Trade & roster moves
│ ├── README.md # Transaction commands documentation
│ ├── __init__.py
│ ├── management.py # /mymoves, /legal commands
│ ├── trade.py # /trade command with builder UI
│ ├── trade_channels.py # /trade-channel commands
│ ├── trade_channel_tracker.py # Channel tracking service
│ └── dropadd.py # /dropadd command
├── admin/ # ✅ COMPLETED - Admin tools
│ ├── __init__.py
│ ├── management.py # /sync, /shutdown commands
│ └── users.py # User management commands
├── custom_commands/ # ✅ COMPLETED - Custom command system
│ ├── __init__.py
│ └── main.py # /custom commands CRUD
├── help/ # ✅ COMPLETED - Help system
│ ├── README.md # Help commands documentation
│ ├── __init__.py
│ └── main.py # /help, /help-create, /help-edit, etc.
├── profile/ # ✅ COMPLETED - User profiles
│ ├── README.md # Profile commands documentation
│ ├── __init__.py
│ └── main.py # /profile commands
├── injuries/ # ✅ COMPLETED - Injury management
│ ├── README.md # Injury commands documentation
│ ├── __init__.py
│ └── management.py # /injury commands
├── dice/ # ✅ COMPLETED - Dice rolling
│ ├── __init__.py
│ └── rolls.py # /roll, /dice commands
├── voice/ # ✅ COMPLETED - Voice channel management
│ ├── README.md # Voice commands documentation
│ ├── __init__.py
│ ├── channels.py # /voice-channel commands
│ ├── tracker.py # Channel tracking
│ └── cleanup_service.py # Automated cleanup task
├── utilities/ # ✅ COMPLETED - Utility commands
│ ├── README.md # Utilities documentation
│ ├── __init__.py
│ └── charts.py # /chart commands
├── soak/ # ✅ COMPLETED - SOAK features
│ ├── __init__.py
│ └── main.py # SOAK-related commands
└── examples/ # 📚 REFERENCE - Migration examples
├── __init__.py
├── migration_example.py # Example migration patterns
└── enhanced_player.py # Enhanced command example
```
#### **Package Documentation Quick Reference**
Each package has its own detailed README.md with implementation specifics:
| Package | README | Key Commands |
|---------|--------|--------------|
| **players/** | [README.md](players/README.md) | `/player` - Player information and stats |
| **teams/** | [README.md](teams/README.md) | `/team`, `/teams`, `/roster` - Team info and rosters |
| **league/** | [README.md](league/README.md) | `/league`, `/standings`, `/schedule`, `/submit-scorecard` |
| **transactions/** | [README.md](transactions/README.md) | `/trade`, `/mymoves`, `/legal`, `/trade-channel`, `/dropadd` |
| **help/** | [README.md](help/README.md) | `/help`, `/help-create`, `/help-edit`, `/help-delete`, `/help-list` |
| **profile/** | [README.md](profile/README.md) | `/profile` - User profile management |
| **injuries/** | [README.md](injuries/README.md) | `/injury` - Injury management and tracking |
| **voice/** | [README.md](voice/README.md) | `/voice-channel` - Voice channel creation and cleanup |
| **utilities/** | [README.md](utilities/README.md) | `/chart` - Chart and utility commands |
#### **Future Expansion Ideas**
- **Draft System** - `/draft` commands for draft management
- **Advanced Stats** - `/player-compare`, `/player-rankings`, `/leaderboard`
- **Team Analytics** - `/team-stats`, `/team-leaders`
- **League Leaders** - `/leaders`, `/awards`
- **Historical Data** - `/history`, `/records`
## 🏗️ Design Principles
### **1. Single Responsibility**
- Each file handles 2-4 closely related commands
- Clear logical grouping by domain (players, teams, etc.)
- Focused functionality reduces complexity
### **2. Resilient Loading**
- One failed cog doesn't break the entire package
- Loop-based loading with comprehensive error handling
- Clear logging for debugging and monitoring
### **3. Scalable Architecture**
- Easy to add new packages and cogs
- Consistent patterns across all command groups
- Future-proof structure for bot growth
### **4. Modern Discord.py Patterns**
- Application commands (slash commands) only
- Proper error handling with user-friendly messages
- Async/await throughout
- Type hints and comprehensive documentation
## 🔧 Implementation Patterns
### **Command Package Structure**
#### **Individual Command File (e.g., `players/info.py`)**
```python
"""
Player Information Commands
Implements slash commands for displaying player information and statistics.
"""
import logging
from typing import Optional
import discord
from discord.ext import commands
from services.player_service import player_service
from exceptions import BotException
logger = logging.getLogger(f'{__name__}.PlayerInfoCommands')
class PlayerInfoCommands(commands.Cog):
"""Player information and statistics command handlers."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@discord.app_commands.command(
name="player",
description="Display player information and statistics"
)
@discord.app_commands.describe(
name="Player name to search for",
season="Season to show stats for (defaults to current season)"
)
async def player_info(
self,
interaction: discord.Interaction,
name: str,
season: Optional[int] = None
):
"""Display player card with statistics."""
try:
# Always defer for potentially slow API calls
await interaction.response.defer()
# Command implementation here
# Use logger for error logging
# Create Discord embeds for responses
except Exception as e:
logger.error(f"Player info command error: {e}", exc_info=True)
error_msg = "❌ Error retrieving player information."
if interaction.response.is_done():
await interaction.followup.send(error_msg, ephemeral=True)
else:
await interaction.response.send_message(error_msg, ephemeral=True)
async def setup(bot: commands.Bot):
"""Load the player info commands cog."""
await bot.add_cog(PlayerInfoCommands(bot))
```
#### **Package __init__.py with Resilient Loading**
```python
"""
Player Commands Package
This package contains all player-related Discord commands organized into focused modules.
"""
import logging
from discord.ext import commands
from .info import PlayerInfoCommands
# Future imports:
# from .search import PlayerSearchCommands
# from .stats import PlayerStatsCommands
logger = logging.getLogger(__name__)
async def setup_players(bot: commands.Bot):
"""
Setup all player command modules.
Returns:
tuple: (successful_count, failed_count, failed_modules)
"""
# Define all player command cogs to load
player_cogs = [
("PlayerInfoCommands", PlayerInfoCommands),
# Future cogs:
# ("PlayerSearchCommands", PlayerSearchCommands),
# ("PlayerStatsCommands", PlayerStatsCommands),
]
successful = 0
failed = 0
failed_modules = []
for cog_name, cog_class in player_cogs:
try:
await bot.add_cog(cog_class(bot))
logger.info(f"✅ Loaded {cog_name}")
successful += 1
except Exception as e:
logger.error(f"❌ Failed to load {cog_name}: {e}", exc_info=True)
failed += 1
failed_modules.append(cog_name)
# Log summary
if failed == 0:
logger.info(f"🎉 All {successful} player command modules loaded successfully")
else:
logger.warning(f"⚠️ Player commands loaded with issues: {successful} successful, {failed} failed")
return successful, failed, failed_modules
# Export the setup function for easy importing
__all__ = ['setup_players', 'PlayerInfoCommands']
```
## 🔄 Smart Command Syncing
### **Hash-Based Change Detection**
The bot implements smart command syncing that only updates Discord when commands actually change:
**Development Mode:**
- Automatically detects command changes using SHA-256 hashing
- Only syncs when changes are detected
- Saves hash to `.last_command_hash` for comparison
- Prevents unnecessary Discord API calls
**Production Mode:**
- No automatic syncing
- Commands must be manually synced using `/sync` command
- Prevents accidental command updates in production
### **How It Works**
1. **Hash Generation**: Creates hash of command names, descriptions, and parameters
2. **Comparison**: Compares current hash with stored hash from `.last_command_hash`
3. **Conditional Sync**: Only syncs if hashes differ or no previous hash exists
4. **Hash Storage**: Saves new hash after successful sync
### **Benefits**
- ✅ **API Efficiency**: Avoids Discord rate limits
- ✅ **Development Speed**: Fast restarts when no command changes
- ✅ **Production Safety**: No accidental command updates
- ✅ **Consistency**: Commands stay consistent across restarts
## 🚀 Bot Integration
### **Command Loading in bot.py**
```python
async def setup_hook(self):
"""Called when the bot is starting up."""
# Load command packages
await self._load_command_packages()
# Smart command syncing: auto-sync in development if changes detected
config = get_config()
if config.is_development:
if await self._should_sync_commands():
self.logger.info("Development mode: changes detected, syncing commands...")
await self._sync_commands()
await self._save_command_hash()
else:
self.logger.info("Development mode: no command changes detected, skipping sync")
else:
self.logger.info("Production mode: commands loaded but not auto-synced")
async def _load_command_packages(self):
"""Load all command packages with resilient error handling."""
from commands.players import setup_players
from commands.teams import setup_teams
from commands.league import setup_league
from commands.custom_commands import setup_custom_commands
from commands.admin import setup_admin
from commands.transactions import setup_transactions
from commands.dice import setup_dice
from commands.voice import setup_voice
from commands.utilities import setup_utilities
from commands.help import setup_help_commands
from commands.profile import setup_profile_commands
from commands.soak import setup_soak
from commands.injuries import setup_injuries
# Define command packages to load (current implementation)
command_packages = [
("players", setup_players),
("teams", setup_teams),
("league", setup_league),
("custom_commands", setup_custom_commands),
("admin", setup_admin),
("transactions", setup_transactions),
("dice", setup_dice),
("voice", setup_voice),
("utilities", setup_utilities),
("help", setup_help_commands),
("profile", setup_profile_commands),
("soak", setup_soak),
("injuries", setup_injuries),
]
# Loop-based loading with error isolation
total_successful = 0
total_failed = 0
for package_name, setup_func in command_packages:
try:
successful, failed, failed_modules = await setup_func(self)
total_successful += successful
total_failed += failed
if failed == 0:
self.logger.info(f"✅ {package_name} commands loaded successfully ({successful} cogs)")
else:
self.logger.warning(
f"⚠️ {package_name} commands loaded with issues: "
f"{successful} successful, {failed} failed"
)
if failed_modules:
self.logger.warning(f"Failed modules: {', '.join(failed_modules)}")
except Exception as e:
self.logger.error(f"❌ Failed to load {package_name} package: {e}", exc_info=True)
total_failed += 1
# Log final summary
if total_failed == 0:
self.logger.info(f"🎉 All command packages loaded successfully ({total_successful} total cogs)")
else:
self.logger.warning(
f"⚠️ Command loading completed with issues: "
f"{total_successful} successful, {total_failed} failed"
)
```
## 📋 Development Guidelines
### **Adding New Command Packages**
**Before Starting:** Review existing package README files for reference implementations:
- Check [Package Documentation Quick Reference](#package-documentation-quick-reference) for relevant examples
- Study similar packages for patterns (e.g., review `teams/` README when creating roster commands)
- Follow the `@logged_command` decorator pattern shown in existing commands
#### **1. Create Package Structure**
```bash
mkdir commands/your_package
touch commands/your_package/__init__.py
touch commands/your_package/your_command.py
touch commands/your_package/README.md # Document your package!
```
#### **2. Implement Command Module**
- Follow the pattern from existing packages (e.g., `players/info.py`, `teams/info.py`)
- **Use `@logged_command` decorator** - eliminates boilerplate error handling
- Use contextual logger: `self.logger = get_contextual_logger(f'{__name__}.ClassName')`
- Always defer responses: `await interaction.response.defer()`
- Type hints and comprehensive docstrings
- See `commands/examples/` for migration patterns
#### **3. Create Package Setup Function**
- Follow the pattern from existing `__init__.py` files (e.g., `players/__init__.py`, `teams/__init__.py`)
- Use loop-based cog loading with error isolation
- Return tuple: `(successful, failed, failed_modules)`
- Comprehensive logging with emojis for quick scanning
#### **4. Document Your Package**
- Create a `README.md` in your package directory
- Document commands, patterns, and implementation details
- Add to the [Package Documentation Quick Reference](#package-documentation-quick-reference) table
#### **5. Register in Bot**
- Add import to `_load_command_packages()` in `bot.py`
- Add to `command_packages` list
- Test in development environment
- Verify commands sync correctly
### **Adding Commands to Existing Packages**
#### **1. Create New Command Module**
```python
# commands/players/search.py
class PlayerSearchCommands(commands.Cog):
# Implementation
pass
async def setup(bot: commands.Bot):
await bot.add_cog(PlayerSearchCommands(bot))
```
#### **2. Update Package __init__.py**
```python
from .search import PlayerSearchCommands
# Add to player_cogs list
player_cogs = [
("PlayerInfoCommands", PlayerInfoCommands),
("PlayerSearchCommands", PlayerSearchCommands), # New cog
]
```
#### **3. Test Import Structure**
```python
# Verify imports work
from commands.players import setup_players
from commands.players.search import PlayerSearchCommands
```
## 🎯 Best Practices
### **Command Implementation**
1. **Always defer responses** for API calls: `await interaction.response.defer()`
2. **Use ephemeral responses** for errors: `ephemeral=True`
3. **Comprehensive error handling** with try/except blocks
4. **User-friendly error messages** with emojis
5. **Proper logging** with context and stack traces
6. **Type hints** on all parameters and return values
7. **Descriptive docstrings** for commands and methods
### **Package Organization**
1. **2-4 commands per file** maximum
2. **Logical grouping** by functionality/domain
3. **Consistent naming** patterns across packages
4. **Module-level logging** for clean, consistent logs
5. **Loop-based cog loading** for error resilience
6. **Comprehensive return values** from setup functions
### **Error Handling**
1. **Package-level isolation** - one failed cog doesn't break the package
2. **Clear error logging** with stack traces for debugging
3. **User-friendly messages** that don't expose internal errors
4. **Graceful degradation** when possible
5. **Metric reporting** for monitoring (success/failure counts)
## 📊 Monitoring & Metrics
### **Startup Logging**
The command loading system provides comprehensive metrics for all packages:
```
INFO - Loading players commands...
INFO - ✅ Loaded PlayerInfoCommands
INFO - 🎉 All 1 player command modules loaded successfully
INFO - ✅ players commands loaded successfully (1 cogs)
INFO - Loading teams commands...
INFO - ✅ Loaded TeamInfoCommands
INFO - ✅ Loaded TeamRosterCommands
INFO - 🎉 All 2 team command modules loaded successfully
INFO - ✅ teams commands loaded successfully (2 cogs)
[... similar output for all 13 packages ...]
INFO - 🎉 All command packages loaded successfully (N total cogs)
```
### **Error Scenarios**
```
ERROR - ❌ Failed to load PlayerInfoCommands: <error details>
WARNING - ⚠️ Player commands loaded with issues: 0 successful, 1 failed
WARNING - Failed modules: PlayerInfoCommands
```
### **Package-Specific Logs**
Each package maintains its own logging with package-level context. Check individual package README files for specific logging patterns and monitoring guidance.
### **Command Sync Logging**
```
INFO - Development mode: changes detected, syncing commands...
INFO - Synced 1 commands to guild 123456789
```
or
```
INFO - Development mode: no command changes detected, skipping sync
```
## 🔧 Troubleshooting
### **Common Issues**
#### **Import Errors**
- Check that `__init__.py` files exist in all packages
- Verify cog class names match imports
- Ensure service dependencies are available
#### **Command Not Loading**
- Check logs for specific error messages
- Verify cog is added to the package's cog list
- Test individual module imports in Python REPL
#### **Commands Not Syncing**
- Check if running in development mode (`config.is_development`)
- Verify `.last_command_hash` file permissions
- Use manual `/sync` command for troubleshooting
- Check Discord API rate limits
#### **Performance Issues**
- Monitor command loading times in logs
- Check for unnecessary API calls during startup
- Verify hash-based sync is working correctly
### **Debugging Tips**
1. **Use the logs** - comprehensive logging shows exactly what's happening
2. **Test imports individually** - isolate package/module issues
3. **Check hash file** - verify command change detection is working
4. **Monitor Discord API** - watch for rate limiting or errors
5. **Use development mode** - auto-sync helps debug command issues
## 📦 Command Groups Pattern
### **⚠️ CRITICAL: Use `app_commands.Group`, NOT `commands.GroupCog`**
Discord.py provides two ways to create command groups (e.g., `/injury roll`, `/injury clear`):
1. **`app_commands.Group`** ✅ **RECOMMENDED - Use this pattern**
2. **`commands.GroupCog`** ❌ **AVOID - Has interaction timing issues**
### **Why `commands.GroupCog` Fails**
`commands.GroupCog` has a critical bug that causes **duplicate interaction processing**, leading to:
- **404 "Unknown interaction" errors** when trying to defer/respond
- **Interaction already acknowledged errors** in error handlers
- **Commands fail randomly** even with proper error handling
**Root Cause:** GroupCog triggers the command handler twice for a single interaction, causing the first execution to consume the interaction token before the second execution can respond.
### **✅ Correct Pattern: `app_commands.Group`**
Use the same pattern as `ChartCategoryGroup` and `ChartManageGroup`:
```python
from discord import app_commands
from discord.ext import commands
from utils.decorators import logged_command
class InjuryGroup(app_commands.Group):
"""Injury management command group."""
def __init__(self):
super().__init__(
name="injury",
description="Injury management commands"
)
self.logger = get_contextual_logger(f'{__name__}.InjuryGroup')
@app_commands.command(name="roll", description="Roll for injury")
@logged_command("/injury roll")
async def injury_roll(self, interaction: discord.Interaction, player_name: str):
"""Roll for injury using player's injury rating."""
await interaction.response.defer()
# Command implementation
# No try/catch needed - @logged_command handles it
async def setup(bot: commands.Bot):
"""Setup function for loading the injury commands."""
bot.tree.add_command(InjuryGroup())
```
### **Key Differences**
| Feature | `app_commands.Group` ✅ | `commands.GroupCog` ❌ |
|---------|------------------------|------------------------|
| **Registration** | `bot.tree.add_command(Group())` | `await bot.add_cog(Cog(bot))` |
| **Initialization** | `__init__(self)` no bot param | `__init__(self, bot)` requires bot |
| **Decorator Support** | `@logged_command` works perfectly | Causes duplicate execution |
| **Interaction Handling** | Single execution, reliable | Duplicate execution, 404 errors |
| **Recommended Use** | ✅ All command groups | ❌ Never use |
### **Migration from GroupCog to Group**
If you have an existing `commands.GroupCog`, convert it:
1. **Change class inheritance:**
```python
# Before
class InjuryCog(commands.GroupCog, name="injury"):
def __init__(self, bot):
self.bot = bot
super().__init__()
# After
class InjuryGroup(app_commands.Group):
def __init__(self):
super().__init__(name="injury", description="...")
```
2. **Update registration:**
```python
# Before
await bot.add_cog(InjuryCog(bot))
# After
bot.tree.add_command(InjuryGroup())
```
3. **Remove duplicate interaction checks:**
```python
# Before (needed for GroupCog bug workaround)
if not interaction.response.is_done():
await interaction.response.defer()
# After (clean, simple)
await interaction.response.defer()
```
### **Working Examples**
**Good examples to reference:**
- `commands/utilities/charts.py` - `ChartManageGroup` and `ChartCategoryGroup`
- `commands/injuries/management.py` - `InjuryGroup`
Both use `app_commands.Group` successfully with `@logged_command` decorators.
## 🚦 Future Enhancements
### **Planned Features**
- **Permission Decorators**: Role-based command restrictions per package
- **Dynamic Loading**: Hot-reload commands without bot restart
- **Usage Metrics**: Command usage tracking and analytics
- **Rate Limiting**: Per-command rate limiting for resource management
### **Architecture Improvements**
- **Shared Utilities**: Common embed builders, decorators, helpers
- **Configuration**: Per-package configuration and feature flags
- **Testing**: Automated testing for command packages
- **Documentation**: Auto-generated command documentation
- **Monitoring**: Health checks and performance metrics per package
This architecture provides a solid foundation for scaling the Discord bot while maintaining code quality, reliability, and developer productivity.
---
## 📊 Current Status Summary
**Total Packages Implemented:** 13
- ✅ players - Player information commands
- ✅ teams - Team information and roster commands
- ✅ league - League-wide commands (standings, schedule, etc.)
- ✅ transactions - Trade and roster management
- ✅ admin - Admin and system commands
- ✅ custom_commands - Custom command system
- ✅ help - Help documentation system
- ✅ profile - User profile management
- ✅ injuries - Injury tracking and management
- ✅ dice - Dice rolling utilities
- ✅ voice - Voice channel management
- ✅ utilities - Chart and utility commands
- ✅ soak - SOAK feature commands
**Architecture Maturity:** Production-ready with comprehensive error handling, logging, and monitoring
---
**Last Updated:** October 2025 - Documentation updated to reflect all implemented packages
**Next Review:** When new major command packages are added

433
commands/help/CLAUDE.md Normal file
View File

@ -0,0 +1,433 @@
# Help Commands System
**Last Updated:** January 2025
**Status:** ✅ Fully Implemented
**Location:** `commands/help/`
## Overview
The Help Commands System provides a comprehensive, admin-managed help system for the Discord server. Administrators and designated "Help Editors" can create, edit, and manage custom help topics covering league documentation, resources, FAQs, links, and guides. This system replaces the originally planned `/links` command with a more flexible and powerful solution.
## Commands
### `/help [topic]`
**Description:** View a help topic or list all available topics
**Parameters:**
- `topic` (optional): Name of the help topic to view
**Behavior:**
- **With topic**: Displays the specified help topic with formatted content
- **Without topic**: Shows a paginated list of all available help topics organized by category
- Automatically increments view count when a topic is viewed
**Permissions:** Available to all server members
**Examples:**
```
/help trading-rules
/help
```
### `/help-create`
**Description:** Create a new help topic
**Permissions:** Administrators + "Help Editor" role
**Modal Fields:**
- **Topic Name**: URL-safe name (2-32 chars, letters/numbers/dashes only)
- **Display Title**: Human-readable title (1-200 chars)
- **Category**: Optional category (rules/guides/resources/info/faq)
- **Content**: Help content with markdown support (1-4000 chars)
**Features:**
- Real-time validation of all fields
- Preview before final creation
- Automatic duplicate detection
**Example:**
```
Topic Name: trading-rules
Display Title: Trading Rules & Guidelines
Category: rules
Content: [Detailed trading rules with markdown formatting]
```
### `/help-edit <topic>`
**Description:** Edit an existing help topic
**Parameters:**
- `topic` (required): Name of the help topic to edit
**Permissions:** Administrators + "Help Editor" role
**Features:**
- Pre-populated modal with current values
- Shows preview of changes before saving
- Tracks last editor and update timestamp
- Autocomplete for topic names
**Example:**
```
/help-edit trading-rules
```
### `/help-delete <topic>`
**Description:** Delete a help topic (soft delete)
**Parameters:**
- `topic` (required): Name of the help topic to delete
**Permissions:** Administrators + "Help Editor" role
**Features:**
- Confirmation dialog before deletion
- Shows topic statistics (view count)
- Soft delete (can be restored later)
- Autocomplete for topic names
**Example:**
```
/help-delete trading-rules
```
### `/help-list [category] [show_deleted]`
**Description:** Browse all help topics
**Parameters:**
- `category` (optional): Filter by category
- `show_deleted` (optional): Show soft-deleted topics (admin only)
**Permissions:** Available to all (show_deleted requires admin/help editor)
**Features:**
- Organized display by category
- Shows view counts
- Paginated interface for many topics
- Filtered views by category
**Examples:**
```
/help-list
/help-list category:rules
/help-list show_deleted:true
```
## Permission System
### Roles with Help Edit Permissions
1. **Server Administrators** - Full access to all help commands
2. **Help Editor Role** - Designated role with editing permissions
- Role name: "Help Editor" (configurable in `constants.py`)
- Can create, edit, and delete help topics
- Cannot view deleted topics unless also admin
### Permission Checks
```python
def has_help_edit_permission(interaction: discord.Interaction) -> bool:
"""Check if user can edit help commands."""
# Admin check
if interaction.user.guild_permissions.administrator:
return True
# Help Editor role check
role = discord.utils.get(interaction.guild.roles, name=HELP_EDITOR_ROLE_NAME)
if role and role in interaction.user.roles:
return True
return False
```
## Architecture
### Components
**Models** (`models/help_command.py`):
- `HelpCommand`: Main data model with validation
- `HelpCommandSearchFilters`: Search/filtering parameters
- `HelpCommandSearchResult`: Paginated search results
- `HelpCommandStats`: Statistics and analytics
**Service Layer** (`services/help_commands_service.py`):
- `HelpCommandsService`: CRUD operations and business logic
- `help_commands_service`: Global service instance
- Integrates with BaseService for API calls
**Views** (`views/help_commands.py`):
- `HelpCommandCreateModal`: Interactive creation modal
- `HelpCommandEditModal`: Interactive editing modal
- `HelpCommandDeleteConfirmView`: Deletion confirmation
- `HelpCommandListView`: Paginated topic browser
- `create_help_topic_embed()`: Formatted topic display
**Commands** (`commands/help/main.py`):
- `HelpCommands`: Cog with all command handlers
- Permission checking integration
- Autocomplete for topic names
- Error handling and user feedback
### Data Flow
1. **User Interaction** → Discord slash command
2. **Permission Check** → Validate user permissions
3. **Modal Display** → Interactive data input (for create/edit)
4. **Service Call** → Business logic and validation
5. **API Request** → Database operations via API
6. **Response** → Formatted embed with success/error message
### Database Integration
**API Endpoints** (via `../database/app/routers_v3/help_commands.py`):
- `GET /api/v3/help_commands` - List with filters
- `GET /api/v3/help_commands/{id}` - Get by ID
- `GET /api/v3/help_commands/by_name/{name}` - Get by name
- `POST /api/v3/help_commands` - Create
- `PUT /api/v3/help_commands/{id}` - Update
- `DELETE /api/v3/help_commands/{id}` - Soft delete
- `PATCH /api/v3/help_commands/{id}/restore` - Restore
- `PATCH /api/v3/help_commands/by_name/{name}/view` - Increment views
- `GET /api/v3/help_commands/autocomplete` - Autocomplete
- `GET /api/v3/help_commands/stats` - Statistics
**Database Table** (`help_commands`):
```sql
CREATE TABLE help_commands (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT,
created_by_discord_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
last_modified_by BIGINT,
is_active BOOLEAN DEFAULT TRUE,
view_count INTEGER DEFAULT 0,
display_order INTEGER DEFAULT 0
);
```
## Features
### Soft Delete
- Topics are never permanently deleted from the database
- `is_active` flag controls visibility
- Admins can restore deleted topics (future enhancement)
- Full audit trail preserved
### View Tracking
- Automatic view count increment when topics are accessed
- Statistics available via API
- Most viewed topics tracked
### Category Organization
- Optional categorization of topics
- Suggested categories:
- `rules` - League rules and regulations
- `guides` - How-to guides and tutorials
- `resources` - Links to external resources
- `info` - General league information
- `faq` - Frequently asked questions
### Markdown Support
- Full markdown formatting in content
- Support for:
- Headers
- Bold/italic text
- Lists (ordered and unordered)
- Links
- Code blocks
- Blockquotes
### Autocomplete
- Fast topic name suggestions
- Searches across names and titles
- Limited to 25 suggestions for performance
## Use Cases
### Example Help Topics
**Trading Rules** (`/help trading-rules`):
```markdown
# Trading Rules & Guidelines
## Trade Deadline
All trades must be completed by Week 15 of the regular season.
## Restrictions
- Maximum 3 trades per team per season
- All trades must be approved by league commissioner
- No trading draft picks beyond 2 seasons ahead
## How to Propose a Trade
Use the `/trade` command to propose a trade. Both teams must accept before the trade is processed.
```
**Discord Links** (`/help links`):
```markdown
# Important League Links
## Website
https://sba-league.com
## Google Sheet
https://docs.google.com/spreadsheets/...
## Discord Invite
https://discord.gg/...
## Rules Document
https://docs.google.com/document/...
```
**How to Trade** (`/help how-to-trade`):
```markdown
# How to Use the Trade System
1. Type `/trade` to start a new trade proposal
2. Select the team you want to trade with
3. Add players/picks to the trade
4. Submit for review
5. Both teams must accept
6. Commissioner approves
7. Trade is processed!
For more information, see `/help trading-rules`
```
## Error Handling
### Common Errors
**Topic Not Found**:
```
❌ Topic Not Found
No help topic named 'xyz' exists.
Use /help to see available topics.
```
**Permission Denied**:
```
❌ Permission Denied
Only administrators and users with the Help Editor role can create help topics.
```
**Topic Already Exists**:
```
❌ Topic Already Exists
A help topic named 'trading-rules' already exists.
Try a different name.
```
**Validation Errors**:
- Topic name too short/long
- Invalid characters in topic name
- Content too long (>4000 chars)
- Title too long (>200 chars)
## Best Practices
### For Administrators
1. **Use Clear Topic Names**
- Use lowercase with hyphens: `trading-rules`, `how-to-draft`
- Keep names short but descriptive
- Avoid special characters
2. **Organize by Category**
- Consistent category naming
- Group related topics together
- Use standard categories (rules, guides, resources, info, faq)
3. **Write Clear Content**
- Use markdown formatting for readability
- Keep content concise and focused
- Link to related topics when appropriate
- Update regularly to keep information current
4. **Monitor Usage**
- Check view counts to see popular topics
- Update frequently accessed topics
- Archive outdated information
### For Users
1. **Browse Topics**
- Use `/help` to see all available topics
- Use `/help-list` to browse by category
- Use autocomplete to find topics quickly
2. **Request New Topics**
- Contact admins or help editors
- Suggest topics that would be useful
- Provide draft content if possible
## Testing
### Test Coverage
- ✅ Model validation tests
- ✅ Service layer CRUD operations
- ✅ Permission checking
- ✅ Autocomplete functionality
- ✅ Soft delete behavior
- ✅ View count incrementing
### Test Files
- `tests/test_models_help_command.py`
- `tests/test_services_help_commands.py`
- `tests/test_commands_help.py`
## Future Enhancements
### Planned Features (Post-Launch)
- Restore command for deleted topics (`/help-restore <topic>`)
- Statistics dashboard (`/help-stats`)
- Search functionality across all content
- Topic versioning and change history
- Attachments support (images, files)
- Related topics linking
- User feedback and ratings
- Full-text search in content
### Potential Improvements
- Rich embed support with custom colors
- Topic aliases (multiple names for same topic)
- Scheduled topic updates
- Topic templates for common formats
- Import/export functionality
- Bulk operations for admins
## Migration from Legacy System
If migrating from an older help/links system:
1. **Export existing content** from old system
2. **Create help topics** using `/help-create`
3. **Test all topics** for formatting and accuracy
4. **Update documentation** to reference new commands
5. **Train help editors** on new system
6. **Announce to users** with usage instructions
## Support
### For Users
- Use `/help` to browse available topics
- Contact server admins for topic requests
- Report broken links or outdated information
### For Administrators
- Review the implementation plan in `.claude/HELP_COMMANDS_PLAN.md`
- Check database migration docs in `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`
- See main project documentation in `CLAUDE.md`
---
**Implementation Details:**
- **Models:** `models/help_command.py`
- **Service:** `services/help_commands_service.py`
- **Views:** `views/help_commands.py`
- **Commands:** `commands/help/main.py`
- **Constants:** `constants.py` (HELP_EDITOR_ROLE_NAME)
- **Tests:** `tests/test_*_help*.py`
**Related Documentation:**
- Implementation Plan: `.claude/HELP_COMMANDS_PLAN.md`
- Database Migration: `.claude/DATABASE_MIGRATION_HELP_COMMANDS.md`
- Project Overview: `CLAUDE.md`
- Roadmap: `PRE_LAUNCH_ROADMAP.md`

519
commands/injuries/CLAUDE.md Normal file
View File

@ -0,0 +1,519 @@
# Injury Commands
**Command Group:** `/injury`
**Permission Required:** SBA Players role (for set-new and clear)
**Subcommands:** roll, set-new, clear
## Overview
The injury command family provides comprehensive player injury management for the SBA league. Team managers can roll for injuries using official Strat-o-Matic injury tables, record confirmed injuries, and clear injuries when players return.
## Commands
### `/injury roll`
Roll for injury based on a player's injury rating using 3d6 dice and official injury tables.
**Usage:**
```
/injury roll <player_name>
```
**Parameters:**
- `player_name` (required, autocomplete): Name of the player - uses smart autocomplete prioritizing your team's players
**Injury Rating Format:**
The player's `injury_rating` field contains both the games played and rating in format `#p##`:
- **Format**: `1p70`, `4p50`, `2p65`, etc.
- **First character**: Games played in current series (1-6)
- **Remaining characters**: Injury rating (p70, p65, p60, p50, p40, p30, p20)
**Examples:**
- `1p70` = 1 game played, p70 rating
- `4p50` = 4 games played, p50 rating
- `2p65` = 2 games played, p65 rating
**Dice Roll:**
- Rolls 3d6 (3-18 range)
- Automatically extracts games played and rating from player's injury_rating field
- Looks up result in official Strat-o-Matic injury tables
- Returns injury duration based on rating and games played
**Possible Results:**
- **OK**: No injury
- **REM**: Remainder of game (batters) or Fatigued (pitchers)
- **Number**: Games player will miss (1-24 games)
**Example:**
```
/injury roll Mike Trout
```
**Response Fields:**
- **Roll**: Total rolled and individual dice (e.g., "15 (3d6: 5 + 5 + 5)")
- **Player**: Player name and position
- **Injury Rating**: Full rating with parsed details (e.g., "4p50 (p50, 4 games)")
- **Result**: Injury outcome (OK, REM, or number of games)
- **Team**: Player's current team
**Response Colors:**
- **Green**: OK (no injury)
- **Gold**: REM (remainder of game/fatigued)
- **Orange**: Number of games (injury occurred)
**Error Handling:**
If a player's `injury_rating` is not in the correct format, an error message will be displayed:
```
Invalid Injury Rating Format
{Player} has an invalid injury rating: `{rating}`
Expected format: #p## (e.g., 1p70, 4p50)
```
---
### `/injury set-new`
Record a new injury for a player on your team.
**Usage:**
```
/injury set-new <player_name> <this_week> <this_game> <injury_games>
```
**Parameters:**
- `player_name` (required): Name of the player to injure
- `this_week` (required): Current week number
- `this_game` (required): Current game number (1-4)
- `injury_games` (required): Total number of games player will be out
**Validation:**
- Player must exist in current season
- Player cannot already have an active injury
- Game number must be between 1 and 4
- Injury duration must be at least 1 game
**Automatic Calculations:**
The command automatically calculates:
1. Injury start date (adjusts for game 4 edge case)
2. Return date based on injury duration
3. Week rollover when games exceed 4 per week
**Example:**
```
/injury set-new Mike Trout 5 2 4
```
This records an injury occurring in week 5, game 2, with player out for 4 games (returns week 6, game 2).
**Response:**
- Confirmation embed with injury details
- Player's name, position, and team
- Total games missed
- Calculated return date
---
### `/injury clear`
Clear a player's active injury and mark them as eligible to play.
**Usage:**
```
/injury clear <player_name>
```
**Parameters:**
- `player_name` (required, autocomplete): Name of the player whose injury to clear - uses smart autocomplete prioritizing your team's players
**Validation:**
- Player must exist in current season
- Player must have an active injury
**User Flow:**
1. Command issued with player name
2. **Confirmation embed displayed** showing:
- Player name and position
- Team name and abbreviation
- Expected return date
- Total games missed
- Team thumbnail (if available)
3. User prompted: "Is {Player Name} cleared to return?"
4. Two buttons presented:
- **"Clear Injury"** → Proceeds with clearing the injury
- **"Cancel"** → Cancels the operation
5. After confirmation, injury is cleared and success message displayed
**Example:**
```
/injury clear Mike Trout
```
**Confirmation Embed:**
- Title: Player name
- Description: "Is **{Player Name}** cleared to return?"
- Fields: Player info, team, expected return date, games missed
- Buttons: "Clear Injury" / "Cancel"
- Timeout: 3 minutes
**Success Response (after confirmation):**
- Confirmation that injury was cleared
- Shows previous return date
- Shows total games that were missed
- Player's team information
**Responders:**
- Command issuer
- Team GM(s) - can also confirm/cancel on behalf of team
---
## Date Format
All injury dates use the format `w##g#`:
- `w##` = Week number (zero-padded to 2 digits)
- `g#` = Game number (1-4)
**Examples:**
- `w05g2` = Week 5, Game 2
- `w12g4` = Week 12, Game 4
- `w01g1` = Week 1, Game 1
## Injury Calculation Logic
### Basic Calculation
For an injury of N games starting at week W, game G:
1. **Calculate weeks and remaining games:**
```
out_weeks = floor(N / 4)
out_games = N % 4
```
2. **Calculate return date:**
```
return_week = W + out_weeks
return_game = G + 1 + out_games
```
3. **Handle week rollover:**
```
if return_game > 4:
return_week += 1
return_game -= 4
```
### Special Cases
#### Game 4 Edge Case
If injury occurs during game 4, the start date is adjusted:
```
start_week = W + 1
start_game = 1
```
#### Examples
**Example 1: Simple injury (same week)**
- Current: Week 5, Game 1
- Injury: 2 games
- Return: Week 5, Game 4
**Example 2: Week rollover**
- Current: Week 5, Game 3
- Injury: 3 games
- Return: Week 6, Game 3
**Example 3: Multi-week injury**
- Current: Week 5, Game 2
- Injury: 8 games
- Return: Week 7, Game 3
**Example 4: Game 4 start**
- Current: Week 5, Game 4
- Injury: 2 games
- Start: Week 6, Game 1
- Return: Week 6, Game 3
## Database Schema
### Injury Model
```python
class Injury(SBABaseModel):
id: int # Injury ID
season: int # Season number
player_id: int # Player ID
total_games: int # Total games player will be out
start_week: int # Week injury started
start_game: int # Game number injury started (1-4)
end_week: int # Week player returns
end_game: int # Game number player returns (1-4)
is_active: bool # Whether injury is currently active
```
### API Integration
The commands interact with the following API endpoints:
- `GET /api/v3/injuries` - Query injuries with filters
- `POST /api/v3/injuries` - Create new injury record
- `PATCH /api/v3/injuries/{id}` - Update injury (clear active status)
- `PATCH /api/v3/players/{id}` - Update player's il_return field
## Service Layer
### InjuryService
**Location:** `services/injury_service.py`
**Key Methods:**
- `get_active_injury(player_id, season)` - Get active injury for player
- `get_injuries_by_player(player_id, season, active_only)` - Get all injuries for player
- `get_injuries_by_team(team_id, season, active_only)` - Get team injuries
- `create_injury(...)` - Create new injury record
- `clear_injury(injury_id)` - Deactivate injury
## Permissions
### Required Roles
**For `/injury check`:**
- No role required (available to all users)
**For `/injury set-new` and `/injury clear`:**
- **SBA Players** role required
- Configured via `SBA_PLAYERS_ROLE_NAME` environment variable
### Permission Checks
The commands use `has_player_role()` method to verify user has appropriate role:
```python
def has_player_role(self, interaction: discord.Interaction) -> bool:
"""Check if user has the SBA Players role."""
player_role = discord.utils.get(
interaction.guild.roles,
name=get_config().sba_players_role_name
)
return player_role in interaction.user.roles if player_role else False
```
## Error Handling
### Common Errors
**Player Not Found:**
```
❌ Player Not Found
I did not find anybody named **{player_name}**.
```
**Already Injured:**
```
❌ Already Injured
Hm. It looks like {player_name} is already hurt.
```
**Not Injured:**
```
❌ No Active Injury
{player_name} isn't injured.
```
**Invalid Input:**
```
❌ Invalid Input
Game number must be between 1 and 4.
```
**Permission Denied:**
```
❌ Permission Denied
This command requires the **SBA Players** role.
```
## Logging
All injury commands use the `@logged_command` decorator for automatic logging:
```python
@app_commands.command(name="check")
@logged_command("/injury check")
async def injury_check(self, interaction, player_name: str):
# Command implementation
```
**Log Context:**
- Command name
- User ID and username
- Player name
- Season
- Injury details (duration, dates)
- Success/failure status
**Example Log:**
```json
{
"level": "INFO",
"command": "/injury set-new",
"user_id": "123456789",
"player_name": "Mike Trout",
"season": 12,
"injury_games": 4,
"return_date": "w06g2",
"message": "Injury set for Mike Trout"
}
```
## Testing
### Test Coverage
**Location:** `tests/test_services_injury.py`
**Test Categories:**
1. **Model Tests** (5 tests) - Injury model creation and properties
2. **Service Tests** (8 tests) - InjuryService CRUD operations with API mocking
3. **Roll Logic Tests** (8 tests) - Injury rating parsing, table lookup, and dice roll logic
4. **Calculation Tests** (5 tests) - Date calculation logic for injury duration
**Total:** 26 comprehensive tests
**Running Tests:**
```bash
# Run all injury tests
python -m pytest tests/test_services_injury.py -v
# Run specific test class
python -m pytest tests/test_services_injury.py::TestInjuryService -v
python -m pytest tests/test_services_injury.py::TestInjuryRollLogic -v
# Run with coverage
python -m pytest tests/test_services_injury.py --cov=services.injury_service --cov=commands.injuries
```
## Injury Roll Tables
### Table Structure
The injury tables are based on official Strat-o-Matic rules with the following structure:
**Ratings:** p70, p65, p60, p50, p40, p30, p20 (higher is better)
**Games Played:** 1-6 games in current series
**Roll:** 3d6 (results from 3-18)
### Rating Availability by Games Played
Not all ratings are available for all games played combinations:
- **1 game**: All ratings (p70-p20)
- **2 games**: All ratings (p70-p20)
- **3 games**: p65-p20 (p70 exempt)
- **4 games**: p60-p20 (p70, p65 exempt)
- **5 games**: p60-p20 (p70, p65 exempt)
- **6 games**: p40-p20 (p70, p65, p60, p50 exempt)
When a rating/games combination has no table, the result is automatically "OK" (no injury).
### Example Table (p65, 1 game):
| Roll | Result |
|------|--------|
| 3 | 2 |
| 4 | 2 |
| 5 | OK |
| 6 | REM |
| 7 | 1 |
| ... | ... |
| 18 | 12 |
## UI/UX Design
### Embed Colors
- **Roll (OK):** Green - No injury
- **Roll (REM):** Gold - Remainder of game/Fatigued
- **Roll (Injury):** Orange - Number of games
- **Set New:** Success (green) - `EmbedTemplate.success()`
- **Clear:** Success (green) - `EmbedTemplate.success()`
- **Errors:** Error (red) - `EmbedTemplate.error()`
### Response Format
All successful responses use Discord embeds with:
- Clear title indicating action/status
- Well-organized field layout
- Team information when applicable
- Consistent formatting for dates
## Integration with Player Model
The Player model includes injury-related fields:
```python
class Player(SBABaseModel):
# ... other fields ...
pitcher_injury: Optional[int] # Pitcher injury rating
injury_rating: Optional[str] # General injury rating
il_return: Optional[str] # Injured list return date (w##g#)
```
When an injury is set or cleared, the player's `il_return` field is automatically updated via PlayerService.
## Future Enhancements
Possible improvements for future versions:
1. **Injury History** - View player's injury history for a season
2. **Team Injury Report** - List all injuries for a team
3. **Injury Notifications** - Automatic notifications when players return from injury
4. **Injury Statistics** - Track injury trends and statistics
5. **Injury Chart Image** - Display the official injury chart as an embed image
## Migration from Legacy
### Legacy Commands
The legacy injury commands were located in:
- `discord-app/cogs/players.py` - `set_injury_slash()` and `clear_injury_slash()`
- `discord-app/cogs/players.py` - `injury_roll_slash()` with manual rating/games input
### Key Improvements
1. **Cleaner Command Structure:** Using GroupCog for organized subcommands (`/injury roll`, `/injury set-new`, `/injury clear`)
2. **Simplified Interface:** Single parameter for injury roll - games played automatically extracted from player data
3. **Smart Injury Ratings:** Automatically reads and parses player's injury rating from database
4. **Player Autocomplete:** Modern autocomplete with team prioritization for better UX
5. **Better Error Handling:** User-friendly error messages via EmbedTemplate with format validation
6. **Improved Logging:** Automatic logging via @logged_command decorator
7. **Service Layer:** Separated business logic from command handlers
8. **Type Safety:** Full type hints and Pydantic models
9. **Testability:** Comprehensive unit tests (26 tests) with mocked API calls
10. **Modern UI:** Consistent embed-based responses with color coding
11. **Official Tables:** Complete Strat-o-Matic injury tables built into the command
### Migration Details
**Old:** `/injuryroll <rating> <games>` - Manual rating and games selection
**New:** `/injury roll <player>` - Single parameter, automatic rating and games extraction from player's `injury_rating` field
**Old:** `/setinjury <player> <week> <game> <duration>`
**New:** `/injury set-new <player> <week> <game> <duration>` - Same functionality, better naming
**Old:** `/clearinjury <player>`
**New:** `/injury clear <player>` - Same functionality, better naming
### Database Field Update
The `injury_rating` field format has changed to include games played:
- **Old Format**: `p65`, `p70`, etc. (rating only)
- **New Format**: `1p70`, `4p50`, `2p65`, etc. (games + rating)
Players must have their `injury_rating` field updated to the new format for the `/injury roll` command to work.
---
**Last Updated:** January 2025
**Version:** 2.0
**Status:** Active

151
commands/league/CLAUDE.md Normal file
View File

@ -0,0 +1,151 @@
# League Commands
This directory contains Discord slash commands related to league-wide information and statistics.
## Files
### `info.py`
- **Command**: `/league`
- **Description**: Display current league status and information
- **Functionality**: Shows current season/week, phase (regular season/playoffs/offseason), transaction status, trade deadlines, and league configuration
- **Service Dependencies**: `league_service.get_current_state()`
- **Key Features**:
- Dynamic phase detection (offseason, playoffs, regular season)
- Transaction freeze status
- Trade deadline and playoff schedule information
- Draft pick trading status
### `standings.py`
- **Commands**:
- `/standings` - Display league standings by division
- `/playoff-picture` - Show current playoff picture and wild card race
- **Parameters**:
- `season`: Optional season number (defaults to current)
- `division`: Optional division filter for standings
- **Service Dependencies**: `standings_service`
- **Key Features**:
- Division-based standings display
- Games behind calculations
- Recent form statistics (home record, last 8 games, current streak)
- Playoff cutoff visualization
- Wild card race tracking
### `schedule.py`
- **Commands**:
- `/schedule` - Display game schedules
- `/results` - Show recent game results
- **Parameters**:
- `season`: Optional season number (defaults to current)
- `week`: Optional specific week filter
- `team`: Optional team abbreviation filter
- **Service Dependencies**: `schedule_service`
- **Key Features**:
- Weekly schedule views
- Team-specific schedule filtering
- Series grouping and summary
- Recent/upcoming game overview
- Game completion tracking
### `submit_scorecard.py`
- **Command**: `/submit-scorecard`
- **Description**: Submit Google Sheets scorecards with game results and play-by-play data
- **Parameters**:
- `sheet_url`: Full URL to the Google Sheets scorecard
- **Required Role**: `Season 12 Players`
- **Service Dependencies**:
- `SheetsService` - Google Sheets data extraction
- `game_service` - Game CRUD operations
- `play_service` - Play-by-play data management
- `decision_service` - Pitching decision management
- `standings_service` - Standings recalculation
- `league_service` - Current state retrieval
- `team_service` - Team lookup
- `player_service` - Player lookup for results display
- **Key Features**:
- **Scorecard Validation**: Checks sheet access and version compatibility
- **Permission Control**: Only GMs of playing teams can submit
- **Duplicate Detection**: Identifies already-played games with confirmation dialog
- **Transaction Rollback**: Full rollback support at 3 states:
- `PLAYS_POSTED`: Deletes plays on error
- `GAME_PATCHED`: Wipes game and deletes plays on error
- `COMPLETE`: All data committed successfully
- **Data Extraction**: Reads 68 fields from Playtable, 14 fields from Pitcherstats, box score, and game metadata
- **Results Display**: Rich embed with box score, pitching decisions, and top 3 key plays by WPA
- **Automated Standings**: Triggers standings recalculation after successful submission
- **News Channel Posting**: Automatically posts results to configured channel
**Workflow (14 Phases)**:
1. Validate scorecard access and version
2. Extract game metadata from Setup tab
3. Lookup teams and match managers
4. Check user permissions (must be GM of one team or bot owner)
5. Check for duplicate games (with confirmation if found)
6. Find scheduled game in database
7. Read play-by-play data (up to 297 plays)
8. Submit plays to database
9. Read box score
10. Update game with scores and managers
11. Read pitching decisions (up to 27 pitchers)
12. Submit decisions to database
13. Create and post results embed to news channel
14. Recalculate league standings
**Error Handling**:
- User-friendly error messages for common issues
- Graceful rollback on validation errors
- API error parsing for actionable feedback
- Non-critical errors (key plays, standings) don't fail submission
**Configuration**:
- `sheets_credentials_path` (in config.py): Path to Google service account credentials JSON (set via `SHEETS_CREDENTIALS_PATH` env var)
- `SBA_NETWORK_NEWS_CHANNEL`: Channel name for results posting
- `SBA_PLAYERS_ROLE_NAME`: Role required to submit scorecards
## Architecture Notes
### Decorator Usage
All commands use the `@logged_command` decorator pattern:
- Eliminates boilerplate logging code
- Provides consistent error handling
- Automatic request tracing and timing
### Error Handling
- Graceful fallbacks for missing data
- User-friendly error messages
- Ephemeral responses for errors
### Embed Structure
- Uses `EmbedTemplate` for consistent styling
- Color coding based on context (success/error/info)
- Rich formatting with team logos and thumbnails
## Troubleshooting
### Common Issues
1. **No league data available**: Check `league_service.get_current_state()` API endpoint
2. **Standings not loading**: Verify `standings_service.get_standings_by_division()` returns valid data
3. **Schedule commands failing**: Ensure `schedule_service` methods are properly handling season/week parameters
### Dependencies
- `services.league_service`
- `services.standings_service`
- `services.schedule_service`
- `services.sheets_service` (NEW) - Google Sheets integration
- `services.game_service` (NEW) - Game management
- `services.play_service` (NEW) - Play-by-play data
- `services.decision_service` (NEW) - Pitching decisions
- `services.team_service`
- `services.player_service`
- `utils.decorators.logged_command`
- `utils.discord_helpers` (NEW) - Channel and message utilities
- `utils.team_utils`
- `views.embeds.EmbedTemplate`
- `views.confirmations.ConfirmationView` (NEW) - Reusable confirmation dialog
- `constants.SBA_CURRENT_SEASON`
- `config.BotConfig.sheets_credentials_path` (NEW) - Google Sheets credentials path
- `constants.SBA_NETWORK_NEWS_CHANNEL` (NEW)
- `constants.SBA_PLAYERS_ROLE_NAME` (NEW)
### Testing
Run tests with: `python -m pytest tests/test_commands_league.py -v`

173
commands/players/CLAUDE.md Normal file
View File

@ -0,0 +1,173 @@
# Player Commands
This directory contains Discord slash commands for player information and statistics.
## Files
### `info.py`
- **Command**: `/player`
- **Description**: Display comprehensive player information and statistics
- **Parameters**:
- `name` (required): Player name to search for
- `season` (optional): Season for statistics (defaults to current season)
- **Service Dependencies**:
- `player_service.get_players_by_name()`
- `player_service.search_players_fuzzy()`
- `player_service.get_player()`
- `stats_service.get_player_stats()`
## Key Features
### Player Search
- **Exact Name Matching**: Primary search method using player name
- **Fuzzy Search Fallback**: If no exact match, suggests similar player names
- **Multiple Player Handling**: When multiple players match, attempts exact match or asks user to be more specific
- **Suggestion System**: Shows up to 10 suggested players with positions when no exact match found
### Player Information Display
- **Basic Info**: Name, position(s), team, season, sWAR, injury status
- **Injury Indicator**: 🤕 emoji in title if player has active injury
- **Injury Information**:
- Injury Rating (always displayed)
- Injury Return date (displayed only when injured, format: w##g#)
- **Statistics Integration**:
- **Batting stats** displayed in two inline fields with rounded box code blocks:
- **Rate Stats**: AVG, OBP, SLG, OPS, wOBA
- **Counting Stats**: HR, RBI, R, AB, H, BB, SO
- **Pitching stats** displayed in two inline fields with rounded box code blocks:
- **Record Stats**: G-GS, W-L, H-SV, ERA, WHIP, IP
- **Counting Stats**: SO, BB, H
- Two-way player detection and display
- **Visual Elements**:
- Player name as embed title (with 🤕 emoji if injured)
- Player card image as main image
- Thumbnail priority: fancy card → headshot → team logo
- Team color theming for embed
### Advanced Features
- **Concurrent Data Fetching**: Player data and statistics retrieved in parallel for performance
- **sWAR Display**: Shows Strat-o-Matic WAR value
- **Multi-Position Support**: Displays all eligible positions
- **Rich Error Handling**: Graceful fallbacks when data is unavailable
## Architecture Notes
### Search Logic Flow
1. Search by exact name in specified season
2. If no results, try fuzzy search across all players
3. If single result, display player card
4. If multiple results, attempt exact name match
5. If still multiple, show disambiguation list
### Performance Optimizations
- `asyncio.gather()` for concurrent API calls
- Efficient player data and statistics retrieval
- Lazy loading of optional player images
### Error Handling
- No players found: Suggests fuzzy matches
- Multiple matches: Provides clarification options
- Missing data: Shows partial information with clear indicators
- API failures: Graceful degradation with fallback data
## Troubleshooting
### Common Issues
1. **Player not found**:
- Check player name spelling
- Verify player exists in the specified season
- Use fuzzy search suggestions
2. **Statistics not loading**:
- Verify `stats_service.get_player_stats()` API endpoint
- Check if player has statistics for the requested season
- Ensure season parameter is valid
3. **Images not displaying**:
- Check player image URLs in database
- Verify team thumbnail URLs
- Ensure image hosting is accessible
4. **Performance issues**:
- Monitor concurrent API call efficiency
- Check database query performance
- Verify embed size limits
### Dependencies
- `services.player_service`
- `services.stats_service`
- `utils.decorators.logged_command`
- `views.embeds.EmbedTemplate`
- `constants.SBA_CURRENT_SEASON`
- `exceptions.BotException`
### Testing
Run tests with: `python -m pytest tests/test_commands_players.py -v`
## Stat Display Format (January 2025)
### Batting Stats
Stats are displayed in two side-by-side inline fields using rounded box code blocks:
**Rate Stats (Left):**
```
╭─────────────╮
│ AVG .305 │
│ OBP .385 │
│ SLG .545 │
│ OPS .830 │
│ wOBA .355 │
╰─────────────╯
```
**Counting Stats (Right):**
```
╭───────────╮
│ HR 25 │
│ RBI 80 │
│ R 95 │
│ AB 450 │
│ H 137 │
│ BB 55 │
│ SO 98 │
╰───────────╯
```
### Pitching Stats
Stats are displayed in two side-by-side inline fields using rounded box code blocks:
**Record Stats (Left):**
```
╭─────────────╮
│ G-GS 28-28 │
│ W-L 12-8 │
│ H-SV 3-0 │
│ ERA 3.45 │
│ WHIP 1.25 │
│ IP 165.1 │
╰─────────────╯
```
**Counting Stats (Right):**
```
╭────────╮
│ SO 185 │
│ BB 48 │
│ H 145 │
╰────────╯
```
**Design Features:**
- Compact, professional appearance using rounded box characters (`╭╮╰╯─│`)
- Right-aligned numeric values for clean alignment
- Inline fields allow side-by-side display
- Empty field separator above stats for visual spacing
- Consistent styling between batting and pitching displays
## Database Requirements
- Player records with name, positions, team associations
- Player injury data (injury_rating, il_return fields)
- Statistics tables for batting and pitching
- Image URLs for player cards, headshots, and fancy cards
- Team logo and color information

424
commands/profile/CLAUDE.md Normal file
View File

@ -0,0 +1,424 @@
# Player Image Management Commands
**Last Updated:** January 2025
**Status:** ✅ Fully Implemented
**Location:** `commands/profile/`
## Overview
The Player Image Management system allows users to update player fancy card and headshot images for players on teams they own. Administrators can update any player's images.
## Commands
### `/set-image <image_type> <player_name> <image_url>`
**Description:** Update a player's fancy card or headshot image
**Parameters:**
- `image_type` (choice): Choose "Fancy Card" or "Headshot"
- **Fancy Card**: Shows as thumbnail in player cards (takes priority)
- **Headshot**: Shows as thumbnail if no fancy card exists
- `player_name` (string with autocomplete): Player to update
- `image_url` (string): Direct URL to the image file
**Permissions:**
- **Regular Users**: Can update images for players on teams they own (ML/MiL/IL)
- **Administrators**: Can update any player's images (bypasses organization check)
**Usage Examples:**
```
/set-image fancy-card "Mike Trout" https://example.com/cards/trout.png
/set-image headshot "Shohei Ohtani" https://example.com/headshots/ohtani.jpg
```
## Permission System
### Regular Users
Users can update images for players in their organization:
- **Major League team players** - Direct team ownership
- **Minor League team players** - Owned via organizational affiliation
- **Injured List team players** - Owned via organizational affiliation
**Example:**
If you own the NYY team, you can update images for players on:
- NYY (Major League)
- NYYMIL (Minor League)
- NYYIL (Injured List)
### Administrators
Administrators have unrestricted access to update any player's images regardless of team ownership.
### Permission Check Logic
```python
# Check order:
1. Is user an administrator? → Grant access
2. Does user own any teams? → Continue check
3. Does player belong to user's organization? → Grant access
4. Otherwise → Deny access
```
## URL Requirements
### Format Validation
URLs must meet the following criteria:
- **Protocol**: Must start with `http://` or `https://`
- **Extension**: Must end with valid image extension:
- `.jpg`, `.jpeg` - JPEG format
- `.png` - PNG format
- `.gif` - GIF format (includes animated GIFs)
- `.webp` - WebP format
- **Length**: Maximum 500 characters
- **Query parameters**: Allowed (e.g., `?size=large`)
**Valid Examples:**
```
https://example.com/image.jpg
https://cdn.discord.com/attachments/123/456/player.png
https://i.imgur.com/abc123.webp
https://example.com/image.jpg?size=large&format=original
```
**Invalid Examples:**
```
example.com/image.jpg ❌ Missing protocol
ftp://example.com/image.jpg ❌ Wrong protocol
https://example.com/document.pdf ❌ Wrong extension
https://example.com/page ❌ No extension
```
### Accessibility Testing
After format validation, the bot tests URL accessibility:
- **HTTP HEAD Request**: Checks if URL is reachable
- **Status Code**: Must return 200 OK
- **Content-Type**: Must return `image/*` header
- **Timeout**: 5 seconds maximum
**Common Accessibility Errors:**
- `404 Not Found` - Image doesn't exist at URL
- `403 Forbidden` - Permission denied
- `Timeout` - Server too slow or unresponsive
- `Wrong content-type` - URL points to webpage, not image
## Workflow
### Step-by-Step Process
1. **User invokes command**
```
/set-image fancy-card "Mike Trout" https://example.com/card.png
```
2. **URL Format Validation**
- Checks protocol, extension, length
- If invalid: Shows error with requirements
3. **URL Accessibility Test**
- HTTP HEAD request to URL
- Checks status code and content-type
- If inaccessible: Shows error with troubleshooting tips
4. **Player Lookup**
- Searches for player by name
- Handles multiple matches (asks for exact name)
- If not found: Shows error
5. **Permission Check**
- Admin check → Grant access
- Organization ownership check → Grant/deny access
- If denied: Shows permission error
6. **Preview with Confirmation**
- Shows embed with new image as thumbnail
- Displays current vs new image info
- **Confirm Update** button → Proceed
- **Cancel** button → Abort
7. **Database Update**
- Updates `vanity_card` or `headshot` field
- If failure: Shows error
8. **Success Message**
- Confirms update
- Shows new image
- Displays updated player info
## Field Mapping
| Choice | Database Field | Display Priority | Notes |
|--------|----------------|------------------|-------|
| Fancy Card | `vanity_card` | 1st (highest) | Custom fancy player card |
| Headshot | `headshot` | 2nd | Player headshot photo |
| *(default)* | `team.thumbnail` | 3rd (fallback) | Team logo |
**Display Logic in Player Cards:**
```
IF player.vanity_card exists:
Show vanity_card as thumbnail
ELSE IF player.headshot exists:
Show headshot as thumbnail
ELSE IF player.team.thumbnail exists:
Show team logo as thumbnail
ELSE:
No thumbnail
```
## Best Practices
### For Users
#### Choosing Image URLs
✅ **DO:**
- Use reliable image hosting (Discord CDN, Imgur, established hosts)
- Use direct image links (right-click image → "Copy Image Address")
- Test URLs in browser before submitting
- Use permanent URLs, not temporary upload links
❌ **DON'T:**
- Use image hosting page URLs (must be direct image file)
- Use temporary or expiring URLs
- Use images from unreliable hosts
- Use extremely large images (impacts Discord performance)
#### Image Recommendations
**Fancy Cards:**
- Recommended size: 400x600px (or similar 2:3 aspect ratio)
- Format: PNG or JPEG
- File size: < 2MB for best performance
- Style: Custom designs, player stats, artistic renditions
**Headshots:**
- Recommended size: 256x256px (square aspect ratio)
- Format: PNG or JPEG with transparent background
- File size: < 500KB
- Style: Professional headshot, clean background
#### Finding Good Image URLs
1. **Discord CDN** (best option):
- Upload image to Discord
- Right-click → Copy Link
- Paste as image URL
2. **Imgur**:
- Upload to Imgur
- Right-click image → Copy Image Address
- Use direct link (ends with `.png` or `.jpg`)
3. **Other hosts**:
- Ensure stable, permanent hosting
- Verify URL accessibility before using
### For Administrators
#### Managing Player Images
- Set consistent style guidelines for your league
- Use standard image dimensions for uniformity
- Maintain backup copies of custom images
- Document image sources for attribution
#### Troubleshooting User Issues
Common problems and solutions:
| Issue | Cause | Solution |
|-------|-------|----------|
| "URL not accessible" | Host down, URL expired | Ask for new URL from stable host |
| "Not a valid image" | URL points to webpage | Get direct image link |
| "Permission denied" | User doesn't own team | Verify team ownership |
| "Player not found" | Typo in name | Use autocomplete feature |
## Error Messages
### Format Errors
```
❌ Invalid URL Format
URL must start with http:// or https://
Requirements:
• Must start with `http://` or `https://`
• Must end with `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`
• Maximum 500 characters
```
### Accessibility Errors
```
❌ URL Not Accessible
URL returned status 404
Please check:
• URL is correct and not expired
• Image host is online
• URL points directly to an image file
• URL is publicly accessible
```
### Permission Errors
```
❌ Permission Denied
You don't own a team in the NYY organization
You can only update images for players on teams you own.
```
### Player Not Found
```
❌ Player Not Found
No player found matching 'Mike Trut' in the current season.
```
### Multiple Players Found
```
🔍 Multiple Players Found
Multiple players match 'Mike':
• Mike Trout (OF)
• Mike Zunino (C)
Please use the exact name from autocomplete.
```
## Technical Implementation
### Architecture
```
commands/profile/
├── __init__.py # Package setup
├── images.py # Main command implementation
│ ├── validate_url_format() # Format validation
│ ├── test_url_accessibility() # Accessibility testing
│ ├── can_edit_player_image() # Permission checking
│ ├── ImageUpdateConfirmView # Confirmation UI
│ ├── player_name_autocomplete() # Autocomplete function
│ └── ImageCommands # Command cog
└── README.md # This file
```
### Dependencies
- `aiohttp` - Async HTTP requests for URL testing
- `discord.py` - Discord bot framework
- `player_service` - Player CRUD operations
- `team_service` - Team queries and ownership
- Standard bot utilities (logging, decorators, embeds)
### Database Fields
**Player Model** (`models/player.py`):
```python
vanity_card: Optional[str] = Field(None, description="Custom vanity card URL")
headshot: Optional[str] = Field(None, description="Player headshot URL")
```
Both fields are optional and store direct image URLs.
### API Integration
**Update Operation:**
```python
# Update player image
update_data = {"vanity_card": "https://example.com/card.png"}
updated_player = await player_service.update_player(player_id, update_data)
```
**Endpoints Used:**
- `GET /api/v3/players?name={name}&season={season}` - Player search
- `PATCH /api/v3/players/{player_id}?vanity_card={url}` - Update player data
- `GET /api/v3/teams?owner_id={user_id}&season={season}` - User's teams
**Important Note:**
The player PATCH endpoint uses **query parameters** instead of JSON body for data updates. The `player_service.update_player()` method automatically handles this by setting `use_query_params=True` when calling the API client.
## Testing
### Test Coverage
**Test File:** `tests/test_commands_profile_images.py`
**Test Categories:**
1. **URL Format Validation** (10 tests)
- Valid formats (JPG, PNG, WebP, with query params)
- Invalid protocols (no protocol, FTP)
- Invalid extensions (PDF, no extension)
- URL length limits
2. **URL Accessibility** (5 tests)
- Successful access
- 404 errors
- Wrong content-type
- Timeouts
- Connection errors
3. **Permission Checking** (7 tests)
- Admin access to all players
- User access to owned teams
- User access to MiL/IL players
- Denial for other organizations
- Denial for users without teams
- Players without team assignment
4. **Integration Tests** (3 tests)
- Command structure validation
- Field mapping logic
### Running Tests
```bash
# Run all image management tests
python -m pytest tests/test_commands_profile_images.py -v
# Run specific test class
python -m pytest tests/test_commands_profile_images.py::TestURLValidation -v
# Run with coverage
python -m pytest tests/test_commands_profile_images.py --cov=commands.profile
```
## Future Enhancements
### Planned Features (Post-Launch)
- **Image size validation**: Check image dimensions
- **Image upload support**: Upload images directly instead of URLs
- **Bulk image updates**: Update multiple players at once
- **Image preview history**: See previous images
- **Image moderation**: Admin approval queue for user submissions
- **Default images**: Set default fancy cards per team
- **Image gallery**: View all player images for a team
### Potential Improvements
- **Automatic image optimization**: Resize/compress large images
- **CDN integration**: Auto-upload to Discord CDN for permanence
- **Image templates**: Pre-designed templates users can fill in
- **Batch operations**: Admin tool to set multiple images
- **Image analytics**: Track which images are most viewed
## Troubleshooting
### Common Issues
**Problem:** "URL not accessible" but URL works in browser
- **Cause:** Content-Delivery-Network (CDN) may require browser headers
- **Solution:** Use Discord CDN or Imgur instead
**Problem:** Permission denied even though I own the team
- **Cause:** Season mismatch or ownership data not synced
- **Solution:** Contact admin to verify team ownership data
**Problem:** Image appears broken in Discord
- **Cause:** Discord can't load the image (blocked, wrong format, too large)
- **Solution:** Try different host or smaller file size
**Problem:** Autocomplete doesn't show player
- **Cause:** Player doesn't exist in current season
- **Solution:** Verify player name and season
### Support
For issues or questions:
1. Check this README for solutions
2. Review error messages carefully (they include troubleshooting steps)
3. Contact server administrators
4. Check bot logs for detailed error information
---
**Implementation Details:**
- **Commands:** `commands/profile/images.py`
- **Tests:** `tests/test_commands_profile_images.py`
- **Models:** `models/player.py` (vanity_card, headshot fields)
- **Services:** `services/player_service.py`, `services/team_service.py`
**Related Documentation:**
- **Bot Architecture:** `/discord-app-v2/CLAUDE.md`
- **Command Patterns:** `/discord-app-v2/commands/README.md`
- **Testing Guide:** `/discord-app-v2/tests/README.md`

134
commands/teams/CLAUDE.md Normal file
View File

@ -0,0 +1,134 @@
# Team Commands
This directory contains Discord slash commands for team information and roster management.
## Files
### `info.py`
- **Commands**:
- `/team` - Display comprehensive team information
- `/teams` - List all teams in a season
- **Parameters**:
- `abbrev` (required for `/team`): Team abbreviation (e.g., NYY, BOS, LAD)
- `season` (optional): Season to display (defaults to current season)
- **Service Dependencies**:
- `team_service.get_team_by_abbrev()`
- `team_service.get_teams_by_season()`
- `team_service.get_team_standings_position()`
### `roster.py`
- **Command**: `/roster`
- **Description**: Display detailed team roster with position breakdowns
- **Parameters**:
- `abbrev` (required): Team abbreviation
- `roster_type` (optional): "current" or "next" week roster (defaults to current)
- **Service Dependencies**:
- `team_service.get_team_by_abbrev()`
- `team_service.get_team_roster()`
## Key Features
### Team Information Display (`info.py`)
- **Comprehensive Team Data**:
- Team names (long name, short name, abbreviation)
- Stadium information
- Division assignment
- Team colors and logos
- **Standings Integration**:
- Win-loss record and winning percentage
- Games behind division leader
- Current standings position
- **Visual Elements**:
- Team color theming for embeds
- Team logo thumbnails
- Consistent branding across displays
### Team Listing (`/teams`)
- **Season Overview**: All teams organized by division
- **Division Grouping**: Automatically groups teams by division ID
- **Fallback Display**: Shows simple list if division data unavailable
- **Team Count**: Total team summary
### Roster Management (`roster.py`)
- **Multi-Week Support**: Current and next week roster views
- **Position Breakdown**:
- Batting positions (C, 1B, 2B, 3B, SS, LF, CF, RF, DH)
- Pitching positions (SP, RP, CP)
- Position player counts and totals
- **Advanced Features**:
- Total sWAR calculation and display
- Minor League (shortil) player tracking
- Injured List (longil) player management
- Detailed player lists with positions and WAR values
### Roster Display Structure
- **Summary Embed**: Position counts and totals
- **Detailed Player Lists**: Separate embeds for each roster type
- **Player Organization**: Batters and pitchers grouped separately
- **Chunked Display**: Long player lists split across multiple fields
## Architecture Notes
### Embed Design
- **Team Color Integration**: Uses team hex colors for embed theming
- **Fallback Colors**: Default colors when team colors unavailable
- **Thumbnail Priority**: Team logos displayed consistently
- **Multi-Embed Support**: Complex data split across multiple embeds
### Error Handling
- **Team Not Found**: Clear messaging with season context
- **Missing Roster Data**: Graceful handling of unavailable data
- **API Failures**: Fallback to partial information display
### Performance Considerations
- **Concurrent Data Fetching**: Standings and roster data retrieved in parallel
- **Efficient Roster Processing**: Position grouping and calculations optimized
- **Chunked Player Lists**: Prevents Discord embed size limits
## Troubleshooting
### Common Issues
1. **Team not found**:
- Verify team abbreviation spelling
- Check if team exists in the specified season
- Ensure abbreviation matches database format
2. **Roster data missing**:
- Verify `team_service.get_team_roster()` API endpoint
- Check if roster data exists for the requested week type
- Ensure team ID is correctly passed to roster service
3. **Position counts incorrect**:
- Verify roster data structure and position field names
- Check sWAR calculation logic
- Ensure player position arrays are properly parsed
4. **Standings not displaying**:
- Check `get_team_standings_position()` API response
- Verify standings data structure matches expected format
- Ensure error handling for malformed standings data
### Dependencies
- `services.team_service`
- `models.team.Team`
- `utils.decorators.logged_command`
- `views.embeds.EmbedTemplate`
- `constants.SBA_CURRENT_SEASON`
- `exceptions.BotException`
### Testing
Run tests with: `python -m pytest tests/test_commands_teams.py -v`
## Database Requirements
- Team records with abbreviations, names, colors, logos
- Division assignment and organization
- Roster data with position assignments and player details
- Standings calculations and team statistics
- Stadium and venue information
## Future Enhancements
- Team statistics and performance metrics
- Historical team data and comparisons
- Roster change tracking and transaction history
- Advanced roster analytics and projections

View File

@ -0,0 +1,328 @@
# Transaction Commands
This directory contains Discord slash commands for transaction management and roster legality checking.
## Files
### `management.py`
- **Commands**:
- `/mymoves` - View user's pending and scheduled transactions
- `/legal` - Check roster legality for current and next week
- **Service Dependencies**:
- `transaction_service` (multiple methods for transaction retrieval)
- `roster_service` (roster validation and retrieval)
- `team_service.get_teams_by_owner()` and `get_team_by_abbrev()`
### `dropadd.py`
- **Commands**:
- `/dropadd` - Interactive transaction builder for single-team roster moves (scheduled for next week)
- `/cleartransaction` - Clear current transaction builder
- **Service Dependencies**:
- `transaction_builder` (transaction creation and validation)
- `player_service.search_players()` (player autocomplete)
- `team_service.get_teams_by_owner()`
### `ilmove.py` *(NEW - October 2025)*
- **Commands**:
- `/ilmove` - Interactive transaction builder for **real-time** roster moves (executed immediately for THIS week)
- `/clearilmove` - Clear current IL move transaction builder
- **Service Dependencies**:
- `transaction_builder` (transaction creation and validation - **shared with /dropadd**)
- `transaction_service.create_transaction_batch()` (POST transactions to database)
- `player_service.search_players()` (player autocomplete)
- `player_service.update_player_team()` (immediate team assignment updates)
- `team_service.get_teams_by_owner()`
- **Key Differences from /dropadd**:
- **Week**: Creates transactions for THIS week (current) instead of next week
- **Execution**: Immediately POSTs to database and updates player teams
- **Use Case**: Real-time IL moves, activations, emergency roster changes
- **UI**: Same interactive transaction builder with "immediate" submission handler
### `trade.py`
- **Commands**:
- `/trade initiate` - Start a new multi-team trade
- `/trade add-team` - Add additional teams to trade (3+ team trades)
- `/trade add-player` - Add player exchanges between teams
- `/trade supplementary` - Add internal organizational moves for roster legality
- `/trade view` - View current trade status
- `/trade clear` - Clear current trade
- **Service Dependencies**:
- `trade_builder` (multi-team trade management)
- `player_service.search_players()` (player autocomplete)
- `team_service.get_teams_by_owner()`, `get_team_by_abbrev()`, and `get_team()`
- **Channel Management**:
- Automatically creates private discussion channels for trades
- Uses `TradeChannelManager` and `TradeChannelTracker` for channel lifecycle
- Requires bot to have `Manage Channels` permission at server level
## Key Features
### Transaction Status Display (`/mymoves`)
- **User Team Detection**: Automatically finds user's team by Discord ID
- **Transaction Categories**:
- **Pending**: Transactions awaiting processing
- **Frozen**: Scheduled transactions ready for processing
- **Processed**: Recently completed transactions
- **Cancelled**: Optional display of cancelled transactions
- **Status Visualization**:
- Status emojis for each transaction type
- Week numbering and move descriptions
- Transaction count summaries
- **Smart Limiting**: Shows recent transactions (last 5 pending, 3 frozen/processed, 2 cancelled)
### Roster Legality Checking (`/legal`)
- **Dual Roster Validation**: Checks both current and next week rosters
- **Flexible Team Selection**:
- Auto-detects user's team
- Allows manual team specification via abbreviation
- **Comprehensive Validation**:
- Player count verification (active roster + IL)
- sWAR calculations and limits
- League rule compliance checking
- Error and warning categorization
- **Parallel Processing**: Roster retrieval and validation run concurrently
### Real-Time IL Move System (`/ilmove`) *(NEW - October 2025)*
- **Immediate Execution**: Transactions posted to database and players moved in real-time
- **Current Week Transactions**: All moves effective for THIS week, not next week
- **Shared Transaction Builder**: Uses same builder as `/dropadd` for consistency
- **Interactive UI**: Identical transaction builder interface with validation
- **Maximum Code Reuse**: 95% code shared with `/dropadd`, only submission differs
#### IL Move Command Workflow:
1. **`/ilmove player:"Mike Trout" destination:"Injured List"`** - Add first move
- Shows transaction builder with current move
- Validates roster legality in real-time
- Instructions say "Use `/ilmove` to add more moves"
2. **`/ilmove player:"Shohei Ohtani" destination:"Major League"`** - Add second move
- Adds to same transaction builder (shared with user)
- Shows updated roster projections with both moves
- Validates combined impact on roster limits
3. **Submit via "Submit Transaction" button** - Click to execute
- Shows CONFIRM modal (type "CONFIRM")
- **Immediately POSTs** all transactions to database API
- **Immediately updates** each player's team_id in database
- Success message shows database transaction IDs
4. **Players show on new teams instantly** - Real-time roster changes
#### IL Move vs Drop/Add Comparison:
| Feature | `/dropadd` | `/ilmove` |
|---------|------------|-----------|
| **Effective Week** | Next week (current + 1) | THIS week (current) |
| **Execution** | Scheduled (background task) | **Immediate** (real-time) |
| **Database POST** | No (builder only) | **Yes** (POSTs to API) |
| **Player Teams** | Unchanged until processing | **Updated immediately** |
| **Use Case** | Future planning | IL moves, activations |
| **Destination Options** | ML, MiL, FA | ML, MiL, **IL** |
| **Builder Shared** | Yes (same TransactionBuilder) | Yes (same TransactionBuilder) |
#### Real-Time Execution Flow:
1. **User submits transaction** → Modal confirmation
2. **Create Transaction objects** → For current week
3. **POST to database API**`transaction_service.create_transaction_batch()`
4. **Update player teams**`player_service.update_player_team()` for each player
5. **Success confirmation** → Shows actual database IDs and completed status
**Important Notes**:
- `/ilmove` only works for players **on your roster** (not FA signings)
- Players must exist on ML, MiL, or IL rosters
- Cannot use `/ilmove` to sign free agents (use `/dropadd` for that)
- All roster legality rules still apply (limits, sWAR, etc.)
### Multi-Team Trade System (`/trade`)
- **Trade Initiation**: Start trades between multiple teams using proper Discord command groups
- **Team Management**: Add/remove teams to create complex multi-team trades (2+ teams supported)
- **Player Exchanges**: Add cross-team player movements with source and destination validation
- **Supplementary Moves**: Add internal organizational moves for roster legality compliance
- **Interactive UI**: Rich Discord embeds with validation feedback and trade status
- **Real-time Validation**: Live roster checking across all participating teams
- **Authority Model**: Major League team owners control all players in their organization (ML/MiL/IL)
#### Trade Command Workflow:
1. **`/trade initiate other_team:LAA`** - Start trade between your team and LAA
- Creates a private discussion channel for the trade
- Only you see the ephemeral response
2. **`/trade add-team other_team:BOS`** - Add BOS for 3-team trade
- Updates are posted to the trade channel if executed elsewhere
- Other team members can see the progress
3. **`/trade add-player player_name:"Mike Trout" destination_team:BOS`** - Exchange players
- Trade embed updates posted to dedicated channel automatically
- Keeps all participants informed of changes
4. **`/trade supplementary player_name:"Player X" destination:ml`** - Internal roster moves
- Channel receives real-time updates
5. **`/trade view`** - Review complete trade with validation
- Posts current state to trade channel if viewed elsewhere
6. **Submit via interactive UI** - Trade submission through Discord buttons
**Channel Behavior**:
- Commands executed **in** the trade channel: Only ephemeral response to user
- Commands executed **outside** trade channel: Ephemeral response to user + public post to trade channel
- This ensures all participating teams stay informed of trade progress
#### Autocomplete System:
- **Team Initiation**: Only Major League teams (ML team owners initiate trades)
- **Player Destinations**: All roster types (ML/MiL/IL) available for player placement
- **Player Search**: Prioritizes user's team players, supports fuzzy name matching
- **Smart Filtering**: Context-aware suggestions based on user permissions
#### Trade Channel Management (`trade_channels.py`, `trade_channel_tracker.py`):
- **Automatic Channel Creation**: Private discussion channels created when trades are initiated
- **Channel Naming**: Format `trade-{team1}-{team2}-{short_id}` (e.g., `trade-wv-por-681f`)
- **Permission Management**:
- Channel hidden from @everyone
- Only participating team roles can view/message
- Bot has view and send message permissions
- Created in "Transactions" category (if it exists)
- **Channel Tracking**: JSON-based persistence for cleanup and management
- **Multi-Team Support**: Channels automatically update when teams are added to trades
- **Automatic Cleanup**: Channels deleted when trades are cleared
- **Smart Updates**: When trade commands are executed outside the dedicated trade channel, the trade embed is automatically posted to the trade channel (non-ephemeral) for visibility
**Bot Permission Requirements**:
- Server-level `Manage Channels` - Required to create/delete trade channels
- Server-level `Manage Permissions` - Optional, for enhanced permission management
- **Note**: Bot should NOT have these permissions in channel-specific overwrites (causes Discord API error 50013)
**Recent Fix (January 2025)**:
- Removed `manage_channels` and `manage_permissions` from bot's channel-specific overwrites
- Discord prohibits bots from granting themselves elevated permissions in channel overwrites
- Server-level permissions are sufficient for all channel management operations
### Advanced Transaction Features
- **Concurrent Data Fetching**: Multiple transaction types retrieved in parallel
- **Owner-Based Filtering**: Transactions filtered by team ownership
- **Status Tracking**: Real-time transaction status with emoji indicators
- **Team Integration**: Team logos and colors in transaction displays
## Architecture Notes
### Permission Model
- **Team Ownership**: Commands use Discord user ID to determine team ownership
- **Cross-Team Viewing**: `/legal` allows checking other teams' roster status
- **Access Control**: Users can only view their own transactions via `/mymoves`
### Data Processing
- **Async Operations**: Heavy use of `asyncio.gather()` for performance
- **Error Resilience**: Graceful handling of missing roster data
- **Validation Pipeline**: Multi-step roster validation with detailed feedback
### Embed Structure
- **Status-Based Coloring**: Success (green) vs Error (red) color coding
- **Information Hierarchy**: Important information prioritized in embed layout
- **Team Branding**: Consistent use of team thumbnails and colors
## Troubleshooting
### Common Issues
1. **User team not found**:
- Verify user has team ownership record in database
- Check Discord user ID mapping to team ownership
- Ensure current season team assignments are correct
2. **Transaction data missing**:
- Verify `transaction_service` API endpoints are functional
- Check transaction status filtering logic
- Ensure transaction records exist for the team/season
3. **Roster validation failing**:
- Check `roster_service.get_current_roster()` and `get_next_roster()` responses
- Verify roster validation rules and logic
- Ensure player data integrity in roster records
4. **Legal command errors**:
- Verify team abbreviation exists in database
- Check roster data availability for both current and next weeks
- Ensure validation service handles edge cases properly
5. **Trade channel creation fails** *(Fixed January 2025)*:
- Error: `Discord error: Missing Permissions. Code: 50013`
- **Root Cause**: Bot was trying to grant itself `manage_channels` and `manage_permissions` in channel-specific permission overwrites
- **Fix**: Removed elevated permissions from channel overwrites (line 74-77 in `trade_channels.py`)
- **Verification**: Bot only needs server-level `Manage Channels` permission
- Channels now create successfully with basic bot permissions (view, send messages, read history)
6. **AttributeError when adding players to trades** *(Fixed January 2025)*:
- Error: `'TeamService' object has no attribute 'get_team_by_id'`
- **Root Cause**: Code was calling non-existent method `team_service.get_team_by_id()`
- **Fix**: Changed to correct method name `team_service.get_team()` (line 201 in `trade_builder.py`)
- **Location**: `services/trade_builder.py` and test mocks in `tests/test_services_trade_builder.py`
- All 18 trade builder tests pass after fix
### Service Dependencies
- `services.transaction_service`:
- `get_pending_transactions()`
- `get_frozen_transactions()`
- `get_processed_transactions()`
- `get_team_transactions()`
- `create_transaction_batch()` *(NEW - for /ilmove)* - POST transactions to database
- `services.roster_service`:
- `get_current_roster()`
- `get_next_roster()`
- `validate_roster()`
- `services.team_service`:
- `get_teams_by_owner()`
- `get_team_by_abbrev()`
- `get_teams_by_season()` *(trade autocomplete)*
- `services.trade_builder`:
- `TradeBuilder` class for multi-team transaction management
- `get_trade_builder()` and `clear_trade_builder()` cache functions
- `TradeValidationResult` for comprehensive trade validation
- `services.transaction_builder`:
- `TransactionBuilder` class for single-team roster moves (shared by /dropadd and /ilmove)
- `get_transaction_builder()` and `clear_transaction_builder()` cache functions
- `RosterValidationResult` for roster legality validation
- `services.player_service`:
- `search_players()` for autocomplete functionality
- `update_player_team()` *(NEW - for /ilmove)* - Update player team assignments
### Core Dependencies
- `utils.decorators.logged_command`
- `views.embeds.EmbedTemplate`
- `views.trade_embed` *(NEW)*: Trade-specific UI components
- `utils.autocomplete` *(ENHANCED)*: Player and team autocomplete functions
- `utils.team_utils` *(NEW)*: Shared team validation utilities
- `constants.SBA_CURRENT_SEASON`
### Testing
Run tests with:
- `python -m pytest tests/test_commands_transactions.py -v` (management commands)
- `python -m pytest tests/test_models_trade.py -v` *(NEW)* (trade models)
- `python -m pytest tests/test_services_trade_builder.py -v` *(NEW)* (trade builder service)
## Database Requirements
- Team ownership mapping (Discord user ID to team)
- Transaction records with status tracking
- Roster data for current and next weeks
- Player assignments and position information
- League rules and validation criteria
## Recent Enhancements
### October 2025
- ✅ **Real-Time IL Move System (`/ilmove`)**: Immediate transaction execution for current week roster changes
- 95% code reuse with `/dropadd` via shared `TransactionBuilder`
- Immediate database POST and player team updates
- Same interactive UI with "immediate" submission handler
- Supports multiple moves in single transaction
### Previous Enhancements
- ✅ **Multi-Team Trade System**: Complete `/trade` command group for 2+ team trades
- ✅ **Enhanced Autocomplete**: Major League team filtering and smart player suggestions
- ✅ **Shared Utilities**: Reusable team validation and autocomplete functions
- ✅ **Comprehensive Testing**: Factory-based tests for trade models and services
- ✅ **Interactive Trade UI**: Rich Discord embeds with real-time validation
## Future Enhancements
- **Trade Submission Integration**: Connect trade system to transaction processing pipeline
- **Advanced transaction analytics and history
- **Trade Approval Workflow**: Multi-party trade approval system
- **Roster optimization suggestions
- **Automated roster validation alerts
- **Trade History Tracking**: Complete audit trail for multi-team trades
## Security Considerations
- User authentication via Discord IDs
- Team ownership verification for sensitive operations
- Transaction privacy (users can only see their own transactions)
- Input validation for team abbreviations and parameters

View File

@ -0,0 +1,235 @@
# Utility Commands
This directory contains general utility commands that enhance the user experience for the SBA Discord bot.
## Commands
### `/weather [team_abbrev]`
**Description**: Roll ballpark weather for gameplay.
**Usage**:
- `/weather` - Roll weather for your team or current channel's team
- `/weather NYY` - Roll weather for a specific team
**Features**:
- **Smart Team Resolution** (3-tier priority):
1. Explicit team abbreviation parameter
2. Channel name parsing (e.g., `NYY-Yankee Stadium``NYY`)
3. User's owned team (fallback)
- **Season Display**:
- Weeks 1-5: 🌼 Spring
- Weeks 6-14: 🏖️ Summer
- Weeks 15+: 🍂 Fall
- **Time of Day Logic**:
- Based on games played this week
- Division weeks: [1, 3, 6, 14, 16, 18]
- 0/2 games OR (1 game in division week): 🌙 Night
- 1/3 games: 🌞 Day
- 4+ games: 🕸️ Spidey Time (special case)
- **Weather Roll**: Random d20 (1-20) displayed in markdown format
**Embed Layout**:
```
┌─────────────────────────────────┐
│ 🌤️ Weather Check │
│ [Team Colors] │
├─────────────────────────────────┤
│ Season: 🌼 Spring │
│ Time of Day: 🌙 Night │
│ Week: 5 | Games Played: 2/4 │
├─────────────────────────────────┤
│ Weather Roll │
│ ```md │
│ # 14 │
│ Details: [1d20 (14)] │
│ ``` │
├─────────────────────────────────┤
│ [Stadium Image] │
└─────────────────────────────────┘
```
**Implementation Details**:
- **File**: `commands/utilities/weather.py`
- **Service Dependencies**:
- `LeagueService` - Current league state
- `ScheduleService` - Week schedule and games
- `TeamService` - Team resolution
- **Logging**: Uses `@logged_command` decorator for automatic logging
- **Error Handling**: Graceful fallback with user-friendly error messages
**Examples**:
1. In a team channel (`#NYY-Yankee-Stadium`):
```
/weather
→ Automatically uses NYY from channel name
```
2. Explicit team:
```
/weather BOS
→ Shows weather for Boston team
```
3. As team owner:
```
/weather
→ Defaults to your owned team if not in a team channel
```
## Architecture
### Command Pattern
All utility commands follow the standard bot architecture:
```python
@discord.app_commands.command(name="command")
@discord.app_commands.describe(param="Description")
@logged_command("/command")
async def command_handler(self, interaction, param: str):
await interaction.response.defer()
# Command logic using services
await interaction.followup.send(embed=embed)
```
### Service Layer
Utility commands leverage the service layer for all data access:
- **No direct database calls** - all data through services
- **Async operations** - proper async/await patterns
- **Error handling** - graceful degradation with user feedback
### Embed Templates
Use `EmbedTemplate` from `views.embeds` for consistent styling:
- Team colors via `team.color`
- Standard error/success/info templates
- Image support (thumbnails and full images)
## Testing
All utility commands have comprehensive test coverage:
**Weather Command** (`tests/test_commands_weather.py` - 20 tests):
- Team resolution (3-tier priority)
- Season calculation
- Time of day logic (including division weeks)
- Weather roll randomization
- Embed formatting and layout
- Error handling scenarios
**Charts Command** (`tests/test_commands_charts.py` - 26 tests):
- Chart service operations (loading, adding, updating, removing)
- Chart display (single and multi-image)
- Autocomplete functionality
- Admin command operations
- Error handling (invalid charts, categories)
- JSON persistence
### `/charts <chart-name>`
**Description**: Display gameplay charts and infographics from the league library.
**Usage**:
- `/charts rest` - Display pitcher rest chart
- `/charts defense` - Display defense chart
- `/charts hit-and-run` - Display hit and run strategy chart
**Features**:
- **Autocomplete**: Smart chart name suggestions with category display
- **Multi-image Support**: Automatically sends multiple images for complex charts
- **Categorized Library**: Charts organized by gameplay, defense, reference, and stats
- **Proper Embeds**: Charts displayed in formatted Discord embeds with descriptions
**Available Charts** (12 total):
- **Gameplay**: rest, sac-bunt, squeeze-bunt, hit-and-run, g1, g2, g3, groundball, fly-b
- **Defense**: rob-hr, defense, block-plate
**Admin Commands**:
Administrators can manage the chart library using these commands:
- `/chart-add <key> <name> <category> <url> [description]` - Add a new chart
- `/chart-remove <key>` - Remove a chart from the library
- `/chart-list [category]` - List all charts (optionally filtered by category)
- `/chart-update <key> [name] [category] [url] [description]` - Update chart properties
**Implementation Details**:
- **Files**:
- `commands/utilities/charts.py` - Command handlers
- `services/chart_service.py` - Chart management service
- `data/charts.json` - Chart definitions storage
- **Service**: `ChartService` - Manages chart loading, saving, and retrieval
- **Categories**: gameplay, defense, reference, stats
- **Logging**: Uses `@logged_command` decorator for automatic logging
**Examples**:
1. Display a single-image chart:
```
/charts defense
→ Shows defense chart embed with image
```
2. Display multi-image chart:
```
/charts hit-and-run
→ Shows first image in response, additional images in followups
```
3. Admin: Add new chart:
```
/chart-add steal-chart "Steal Chart" gameplay https://example.com/steal.png
→ Adds new chart to the library
```
4. Admin: List charts by category:
```
/chart-list gameplay
→ Shows all gameplay charts
```
**Data Structure** (`data/charts.json`):
```json
{
"charts": {
"chart-key": {
"name": "Display Name",
"category": "gameplay",
"description": "Chart description",
"urls": ["https://example.com/image.png"]
}
},
"categories": {
"gameplay": "Gameplay Mechanics",
"defense": "Defensive Play"
}
}
```
## Future Commands
Planned utility commands (see PRE_LAUNCH_ROADMAP.md):
- `/links <resource-name>` - Quick access to league resources
## Development Guidelines
When adding new utility commands:
1. **Follow existing patterns** - Use weather.py as a reference
2. **Use @logged_command** - Automatic logging and error handling
3. **Service layer only** - No direct database access
4. **Comprehensive tests** - Cover all edge cases
5. **User-friendly errors** - Clear, actionable error messages
6. **Document in README** - Update this file with new commands
---
**Last Updated**: January 2025
**Maintainer**: Major Domo Bot Development Team

251
commands/voice/CLAUDE.md Normal file
View File

@ -0,0 +1,251 @@
# Voice Channel Commands
This directory contains Discord slash commands for creating and managing voice channels for gameplay.
## Files
### `channels.py`
- **Commands**:
- `/voice-channel public` - Create a public voice channel for gameplay
- `/voice-channel private` - Create a private team vs team voice channel
- **Description**: Main command implementation with VoiceChannelCommands cog
- **Service Dependencies**:
- `team_service.get_teams_by_owner()` - Verify user has a team
- `league_service.get_current_state()` - Get current season/week info
- `schedule_service.get_team_schedule()` - Find opponent for private channels
- **Deprecated Commands**:
- `!vc`, `!voice`, `!gameplay` → Shows migration message to `/voice-channel public`
- `!private` → Shows migration message to `/voice-channel private`
### `cleanup_service.py`
- **Class**: `VoiceChannelCleanupService`
- **Description**: Manages automatic cleanup of bot-created voice channels
- **Features**:
- Restart-resilient channel tracking using JSON persistence
- Configurable cleanup intervals and empty thresholds
- Background monitoring loop with error recovery
- Startup verification to clean stale tracking entries
### `tracker.py`
- **Class**: `VoiceChannelTracker`
- **Description**: JSON-based persistent tracking of voice channels
- **Features**:
- Channel creation and status tracking
- Empty duration monitoring with datetime handling
- Cleanup candidate identification
- Automatic stale entry removal
### `__init__.py`
- **Function**: `setup_voice(bot)`
- **Description**: Package initialization with resilient cog loading
- **Integration**: Follows established bot architecture patterns
## Key Features
### Public Voice Channels (`/voice-channel public`)
- **Permissions**: Everyone can connect and speak
- **Naming**: Random codename generation (e.g., "Gameplay Phoenix", "Gameplay Thunder")
- **Requirements**: User must own a Major League team (3-character abbreviations like NYY, BOS)
- **Auto-cleanup**: Configurable threshold (default: empty for configured minutes)
### Private Voice Channels (`/voice-channel private`)
- **Permissions**:
- Team members can connect and speak (using `team.lname` Discord roles)
- Everyone else can connect but only listen
- **Naming**: Automatic "{Away} vs {Home}" format based on current week's schedule
- **Opponent Detection**: Uses current league week to find scheduled opponent
- **Requirements**:
- User must own a Major League team (3-character abbreviations like NYY, BOS)
- Team must have upcoming games in current week
- **Role Integration**: Finds Discord roles matching team full names (`team.lname`)
### Automatic Cleanup System
- **Monitoring Interval**: Configurable (default: 60 seconds)
- **Empty Threshold**: Configurable (default: 5 minutes empty before deletion)
- **Restart Resilience**: JSON file persistence survives bot restarts
- **Startup Verification**: Validates tracked channels still exist on bot startup
- **Graceful Error Handling**: Continues operation even if individual operations fail
## Architecture
### Command Flow
1. **Major League Team Verification**: Check user owns a Major League team using `team_service`
2. **Channel Creation**: Create voice channel with appropriate permissions
3. **Tracking Registration**: Add channel to cleanup service tracking
4. **User Feedback**: Send success embed with channel details
### Team Validation Logic
The voice channel system validates that users own **Major League teams** specifically:
```python
async def _get_user_major_league_team(self, user_id: int, season: Optional[int] = None):
"""Get the user's Major League team for schedule/game purposes."""
teams = await team_service.get_teams_by_owner(user_id, season)
# Filter to only Major League teams (3-character abbreviations)
major_league_teams = [team for team in teams if team.roster_type() == RosterType.MAJOR_LEAGUE]
return major_league_teams[0] if major_league_teams else None
```
**Team Types:**
- **Major League**: 3-character abbreviations (e.g., NYY, BOS, LAD) - **Required for voice channels**
- **Minor League**: 4+ characters ending in "MIL" (e.g., NYYMIL, BOSMIL) - **Not eligible**
- **Injured List**: Ending in "IL" (e.g., NYYIL, BOSIL) - **Not eligible**
**Rationale:** Only Major League teams participate in weekly scheduled games, so voice channel creation is restricted to active Major League team owners.
### Permission System
```python
# Public channels - everyone can speak
overwrites = {
guild.default_role: discord.PermissionOverwrite(speak=True, connect=True)
}
# Private channels - team roles only can speak
overwrites = {
guild.default_role: discord.PermissionOverwrite(speak=False, connect=True),
user_team_role: discord.PermissionOverwrite(speak=True, connect=True),
opponent_team_role: discord.PermissionOverwrite(speak=True, connect=True)
}
```
### Cleanup Service Integration
```python
# Bot initialization (bot.py)
from commands.voice.cleanup_service import VoiceChannelCleanupService
self.voice_cleanup_service = VoiceChannelCleanupService()
asyncio.create_task(self.voice_cleanup_service.start_monitoring(self))
# Channel tracking
if hasattr(self.bot, 'voice_cleanup_service'):
cleanup_service = self.bot.voice_cleanup_service
cleanup_service.tracker.add_channel(channel, channel_type, interaction.user.id)
```
### JSON Data Structure
```json
{
"voice_channels": {
"123456789": {
"channel_id": "123456789",
"guild_id": "987654321",
"name": "Gameplay Phoenix",
"type": "public",
"created_at": "2025-01-15T10:30:00",
"last_checked": "2025-01-15T10:35:00",
"empty_since": "2025-01-15T10:32:00",
"creator_id": "111222333"
}
}
}
```
## Configuration
### Cleanup Service Settings
- **`cleanup_interval`**: How often to check channels (default: 60 seconds)
- **`empty_threshold`**: Minutes empty before deletion (default: 5 minutes)
- **`data_file`**: JSON persistence file path (default: "data/voice_channels.json")
### Channel Categories
- Channels are created in the "Voice Channels" category if it exists
- Falls back to no category if "Voice Channels" category not found
### Random Codenames
```python
CODENAMES = [
"Phoenix", "Thunder", "Lightning", "Storm", "Blaze", "Frost", "Shadow", "Nova",
"Viper", "Falcon", "Wolf", "Eagle", "Tiger", "Shark", "Bear", "Dragon",
"Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot", "Golf", "Hotel",
"Crimson", "Azure", "Emerald", "Golden", "Silver", "Bronze", "Platinum", "Diamond"
]
```
## Error Handling
### Common Scenarios
- **No Team Found**: User-friendly message directing to contact league administrator
- **No Upcoming Games**: Informative message about being between series
- **Missing Discord Roles**: Warning in embed about teams without speaking permissions
- **Permission Errors**: Clear message to contact server administrator
- **League Info Unavailable**: Graceful fallback with retry suggestion
### Service Dependencies
- **Graceful Degradation**: Voice channels work without cleanup service
- **API Failures**: Comprehensive error handling for external service calls
- **Discord Errors**: Specific handling for Forbidden, NotFound, etc.
## Testing Coverage
### Test Files
- **`tests/test_commands_voice.py`**: Comprehensive test suite covering:
- VoiceChannelTracker JSON persistence and datetime handling
- VoiceChannelCleanupService restart resilience and monitoring
- VoiceChannelCommands slash command functionality
- Error scenarios and edge cases
- Deprecated command migration messages
### Mock Objects
- Discord guild, channels, roles, and interactions
- Team service responses and player data
- Schedule service responses and game data
- League service current state information
## Integration Points
### Bot Integration
- **Package Loading**: Integrated into `bot.py` command package loading sequence
- **Background Tasks**: Cleanup service started in `_setup_background_tasks()`
- **Shutdown Handling**: Cleanup service stopped in `bot.close()`
### Service Layer
- **Team Service**: User team verification and ownership lookup
- **League Service**: Current season/week information retrieval
- **Schedule Service**: Team schedule and opponent detection
### Discord Integration
- **Application Commands**: Modern slash command interface with command groups
- **Permission Overwrites**: Fine-grained voice channel permission control
- **Embed Templates**: Consistent styling using established embed patterns
- **Error Handling**: Integration with global application command error handler
## Usage Examples
### Creating Public Channel
```
/voice-channel public
```
**Result**: Creates "Gameplay [Codename]" with public speaking permissions
### Creating Private Channel
```
/voice-channel private
```
**Result**: Creates "[Away] vs [Home]" with team-only speaking permissions
### Migration from Old Commands
```
!vc
```
**Result**: Shows deprecation message suggesting `/voice-channel public`
## Future Enhancements
### Potential Features
- **Channel Limits**: Per-user or per-team channel creation limits
- **Custom Names**: Allow users to specify custom channel names
- **Extended Permissions**: More granular permission control options
- **Channel Templates**: Predefined setups for different game types
- **Integration Webhooks**: Notifications when channels are created/deleted
### Configuration Options
- **Environment Variables**: Make cleanup intervals configurable via env vars
- **Per-Guild Settings**: Different settings for different Discord servers
- **Role Mapping**: Custom role name patterns for team permissions
---
**Last Updated**: January 2025
**Architecture**: Modern async Discord.py with JSON persistence
**Dependencies**: discord.py, team_service, league_service, schedule_service

691
models/CLAUDE.md Normal file
View File

@ -0,0 +1,691 @@
# Models Directory
The models directory contains Pydantic data models for Discord Bot v2.0, providing type-safe representations of all SBA (Strat-o-Matic Baseball Association) entities. All models inherit from `SBABaseModel` and follow consistent validation patterns.
## Architecture
### Pydantic Foundation
All models use Pydantic v2 with:
- **Automatic validation** of field types and constraints
- **Serialization/deserialization** for API interactions
- **Type safety** with full IDE support
- **JSON schema generation** for documentation
- **Field validation** with custom validators
### Base Model (`base.py`)
The foundation for all SBA models:
```python
class SBABaseModel(BaseModel):
model_config = {
"validate_assignment": True,
"use_enum_values": True,
"arbitrary_types_allowed": True,
"json_encoders": {datetime: lambda v: v.isoformat() if v else None}
}
id: Optional[int] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
```
### Breaking Changes (August 2025)
**Database entities now require `id` fields** since they're always fetched from the database:
- `Player` model: `id: int = Field(..., description="Player ID from database")`
- `Team` model: `id: int = Field(..., description="Team ID from database")`
### Game Submission Models (January 2025)
New models for comprehensive game data submission from Google Sheets scorecards:
#### Play Model (`play.py`)
Represents a single play in a baseball game with complete statistics and game state.
**Key Features:**
- **92 total fields** supporting comprehensive play-by-play tracking
- **68 fields from scorecard**: All data read from Google Sheets Playtable
- **Required fields**: game_id, play_num, pitcher_id, on_base_code, inning details, outs, scores
- **Base running**: Tracks up to 3 runners with starting and ending positions
- **Statistics**: PA, AB, H, HR, RBI, BB, SO, SB, CS, errors, and 20+ more
- **Advanced metrics**: WPA, RE24, ballpark effects
- **Descriptive text generation**: Automatic play descriptions for key plays display
**Field Validators:**
```python
@field_validator('on_first_final')
@classmethod
def no_final_if_no_runner_one(cls, v, info):
"""Ensure on_first_final is None if no runner on first."""
if info.data.get('on_first_id') is None:
return None
return v
```
**Usage Example:**
```python
play = Play(
id=1234,
game_id=567,
play_num=1,
pitcher_id=100,
batter_id=101,
on_base_code="000",
inning_half="top",
inning_num=1,
batting_order=1,
starting_outs=0,
away_score=0,
home_score=0,
homerun=1,
rbi=1,
wpa=0.15
)
# Generate human-readable description
description = play.descriptive_text(away_team, home_team)
# Output: "Top 1: (NYY) homers"
```
**Field Categories:**
- **Game Context**: game_id, play_num, inning_half, inning_num, starting_outs
- **Players**: batter_id, pitcher_id, catcher_id, defender_id, runner_id
- **Base Runners**: on_first_id, on_second_id, on_third_id (with _final positions)
- **Offensive Stats**: pa, ab, hit, rbi, double, triple, homerun, bb, so, hbp, sac
- **Defensive Stats**: outs, error, wild_pitch, passed_ball, pick_off, balk
- **Advanced**: wpa, re24_primary, re24_running, ballpark effects (bphr, bpfo, bp1b, bplo)
- **Pitching**: pitcher_rest_outs, inherited_runners, inherited_scored, on_hook_for_loss
**API-Populated Nested Objects:**
The Play model includes optional nested object fields for all ID references. These are populated by the API endpoint to provide complete context without additional lookups:
```python
class Play(SBABaseModel):
# ID field with corresponding optional object
game_id: int = Field(..., description="Game ID this play belongs to")
game: Optional[Game] = Field(None, description="Game object (API-populated)")
pitcher_id: int = Field(..., description="Pitcher ID")
pitcher: Optional[Player] = Field(None, description="Pitcher object (API-populated)")
batter_id: Optional[int] = Field(None, description="Batter ID")
batter: Optional[Player] = Field(None, description="Batter object (API-populated)")
# ... and so on for all player/team IDs
```
**Pattern Details:**
- **Placement**: Optional object field immediately follows its corresponding ID field
- **Naming**: Object field uses singular form of ID field name (e.g., `batter_id``batter`)
- **API Population**: Database endpoint includes nested objects in response
- **Future Enhancement**: Validators could ensure consistency between ID and object fields
**ID Fields with Nested Objects:**
- `game_id``game: Optional[Game]`
- `pitcher_id``pitcher: Optional[Player]`
- `batter_id``batter: Optional[Player]`
- `batter_team_id``batter_team: Optional[Team]`
- `pitcher_team_id``pitcher_team: Optional[Team]`
- `on_first_id``on_first: Optional[Player]`
- `on_second_id``on_second: Optional[Player]`
- `on_third_id``on_third: Optional[Player]`
- `catcher_id``catcher: Optional[Player]`
- `catcher_team_id``catcher_team: Optional[Team]`
- `defender_id``defender: Optional[Player]`
- `defender_team_id``defender_team: Optional[Team]`
- `runner_id``runner: Optional[Player]`
- `runner_team_id``runner_team: Optional[Team]`
**Usage Example:**
```python
# API returns play with nested objects populated
play = await play_service.get_play(play_id=123)
# Access nested objects directly without additional lookups
if play.batter:
print(f"Batter: {play.batter.name}")
if play.pitcher:
print(f"Pitcher: {play.pitcher.name}")
if play.game:
print(f"Game: {play.game.matchup_display}")
```
#### Decision Model (`decision.py`)
Tracks pitching decisions (wins, losses, saves, holds) for game results.
**Key Features:**
- **Pitching decisions**: Win, Loss, Save, Hold, Blown Save flags
- **Game metadata**: game_id, season, week, game_num
- **Pitcher workload**: rest_ip, rest_required, inherited runners
- **Human-readable repr**: Shows decision type (W/L/SV/HLD/BS)
**Usage Example:**
```python
decision = Decision(
id=456,
game_id=567,
season=12,
week=5,
game_num=2,
pitcher_id=200,
team_id=10,
win=1, # Winning pitcher
is_start=True,
rest_ip=7.0,
rest_required=4
)
print(decision)
# Output: Decision(pitcher_id=200, game_id=567, type=W)
```
**Field Categories:**
- **Game Context**: game_id, season, week, game_num
- **Pitcher**: pitcher_id, team_id
- **Decisions**: win, loss, hold, is_save, b_save (all 0 or 1)
- **Workload**: is_start, irunners, irunners_scored, rest_ip, rest_required
**Data Pipeline:**
```
Google Sheets Scorecard
SheetsService.read_playtable_data() → 68 fields per play
PlayService.create_plays_batch() → Validate with Play model
Database API /plays endpoint
PlayService.get_top_plays_by_wpa() → Return Play objects
Play.descriptive_text() → Human-readable descriptions
```
## Model Categories
### Core Entities
#### League Structure
- **`team.py`** - Team information, abbreviations, divisions, and organizational affiliates
- **`division.py`** - Division structure and organization
- **`manager.py`** - Team managers and ownership
- **`standings.py`** - Team standings and rankings
#### Player Data
- **`player.py`** - Core player information and identifiers
- **`sbaplayer.py`** - Extended SBA-specific player data
- **`batting_stats.py`** - Batting statistics and performance metrics
- **`pitching_stats.py`** - Pitching statistics and performance metrics
- **`roster.py`** - Team roster assignments and positions
#### Game Operations
- **`game.py`** - Individual game results and scheduling
- **`play.py`** (NEW - January 2025) - Play-by-play data for game submissions
- **`decision.py`** (NEW - January 2025) - Pitching decisions and game results
- **`transaction.py`** - Player transactions (trades, waivers, etc.)
#### Draft System
- **`draft_pick.py`** - Individual draft pick information
- **`draft_data.py`** - Draft round and selection data
- **`draft_list.py`** - Complete draft lists and results
#### Custom Features
- **`custom_command.py`** - User-created Discord commands
### Custom Command Model (October 2025)
**Optional Creator ID Field**
The `CustomCommand` model now has an optional `creator_id` field to support different API endpoints:
```python
class CustomCommand(SBABaseModel):
id: int = Field(..., description="Database ID")
name: str = Field(..., description="Command name (unique)")
content: str = Field(..., description="Command response content")
creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
creator: Optional[CustomCommandCreator] = Field(None, description="Creator details")
```
**Why Optional:**
- The `/custom_commands/by_name/{name}/execute` API endpoint returns command data **without** `creator_id`
- This endpoint only needs to return the command content for execution
- Other endpoints (get, create, update, delete) include full creator information
**Usage Pattern:**
```python
# Execute endpoint - creator_id may be None
command, content = await custom_commands_service.execute_command("BPFO")
# command.creator_id might be None, but command.content is always present
# Get endpoint - creator_id is always present
command = await custom_commands_service.get_command_by_name("BPFO")
# command.creator_id is populated, command.creator has full details
```
**Important Note:**
This change maintains backward compatibility while fixing validation errors when executing custom commands. Permission checks (edit/delete) use endpoints that return complete creator information.
#### Trade System
- **`trade.py`** - Multi-team trade structures and validation
### Legacy Models
- **`current.py`** - Legacy model definitions for backward compatibility
## Model Validation Patterns
### Required Fields
Models distinguish between required and optional fields:
```python
class Player(SBABaseModel):
id: int = Field(..., description="Player ID from database") # Required
name: str = Field(..., description="Player full name") # Required
team_id: Optional[int] = None # Optional
position: Optional[str] = None # Optional
```
### Field Constraints
Models use Pydantic validators for data integrity:
```python
class BattingStats(SBABaseModel):
at_bats: int = Field(ge=0, description="At bats (non-negative)")
hits: int = Field(ge=0, le=Field('at_bats'), description="Hits (cannot exceed at_bats)")
@field_validator('batting_average')
@classmethod
def validate_batting_average(cls, v):
if v is not None and not 0.0 <= v <= 1.0:
raise ValueError('Batting average must be between 0.0 and 1.0')
return v
```
### Custom Validators
Models implement business logic validation:
```python
class Transaction(SBABaseModel):
transaction_type: str
player_id: int
from_team_id: Optional[int] = None
to_team_id: Optional[int] = None
@model_validator(mode='after')
def validate_team_requirements(self):
if self.transaction_type == 'trade':
if not self.from_team_id or not self.to_team_id:
raise ValueError('Trade transactions require both from_team_id and to_team_id')
return self
```
## API Integration
### Data Transformation
Models provide methods for API interaction:
```python
class Player(SBABaseModel):
@classmethod
def from_api_data(cls, data: Dict[str, Any]):
"""Create model instance from API response data."""
if not data:
raise ValueError(f"Cannot create {cls.__name__} from empty data")
return cls(**data)
def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:
"""Convert model to dictionary for API requests."""
return self.model_dump(exclude_none=exclude_none)
```
### Serialization Examples
Models handle various data formats:
```python
# From API JSON
player_data = {"id": 123, "name": "Player Name", "team_id": 5}
player = Player.from_api_data(player_data)
# To API JSON
api_payload = player.to_dict(exclude_none=True)
# JSON string serialization
json_string = player.model_dump_json()
# From JSON string
player_copy = Player.model_validate_json(json_string)
```
## Testing Requirements
### Model Validation Testing
All model tests must provide complete data:
```python
def test_player_creation():
# ✅ Correct - provides required ID field
player = Player(
id=123,
name="Test Player",
team_id=5,
position="1B"
)
assert player.id == 123
def test_incomplete_data():
# ❌ This will fail - missing required ID
with pytest.raises(ValidationError):
Player(name="Test Player") # Missing required id field
```
### Test Data Patterns
Use helper functions for consistent test data:
```python
def create_test_player(**overrides) -> Player:
"""Create a test player with default values."""
defaults = {
"id": 123,
"name": "Test Player",
"team_id": 1,
"position": "1B"
}
defaults.update(overrides)
return Player(**defaults)
def test_player_with_stats():
player = create_test_player(name="Star Player")
assert player.name == "Star Player"
assert player.id == 123 # Default from helper
```
## Field Types and Constraints
### Common Field Patterns
#### Identifiers
```python
id: int = Field(..., description="Database primary key")
player_id: int = Field(..., description="Foreign key to player")
team_id: Optional[int] = Field(None, description="Foreign key to team")
```
#### Names and Text
```python
name: str = Field(..., min_length=1, max_length=100)
abbreviation: str = Field(..., min_length=2, max_length=5)
description: Optional[str] = Field(None, max_length=500)
```
#### Statistics
```python
games_played: int = Field(ge=0, description="Games played (non-negative)")
batting_average: Optional[float] = Field(None, ge=0.0, le=1.0)
era: Optional[float] = Field(None, ge=0.0, description="Earned run average")
```
#### Dates and Times
```python
game_date: Optional[datetime] = None
created_at: Optional[datetime] = None
season_year: int = Field(..., ge=1900, le=2100)
```
## Model Relationships
### Foreign Key Patterns
Models reference related entities via ID fields:
```python
class Player(SBABaseModel):
id: int
team_id: Optional[int] = None # References Team.id
class BattingStats(SBABaseModel):
player_id: int # References Player.id
season: int
team_id: int # References Team.id
```
### Nested Objects
Some models contain nested structures:
```python
class CustomCommand(SBABaseModel):
name: str
creator: Manager # Nested Manager object
response: str
class DraftPick(SBABaseModel):
pick_number: int
player: Optional[Player] = None # Optional nested Player
team: Team # Required nested Team
```
## Validation Error Handling
### Common Validation Errors
- **Missing required fields** - Provide all required model fields
- **Type mismatches** - Ensure field types match model definitions
- **Constraint violations** - Check field validators and constraints
- **Invalid nested objects** - Validate all nested model data
### Error Examples
```python
try:
player = Player(name="Test") # Missing required id
except ValidationError as e:
print(e.errors())
# [{'type': 'missing', 'loc': ('id',), 'msg': 'Field required'}]
try:
stats = BattingStats(hits=5, at_bats=3) # hits > at_bats
except ValidationError as e:
print(e.errors())
# Constraint violation error
```
## Performance Considerations
### Model Instantiation
- Use `model_validate()` for external data
- Use `model_construct()` for trusted internal data (faster)
- Cache model instances when possible
- Avoid repeated validation of the same data
### Memory Usage
- Models are relatively lightweight
- Nested objects can increase memory footprint
- Consider using `__slots__` for high-volume models
- Use `exclude_none=True` to reduce serialization size
## Development Guidelines
### Adding New Models
1. **Inherit from SBABaseModel** for consistency
2. **Define required fields explicitly** with proper types
3. **Add field descriptions** for documentation
4. **Include validation rules** for data integrity
5. **Provide `from_api_data()` class method** if needed
6. **Write comprehensive tests** covering edge cases
## Team Model Enhancements (January 2025)
### Organizational Affiliate Methods
The Team model now includes methods to work with organizational affiliates (Major League, Minor League, and Injured List teams):
```python
class Team(SBABaseModel):
async def major_league_affiliate(self) -> 'Team':
"""Get the major league team for this organization via API call."""
async def minor_league_affiliate(self) -> 'Team':
"""Get the minor league team for this organization via API call."""
async def injured_list_affiliate(self) -> 'Team':
"""Get the injured list team for this organization via API call."""
def is_same_organization(self, other_team: 'Team') -> bool:
"""Check if this team and another team are from the same organization."""
```
### Usage Examples
#### Organizational Relationships
```python
# Get affiliate teams
por_team = await team_service.get_team_by_abbrev("POR", 12)
por_mil = await por_team.minor_league_affiliate() # Returns "PORMIL" team
por_il = await por_team.injured_list_affiliate() # Returns "PORIL" team
# Check organizational relationships
assert por_team.is_same_organization(por_mil) # True
assert por_team.is_same_organization(por_il) # True
# Different organizations
nyy_team = await team_service.get_team_by_abbrev("NYY", 12)
assert not por_team.is_same_organization(nyy_team) # False
```
#### Roster Type Detection
```python
# Determine roster type from team abbreviation
assert por_team.roster_type() == RosterType.MAJOR_LEAGUE # "POR"
assert por_mil.roster_type() == RosterType.MINOR_LEAGUE # "PORMIL"
assert por_il.roster_type() == RosterType.INJURED_LIST # "PORIL"
# Handle edge cases
bhm_il = Team(abbrev="BHMIL") # BHM + IL, not BH + MIL
assert bhm_il.roster_type() == RosterType.INJURED_LIST
```
### Implementation Notes
- **API Integration**: Affiliate methods make actual API calls to fetch team data
- **Error Handling**: Methods raise `ValueError` if affiliate teams cannot be found
- **Edge Cases**: Correctly handles teams like "BHMIL" (Birmingham IL)
- **Performance**: Base abbreviation extraction is cached internally
### Model Evolution
- **Backward compatibility** - Add optional fields for new features
- **Migration patterns** - Handle schema changes gracefully
- **Version management** - Document breaking changes
- **API alignment** - Keep models synchronized with API
### Testing Strategy
- **Unit tests** for individual model validation
- **Integration tests** with service layer
- **Edge case testing** for validation rules
- **Performance tests** for large data sets
## Trade Model Enhancements (January 2025)
### Multi-Team Trade Support
The Trade model now supports complex multi-team player exchanges with proper organizational authority handling:
```python
class Trade(SBABaseModel):
def get_participant_by_organization(self, team: Team) -> Optional[TradeParticipant]:
"""Find participant by organization affiliation.
Major League team owners control their entire organization (ML/MiL/IL),
so if a ML team is participating, their MiL and IL teams are also valid.
"""
@property
def cross_team_moves(self) -> List[TradeMove]:
"""Get all moves that cross team boundaries (deduplicated)."""
```
### Key Features
#### Organizational Authority Model
```python
# ML team owners can trade from/to any affiliate
wv_team = Team(abbrev="WV") # Major League
wv_mil = Team(abbrev="WVMIL") # Minor League
wv_il = Team(abbrev="WVIL") # Injured List
# If WV is participating in trade, WVMIL and WVIL moves are valid
trade.add_participant(wv_team) # Add ML team
# Now can move players to/from WVMIL and WVIL
```
#### Deduplication Fix
```python
# Before: Each move appeared twice (giving + receiving perspective)
cross_moves = trade.cross_team_moves # Would show duplicates
# After: Clean single view of each player exchange
cross_moves = trade.cross_team_moves # Shows each move once
```
### Trade Move Descriptions
Enhanced move descriptions with clear team-to-team visualization:
```python
# Team-to-team trade
"🔄 Mike Trout: WV (ML) → NY (ML)"
# Free agency signing
" Mike Trout: FA → WV (ML)"
# Release to free agency
" Mike Trout: WV (ML) → FA"
```
### Usage Examples
#### Basic Trade Setup
```python
# Create trade
trade = Trade(trade_id="abc123", participants=[], status=TradeStatus.DRAFT)
# Add participating teams
wv_participant = trade.add_participant(wv_team)
ny_participant = trade.add_participant(ny_team)
# Create player moves
move = TradeMove(
player=player,
from_team=wv_team,
to_team=ny_team,
source_team=wv_team,
destination_team=ny_team
)
```
#### Organizational Flexibility
```python
# Trade builder allows MiL/IL destinations when ML team participates
builder = TradeBuilder(user_id, wv_team) # WV is participating
builder.add_team(ny_team)
# This now works - can send player to NYMIL
success, error = await builder.add_player_move(
player=player,
from_team=wv_team,
to_team=ny_mil_team, # Minor league affiliate
from_roster=RosterType.MAJOR_LEAGUE,
to_roster=RosterType.MINOR_LEAGUE
)
assert success # ✅ Works due to organizational authority
```
### Implementation Notes
- **Deduplication**: `cross_team_moves` now uses only `moves_giving` to avoid showing same move twice
- **Organizational Lookup**: Trade participants can be found by any team in the organization
- **Validation**: Trade balance validation ensures moves are properly matched
- **UI Integration**: Embeds show clean, deduplicated player exchange lists
### Breaking Changes Fixed
- **Team Roster Type Detection**: Updated logic to handle edge cases like "BHMIL" correctly
- **Autocomplete Functions**: Fixed invalid parameter passing in team filtering
- **Trade Participant Validation**: Now properly handles organizational affiliates
---
**Next Steps for AI Agents:**
1. Review existing model implementations for patterns
2. Understand the validation rules and field constraints
3. Check the service layer integration in `/services`
4. Follow the testing patterns with complete model data
5. Consider the API data format when creating new models

607
services/CLAUDE.md Normal file
View File

@ -0,0 +1,607 @@
# Services Directory
The services directory contains the service layer for Discord Bot v2.0, providing clean abstractions for API interactions and business logic. All services inherit from `BaseService` and follow consistent patterns for data operations.
## Architecture
### Service Layer Pattern
Services act as the interface between Discord commands and the external API, providing:
- **Data validation** using Pydantic models
- **Error handling** with consistent exception patterns
- **Caching support** via Redis decorators
- **Type safety** with generic TypeVar support
- **Logging integration** with structured logging
### Base Service (`base_service.py`)
The foundation for all services, providing:
- **Generic CRUD operations** (Create, Read, Update, Delete)
- **API client management** with connection pooling
- **Response format handling** for API responses
- **Cache key generation** and management
- **Error handling** with APIException wrapping
```python
class BaseService(Generic[T]):
def __init__(self, model_class: Type[T], endpoint: str)
async def get_by_id(self, object_id: int) -> Optional[T]
async def get_all(self, params: Optional[List[tuple]] = None) -> Tuple[List[T], int]
async def create(self, model_data: Dict[str, Any]) -> Optional[T]
async def update(self, object_id: int, model_data: Dict[str, Any]) -> Optional[T]
async def patch(self, object_id: int, model_data: Dict[str, Any], use_query_params: bool = False) -> Optional[T]
async def delete(self, object_id: int) -> bool
```
**PATCH vs PUT Operations:**
- `update()` uses HTTP PUT for full resource replacement
- `patch()` uses HTTP PATCH for partial updates
- `use_query_params=True` sends data as URL query parameters instead of JSON body
**When to use `use_query_params=True`:**
Some API endpoints (notably the player PATCH endpoint) expect data as query parameters instead of JSON body. Example:
```python
# Standard PATCH with JSON body
await base_service.patch(object_id, {"field": "value"})
# → PATCH /api/v3/endpoint/{id} with JSON: {"field": "value"}
# PATCH with query parameters
await base_service.patch(object_id, {"field": "value"}, use_query_params=True)
# → PATCH /api/v3/endpoint/{id}?field=value
```
## 🚨 Service Layer Abstraction - CRITICAL BEST PRACTICE
**NEVER bypass the service layer by directly accessing the API client.** This is a critical architectural principle that must be followed in all code.
### ❌ Anti-Pattern: Direct Client Access
```python
# BAD: Bypassing service layer
async def my_task():
client = await some_service.get_client()
await client.patch(f'endpoint/{id}', data={'field': 'value'}) # ❌ WRONG
```
**Problems:**
1. **Breaks Abstraction** - Services exist to abstract API details
2. **Harder to Test** - Can't easily mock individual operations
3. **Duplicated Logic** - Same API calls repeated in multiple places
4. **Maintenance Nightmare** - API changes require updates everywhere
5. **Missing Validation** - Services provide business logic validation
6. **No Caching** - Bypass caching decorators on service methods
### ✅ Correct Pattern: Use Service Methods
```python
# GOOD: Using service layer
async def my_task():
updated = await some_service.update_item(id, {'field': 'value'}) # ✅ CORRECT
```
### When Service Methods Don't Exist
**If a service method doesn't exist for your use case:**
1. **Add the method to the service** (preferred approach)
2. **Document it properly** with docstrings
3. **Use existing BaseService methods** when possible
#### Example: Adding Missing Service Method
```python
# In league_service.py
class LeagueService(BaseService[Current]):
async def update_current_state(
self,
week: Optional[int] = None,
freeze: Optional[bool] = None
) -> Optional[Current]:
"""
Update current league state (week and/or freeze status).
Args:
week: New week number (None to leave unchanged)
freeze: New freeze status (None to leave unchanged)
Returns:
Updated Current object or None if update failed
"""
update_data = {}
if week is not None:
update_data['week'] = week
if freeze is not None:
update_data['freeze'] = freeze
# Use BaseService patch method
return await self.patch(current_id=1, model_data=update_data)
```
Then use it in tasks/commands:
```python
# In task code
updated_current = await league_service.update_current_state(
week=new_week,
freeze=True
)
```
### Real-World Example: Transaction Freeze Task
**❌ BEFORE (Bad - Direct Client Access):**
```python
# Anti-pattern: bypassing services
client = await league_service.get_client()
await client.patch(f'current/{current_id}', data={'week': new_week, 'freeze': True})
client = await transaction_service.get_client()
response = await client.get('transactions', params=[...])
moves_data = response.get('transactions', [])
transactions = [Transaction.from_api_data(move) for move in moves_data]
await client.patch(f'transactions/{move.id}', data={'frozen': False})
```
**✅ AFTER (Good - Using Service Methods):**
```python
# Proper pattern: using service layer
updated_current = await league_service.update_current_state(
week=new_week,
freeze=True
)
transactions = await transaction_service.get_frozen_transactions_by_week(
season=current.season,
week_start=current.week,
week_end=current.week + 1
)
await transaction_service.unfreeze_transaction(move.id)
```
### Benefits of Service Layer Approach
1. **Testability** - Mock `league_service.update_current_state()` easily
2. **Consistency** - All API calls go through services
3. **Maintainability** - API changes only need service updates
4. **Validation** - Services add business logic validation
5. **Reusability** - Other code can use the same service methods
6. **Abstraction** - Tasks don't need to know about API structure
7. **Caching** - Service methods can be cached with decorators
8. **Error Handling** - Consistent exception handling
### Code Review Checklist
When reviewing code, **reject any PR that:**
- ✘ Calls `await service.get_client()` outside of service layer
- ✘ Makes direct API calls in commands, tasks, or views
- ✘ Parses API responses outside of services
- ✘ Uses `client.get()`, `client.post()`, `client.patch()` outside services
**Accept only code that:**
- ✓ Uses service methods for ALL API interactions
- ✓ Adds new service methods when needed
- ✓ Properly documents new service methods
- ✓ Uses BaseService inherited methods when appropriate
## Service Files
### Core Entity Services
- **`player_service.py`** - Player data operations and search functionality
- **`team_service.py`** - Team information and roster management
- **`league_service.py`** - League-wide data and current season info
- **`standings_service.py`** - Team standings and division rankings
- **`schedule_service.py`** - Game scheduling and results
- **`stats_service.py`** - Player statistics (batting, pitching, fielding)
- **`roster_service.py`** - Team roster composition and position assignments
#### TeamService Key Methods
The `TeamService` provides team data operations with specific method names:
```python
class TeamService(BaseService[Team]):
async def get_team(team_id: int) -> Optional[Team] # ✅ Correct method name
async def get_teams_by_owner(owner_id: int, season: Optional[int], roster_type: Optional[str]) -> List[Team]
async def get_team_by_abbrev(abbrev: str, season: Optional[int]) -> Optional[Team]
async def get_teams_by_season(season: int) -> List[Team]
async def get_team_roster(team_id: int, roster_type: str = 'current') -> Optional[Dict[str, Any]]
```
**⚠️ Common Mistake (Fixed January 2025)**:
- **Incorrect**: `team_service.get_team_by_id(team_id)` ❌ (method does not exist)
- **Correct**: `team_service.get_team(team_id)`
This naming inconsistency was fixed in `services/trade_builder.py` line 201 and corresponding test mocks.
### Transaction Services
- **`transaction_service.py`** - Player transaction operations (trades, waivers, etc.)
- **`transaction_builder.py`** - Complex transaction building and validation
#### TransactionService Key Methods (October 2025 Update)
```python
class TransactionService(BaseService[Transaction]):
# Transaction retrieval methods
async def get_team_transactions(...) -> List[Transaction]
async def get_pending_transactions(...) -> List[Transaction]
async def get_frozen_transactions(...) -> List[Transaction]
async def get_processed_transactions(...) -> List[Transaction]
# NEW: Real-time transaction creation (for /ilmove)
async def create_transaction_batch(transactions: List[Transaction]) -> List[Transaction]:
"""
Create multiple transactions via API POST (for immediate execution).
This is used for real-time transactions (like IL moves) that need to be
posted to the database immediately rather than scheduled for later processing.
Args:
transactions: List of Transaction objects to create
Returns:
List of created Transaction objects with API-assigned IDs
Raises:
APIException: If transaction creation fails
"""
```
**Usage Example:**
```python
# Create transactions for immediate execution
created_transactions = await transaction_service.create_transaction_batch(transactions)
# Each transaction now has a database-assigned ID
for txn in created_transactions:
print(f"Created transaction {txn.id}: {txn.move_description}")
```
#### PlayerService Key Methods (October 2025 Update)
```python
class PlayerService(BaseService[Player]):
# Player search and retrieval methods
async def get_player(...) -> Optional[Player]
async def search_players(...) -> List[Player]
async def get_players_by_team(...) -> List[Player]
# NEW: Team assignment updates (for /ilmove)
async def update_player_team(player_id: int, new_team_id: int) -> Optional[Player]:
"""
Update a player's team assignment (for real-time IL moves).
This is used for immediate roster changes where the player needs to show
up on their new team right away, rather than waiting for transaction processing.
Args:
player_id: Player ID to update
new_team_id: New team ID to assign
Returns:
Updated player instance or None
Raises:
APIException: If player update fails
"""
```
**Usage Example:**
```python
# Update player team assignment immediately
updated_player = await player_service.update_player_team(
player_id=player.id,
new_team_id=new_team.id
)
# Player now shows on new team in all queries
print(f"{updated_player.name} now on team {updated_player.team_id}")
```
### Game Submission Services (NEW - January 2025)
- **`game_service.py`** - Game CRUD operations and scorecard submission support
- **`play_service.py`** - Play-by-play data management for game submissions
- **`decision_service.py`** - Pitching decision operations for game results
- **`sheets_service.py`** - Google Sheets integration for scorecard reading
#### GameService Key Methods
```python
class GameService(BaseService[Game]):
async def find_duplicate_game(season: int, week: int, game_num: int,
away_team_id: int, home_team_id: int) -> Optional[Game]
async def find_scheduled_game(season: int, week: int,
away_team_id: int, home_team_id: int) -> Optional[Game]
async def wipe_game_data(game_id: int) -> bool # Transaction rollback support
async def update_game_result(game_id: int, away_score: int, home_score: int,
away_manager_id: int, home_manager_id: int,
game_num: int, scorecard_url: str) -> Game
```
#### PlayService Key Methods
```python
class PlayService:
async def create_plays_batch(plays: List[Dict[str, Any]]) -> bool
async def delete_plays_for_game(game_id: int) -> bool # Transaction rollback
async def get_top_plays_by_wpa(game_id: int, limit: int = 3) -> List[Play]
```
#### DecisionService Key Methods
```python
class DecisionService:
async def create_decisions_batch(decisions: List[Dict[str, Any]]) -> bool
async def delete_decisions_for_game(game_id: int) -> bool # Transaction rollback
def find_winning_losing_pitchers(decisions_data: List[Dict[str, Any]])
-> Tuple[Optional[int], Optional[int], Optional[int], List[int], List[int]]
```
#### SheetsService Key Methods
```python
class SheetsService:
async def open_scorecard(sheet_url: str) -> pygsheets.Spreadsheet
async def read_setup_data(scorecard: pygsheets.Spreadsheet) -> Dict[str, Any]
async def read_playtable_data(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
async def read_pitching_decisions(scorecard: pygsheets.Spreadsheet) -> List[Dict[str, Any]]
async def read_box_score(scorecard: pygsheets.Spreadsheet) -> Dict[str, List[int]]
```
**Transaction Rollback Pattern:**
The game submission services implement a 3-state transaction rollback pattern:
1. **PLAYS_POSTED**: Plays submitted → Rollback: Delete plays
2. **GAME_PATCHED**: Game updated → Rollback: Wipe game + Delete plays
3. **COMPLETE**: All data committed → No rollback needed
**Usage Example:**
```python
# Create plays (state: PLAYS_POSTED)
await play_service.create_plays_batch(plays_data)
rollback_state = "PLAYS_POSTED"
try:
# Update game (state: GAME_PATCHED)
await game_service.update_game_result(game_id, ...)
rollback_state = "GAME_PATCHED"
# Create decisions (state: COMPLETE)
await decision_service.create_decisions_batch(decisions_data)
rollback_state = "COMPLETE"
except APIException as e:
# Rollback based on current state
if rollback_state == "GAME_PATCHED":
await game_service.wipe_game_data(game_id)
await play_service.delete_plays_for_game(game_id)
elif rollback_state == "PLAYS_POSTED":
await play_service.delete_plays_for_game(game_id)
```
### Custom Features
- **`custom_commands_service.py`** - User-created custom Discord commands
- **`help_commands_service.py`** - Admin-managed help system and documentation
## Caching Integration
Services support optional Redis caching via decorators:
```python
from utils.decorators import cached_api_call, cached_single_item
class PlayerService(BaseService[Player]):
@cached_api_call(ttl=600) # Cache for 10 minutes
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
@cached_single_item(ttl=300) # Cache for 5 minutes
async def get_player_by_name(self, name: str) -> Optional[Player]:
players = await self.get_by_field('name', name)
return players[0] if players else None
```
### Caching Features
- **Graceful degradation** - Works without Redis
- **Automatic key generation** based on method parameters
- **TTL support** with configurable expiration
- **Cache invalidation** patterns for data updates
## Error Handling
All services use consistent error handling:
```python
try:
result = await some_service.get_data()
return result
except APIException as e:
logger.error("API error occurred", error=e)
raise # Re-raise for command handlers
except Exception as e:
logger.error("Unexpected error", error=e)
raise APIException(f"Service operation failed: {e}")
```
### Exception Types
- **`APIException`** - API communication errors
- **`ValueError`** - Data validation errors
- **`ConnectionError`** - Network connectivity issues
## Usage Patterns
### Service Initialization
Services are typically initialized once and reused:
```python
# In services/__init__.py
from .player_service import PlayerService
from models.player import Player
player_service = PlayerService(Player, 'players')
```
### Command Integration
Services integrate with Discord commands via the `@logged_command` decorator:
```python
@discord.app_commands.command(name="player")
@logged_command("/player")
async def player_info(self, interaction: discord.Interaction, name: str):
player = await player_service.get_player_by_name(name)
if not player:
await interaction.followup.send("Player not found")
return
embed = create_player_embed(player)
await interaction.followup.send(embed=embed)
```
## API Response Format
Services handle the standard API response format:
```json
{
"count": 150,
"players": [
{"id": 1, "name": "Player Name", ...},
{"id": 2, "name": "Another Player", ...}
]
}
```
The `BaseService._extract_items_and_count_from_response()` method automatically parses this format and returns typed model instances.
## Development Guidelines
### Adding New Services
1. **Inherit from BaseService** with appropriate model type
2. **Define specific business methods** beyond CRUD operations
3. **Add caching decorators** for expensive operations
4. **Include comprehensive logging** with structured context
5. **Handle edge cases** and provide meaningful error messages
### Service Method Patterns
- **Query methods** should return `List[T]` or `Optional[T]`
- **Mutation methods** should return the updated model or `None`
- **Search methods** should accept flexible parameters
- **Bulk operations** should handle batching efficiently
### Testing Services
- Use `aioresponses` for HTTP client mocking
- Test both success and error scenarios
- Validate model parsing and transformation
- Verify caching behavior when Redis is available
## Environment Integration
Services respect environment configuration:
- **`DB_URL`** - Database API endpoint
- **`API_TOKEN`** - Authentication token
- **`REDIS_URL`** - Optional caching backend
- **`LOG_LEVEL`** - Logging verbosity
## Performance Considerations
### Optimization Strategies
- **Connection pooling** via global API client
- **Response caching** for frequently accessed data
- **Batch operations** for bulk data processing
- **Lazy loading** for expensive computations
### Monitoring
- All operations are logged with timing information
- Cache hit/miss ratios are tracked
- API error rates are monitored
- Service response times are measured
## Transaction Builder Enhancements (January 2025)
### Enhanced sWAR Calculations
The `TransactionBuilder` now includes comprehensive sWAR (sum of WARA) tracking for both current moves and pre-existing transactions:
```python
class TransactionBuilder:
async def validate_transaction(self, next_week: Optional[int] = None) -> RosterValidationResult:
"""
Validate transaction with optional pre-existing transaction analysis.
Args:
next_week: Week to check for existing transactions (includes pre-existing analysis)
Returns:
RosterValidationResult with projected roster counts and sWAR values
"""
```
### Pre-existing Transaction Support
When `next_week` is provided, the transaction builder:
- **Fetches existing transactions** for the specified week via API
- **Calculates roster impact** of scheduled moves using organizational team matching
- **Tracks sWAR changes** separately for Major League and Minor League rosters
- **Provides contextual display** for user transparency
#### Usage Examples
```python
# Basic validation (current functionality)
validation = await builder.validate_transaction()
# Enhanced validation with pre-existing transactions
current_week = await league_service.get_current_week()
validation = await builder.validate_transaction(next_week=current_week + 1)
# Access enhanced data
print(f"Projected ML sWAR: {validation.major_league_swar}")
print(f"Pre-existing impact: {validation.pre_existing_transactions_note}")
```
### Enhanced RosterValidationResult
New fields provide complete transaction context:
```python
@dataclass
class RosterValidationResult:
# Existing fields...
major_league_swar: float = 0.0
minor_league_swar: float = 0.0
pre_existing_ml_swar_change: float = 0.0
pre_existing_mil_swar_change: float = 0.0
pre_existing_transaction_count: int = 0
@property
def major_league_swar_status(self) -> str:
"""Formatted sWAR display with emoji."""
@property
def pre_existing_transactions_note(self) -> str:
"""User-friendly note about pre-existing moves impact."""
```
### Organizational Team Matching
Transaction processing now uses sophisticated team matching:
```python
# Enhanced logic using Team.is_same_organization()
if transaction.oldteam.is_same_organization(self.team):
# Accurately determine which roster the player is leaving
from_roster_type = transaction.oldteam.roster_type()
if from_roster_type == RosterType.MAJOR_LEAGUE:
# Update ML roster and sWAR
elif from_roster_type == RosterType.MINOR_LEAGUE:
# Update MiL roster and sWAR
```
### Key Improvements
- **Accurate Roster Detection**: Uses `Team.roster_type()` instead of assumptions
- **Organization Awareness**: Properly handles PORMIL, PORIL transactions for POR team
- **Separate sWAR Tracking**: ML and MiL sWAR changes tracked independently
- **Performance Optimization**: Pre-existing transactions loaded once and cached
- **User Transparency**: Clear display of how pre-existing moves affect calculations
### Implementation Details
- **Backwards Compatible**: All existing functionality preserved
- **Optional Enhancement**: `next_week` parameter is optional
- **Error Handling**: Graceful fallback if pre-existing transactions cannot be loaded
- **Caching**: Transaction and roster data cached to avoid repeated API calls
---
**Next Steps for AI Agents:**
1. Review existing service implementations for patterns
2. Check the corresponding model definitions in `/models`
3. Understand the caching decorators in `/utils/decorators.py`
4. Follow the error handling patterns established in `BaseService`
5. Use structured logging with contextual information
6. Consider pre-existing transaction impact when building new transaction features

498
tasks/CLAUDE.md Normal file
View File

@ -0,0 +1,498 @@
# Tasks Directory
The tasks directory contains automated background tasks for Discord Bot v2.0. These tasks handle periodic maintenance, data cleanup, and scheduled operations that run independently of user interactions.
## Architecture
### Task System Design
Tasks in Discord Bot v2.0 follow these patterns:
- **Discord.py tasks** using the `@tasks.loop` decorator
- **Structured logging** with contextual information
- **Error handling** with graceful degradation
- **Guild-specific operations** respecting bot permissions
- **Configurable intervals** via task decorators
- **Service layer integration** - ALWAYS use service methods, never direct API client access
### 🚨 CRITICAL: Service Layer Usage in Tasks
**Tasks MUST use the service layer for ALL API interactions.** Never bypass services by directly accessing the API client.
#### ❌ Anti-Pattern: Direct Client Access in Tasks
```python
# BAD: Don't do this in tasks
async def my_background_task(self):
client = await some_service.get_client() # ❌ WRONG
response = await client.get('endpoint', params=[...])
await client.patch(f'endpoint/{id}', data={'field': 'value'})
```
**Why this is bad:**
1. Breaks service layer abstraction
2. Makes testing harder (can't mock service methods)
3. Duplicates API logic across codebase
4. Misses service-level validation and caching
5. Creates maintenance nightmares when API changes
#### ✅ Correct Pattern: Use Service Methods
```python
# GOOD: Always use service methods
async def my_background_task(self):
items = await some_service.get_items_by_criteria(...) # ✅ CORRECT
updated = await some_service.update_item(id, data) # ✅ CORRECT
```
#### When Service Methods Don't Exist
If you need functionality that doesn't exist in a service:
1. **Add the method to the appropriate service** (preferred)
2. **Use existing BaseService methods** when possible
3. **Document the new method** with clear docstrings
**Example:**
```python
# In services/league_service.py
async def update_current_state(
self,
week: Optional[int] = None,
freeze: Optional[bool] = None
) -> Optional[Current]:
"""Update current league state (week and/or freeze status)."""
update_data = {}
if week is not None:
update_data['week'] = week
if freeze is not None:
update_data['freeze'] = freeze
return await self.patch(current_id=1, model_data=update_data)
# In tasks/transaction_freeze.py
async def _begin_freeze(self, current: Current):
updated_current = await league_service.update_current_state(
week=new_week,
freeze=True
) # ✅ Using service method
```
See `services/CLAUDE.md` for complete service layer best practices.
### Base Task Pattern
All tasks follow a consistent structure:
```python
from discord.ext import tasks
from utils.logging import get_contextual_logger
class ExampleTask:
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.ExampleTask')
self.task_loop.start()
def cog_unload(self):
"""Stop the task when cog is unloaded."""
self.task_loop.cancel()
@tasks.loop(hours=24) # Run daily
async def task_loop(self):
"""Main task implementation."""
try:
# Task logic here
pass
except Exception as e:
self.logger.error("Task failed", error=e)
@task_loop.before_loop
async def before_task(self):
"""Wait for bot to be ready before starting."""
await self.bot.wait_until_ready()
```
## Current Tasks
### Transaction Freeze/Thaw (`transaction_freeze.py`)
**Purpose:** Automated weekly system for freezing transactions and processing contested player acquisitions
**Schedule:** Every minute (checks for specific times to trigger actions)
**Operations:**
- **Freeze Begin (Monday 00:00):**
- Increments league week
- Sets freeze flag to True
- Runs regular transactions for the new week
- Announces freeze period in #transaction-log
- Posts weekly schedule info to #weekly-info (weeks 1-18 only)
- **Freeze End (Saturday 00:00):**
- Processes frozen transactions with priority resolution
- Resolves contested players (multiple teams want same player)
- Uses team standings to determine priority (worst teams get first priority)
- Cancels losing transactions and notifies GMs via DM
- Unfreezes winning transactions
- Posts successful transactions to #transaction-log
- Announces thaw period
#### Key Features
- **Priority Resolution:** Uses team win percentage with random tiebreaker
- **Offseason Mode:** Respects `offseason_flag` config to skip operations
- **Contested Transaction Handling:** Fair resolution system for player conflicts
- **GM Notifications:** Direct messages to managers about cancelled moves
- **Comprehensive Logging:** Detailed logs for all freeze/thaw operations
- **Error Recovery:** Owner notifications on failures
#### Configuration
The freeze task respects configuration settings:
```python
# config.py settings
offseason_flag: bool = False # When True, disables freeze/thaw operations
guild_id: int # Target guild for operations
```
**Environment Variables:**
- `OFFSEASON_FLAG=true` - Enables offseason mode (skips freeze/thaw)
- `GUILD_ID` - Discord server ID
#### Transaction Priority Logic
When multiple teams try to acquire the same player:
1. Calculate team win percentage from standings
2. Add small random component (5 decimal precision) for tiebreaking
3. Sort by priority (lowest win% = highest priority)
4. Team with lowest priority wins the player
5. All other teams have their transactions cancelled
**Tiebreaker Formula:**
```python
tiebreaker = team_win_percentage + random(0.00010000 to 0.00099999)
```
This ensures worst-record teams get priority while maintaining fairness through randomization.
#### Channel Requirements
- **#transaction-log** - Announcements and transaction posts
- **#weekly-info** - Weekly schedule information (cleared and updated)
#### Error Handling
- Comprehensive try/catch blocks with structured logging
- Owner DM notifications on failures
- Prevents duplicate error messages with warning flag
- Graceful degradation if channels not found
- Continues operation despite individual transaction failures
### Custom Command Cleanup (`custom_command_cleanup.py`)
**Purpose:** Automated cleanup system for user-created custom commands
**Schedule:** Daily (24 hours)
**Operations:**
- **Warning Phase:** Notifies users about commands at risk (unused for 60+ days)
- **Deletion Phase:** Removes commands unused for 90+ days
- **Admin Reporting:** Sends cleanup summaries to admin channels
#### Key Features
- **User Notifications:** Direct messages to command creators
- **Grace Period:** 30-day warning before deletion
- **Admin Transparency:** Optional summary reports
- **Bulk Operations:** Efficient batch processing
- **Error Resilience:** Continues operation despite individual failures
#### Configuration
The cleanup task respects guild settings and permissions:
```python
# Configuration via get_config()
guild_id = config.guild_id # Target guild
admin_channels = ['admin', 'bot-logs'] # Admin notification channels
```
#### Notification System
**Warning Embed (30 days before deletion):**
- Lists commands at risk
- Shows days since last use
- Provides usage instructions
- Links to command management
**Deletion Embed (after deletion):**
- Lists deleted commands
- Shows final usage statistics
- Provides recreation instructions
- Explains cleanup policy
#### Admin Summary
Optional admin channel reporting includes:
- Number of warnings sent
- Number of commands deleted
- Current system statistics
- Next cleanup schedule
## Task Lifecycle
### Initialization
Tasks are initialized when the bot starts:
```python
# In bot startup
def setup_cleanup_task(bot: commands.Bot) -> CustomCommandCleanupTask:
return CustomCommandCleanupTask(bot)
# Usage
cleanup_task = setup_cleanup_task(bot)
```
### Execution Flow
1. **Bot Ready Check:** Wait for `bot.wait_until_ready()`
2. **Guild Validation:** Verify bot has access to configured guild
3. **Permission Checks:** Ensure bot can send messages/DMs
4. **Main Operation:** Execute task logic with error handling
5. **Logging:** Record operation results and performance metrics
6. **Cleanup:** Reset state for next iteration
### Error Handling
Tasks implement comprehensive error handling:
```python
async def task_operation(self):
try:
# Main task logic
result = await self.perform_operation()
self.logger.info("Task completed", result=result)
except SpecificException as e:
self.logger.warning("Recoverable error", error=e)
# Continue with degraded functionality
except Exception as e:
self.logger.error("Task failed", error=e)
# Task will retry on next interval
```
## Development Patterns
### Creating New Tasks
1. **Inherit from Base Pattern**
```python
class NewTask:
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.NewTask')
self.main_loop.start()
```
2. **Configure Task Schedule**
```python
@tasks.loop(minutes=30) # Every 30 minutes
# or
@tasks.loop(hours=6) # Every 6 hours
# or
@tasks.loop(time=datetime.time(hour=3)) # Daily at 3 AM UTC
```
3. **Implement Before Loop**
```python
@main_loop.before_loop
async def before_loop(self):
await self.bot.wait_until_ready()
self.logger.info("Task initialized and ready")
```
4. **Add Cleanup Handling**
```python
def cog_unload(self):
self.main_loop.cancel()
self.logger.info("Task stopped")
```
### Task Categories
#### Maintenance Tasks
- **Data cleanup** (expired records, unused resources)
- **Cache management** (clear stale entries, optimize storage)
- **Log rotation** (archive old logs, manage disk space)
#### User Management
- **Inactive user cleanup** (remove old user data)
- **Permission auditing** (validate role assignments)
- **Usage analytics** (collect usage statistics)
#### System Monitoring
- **Health checks** (verify system components)
- **Performance monitoring** (track response times)
- **Error rate tracking** (monitor failure rates)
### Task Configuration
#### Environment Variables
Tasks respect standard bot configuration:
```python
GUILD_ID=12345... # Target Discord guild
LOG_LEVEL=INFO # Logging verbosity
REDIS_URL=redis://... # Optional caching backend
```
#### Runtime Configuration
Tasks use the central config system:
```python
from config import get_config
config = get_config()
guild = self.bot.get_guild(config.guild_id)
```
## Logging and Monitoring
### Structured Logging
Tasks use contextual logging for observability:
```python
self.logger.info(
"Cleanup task starting",
guild_id=guild.id,
commands_at_risk=len(at_risk_commands)
)
self.logger.warning(
"User DM failed",
user_id=user.id,
reason="DMs disabled"
)
self.logger.error(
"Task operation failed",
operation="delete_commands",
error=str(e)
)
```
### Performance Tracking
Tasks log timing and performance metrics:
```python
start_time = datetime.utcnow()
# ... task operations ...
duration = (datetime.utcnow() - start_time).total_seconds()
self.logger.info(
"Task completed",
duration_seconds=duration,
operations_completed=operation_count
)
```
### Error Recovery
Tasks implement retry logic and graceful degradation:
```python
async def process_with_retry(self, operation, max_retries=3):
for attempt in range(max_retries):
try:
return await operation()
except RecoverableError as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(2 ** attempt) # Exponential backoff
```
## Testing Strategies
### Unit Testing Tasks
```python
@pytest.mark.asyncio
async def test_custom_command_cleanup():
# Mock bot and services
bot = AsyncMock()
task = CustomCommandCleanupTask(bot)
# Mock service responses
with patch('services.custom_commands_service') as mock_service:
mock_service.get_commands_needing_warning.return_value = []
# Test task execution
await task.cleanup_task()
# Verify service calls
mock_service.get_commands_needing_warning.assert_called_once()
```
### Integration Testing
```python
@pytest.mark.integration
async def test_cleanup_task_with_real_data():
# Test with actual Discord bot instance
# Use test guild and test data
# Verify real Discord API interactions
```
### Performance Testing
```python
@pytest.mark.performance
async def test_cleanup_task_performance():
# Test with large datasets
# Measure execution time
# Verify memory usage
```
## Security Considerations
### Permission Validation
Tasks verify bot permissions before operations:
```python
async def check_permissions(self, guild: discord.Guild) -> bool:
"""Verify bot has required permissions."""
bot_member = guild.me
# Check for required permissions
if not bot_member.guild_permissions.send_messages:
self.logger.warning("Missing send_messages permission")
return False
return True
```
### Data Privacy
Tasks handle user data responsibly:
- **Minimal data access** - Only access required data
- **Secure logging** - Avoid logging sensitive information
- **GDPR compliance** - Respect user data rights
- **Permission respect** - Honor user privacy settings
### Rate Limiting
Tasks implement Discord API rate limiting:
```python
async def send_notifications_with_rate_limiting(self, notifications):
"""Send notifications with rate limiting."""
for notification in notifications:
try:
await self.send_notification(notification)
await asyncio.sleep(1) # Avoid rate limits
except discord.HTTPException as e:
if e.status == 429: # Rate limited
retry_after = e.response.headers.get('Retry-After', 60)
await asyncio.sleep(int(retry_after))
```
## Future Task Ideas
### Potential Additions
- **Database maintenance** - Optimize database performance
- **Backup automation** - Create data backups
- **Usage analytics** - Generate usage reports
- **Health monitoring** - System health checks
- **Cache warming** - Pre-populate frequently accessed data
### Scalability Patterns
- **Task queues** - Distribute work across multiple workers
- **Sharding support** - Handle multiple Discord guilds
- **Load balancing** - Distribute task execution
- **Monitoring integration** - External monitoring systems
---
**Next Steps for AI Agents:**
1. Review the existing cleanup task implementation
2. Understand the Discord.py tasks framework
3. Follow the structured logging patterns
4. Implement proper error handling and recovery
5. Consider guild permissions and user privacy
6. Test tasks thoroughly before deployment

293
tests/CLAUDE.md Normal file
View File

@ -0,0 +1,293 @@
# Testing Guide for Discord Bot v2.0
This document provides guidance on testing strategies, patterns, and lessons learned during the development of the Discord Bot v2.0 test suite.
## Test Structure Overview
```
tests/
├── README.md # This guide
├── __init__.py # Test package
├── fixtures/ # Test data fixtures
├── test_config.py # Configuration tests
├── test_constants.py # Constants tests
├── test_exceptions.py # Exception handling tests
├── test_models.py # Pydantic model tests
├── test_services.py # Service layer tests (25 tests)
└── test_api_client_with_aioresponses.py # API client HTTP tests (19 tests)
```
**Total Coverage**: 44 comprehensive tests covering all core functionality.
## Key Testing Patterns
### 1. HTTP Testing with aioresponsesf
**✅ Recommended Approach:**
```python
from aioresponses import aioresponses
@pytest.mark.asyncio
async def test_api_request(api_client):
with aioresponses() as m:
m.get(
"https://api.example.com/v3/players/1",
payload={"id": 1, "name": "Test Player"},
status=200
)
result = await api_client.get("players", object_id=1)
assert result["name"] == "Test Player"
```
**❌ Avoid Complex AsyncMock:**
We initially tried mocking aiohttp's async context managers manually with AsyncMock, which led to complex, brittle tests that failed due to coroutine protocol issues.
### 2. Service Layer Testing
**✅ Complete Model Data:**
Always provide complete model data that satisfies Pydantic validation:
```python
def create_player_data(self, player_id: int, name: str, **kwargs):
"""Create complete player data for testing."""
base_data = {
'id': player_id,
'name': name,
'wara': 2.5, # Required field
'season': 12, # Required field
'team_id': team_id, # Required field
'image': f'https://example.com/player{player_id}.jpg', # Required field
'pos_1': position, # Required field
}
base_data.update(kwargs)
return base_data
```
**❌ Partial Model Data:**
Providing incomplete data leads to Pydantic validation errors that are hard to debug.
### 3. API Response Format Testing
Our API returns responses in this format:
```json
{
"count": 25,
"players": [...]
}
```
**✅ Test Both Formats:**
```python
# Test the count + list format
mock_data = {
"count": 2,
"players": [player1_data, player2_data]
}
# Test single object format (for get_by_id)
mock_data = player1_data
```
## Lessons Learned
### 1. aiohttp Testing Complexity
**Problem**: Manually mocking aiohttp's async context managers is extremely complex and error-prone.
**Solution**: Use `aioresponses` library specifically designed for this purpose.
**Code Example**:
```bash
pip install aioresponses>=0.7.4
```
```python
# Clean, readable, reliable
with aioresponses() as m:
m.get("https://api.example.com/endpoint", payload=expected_data)
result = await client.get("endpoint")
```
### 2. Pydantic Model Validation in Tests
**Problem**: Our models have many required fields. Partial test data causes validation errors.
**Solution**: Create helper functions that generate complete, valid model data.
**Pattern**:
```python
def create_model_data(self, id: int, name: str, **overrides):
"""Create complete model data with all required fields."""
base_data = {
# All required fields with sensible defaults
'id': id,
'name': name,
'required_field1': 'default_value',
'required_field2': 42,
}
base_data.update(overrides)
return base_data
```
### 3. Async Context Manager Mocking
**Problem**: This doesn't work reliably:
```python
# ❌ Brittle and complex
mock_session.get.return_value.__aenter__ = AsyncMock(return_value=mock_response)
mock_session.get.return_value.__aexit__ = AsyncMock(return_value=None)
```
**Solution**: Use specialized libraries or patch at higher levels:
```python
# ✅ Clean with aioresponses
with aioresponses() as m:
m.get("url", payload=data)
# Test the actual HTTP call
```
### 4. Service Layer Mocking Strategy
**✅ Mock at the Client Level:**
```python
@pytest.fixture
def player_service_instance(self, mock_client):
service = PlayerService()
service._client = mock_client # Inject mock client
return service
```
This allows testing service logic while controlling API responses.
### 5. Global Instance Testing
**Pattern for Singleton Services:**
```python
def test_global_service_independence():
service1 = PlayerService()
service2 = PlayerService()
# Should be different instances
assert service1 is not service2
# But same configuration
assert service1.endpoint == service2.endpoint
```
## Testing Anti-Patterns to Avoid
### 1. ❌ Incomplete Test Data
```python
# This will fail Pydantic validation
mock_data = {'id': 1, 'name': 'Test'} # Missing required fields
```
### 2. ❌ Complex Manual Mocking
```python
# Avoid complex AsyncMock setups for HTTP clients
mock_response = AsyncMock()
mock_response.__aenter__ = AsyncMock(...) # Too complex
```
### 3. ❌ Testing Implementation Details
```python
# Don't test internal method calls
assert mock_client.get.call_count == 2 # Brittle
# Instead test behavior
assert len(result) == 2 # What matters to users
```
### 4. ❌ Mixing Test Concerns
```python
# Don't test multiple unrelated things in one test
def test_everything(): # Too broad
# Test HTTP client
# Test service logic
# Test model validation
# All in one test - hard to debug
```
## Best Practices Summary
### ✅ Do:
1. **Use aioresponses** for HTTP client testing
2. **Create complete model data** with helper functions
3. **Test behavior, not implementation** details
4. **Mock at appropriate levels** (client level for services)
5. **Use realistic data** that matches actual API responses
6. **Test error scenarios** as thoroughly as happy paths
7. **Keep tests focused** on single responsibilities
### ❌ Don't:
1. **Manually mock async context managers** - use specialized tools
2. **Use partial model data** - always provide complete valid data
3. **Test implementation details** - focus on behavior
4. **Mix multiple concerns** in single tests
5. **Ignore error paths** - test failure scenarios
6. **Skip integration scenarios** - test realistic workflows
## Running Tests
```bash
# Run all tests
pytest
# Run specific test files
pytest tests/test_services.py
pytest tests/test_api_client_with_aioresponses.py
# Run with coverage
pytest --cov=api --cov=services
# Run with verbose output
pytest -v
# Run specific test patterns
pytest -k "test_player" -v
```
## Adding New Tests
### For New API Endpoints:
1. Add aioresponses-based tests in `test_api_client_with_aioresponses.py`
2. Follow existing patterns for success/error scenarios
### For New Services:
1. Add service tests in `test_services.py`
2. Create helper functions for complete model data
3. Mock at the client level, not HTTP level
### For New Models:
1. Add model tests in `test_models.py`
2. Test validation, serialization, and edge cases
3. Use `from_api_data()` pattern for realistic data
## Dependencies
Core testing dependencies in `requirements.txt`:
```
pytest>=7.0.0
pytest-asyncio>=0.21.0
pytest-mock>=3.10.0
aioresponses>=0.7.4 # Essential for HTTP testing
```
## Troubleshooting Common Issues
### "coroutine object does not support async context manager"
- **Cause**: Manually mocking aiohttp async context managers
- **Solution**: Use aioresponses instead of manual mocking
### "ValidationError: Field required"
- **Cause**: Incomplete test data for Pydantic models
- **Solution**: Use helper functions that provide all required fields
### "AssertionError: Regex pattern did not match"
- **Cause**: Exception message doesn't match expected pattern
- **Solution**: Check actual error message and adjust test expectations
### Tests hanging or timing out
- **Cause**: Unclosed aiohttp sessions or improper async handling
- **Solution**: Ensure proper session cleanup and use async context managers
This guide should help maintain high-quality, reliable tests as the project grows!

941
utils/CLAUDE.md Normal file
View File

@ -0,0 +1,941 @@
# Utils Package Documentation
**Discord Bot v2.0 - Utility Functions and Helpers**
This package contains utility functions, helpers, and shared components used throughout the Discord bot application.
## 📋 Table of Contents
1. [**Structured Logging**](#-structured-logging) - Contextual logging with Discord integration
2. [**Redis Caching**](#-redis-caching) - Optional performance caching system
3. [**Command Decorators**](#-command-decorators) - Boilerplate reduction decorators
4. [**Future Utilities**](#-future-utilities) - Planned utility modules
---
## 🔍 Structured Logging
**Location:** `utils/logging.py`
**Purpose:** Provides hybrid logging system with contextual information for Discord bot debugging and monitoring.
### **Quick Start**
```python
from utils.logging import get_contextual_logger, set_discord_context
class YourCommandCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.YourCommandCog')
async def your_command(self, interaction: discord.Interaction, param: str):
# Set Discord context for all subsequent log entries
set_discord_context(
interaction=interaction,
command="/your-command",
param_value=param
)
# Start operation timing and get trace ID
trace_id = self.logger.start_operation("your_command_operation")
try:
self.logger.info("Command started")
# Your command logic here
result = await some_api_call(param)
self.logger.debug("API call completed", result_count=len(result))
self.logger.info("Command completed successfully")
except Exception as e:
self.logger.error("Command failed", error=e)
self.logger.end_operation(trace_id, "failed")
raise
else:
self.logger.end_operation(trace_id, "completed")
```
### **Key Features**
#### **🎯 Contextual Information**
Every log entry automatically includes:
- **Discord Context**: User ID, guild ID, guild name, channel ID
- **Command Context**: Command name, parameters
- **Operation Context**: Trace ID, operation name, execution duration
- **Custom Fields**: Additional context via keyword arguments
#### **⏱️ Automatic Timing & Tracing**
```python
trace_id = self.logger.start_operation("complex_operation")
# ... do work ...
self.logger.info("Operation in progress") # Includes duration_ms in extras
# ... more work ...
self.logger.end_operation(trace_id, "completed") # Final timing log
```
**Key Behavior:**
- **`trace_id`**: Promoted to **standard JSON key** (root level) for easy filtering
- **`duration_ms`**: Available in **extras** when timing is active (optional field)
- **Context**: All operation context preserved throughout the async operation
#### **🔗 Request Tracing**
Track a single request through all log entries using trace IDs:
```bash
# Find all logs for a specific request (trace_id is now a standard key)
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json
```
#### **📤 Hybrid Output**
- **Console**: Human-readable for development
- **Traditional File** (`discord_bot_v2.log`): Human-readable with debug info
- **JSON File** (`discord_bot_v2.json`): Structured for analysis
### **API Reference**
#### **Core Functions**
**`get_contextual_logger(logger_name: str) -> ContextualLogger`**
```python
# Get a logger instance for your module
logger = get_contextual_logger(f'{__name__}.MyClass')
```
**`set_discord_context(interaction=None, user_id=None, guild_id=None, **kwargs)`**
```python
# Set context from Discord interaction (recommended)
set_discord_context(interaction=interaction, command="/player", player_name="Mike Trout")
# Or set context manually
set_discord_context(user_id="123456", guild_id="987654", custom_field="value")
```
**`clear_context()`**
```python
# Clear the current logging context (usually not needed)
clear_context()
```
#### **ContextualLogger Methods**
**`start_operation(operation_name: str = None) -> str`**
```python
# Start timing and get trace ID
trace_id = logger.start_operation("player_search")
```
**`end_operation(trace_id: str, operation_result: str = "completed")`**
```python
# End operation and log final duration
logger.end_operation(trace_id, "completed")
# or
logger.end_operation(trace_id, "failed")
```
**`info(message: str, **kwargs)`**
```python
logger.info("Player found", player_id=123, team_name="Yankees")
```
**`debug(message: str, **kwargs)`**
```python
logger.debug("API call started", endpoint="players/search", timeout=30)
```
**`warning(message: str, **kwargs)`**
```python
logger.warning("Multiple players found", candidates=["Player A", "Player B"])
```
**`error(message: str, error: Exception = None, **kwargs)`**
```python
# With exception
logger.error("API call failed", error=e, retry_count=3)
# Without exception
logger.error("Validation failed", field="player_name", value="invalid")
```
**`exception(message: str, **kwargs)`**
```python
# Automatically captures current exception
try:
risky_operation()
except:
logger.exception("Unexpected error in operation", operation_id=123)
```
### **Output Examples**
#### **Console Output (Development)**
```
2025-08-14 14:32:15,123 - commands.players.info.PlayerInfoCommands - INFO - Player info command started
2025-08-14 14:32:16,456 - commands.players.info.PlayerInfoCommands - DEBUG - Starting player search
2025-08-14 14:32:18,789 - commands.players.info.PlayerInfoCommands - INFO - Command completed successfully
```
#### **JSON Output (Monitoring & Analysis)**
```json
{
"timestamp": "2025-08-15T14:32:15.123Z",
"level": "INFO",
"logger": "commands.players.info.PlayerInfoCommands",
"message": "Player info command started",
"function": "player_info",
"line": 50,
"trace_id": "abc12345",
"context": {
"user_id": "123456789",
"guild_id": "987654321",
"guild_name": "SBA League",
"channel_id": "555666777",
"command": "/player",
"player_name": "Mike Trout",
"season": 12,
"trace_id": "abc12345",
"operation": "player_info_command"
},
"extra": {
"duration_ms": 0
}
}
```
#### **Error Output with Exception**
```json
{
"timestamp": "2025-08-15T14:32:18.789Z",
"level": "ERROR",
"logger": "commands.players.info.PlayerInfoCommands",
"message": "API call failed",
"function": "player_info",
"line": 125,
"trace_id": "abc12345",
"exception": {
"type": "APITimeout",
"message": "Request timed out after 30s",
"traceback": "Traceback (most recent call last):\n File ..."
},
"context": {
"user_id": "123456789",
"guild_id": "987654321",
"command": "/player",
"player_name": "Mike Trout",
"trace_id": "abc12345",
"operation": "player_info_command"
},
"extra": {
"duration_ms": 30000,
"retry_count": 3,
"endpoint": "players/search"
}
}
```
### **Advanced Usage Patterns**
#### **API Call Logging**
```python
async def fetch_player_data(self, player_name: str):
self.logger.debug("API call started",
api_endpoint="players/search",
search_term=player_name,
timeout_ms=30000)
try:
result = await api_client.get("players", params=[("name", player_name)])
self.logger.info("API call successful",
results_found=len(result) if result else 0,
response_size_kb=len(str(result)) // 1024)
return result
except TimeoutError as e:
self.logger.error("API timeout",
error=e,
endpoint="players/search",
search_term=player_name)
raise
```
#### **Performance Monitoring**
```python
async def complex_operation(self, data):
trace_id = self.logger.start_operation("complex_operation")
# Step 1
self.logger.debug("Processing step 1", step="validation")
validate_data(data)
# Step 2
self.logger.debug("Processing step 2", step="transformation")
processed = transform_data(data)
# Step 3
self.logger.debug("Processing step 3", step="persistence")
result = await save_data(processed)
self.logger.info("Complex operation completed",
input_size=len(data),
output_size=len(result),
steps_completed=3)
# Final log automatically includes total duration_ms
```
#### **Error Context Enrichment**
```python
async def handle_player_command(self, interaction, player_name):
set_discord_context(
interaction=interaction,
command="/player",
player_name=player_name,
# Add additional context that helps debugging
user_permissions=interaction.user.guild_permissions.administrator,
guild_member_count=len(interaction.guild.members),
request_timestamp=discord.utils.utcnow().isoformat()
)
try:
# Command logic
pass
except Exception as e:
# Error logs will include all the above context automatically
self.logger.error("Player command failed",
error=e,
# Additional error-specific context
error_code="PLAYER_NOT_FOUND",
suggestion="Try using the full player name")
raise
```
### **Querying JSON Logs**
#### **Using jq for Analysis**
**Find all errors:**
```bash
jq 'select(.level == "ERROR")' logs/discord_bot_v2.json
```
**Find slow operations (>5 seconds):**
```bash
jq 'select(.extra.duration_ms > 5000)' logs/discord_bot_v2.json
```
**Track a specific user's activity:**
```bash
jq 'select(.context.user_id == "123456789")' logs/discord_bot_v2.json
```
**Find API timeout errors:**
```bash
jq 'select(.exception.type == "APITimeout")' logs/discord_bot_v2.json
```
**Get error summary by type:**
```bash
jq -r 'select(.level == "ERROR") | .exception.type' logs/discord_bot_v2.json | sort | uniq -c
```
**Trace a complete request:**
```bash
jq 'select(.trace_id == "abc12345")' logs/discord_bot_v2.json | jq -s 'sort_by(.timestamp)'
```
#### **Performance Analysis**
**Average command execution time:**
```bash
jq -r 'select(.message == "Command completed successfully") | .extra.duration_ms' logs/discord_bot_v2.json | awk '{sum+=$1; n++} END {print sum/n}'
```
**Most active users:**
```bash
jq -r '.context.user_id' logs/discord_bot_v2.json | sort | uniq -c | sort -nr | head -10
```
**Command usage statistics:**
```bash
jq -r '.context.command' logs/discord_bot_v2.json | sort | uniq -c | sort -nr
```
### **Best Practices**
#### **✅ Do:**
1. **Always set Discord context** at the start of command handlers
2. **Use start_operation()** for timing critical operations
3. **Call end_operation()** to complete operation timing
4. **Include relevant context** in log messages via keyword arguments
5. **Log at appropriate levels** (debug for detailed flow, info for milestones, warning for recoverable issues, error for failures)
6. **Include error context** when logging exceptions
7. **Use trace_id for correlation** - it's automatically available as a standard key
#### **❌ Don't:**
1. **Don't log sensitive information** (passwords, tokens, personal data)
2. **Don't over-log in tight loops** (use sampling or conditional logging)
3. **Don't use string formatting in log messages** (use keyword arguments instead)
4. **Don't forget to handle exceptions** in logging code itself
5. **Don't manually add trace_id to log messages** - it's handled automatically
#### **🎯 Trace ID & Duration Guidelines:**
- **`trace_id`**: Automatically promoted to standard key when operation is active
- **`duration_ms`**: Appears in extras for logs during timed operations
- **Operation flow**: Always call `start_operation()` → log messages → `end_operation()`
- **Query logs**: Use `jq 'select(.trace_id == "xyz")'` for request tracing
#### **Performance Considerations**
- JSON serialization adds minimal overhead (~1-2ms per log entry)
- Context variables are async-safe and thread-local
- Log rotation prevents disk space issues
- Structured queries are much faster than grep on large files
### **Troubleshooting**
#### **Common Issues**
**Logs not appearing:**
- Check log level configuration in environment
- Verify logs/ directory permissions
- Ensure handlers are properly configured
**JSON serialization errors:**
- Avoid logging complex objects directly
- Convert objects to strings or dicts before logging
- The JSONFormatter handles most common types automatically
**Context not appearing in logs:**
- Ensure `set_discord_context()` is called before logging
- Context is tied to the current async task
- Check that context is not cleared prematurely
**Performance issues:**
- Monitor log file sizes and rotation
- Consider reducing log level in production
- Use sampling for high-frequency operations
---
## 🔄 Redis Caching
**Location:** `utils/cache.py`
**Purpose:** Optional Redis-based caching system to improve performance for expensive API operations.
### **Quick Start**
```python
# In your service - caching is added via decorators
from utils.decorators import cached_api_call, cached_single_item
class PlayerService(BaseService[Player]):
@cached_api_call(ttl=600) # Cache for 10 minutes
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
# Existing method - no changes needed
return await self.get_all_items(params=[('team_id', team_id), ('season', season)])
@cached_single_item(ttl=300) # Cache for 5 minutes
async def get_player(self, player_id: int) -> Optional[Player]:
# Existing method - no changes needed
return await self.get_by_id(player_id)
```
### **Configuration**
**Environment Variables** (optional):
```bash
REDIS_URL=redis://localhost:6379 # Empty string disables caching
REDIS_CACHE_TTL=300 # Default TTL in seconds
```
### **Key Features**
- **Graceful Fallback**: Works perfectly without Redis installed/configured
- **Zero Breaking Changes**: All existing functionality preserved
- **Selective Caching**: Add decorators only to expensive methods
- **Automatic Key Generation**: Cache keys based on method parameters
- **Intelligent Invalidation**: Cache patterns for data modification
### **Available Decorators**
**`@cached_api_call(ttl=None, cache_key_suffix="")`**
- For methods returning `List[T]`
- Caches full result sets (e.g., team rosters, player searches)
**`@cached_single_item(ttl=None, cache_key_suffix="")`**
- For methods returning `Optional[T]`
- Caches individual entities (e.g., specific players, teams)
**`@cache_invalidate("pattern1", "pattern2")`**
- For data modification methods
- Clears related cache entries when data changes
### **Usage Examples**
#### **Team Roster Caching**
```python
@cached_api_call(ttl=600, cache_key_suffix="roster")
async def get_players_by_team(self, team_id: int, season: int) -> List[Player]:
# 500+ players cached for 10 minutes
# Cache key: sba:players_get_players_by_team_roster_<hash>
```
#### **Search Results Caching**
```python
@cached_api_call(ttl=180, cache_key_suffix="search")
async def get_players_by_name(self, name: str, season: int) -> List[Player]:
# Search results cached for 3 minutes
# Reduces API load for common player searches
```
#### **Cache Invalidation**
```python
@cache_invalidate("by_team", "search")
async def update_player(self, player_id: int, updates: dict) -> Optional[Player]:
# Clears team roster and search caches when player data changes
result = await self.update_by_id(player_id, updates)
return result
```
### **Performance Impact**
**Memory Usage:**
- ~1-5MB per cached team roster (500 players)
- ~1KB per cached individual player
**Performance Gains:**
- 80-90% reduction in API calls for repeated queries
- ~50-200ms response time improvement for large datasets
- Significant reduction in database/API server load
### **Implementation Details**
**Cache Manager** (`utils/cache.py`):
- Redis connection management with auto-reconnection
- JSON serialization/deserialization
- TTL-based expiration
- Prefix-based cache invalidation
**Base Service Integration**:
- Automatic cache key generation from method parameters
- Model serialization/deserialization
- Error handling and fallback to API calls
---
## 🎯 Command Decorators
**Location:** `utils/decorators.py`
**Purpose:** Decorators to reduce boilerplate code in Discord commands and service methods.
### **Command Logging Decorator**
**`@logged_command(command_name=None, log_params=True, exclude_params=None)`**
Automatically handles comprehensive logging for Discord commands:
```python
from utils.decorators import logged_command
class PlayerCommands(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.PlayerCommands')
@discord.app_commands.command(name="player")
@logged_command("/player", exclude_params=["sensitive_data"])
async def player_info(self, interaction, player_name: str, season: int = None):
# Clean business logic only - no logging boilerplate needed
player = await player_service.search_player(player_name, season)
embed = create_player_embed(player)
await interaction.followup.send(embed=embed)
```
**Features:**
- Automatic Discord context setting with interaction details
- Operation timing with trace ID generation
- Parameter logging with exclusion support
- Error handling and re-raising
- Preserves Discord.py command registration compatibility
### **Caching Decorators**
See [Redis Caching](#-redis-caching) section above for caching decorator documentation.
---
## 🚀 Discord Helpers
**Location:** `utils/discord_helpers.py` (NEW - January 2025)
**Purpose:** Common Discord-related helper functions for channel lookups, message sending, and formatting.
### **Available Functions**
#### **`get_channel_by_name(bot, channel_name)`**
Get a text channel by name from the configured guild:
```python
from utils.discord_helpers import get_channel_by_name
# In your command or cog
channel = await get_channel_by_name(self.bot, "sba-network-news")
if channel:
await channel.send("Message content")
```
**Features:**
- Retrieves guild ID from environment (`GUILD_ID`)
- Returns `TextChannel` object or `None` if not found
- Handles errors gracefully with logging
- Works across all guilds the bot is in
#### **`send_to_channel(bot, channel_name, content=None, embed=None)`**
Send a message to a channel by name:
```python
from utils.discord_helpers import send_to_channel
# Send text message
success = await send_to_channel(
self.bot,
"sba-network-news",
content="Game results posted!"
)
# Send embed
success = await send_to_channel(
self.bot,
"sba-network-news",
embed=results_embed
)
# Send both
success = await send_to_channel(
self.bot,
"sba-network-news",
content="Check out these results:",
embed=results_embed
)
```
**Features:**
- Combined channel lookup and message sending
- Supports text content, embeds, or both
- Returns `True` on success, `False` on failure
- Comprehensive error logging
- Non-critical - doesn't raise exceptions
#### **`format_key_plays(plays, away_team, home_team)`**
Format top plays into embed field text for game results:
```python
from utils.discord_helpers import format_key_plays
from services.play_service import play_service
# Get top 3 plays by WPA
top_plays = await play_service.get_top_plays_by_wpa(game_id, limit=3)
# Format for display
key_plays_text = format_key_plays(top_plays, away_team, home_team)
# Add to embed
if key_plays_text:
embed.add_field(name="Key Plays", value=key_plays_text, inline=False)
```
**Output Example:**
```
Top 3: (NYY) homers in 2 runs, NYY up 3-1
Bot 5: (BOS) doubles scoring 1 run, tied at 3
Top 9: (NYY) singles scoring 1 run, NYY up 4-3
```
**Features:**
- Uses `Play.descriptive_text()` for human-readable descriptions
- Adds score context after each play
- Shows which team is leading or if tied
- Returns empty string if no plays provided
- Handles RBI adjustments for accurate score display
### **Real-World Usage**
#### **Scorecard Submission Results Posting**
From `commands/league/submit_scorecard.py`:
```python
# Create results embed
results_embed = await self._create_results_embed(
away_team, home_team, box_score, setup_data,
current, sheet_url, wp_id, lp_id, sv_id, hold_ids, game_id
)
# Post to news channel automatically
await send_to_channel(
self.bot,
SBA_NETWORK_NEWS_CHANNEL, # "sba-network-news"
content=None,
embed=results_embed
)
```
### **Configuration**
These functions rely on environment variables:
- **`GUILD_ID`**: Discord server ID where channels should be found
- **`SBA_NETWORK_NEWS_CHANNEL`**: Channel name for game results (constant)
### **Error Handling**
All functions handle errors gracefully:
- **Channel not found**: Logs warning and returns `None` or `False`
- **Missing GUILD_ID**: Logs error and returns `None` or `False`
- **Send failures**: Logs error with details and returns `False`
- **Empty data**: Returns empty string or `False` without errors
### **Testing Considerations**
When testing commands that use these utilities:
- Mock `get_channel_by_name()` to return test channel objects
- Mock `send_to_channel()` to verify message content
- Mock `format_key_plays()` to verify play formatting logic
- Use test guild IDs in environment variables
---
## 🚀 Future Utilities
Additional utility modules planned for future implementation:
### **Permission Utilities** (Planned)
- Permission checking decorators
- Role validation helpers
- User authorization utilities
### **API Utilities** (Planned)
- Rate limiting decorators
- Response caching mechanisms
- Retry logic with exponential backoff
- Request validation helpers
### **Data Processing** (Planned)
- CSV/JSON export utilities
- Statistical calculation helpers
- Date/time formatting for baseball seasons
- Text processing and search utilities
### **Testing Utilities** (Planned)
- Mock Discord objects for testing
- Fixture generators for common test data
- Assertion helpers for Discord responses
- Test database setup and teardown
---
## 📚 Usage Examples by Module
### **Logging Integration in Commands**
```python
# commands/teams/roster.py
from utils.logging import get_contextual_logger, set_discord_context
class TeamRosterCommands(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.TeamRosterCommands')
@discord.app_commands.command(name="roster")
async def team_roster(self, interaction, team_name: str, season: int = None):
set_discord_context(
interaction=interaction,
command="/roster",
team_name=team_name,
season=season
)
trace_id = self.logger.start_operation("team_roster_command")
try:
self.logger.info("Team roster command started")
# Command implementation
team = await team_service.find_team(team_name)
self.logger.debug("Team found", team_id=team.id, team_abbreviation=team.abbrev)
players = await team_service.get_roster(team.id, season)
self.logger.info("Roster retrieved", player_count=len(players))
# Create and send response
embed = create_roster_embed(team, players)
await interaction.followup.send(embed=embed)
self.logger.info("Team roster command completed")
except TeamNotFoundError as e:
self.logger.warning("Team not found", search_term=team_name)
await interaction.followup.send(f"❌ Team '{team_name}' not found", ephemeral=True)
except Exception as e:
self.logger.error("Team roster command failed", error=e)
await interaction.followup.send("❌ Error retrieving team roster", ephemeral=True)
```
### **Service Layer Logging**
```python
# services/team_service.py
from utils.logging import get_contextual_logger
class TeamService(BaseService[Team]):
def __init__(self):
super().__init__(Team, 'teams')
self.logger = get_contextual_logger(f'{__name__}.TeamService')
async def find_team(self, team_name: str) -> Team:
self.logger.debug("Starting team search", search_term=team_name)
# Try exact match first
teams = await self.get_by_field('name', team_name)
if len(teams) == 1:
self.logger.debug("Exact team match found", team_id=teams[0].id)
return teams[0]
# Try abbreviation match
teams = await self.get_by_field('abbrev', team_name.upper())
if len(teams) == 1:
self.logger.debug("Team abbreviation match found", team_id=teams[0].id)
return teams[0]
# Try fuzzy search
all_teams = await self.get_all_items()
matches = [t for t in all_teams if team_name.lower() in t.name.lower()]
if len(matches) == 0:
self.logger.warning("No team matches found", search_term=team_name)
raise TeamNotFoundError(f"No team found matching '{team_name}'")
elif len(matches) > 1:
match_names = [t.name for t in matches]
self.logger.warning("Multiple team matches found",
search_term=team_name,
matches=match_names)
raise MultipleTeamsFoundError(f"Multiple teams found: {', '.join(match_names)}")
self.logger.debug("Fuzzy team match found", team_id=matches[0].id)
return matches[0]
```
---
## 📁 File Structure
```
utils/
├── README.md # This documentation
├── __init__.py # Package initialization
├── cache.py # Redis caching system
├── decorators.py # Command and caching decorators
├── logging.py # Structured logging implementation
└── random_gen.py # Random generation utilities
# Future files:
├── discord_helpers.py # Discord utility functions
├── api_utils.py # API helper functions
├── data_processing.py # Data manipulation utilities
└── testing.py # Testing helper functions
```
---
## 🔍 Autocomplete Functions
**Location:** `utils/autocomplete.py`
**Purpose:** Shared autocomplete functions for Discord slash command parameters.
### **Available Functions**
#### **Player Autocomplete**
```python
async def player_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
"""Autocomplete for player names with priority ordering."""
```
**Features:**
- Fuzzy name matching with word boundaries
- Prioritizes exact matches and starts-with matches
- Limits to 25 results (Discord limit)
- Handles API errors gracefully
#### **Team Autocomplete (All Teams)**
```python
async def team_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
"""Autocomplete for all team abbreviations."""
```
**Features:**
- Matches team abbreviations (e.g., "WV", "NY", "WVMIL")
- Case-insensitive matching
- Includes full team names in display
#### **Major League Team Autocomplete**
```python
async def major_league_team_autocomplete(interaction: discord.Interaction, current: str) -> List[discord.app_commands.Choice]:
"""Autocomplete for Major League teams only (filtered by roster type)."""
```
**Features:**
- Filters to only Major League teams (≤3 character abbreviations)
- Uses Team model's `roster_type()` method for accurate filtering
- Excludes Minor League (MiL) and Injured List (IL) teams
### **Usage in Commands**
```python
from utils.autocomplete import player_autocomplete, major_league_team_autocomplete
class RosterCommands(commands.Cog):
@discord.app_commands.command(name="roster")
@discord.app_commands.describe(
team="Team abbreviation",
player="Player name (optional)"
)
async def roster_command(
self,
interaction: discord.Interaction,
team: str,
player: Optional[str] = None
):
# Command logic here
pass
# Autocomplete decorators
@roster_command.autocomplete('team')
async def roster_team_autocomplete(self, interaction, current):
return await major_league_team_autocomplete(interaction, current)
@roster_command.autocomplete('player')
async def roster_player_autocomplete(self, interaction, current):
return await player_autocomplete(interaction, current)
```
### **Recent Fixes (January 2025)**
#### **Team Filtering Issue**
- **Problem**: `major_league_team_autocomplete` was passing invalid `roster_type` parameter to API
- **Solution**: Removed parameter and implemented client-side filtering using `team.roster_type()` method
- **Benefit**: More accurate team filtering that respects edge cases like "BHMIL" vs "BHMMIL"
#### **Test Coverage**
- Added comprehensive test suite in `tests/test_utils_autocomplete.py`
- Tests cover all functions, error handling, and edge cases
- Validates prioritization logic and result limits
### **Implementation Notes**
- **Shared Functions**: Autocomplete logic centralized to avoid duplication across commands
- **Error Handling**: Functions return empty lists on API errors rather than crashing
- **Performance**: Uses cached service calls where possible
- **Discord Limits**: Respects 25-choice limit for autocomplete responses
---
**Last Updated:** January 2025 - Added Autocomplete Functions and Fixed Team Filtering
**Next Update:** When additional utility modules are added
For questions or improvements to the logging system, check the implementation in `utils/logging.py` or refer to the JSON log outputs in `logs/discord_bot_v2.json`.

709
views/CLAUDE.md Normal file
View File

@ -0,0 +1,709 @@
# Views Directory
The views directory contains Discord UI components for Discord Bot v2.0, providing consistent visual interfaces and interactive elements. This includes embeds, modals, buttons, select menus, and other Discord UI components.
## Architecture
### Component-Based UI Design
Views in Discord Bot v2.0 follow these principles:
- **Consistent styling** via centralized templates
- **Reusable components** for common UI patterns
- **Error handling** with graceful degradation
- **User interaction tracking** and validation
- **Accessibility** with proper labeling and feedback
### Base Components
All view components inherit from Discord.py base classes with enhanced functionality:
- **BaseView** - Enhanced discord.ui.View with logging and user validation
- **BaseModal** - Enhanced discord.ui.Modal with error handling
- **EmbedTemplate** - Centralized embed creation with consistent styling
## View Components
### Base View System (`base.py`)
#### BaseView Class
Foundation for all interactive views:
```python
class BaseView(discord.ui.View):
def __init__(self, timeout=180.0, user_id=None):
super().__init__(timeout=timeout)
self.user_id = user_id
self.logger = get_contextual_logger(f'{__name__}.BaseView')
async def interaction_check(self, interaction) -> bool:
"""Validate user permissions for interaction."""
async def on_timeout(self) -> None:
"""Handle view timeout gracefully."""
async def on_error(self, interaction, error, item) -> None:
"""Handle view errors with user feedback."""
```
#### ConfirmationView Class (Updated January 2025)
Reusable confirmation dialog with Confirm/Cancel buttons (`confirmations.py`):
**Key Features:**
- **User restriction**: Only specified users can interact
- **Customizable labels and styles**: Flexible button appearance
- **Timeout handling**: Automatic cleanup after timeout
- **Three-state result**: `True` (confirmed), `False` (cancelled), `None` (timeout)
- **Clean interface**: Automatically removes buttons after interaction
**Usage Pattern:**
```python
from views.confirmations import ConfirmationView
# Create confirmation dialog
view = ConfirmationView(
responders=[interaction.user], # Only this user can interact
timeout=30.0, # 30 second timeout
confirm_label="Yes, delete", # Custom label
cancel_label="No, keep it" # Custom label
)
# Send confirmation
await interaction.edit_original_response(
content="⚠️ Are you sure you want to delete this?",
view=view
)
# Wait for user response
await view.wait()
# Check result
if view.confirmed is True:
# User clicked Confirm
await interaction.edit_original_response(
content="✅ Deleted successfully",
view=None
)
elif view.confirmed is False:
# User clicked Cancel
await interaction.edit_original_response(
content="❌ Cancelled",
view=None
)
else:
# Timeout occurred (view.confirmed is None)
await interaction.edit_original_response(
content="⏱️ Request timed out",
view=None
)
```
**Real-World Example (Scorecard Submission):**
```python
# From commands/league/submit_scorecard.py
if duplicate_game:
view = ConfirmationView(
responders=[interaction.user],
timeout=30.0
)
await interaction.edit_original_response(
content=(
f"⚠️ This game has already been played!\n"
f"Would you like me to wipe the old one and re-submit?"
),
view=view
)
await view.wait()
if view.confirmed:
# User confirmed - proceed with wipe and resubmit
await wipe_old_data()
else:
# User cancelled - exit gracefully
return
```
**Configuration Options:**
```python
ConfirmationView(
responders=[user1, user2], # Multiple users allowed
timeout=60.0, # Custom timeout
confirm_label="Approve", # Custom confirm text
cancel_label="Reject", # Custom cancel text
confirm_style=discord.ButtonStyle.red, # Custom button style
cancel_style=discord.ButtonStyle.grey # Custom button style
)
```
#### PaginationView Class
Multi-page navigation for large datasets:
```python
pages = [embed1, embed2, embed3]
pagination = PaginationView(
pages=pages,
user_id=interaction.user.id,
show_page_numbers=True
)
await interaction.followup.send(embed=pagination.get_current_embed(), view=pagination)
```
### Embed Templates (`embeds.py`)
#### EmbedTemplate Class
Centralized embed creation with consistent styling:
```python
# Success embed
embed = EmbedTemplate.success(
title="Operation Completed",
description="Your request was processed successfully."
)
# Error embed
embed = EmbedTemplate.error(
title="Operation Failed",
description="Please check your input and try again."
)
# Warning embed
embed = EmbedTemplate.warning(
title="Careful!",
description="This action cannot be undone."
)
# Info embed
embed = EmbedTemplate.info(
title="Information",
description="Here's what you need to know."
)
```
#### EmbedColors Dataclass
Consistent color scheme across all embeds:
```python
@dataclass(frozen=True)
class EmbedColors:
PRIMARY: int = 0xa6ce39 # SBA green
SUCCESS: int = 0x28a745 # Green
WARNING: int = 0xffc107 # Yellow
ERROR: int = 0xdc3545 # Red
INFO: int = 0x17a2b8 # Blue
SECONDARY: int = 0x6c757d # Gray
```
### Modal Forms (`modals.py`)
#### BaseModal Class
Foundation for interactive forms:
```python
class BaseModal(discord.ui.Modal):
def __init__(self, title: str, timeout=300.0):
super().__init__(title=title, timeout=timeout)
self.logger = get_contextual_logger(f'{__name__}.BaseModal')
self.result = None
async def on_submit(self, interaction):
"""Handle form submission."""
async def on_error(self, interaction, error):
"""Handle form errors."""
```
#### Usage Pattern
```python
class CustomCommandModal(BaseModal):
def __init__(self):
super().__init__(title="Create Custom Command")
name = discord.ui.TextInput(
label="Command Name",
placeholder="Enter command name...",
required=True,
max_length=50
)
response = discord.ui.TextInput(
label="Response",
placeholder="Enter command response...",
style=discord.TextStyle.paragraph,
required=True,
max_length=2000
)
async def on_submit(self, interaction):
# Process form data
command_data = {
"name": self.name.value,
"response": self.response.value
}
# Handle creation logic
```
### Common UI Elements (`common.py`)
#### Shared Components
- **Loading indicators** for async operations
- **Status messages** for operation feedback
- **Navigation elements** for multi-step processes
- **Validation displays** for form errors
### Specialized Views
#### Custom Commands (`custom_commands.py`)
Views specific to custom command management:
- Command creation forms
- Command listing with actions
- Bulk management interfaces
#### Player Information (`players.py`)
Interactive views for player information display with toggleable statistics:
**PlayerStatsView** - Toggle batting and pitching statistics independently
```python
from views.players import PlayerStatsView
# Create interactive player stats view
view = PlayerStatsView(
player=player_with_team,
season=search_season,
batting_stats=batting_stats,
pitching_stats=pitching_stats,
user_id=interaction.user.id
)
# Get initial embed with stats hidden
embed = await view.get_initial_embed()
# Send with interactive view
await interaction.followup.send(embed=embed, view=view)
```
**Key Features:**
- **Basic Info Always Visible**: Name, position, team, sWAR, injury status displayed by default
- **Stats Hidden by Default**: Batting and pitching stats are hidden until user clicks toggle buttons
- **Independent Toggles**: Users can show/hide batting and pitching stats separately
- **Conditional Buttons**: Buttons only appear if corresponding stats are available
- **User Restriction**: Only the user who ran the command can toggle stats
- **Timeout Handling**: View times out after 5 minutes with graceful cleanup
- **Professional UI**: Uses baseball emojis (💥 batting impact, ⚾ pitching) and primary button style
- **Dynamic Updates**: Embed updates in-place when buttons are clicked
**Button Behavior:**
- **Initial State**: "Show Batting Stats" and "Show Pitching Stats"
- **Toggled State**: "Hide Batting Stats" and "Hide Pitching Stats"
- **Visual Feedback**: Button labels change to reflect current state
- **Clean Interface**: Only relevant buttons are shown based on available data
**Implementation Notes:**
- Inherits from `BaseView` for consistent error handling and logging
- Stats formatting matches existing player card design with rounded box code blocks
- Preserves all player card features (images, thumbnails, team colors)
- Comprehensive logging for debugging and monitoring
#### Transaction Management (`transaction_embed.py`) (Updated October 2025)
Views for player transaction interfaces with dual-mode submission support:
- **Transaction builder** with interactive controls
- **Comprehensive validation** and sWAR display
- **Pre-existing transaction** context
- **Dual submission modes**: Scheduled (/dropadd) and Immediate (/ilmove)
- **Dynamic UI instructions**: Context-aware command references
**Key Classes:**
```python
class TransactionEmbedView(discord.ui.View):
def __init__(
self,
builder: TransactionBuilder,
user_id: int,
submission_handler: str = "scheduled", # "scheduled" or "immediate"
command_name: str = "/dropadd" # Command for UI instructions
):
"""
Interactive transaction builder view supporting both scheduled and immediate execution.
Args:
builder: TransactionBuilder instance
user_id: Discord user ID (for permission checking)
submission_handler: "scheduled" for /dropadd, "immediate" for /ilmove
command_name: Command name shown in "Add More Moves" instructions
"""
async def create_transaction_embed(
builder: TransactionBuilder,
command_name: str = "/dropadd"
) -> discord.Embed:
"""
Create transaction builder embed with context-aware instructions.
Args:
builder: TransactionBuilder instance
command_name: Command name for "Add More Moves" instruction
Returns:
Discord embed showing transaction state with appropriate instructions
"""
```
**Submission Handler Behavior:**
- **"scheduled" mode** (/dropadd):
- Creates transactions for NEXT week
- No database POST - stays in memory
- Background task processes later
- Instructions say "Use `/dropadd` to add more moves"
- **"immediate" mode** (/ilmove):
- Creates transactions for THIS week
- Immediately POSTs to database API
- Immediately updates player teams
- Instructions say "Use `/ilmove` to add more moves"
**Usage Examples:**
```python
# For /dropadd (scheduled submission)
embed = await create_transaction_embed(builder, command_name="/dropadd")
view = TransactionEmbedView(
builder,
user_id,
submission_handler="scheduled",
command_name="/dropadd"
)
# For /ilmove (immediate submission)
embed = await create_transaction_embed(builder, command_name="/ilmove")
view = TransactionEmbedView(
builder,
user_id,
submission_handler="immediate",
command_name="/ilmove"
)
```
**Implementation Notes:**
- **95% code reuse** between /dropadd and /ilmove
- **Same TransactionBuilder** instance shared between both commands
- **Dynamic embed description**: Changes based on command_name
- **Context propagation**: command_name passed through all UI components
- **Backwards compatible**: Default parameters maintain /dropadd behavior
## Styling Guidelines
### Embed Consistency
All embeds should use EmbedTemplate methods:
```python
# ✅ Consistent styling
embed = EmbedTemplate.success("Player Added", "Player successfully added to roster")
# ❌ Inconsistent styling
embed = discord.Embed(title="Player Added", color=0x00ff00)
```
### Color Usage
Use the standard color palette:
- **PRIMARY (SBA Green)** - Default for neutral information
- **SUCCESS (Green)** - Successful operations
- **ERROR (Red)** - Errors and failures
- **WARNING (Yellow)** - Warnings and cautions
- **INFO (Blue)** - General information
- **SECONDARY (Gray)** - Less important information
### User Feedback
Provide clear feedback for all user interactions:
```python
# Loading state
embed = EmbedTemplate.info("Processing", "Please wait while we process your request...")
# Success state
embed = EmbedTemplate.success("Complete", "Your request has been processed successfully.")
# Error state with helpful information
embed = EmbedTemplate.error(
"Request Failed",
"The player name was not found. Please check your spelling and try again."
)
```
## Interactive Components
### Button Patterns
#### Action Buttons
```python
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.success, emoji="✅")
async def confirm_button(self, interaction, button):
self.increment_interaction_count()
# Handle confirmation
await interaction.response.edit_message(content="Confirmed!", view=None)
```
#### Navigation Buttons
```python
@discord.ui.button(emoji="◀️", style=discord.ButtonStyle.primary)
async def previous_page(self, interaction, button):
self.current_page = max(0, self.current_page - 1)
await interaction.response.edit_message(embed=self.get_current_embed(), view=self)
```
### Select Menu Patterns
#### Option Selection
```python
@discord.ui.select(placeholder="Choose an option...")
async def select_option(self, interaction, select):
selected_value = select.values[0]
# Handle selection
await interaction.response.send_message(f"You selected: {selected_value}")
```
#### Dynamic Options
```python
class PlayerSelectMenu(discord.ui.Select):
def __init__(self, players: List[Player]):
options = [
discord.SelectOption(
label=player.name,
value=str(player.id),
description=f"{player.position} - {player.team.abbrev}"
)
for player in players[:25] # Discord limit
]
super().__init__(placeholder="Select a player...", options=options)
```
## Error Handling
### View Error Handling
All views implement comprehensive error handling:
```python
async def on_error(self, interaction, error, item):
"""Handle view errors gracefully."""
self.logger.error("View error", error=error, item_type=type(item).__name__)
try:
embed = EmbedTemplate.error(
"Interaction Error",
"Something went wrong. Please try again."
)
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e:
self.logger.error("Failed to send error message", error=e)
```
### User Input Validation
Forms validate user input before processing:
```python
async def on_submit(self, interaction):
# Validate input
if len(self.name.value) < 2:
embed = EmbedTemplate.error(
"Invalid Input",
"Command name must be at least 2 characters long."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Process valid input
await self.create_command(interaction)
```
## Accessibility Features
### User-Friendly Labels
- **Clear button labels** with descriptive text
- **Helpful placeholders** in form fields
- **Descriptive error messages** with actionable guidance
- **Consistent emoji usage** for visual recognition
### Permission Validation
Views respect user permissions and provide appropriate feedback:
```python
async def interaction_check(self, interaction) -> bool:
"""Check if user can interact with this view."""
if self.user_id and interaction.user.id != self.user_id:
await interaction.response.send_message(
"❌ You cannot interact with this menu.",
ephemeral=True
)
return False
return True
```
## Performance Considerations
### View Lifecycle Management
- **Timeout handling** prevents orphaned views
- **Resource cleanup** in view destructors
- **Interaction tracking** for usage analytics
- **Memory management** for large datasets
### Efficient Updates
```python
# ✅ Efficient - Only update what changed
await interaction.response.edit_message(embed=new_embed, view=self)
# ❌ Inefficient - Sends new message
await interaction.response.send_message(embed=new_embed, view=new_view)
```
## Testing Strategies
### View Testing
```python
@pytest.mark.asyncio
async def test_confirmation_view():
view = ConfirmationView(user_id=123)
# Mock interaction
interaction = Mock()
interaction.user.id = 123
# Test button click
await view.confirm_button.callback(interaction)
assert view.result is True
```
### Modal Testing
```python
@pytest.mark.asyncio
async def test_custom_command_modal():
modal = CustomCommandModal()
# Set form values
modal.name.value = "test"
modal.response.value = "Test response"
# Mock interaction
interaction = Mock()
# Test form submission
await modal.on_submit(interaction)
# Verify processing
assert modal.result is not None
```
## Development Guidelines
### Creating New Views
1. **Inherit from base classes** for consistency
2. **Use EmbedTemplate** for all embed creation
3. **Implement proper error handling** in all interactions
4. **Add user permission checks** where appropriate
5. **Include comprehensive logging** with context
6. **Follow timeout patterns** to prevent resource leaks
### View Composition
- **Keep views focused** on single responsibilities
- **Use composition** over complex inheritance
- **Separate business logic** from UI logic
- **Make views testable** with dependency injection
### UI Guidelines
- **Follow Discord design patterns** for familiarity
- **Use consistent colors** from EmbedColors
- **Provide clear user feedback** for all actions
- **Handle edge cases** gracefully
- **Consider mobile users** in layout design
## Transaction Embed Enhancements (January 2025)
### Enhanced Display Features
The transaction embed now provides comprehensive information for better decision-making:
#### New Embed Sections
```python
async def create_transaction_embed(builder: TransactionBuilder) -> discord.Embed:
"""
Creates enhanced transaction embed with sWAR and pre-existing transaction context.
"""
# Existing sections...
# NEW: Team Cost (sWAR) Display
swar_status = f"{validation.major_league_swar_status}\n{validation.minor_league_swar_status}"
embed.add_field(name="Team sWAR", value=swar_status, inline=False)
# NEW: Pre-existing Transaction Context (when applicable)
if validation.pre_existing_transactions_note:
embed.add_field(
name="📋 Transaction Context",
value=validation.pre_existing_transactions_note,
inline=False
)
```
### Enhanced Information Display
#### sWAR Tracking
- **Major League sWAR**: Projected team cost for ML roster
- **Minor League sWAR**: Projected team cost for MiL roster
- **Formatted Display**: Uses 📊 emoji with 1 decimal precision
#### Pre-existing Transaction Context
Dynamic context display based on scheduled moves:
```python
# Example displays:
" **Pre-existing Moves**: 3 scheduled moves (+3.7 sWAR)"
" **Pre-existing Moves**: 2 scheduled moves (-2.5 sWAR)"
" **Pre-existing Moves**: 1 scheduled moves (no sWAR impact)"
# No display when no pre-existing moves (clean interface)
```
### Complete Embed Structure
The enhanced transaction embed now includes:
1. **Current Moves** - List of moves in transaction builder
2. **Roster Status** - Legal/illegal roster counts with limits
3. **Team Cost (sWAR)** - sWAR for both rosters
4. **Transaction Context** - Pre-existing moves impact (conditional)
5. **Errors/Suggestions** - Validation feedback and recommendations
### Usage Examples
#### Basic Transaction Display
```python
# Standard transaction without pre-existing moves
builder = get_transaction_builder(user_id, team)
embed = await create_transaction_embed(builder)
# Shows: moves, roster status, sWAR, errors/suggestions
```
#### Enhanced Context Display
```python
# Transaction with pre-existing moves context
validation = await builder.validate_transaction(next_week=current_week + 1)
embed = await create_transaction_embed(builder)
# Shows: all above + pre-existing transaction impact
```
### User Experience Improvements
- **Complete Context**: Users see full impact including scheduled moves
- **Visual Clarity**: Consistent emoji usage and formatting
- **Conditional Display**: Context only shown when relevant
- **Decision Support**: sWAR projections help strategic planning
### Implementation Notes
- **Backwards Compatible**: Existing embed functionality preserved
- **Conditional Sections**: Pre-existing context only appears when applicable
- **Performance**: Validation data cached to avoid repeated calculations
- **Accessibility**: Clear visual hierarchy with emojis and formatting
---
**Next Steps for AI Agents:**
1. Review existing view implementations for patterns
2. Understand the Discord UI component system
3. Follow the EmbedTemplate system for consistent styling
4. Implement proper error handling and user validation
5. Test interactive components thoroughly
6. Consider accessibility and user experience in design
7. Leverage enhanced transaction context for better user guidance