Changed from 'Auto-Restarted' to 'Restarted' and made the message
generic since the bot restarts for multiple reasons (manual, deployment,
healthcheck) - not just healthcheck failures.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Discord webhooks require a User-Agent header or they return 403 Forbidden.
Added 'Paper-Dynasty-Discord-Bot/1.0' as User-Agent.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sends instant notifications to Discord when the bot restarts, helping
track stability issues and auto-recovery events.
Changes:
- Add notify_restart.py script to send webhook notifications
- Integrate notification into bot startup (on_ready event)
The notification includes:
- Timestamp of restart (CST)
- Reason (healthcheck failure detection)
- Reference to diagnostics logs
Configuration:
Set RESTART_WEBHOOK_URL environment variable in docker-compose.yml
to enable notifications.
This provides immediate visibility when Docker auto-restarts the bot
due to crashes or healthcheck failures.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds idempotency guard to prevent race conditions when multiple users
submit commands for the same play simultaneously.
Changes:
- Add PlayLockedException for locked play detection
- Implement lock check in checks_log_interaction()
- Acquire lock (play.locked = True) before processing commands
- Release lock (play.locked = False) after play completion
- Add warning logs for rejected duplicate submissions
- Add /diagnostics endpoint to health server for debugging
This prevents database corruption and duplicate processing when users
spam commands like "log xcheck" while the first is still processing.
Tested successfully in Discord - duplicate commands now properly return
PlayLockedException with instructions to wait.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds two new test cases to test_play_locking.py to improve coverage:
1. test_lock_released_after_successful_completion
- Verifies play.locked is set to False after complete_play()
- Confirms play.complete is set to True
- Validates database commit is called
2. test_different_commands_racing_on_locked_play
- Tests that ANY command type is blocked on locked plays
- Prevents race conditions between different command types
- Tests multiple commands: walk, strikeout, single, xcheck
These tests ensure the play locking idempotency guard works correctly
for both lock acquisition and release, and prevents all command types
from racing (not just duplicate commands).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When API returns a pitcher without pitching data (e.g., Ohtani with
pos_1=DH), explicitly fetch pitcherscouting and validate before use.
If validation fails, retry with different sp_rank values.
Retry strategy: increment rank first, if > 5 then decrement from
original rank, ensuring all 5 ranks are tried before giving up.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Card 6605 (Ohtani) placed at position P had pitcherscouting_id=NULL,
causing AttributeError when accessing pitcherscouting.pitchingcard.hand.
Added null check with fallback to batterscouting hand or '?' if neither
scouting record exists.
Also fixed set notation bug on line 240 where {value} created a Python
set instead of a string.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously p_group was only set if pack type already existed in p_data,
which would silently skip new pack types. Now properly initializes the
pack type list when encountering a new type.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add numpy<2 constraint to requirements.txt to fix RuntimeError on
sba-bots where CPU doesn't support X86_V2 instructions required by
numpy 2.x. This was causing cogs.admins and cogs.gameplay to fail
loading on bot startup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add FRANCHISE_NORMALIZE dict and helper to constants.py
- Update economy.py to normalize team_choice and use sname
- Update helpers/main.py franchise queries to use sname
- Update selectors.py to normalize franchise on player updates
Part of cross-era player matching fix for AI rosters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 'Athletics' alias to ALL_MLB_TEAMS, IMAGES['mvp'], and AL_TEAM_IDS
to support both old franchise name ("Oakland Athletics") and new mlbclub
name ("Athletics") after the team's relocation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PD_SEASON was set to 9 in helpers/constants.py while games are
recorded in season 10. This caused /player command to return
no stats for cardset 27 cards since they only have season 10 data.
Changes:
- PD_SEASON: 9 → 10
- SBA_SEASON: 11 → 12
- ranked_cardsets: updated to current cardsets
- Added gauntlet-8 and gauntlet-9 configs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added missing await keyword on line 2981 in logic_gameplay.py
- SPD hit result was calling singles() without await, causing RuntimeWarning
- All groundball tests passing (24/24)
- Bump version to 1.7.7
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bug: Version 1.7.5 added app_legal_channel to helpers.py but production uses
the helpers/ package which imports from helpers/main.py. This caused:
- NameError: name 'app_legal_channel' is not defined
- ImportError: cannot import name 'app_legal_channel' from 'helpers'
Result: cogs.economy and cogs.players failed to load, causing all slash
commands (including /team, /selldupes, /comeonmanineedthis) to be unavailable.
Fix: Add app_legal_channel() function to helpers/main.py so it's exported
via the helpers package __init__.py.
Bumps version to 1.7.6
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bug: Several slash commands (@app_commands.command) were using prefix command
decorators (@commands.has_any_role, @commands.check) which don't work with
app_commands. This caused errors caught by the global error handler, resulting
in "Unknown interaction" (404) errors being displayed before the command executed.
Affected commands:
- /comeonmanineedthis: Both role and channel checks were wrong
- /selldupes: Channel check was wrong
- /team: Channel check was wrong
Fix:
- Created app_legal_channel() decorator in helpers.py for slash commands
- Changed @commands.has_any_role to @app_commands.checks.has_any_role
- Changed @commands.check(legal_channel) to @app_legal_channel()
Bumps version to 1.7.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bug: The is_game_over() function contained a debug print statement that was
printing "1: " to stdout on every call. This was causing massive log spam
in Docker container output (thousands of lines) and making it difficult to
diagnose actual issues.
Fix: Remove the print(f'1: ') statement from line 3251.
Bumps version to 1.7.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Initial version: 1.7.2
This file tracks the current version for Docker builds. When building
and pushing new versions, this file will be updated and the commit
will be tagged with the version number.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Previously, if get_card_or_none returned None (when a card ID from the
Google Sheet doesn't exist in the database), the code would create a
RosterLink with card=None, causing card_id to be null which violates
the NOT NULL constraint on the primary key.
Now we check if this_card is None before creating the RosterLink and
raise a CardNotFoundException with a helpful error message to guide
the user to fix their roster sheet.
Fixes the error: null value in column "card_id" of relation "rosterlink"
violates not-null constraint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bug: When a user attempted to substitute a player who didn't have the required
position rating, the bot would display an error message but leave the database
session in an inconsistent state. The old player was marked inactive and flushed
to the session, but when the position check failed, the function returned early
without rolling back the session. This left the session dirty, causing crashes
on subsequent operations.
Fix: Added session.rollback() before returning when PositionNotFoundException
is caught, ensuring the database session is cleanly reset.
Location: utilities/dropdown.py:479-480 in SelectBatterSub.callback()
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements a comprehensive health check system using aiohttp to support
container orchestration and external monitoring systems.
Features:
- /health endpoint: Basic liveness check (is process running?)
- /ready endpoint: Readiness check (is bot connected to Discord?)
- /metrics endpoint: Detailed bot metrics (guilds, users, cogs, latency)
Changes:
- Add aiohttp to requirements.txt
- Create health_server.py module with HTTP server
- Update paperdynasty.py to run health server alongside bot
- Update docker-compose.yml with HTTP-based healthcheck
- Fix deploy.sh Docker image name
Benefits:
- Auto-restart on bot hangs/deadlocks
- Foundation for external monitoring (Prometheus, Grafana, etc.)
- Detailed diagnostics for troubleshooting
- Industry-standard health check pattern
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a gauntlet game ended for Event ID 9, the post_result function would crash with:
"UnboundLocalError: cannot access local variable 'team_id' where it is not associated with a value"
Event ID 9 was missing team_id initialization, while all other events (3,4,5,6,7,8)
properly set team_id. Added team_id = None to match the pattern used by Events 5, 6, and 8.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Event ID 9 (Live 25) gauntlet rewards were being given without cardset_id
assigned, causing Team Choice packs to be unconfigured and unusable.
Root cause: Line 1923 checked if event ID was in [3, 4, 5, 6, 8], but
Event ID 9 was missing from this list. This caused the entire reward
assignment block (lines 1923-1957) including Event 9's cardset logic
(lines 1945-1948) to be skipped.
Event 9 should assign:
- cardset_id 27 for standard packs (2005 Live)
- cardset_id 26 for Promo Choice packs (pack_type_id 9)
Added 9 to the event ID list at line 1923 to enable proper cardset
assignment for Event 9 gauntlet rewards.
Fixes: Team Choice packs from Event 9 gauntlet missing cardset_id
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Users were receiving "This interaction failed" when trying to open Team
Choice packs that had neither pack_team nor pack_cardset assigned in the
database.
The previous fix only handled packs WITH cardset but WITHOUT team. This
adds handling for completely unconfigured Team Choice packs.
Now shows a helpful error message: "This Team Choice pack needs to be
assigned a team and cardset. Please contact an admin to configure this pack."
This prevents the KeyError exception that was being thrown at
helpers.py:1692 when open_choice_pack() received a pack with pack_team=None.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>