diff --git a/.gitignore b/.gitignore index 9500d65..28d68a6 100644 --- a/.gitignore +++ b/.gitignore @@ -218,5 +218,6 @@ __marimo__/ # Project-specific data/ +storage/ production_logs/ *.json diff --git a/commands/gameplay/scorecard_tracker.py b/commands/gameplay/scorecard_tracker.py index 8b2a674..b5fd6db 100644 --- a/commands/gameplay/scorecard_tracker.py +++ b/commands/gameplay/scorecard_tracker.py @@ -24,7 +24,7 @@ class ScorecardTracker: - Timestamp tracking for monitoring """ - def __init__(self, data_file: str = "data/scorecards.json"): + def __init__(self, data_file: str = "storage/scorecards.json"): """ Initialize the scorecard tracker. diff --git a/commands/soak/tracker.py b/commands/soak/tracker.py index f084a6a..4708a5e 100644 --- a/commands/soak/tracker.py +++ b/commands/soak/tracker.py @@ -3,13 +3,14 @@ Soak Tracker Provides persistent tracking of "soak" mentions using JSON file storage. """ + import json import logging from datetime import datetime, timedelta, UTC from pathlib import Path from typing import Dict, List, Optional, Any -logger = logging.getLogger(f'{__name__}.SoakTracker') +logger = logging.getLogger(f"{__name__}.SoakTracker") class SoakTracker: @@ -22,7 +23,7 @@ class SoakTracker: - Time-based calculations for disappointment tiers """ - def __init__(self, data_file: str = "data/soak_data.json"): + def __init__(self, data_file: str = "storage/soak_data.json"): """ Initialize the soak tracker. @@ -38,28 +39,22 @@ class SoakTracker: """Load soak data from JSON file.""" try: if self.data_file.exists(): - with open(self.data_file, 'r') as f: + with open(self.data_file, "r") as f: self._data = json.load(f) - logger.debug(f"Loaded soak data: {self._data.get('total_count', 0)} total soaks") + logger.debug( + f"Loaded soak data: {self._data.get('total_count', 0)} total soaks" + ) else: - self._data = { - "last_soak": None, - "total_count": 0, - "history": [] - } + self._data = {"last_soak": None, "total_count": 0, "history": []} logger.info("No existing soak data found, starting fresh") except Exception as e: logger.error(f"Failed to load soak data: {e}") - self._data = { - "last_soak": None, - "total_count": 0, - "history": [] - } + self._data = {"last_soak": None, "total_count": 0, "history": []} def save_data(self) -> None: """Save soak data to JSON file.""" try: - with open(self.data_file, 'w') as f: + with open(self.data_file, "w") as f: json.dump(self._data, f, indent=2, default=str) logger.debug("Soak data saved successfully") except Exception as e: @@ -71,7 +66,7 @@ class SoakTracker: username: str, display_name: str, channel_id: int, - message_id: int + message_id: int, ) -> None: """ Record a new soak mention. @@ -89,7 +84,7 @@ class SoakTracker: "username": username, "display_name": display_name, "channel_id": str(channel_id), - "message_id": str(message_id) + "message_id": str(message_id), } # Update last_soak @@ -110,7 +105,9 @@ class SoakTracker: self.save_data() - logger.info(f"Recorded soak by {username} (ID: {user_id}) in channel {channel_id}") + logger.info( + f"Recorded soak by {username} (ID: {user_id}) in channel {channel_id}" + ) def get_last_soak(self) -> Optional[Dict[str, Any]]: """ @@ -135,10 +132,12 @@ class SoakTracker: try: # Parse ISO format timestamp last_timestamp_str = last_soak["timestamp"] - if last_timestamp_str.endswith('Z'): - last_timestamp_str = last_timestamp_str[:-1] + '+00:00' + if last_timestamp_str.endswith("Z"): + last_timestamp_str = last_timestamp_str[:-1] + "+00:00" - last_timestamp = datetime.fromisoformat(last_timestamp_str.replace('Z', '+00:00')) + last_timestamp = datetime.fromisoformat( + last_timestamp_str.replace("Z", "+00:00") + ) # Ensure both times are timezone-aware if last_timestamp.tzinfo is None: diff --git a/commands/transactions/trade_channel_tracker.py b/commands/transactions/trade_channel_tracker.py index f3d34c3..1399649 100644 --- a/commands/transactions/trade_channel_tracker.py +++ b/commands/transactions/trade_channel_tracker.py @@ -3,6 +3,7 @@ Trade Channel Tracker Provides persistent tracking of bot-created trade discussion channels using JSON file storage. """ + import json from datetime import datetime, UTC from pathlib import Path @@ -12,7 +13,7 @@ import discord from utils.logging import get_contextual_logger -logger = get_contextual_logger(f'{__name__}.TradeChannelTracker') +logger = get_contextual_logger(f"{__name__}.TradeChannelTracker") class TradeChannelTracker: @@ -26,7 +27,7 @@ class TradeChannelTracker: - Automatic stale entry removal """ - def __init__(self, data_file: str = "data/trade_channels.json"): + def __init__(self, data_file: str = "storage/trade_channels.json"): """ Initialize the trade channel tracker. @@ -42,9 +43,11 @@ class TradeChannelTracker: """Load channel data from JSON file.""" try: if self.data_file.exists(): - with open(self.data_file, 'r') as f: + with open(self.data_file, "r") as f: self._data = json.load(f) - logger.debug(f"Loaded {len(self._data.get('trade_channels', {}))} tracked trade channels") + logger.debug( + f"Loaded {len(self._data.get('trade_channels', {}))} tracked trade channels" + ) else: self._data = {"trade_channels": {}} logger.info("No existing trade channel data found, starting fresh") @@ -55,7 +58,7 @@ class TradeChannelTracker: def save_data(self) -> None: """Save channel data to JSON file.""" try: - with open(self.data_file, 'w') as f: + with open(self.data_file, "w") as f: json.dump(self._data, f, indent=2, default=str) logger.debug("Trade channel data saved successfully") except Exception as e: @@ -67,7 +70,7 @@ class TradeChannelTracker: trade_id: str, team1_abbrev: str, team2_abbrev: str, - creator_id: int + creator_id: int, ) -> None: """ Add a new trade channel to tracking. @@ -87,10 +90,12 @@ class TradeChannelTracker: "team1_abbrev": team1_abbrev, "team2_abbrev": team2_abbrev, "created_at": datetime.now(UTC).isoformat(), - "creator_id": str(creator_id) + "creator_id": str(creator_id), } self.save_data() - logger.info(f"Added trade channel to tracking: {channel.name} (ID: {channel.id}, Trade: {trade_id})") + logger.info( + f"Added trade channel to tracking: {channel.name} (ID: {channel.id}, Trade: {trade_id})" + ) def remove_channel(self, channel_id: int) -> None: """ @@ -108,7 +113,9 @@ class TradeChannelTracker: channel_name = channel_data["name"] del channels[channel_key] self.save_data() - logger.info(f"Removed trade channel from tracking: {channel_name} (ID: {channel_id}, Trade: {trade_id})") + logger.info( + f"Removed trade channel from tracking: {channel_name} (ID: {channel_id}, Trade: {trade_id})" + ) def get_channel_by_trade_id(self, trade_id: str) -> Optional[Dict[str, Any]]: """ @@ -175,7 +182,9 @@ class TradeChannelTracker: channel_name = channels[channel_id_str].get("name", "unknown") trade_id = channels[channel_id_str].get("trade_id", "unknown") del channels[channel_id_str] - logger.info(f"Removed stale tracking entry: {channel_name} (ID: {channel_id_str}, Trade: {trade_id})") + logger.info( + f"Removed stale tracking entry: {channel_name} (ID: {channel_id_str}, Trade: {trade_id})" + ) if stale_entries: self.save_data() diff --git a/commands/voice/cleanup_service.py b/commands/voice/cleanup_service.py index ab8792c..c7343d9 100644 --- a/commands/voice/cleanup_service.py +++ b/commands/voice/cleanup_service.py @@ -3,6 +3,7 @@ Voice Channel Cleanup Service Provides automatic cleanup of empty voice channels with restart resilience. """ + import logging import discord @@ -12,7 +13,7 @@ from .tracker import VoiceChannelTracker from commands.gameplay.scorecard_tracker import ScorecardTracker from utils.logging import get_contextual_logger -logger = logging.getLogger(f'{__name__}.VoiceChannelCleanupService') +logger = logging.getLogger(f"{__name__}.VoiceChannelCleanupService") class VoiceChannelCleanupService: @@ -27,7 +28,9 @@ class VoiceChannelCleanupService: - Automatic scorecard unpublishing when voice channel is cleaned up """ - def __init__(self, bot: commands.Bot, data_file: str = "data/voice_channels.json"): + def __init__( + self, bot: commands.Bot, data_file: str = "storage/voice_channels.json" + ): """ Initialize the cleanup service. @@ -36,10 +39,10 @@ class VoiceChannelCleanupService: data_file: Path to the JSON data file for persistence """ self.bot = bot - self.logger = get_contextual_logger(f'{__name__}.VoiceChannelCleanupService') + self.logger = get_contextual_logger(f"{__name__}.VoiceChannelCleanupService") self.tracker = VoiceChannelTracker(data_file) self.scorecard_tracker = ScorecardTracker() - self.empty_threshold = 5 # Delete after 5 minutes empty + self.empty_threshold = 5 # Delete after 5 minutes empty # Start the cleanup task - @before_loop will wait for bot readiness self.cleanup_loop.start() @@ -90,13 +93,17 @@ class VoiceChannelCleanupService: guild = bot.get_guild(guild_id) if not guild: - self.logger.warning(f"Guild {guild_id} not found, removing channel {channel_data['name']}") + self.logger.warning( + f"Guild {guild_id} not found, removing channel {channel_data['name']}" + ) channels_to_remove.append(channel_id) continue channel = guild.get_channel(channel_id) if not channel: - self.logger.warning(f"Channel {channel_data['name']} (ID: {channel_id}) no longer exists") + self.logger.warning( + f"Channel {channel_data['name']} (ID: {channel_id}) no longer exists" + ) channels_to_remove.append(channel_id) continue @@ -121,18 +128,26 @@ class VoiceChannelCleanupService: if channel_data and channel_data.get("text_channel_id"): try: text_channel_id_int = int(channel_data["text_channel_id"]) - was_unpublished = self.scorecard_tracker.unpublish_scorecard(text_channel_id_int) + was_unpublished = self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) if was_unpublished: - self.logger.info(f"📋 Unpublished scorecard from text channel {text_channel_id_int} (stale voice channel)") + self.logger.info( + f"📋 Unpublished scorecard from text channel {text_channel_id_int} (stale voice channel)" + ) except (ValueError, TypeError) as e: - self.logger.warning(f"Invalid text_channel_id in stale voice channel data: {e}") + self.logger.warning( + f"Invalid text_channel_id in stale voice channel data: {e}" + ) # Also clean up any additional stale entries stale_removed = self.tracker.cleanup_stale_entries(valid_channel_ids) total_removed = len(channels_to_remove) + stale_removed if total_removed > 0: - self.logger.info(f"Cleaned up {total_removed} stale channel tracking entries") + self.logger.info( + f"Cleaned up {total_removed} stale channel tracking entries" + ) self.logger.info(f"Verified {len(valid_channel_ids)} valid tracked channels") @@ -149,10 +164,14 @@ class VoiceChannelCleanupService: await self.update_all_channel_statuses(bot) # Get channels ready for cleanup - channels_for_cleanup = self.tracker.get_channels_for_cleanup(self.empty_threshold) + channels_for_cleanup = self.tracker.get_channels_for_cleanup( + self.empty_threshold + ) if channels_for_cleanup: - self.logger.info(f"Found {len(channels_for_cleanup)} channels ready for cleanup") + self.logger.info( + f"Found {len(channels_for_cleanup)} channels ready for cleanup" + ) # Delete empty channels for channel_data in channels_for_cleanup: @@ -182,12 +201,16 @@ class VoiceChannelCleanupService: guild = bot.get_guild(guild_id) if not guild: - self.logger.debug(f"Guild {guild_id} not found for channel {channel_data['name']}") + self.logger.debug( + f"Guild {guild_id} not found for channel {channel_data['name']}" + ) return channel = guild.get_channel(channel_id) if not channel: - self.logger.debug(f"Channel {channel_data['name']} no longer exists, removing from tracking") + self.logger.debug( + f"Channel {channel_data['name']} no longer exists, removing from tracking" + ) self.tracker.remove_channel(channel_id) # Unpublish associated scorecard if it exists @@ -195,17 +218,25 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard(text_channel_id_int) + was_unpublished = self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) if was_unpublished: - self.logger.info(f"📋 Unpublished scorecard from text channel {text_channel_id_int} (manually deleted voice channel)") + self.logger.info( + f"📋 Unpublished scorecard from text channel {text_channel_id_int} (manually deleted voice channel)" + ) except (ValueError, TypeError) as e: - self.logger.warning(f"Invalid text_channel_id in manually deleted voice channel data: {e}") + self.logger.warning( + f"Invalid text_channel_id in manually deleted voice channel data: {e}" + ) return # Ensure it's a voice channel before checking members if not isinstance(channel, discord.VoiceChannel): - self.logger.warning(f"Channel {channel_data['name']} is not a voice channel, removing from tracking") + self.logger.warning( + f"Channel {channel_data['name']} is not a voice channel, removing from tracking" + ) self.tracker.remove_channel(channel_id) # Unpublish associated scorecard if it exists @@ -213,11 +244,17 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard(text_channel_id_int) + was_unpublished = self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) if was_unpublished: - self.logger.info(f"📋 Unpublished scorecard from text channel {text_channel_id_int} (wrong channel type)") + self.logger.info( + f"📋 Unpublished scorecard from text channel {text_channel_id_int} (wrong channel type)" + ) except (ValueError, TypeError) as e: - self.logger.warning(f"Invalid text_channel_id in wrong channel type data: {e}") + self.logger.warning( + f"Invalid text_channel_id in wrong channel type data: {e}" + ) return @@ -225,11 +262,15 @@ class VoiceChannelCleanupService: is_empty = len(channel.members) == 0 self.tracker.update_channel_status(channel_id, is_empty) - self.logger.debug(f"Channel {channel_data['name']}: {'empty' if is_empty else 'occupied'} " - f"({len(channel.members)} members)") + self.logger.debug( + f"Channel {channel_data['name']}: {'empty' if is_empty else 'occupied'} " + f"({len(channel.members)} members)" + ) except Exception as e: - self.logger.error(f"Error checking channel status for {channel_data.get('name', 'unknown')}: {e}") + self.logger.error( + f"Error checking channel status for {channel_data.get('name', 'unknown')}: {e}" + ) async def cleanup_channel(self, bot: commands.Bot, channel_data: dict) -> None: """ @@ -246,25 +287,33 @@ class VoiceChannelCleanupService: guild = bot.get_guild(guild_id) if not guild: - self.logger.info(f"Guild {guild_id} not found, removing tracking for {channel_name}") + self.logger.info( + f"Guild {guild_id} not found, removing tracking for {channel_name}" + ) self.tracker.remove_channel(channel_id) return channel = guild.get_channel(channel_id) if not channel: - self.logger.info(f"Channel {channel_name} already deleted, removing from tracking") + self.logger.info( + f"Channel {channel_name} already deleted, removing from tracking" + ) self.tracker.remove_channel(channel_id) return # Ensure it's a voice channel before checking members if not isinstance(channel, discord.VoiceChannel): - self.logger.warning(f"Channel {channel_name} is not a voice channel, removing from tracking") + self.logger.warning( + f"Channel {channel_name} is not a voice channel, removing from tracking" + ) self.tracker.remove_channel(channel_id) return # Final check: make sure channel is still empty before deleting if len(channel.members) > 0: - self.logger.info(f"Channel {channel_name} is no longer empty, skipping cleanup") + self.logger.info( + f"Channel {channel_name} is no longer empty, skipping cleanup" + ) self.tracker.update_channel_status(channel_id, False) return @@ -272,24 +321,36 @@ class VoiceChannelCleanupService: await channel.delete(reason="Automatic cleanup - empty for 5+ minutes") self.tracker.remove_channel(channel_id) - self.logger.info(f"✅ Cleaned up empty voice channel: {channel_name} (ID: {channel_id})") + self.logger.info( + f"✅ Cleaned up empty voice channel: {channel_name} (ID: {channel_id})" + ) # Unpublish associated scorecard if it exists text_channel_id = channel_data.get("text_channel_id") if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard(text_channel_id_int) + was_unpublished = self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) if was_unpublished: - self.logger.info(f"📋 Unpublished scorecard from text channel {text_channel_id_int} (voice channel cleanup)") + self.logger.info( + f"📋 Unpublished scorecard from text channel {text_channel_id_int} (voice channel cleanup)" + ) else: - self.logger.debug(f"No scorecard found for text channel {text_channel_id_int}") + self.logger.debug( + f"No scorecard found for text channel {text_channel_id_int}" + ) except (ValueError, TypeError) as e: - self.logger.warning(f"Invalid text_channel_id in voice channel data: {e}") + self.logger.warning( + f"Invalid text_channel_id in voice channel data: {e}" + ) except discord.NotFound: # Channel was already deleted - self.logger.info(f"Channel {channel_data.get('name', 'unknown')} was already deleted") + self.logger.info( + f"Channel {channel_data.get('name', 'unknown')} was already deleted" + ) self.tracker.remove_channel(int(channel_data["channel_id"])) # Still try to unpublish associated scorecard @@ -297,15 +358,25 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard(text_channel_id_int) + was_unpublished = self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) if was_unpublished: - self.logger.info(f"📋 Unpublished scorecard from text channel {text_channel_id_int} (stale voice channel cleanup)") + self.logger.info( + f"📋 Unpublished scorecard from text channel {text_channel_id_int} (stale voice channel cleanup)" + ) except (ValueError, TypeError) as e: - self.logger.warning(f"Invalid text_channel_id in voice channel data: {e}") + self.logger.warning( + f"Invalid text_channel_id in voice channel data: {e}" + ) except discord.Forbidden: - self.logger.error(f"Missing permissions to delete channel {channel_data.get('name', 'unknown')}") + self.logger.error( + f"Missing permissions to delete channel {channel_data.get('name', 'unknown')}" + ) except Exception as e: - self.logger.error(f"Error cleaning up channel {channel_data.get('name', 'unknown')}: {e}") + self.logger.error( + f"Error cleaning up channel {channel_data.get('name', 'unknown')}: {e}" + ) def get_tracker(self) -> VoiceChannelTracker: """ @@ -330,7 +401,7 @@ class VoiceChannelCleanupService: "running": self.cleanup_loop.is_running(), "total_tracked": len(all_channels), "empty_channels": len(empty_channels), - "empty_threshold": self.empty_threshold + "empty_threshold": self.empty_threshold, } @@ -344,4 +415,4 @@ def setup_voice_cleanup(bot: commands.Bot) -> VoiceChannelCleanupService: Returns: VoiceChannelCleanupService instance """ - return VoiceChannelCleanupService(bot) \ No newline at end of file + return VoiceChannelCleanupService(bot) diff --git a/commands/voice/tracker.py b/commands/voice/tracker.py index 4e85080..3002d1d 100644 --- a/commands/voice/tracker.py +++ b/commands/voice/tracker.py @@ -3,6 +3,7 @@ Voice Channel Tracker Provides persistent tracking of bot-created voice channels using JSON file storage. """ + import json import logging from datetime import datetime, timedelta, UTC @@ -11,7 +12,7 @@ from typing import Dict, List, Optional, Any import discord -logger = logging.getLogger(f'{__name__}.VoiceChannelTracker') +logger = logging.getLogger(f"{__name__}.VoiceChannelTracker") class VoiceChannelTracker: @@ -25,7 +26,7 @@ class VoiceChannelTracker: - Automatic stale entry removal """ - def __init__(self, data_file: str = "data/voice_channels.json"): + def __init__(self, data_file: str = "storage/voice_channels.json"): """ Initialize the voice channel tracker. @@ -41,9 +42,11 @@ class VoiceChannelTracker: """Load channel data from JSON file.""" try: if self.data_file.exists(): - with open(self.data_file, 'r') as f: + with open(self.data_file, "r") as f: self._data = json.load(f) - logger.debug(f"Loaded {len(self._data.get('voice_channels', {}))} tracked channels") + logger.debug( + f"Loaded {len(self._data.get('voice_channels', {}))} tracked channels" + ) else: self._data = {"voice_channels": {}} logger.info("No existing voice channel data found, starting fresh") @@ -54,7 +57,7 @@ class VoiceChannelTracker: def save_data(self) -> None: """Save channel data to JSON file.""" try: - with open(self.data_file, 'w') as f: + with open(self.data_file, "w") as f: json.dump(self._data, f, indent=2, default=str) logger.debug("Voice channel data saved successfully") except Exception as e: @@ -65,7 +68,7 @@ class VoiceChannelTracker: channel: discord.VoiceChannel, channel_type: str, creator_id: int, - text_channel_id: Optional[int] = None + text_channel_id: Optional[int] = None, ) -> None: """ Add a new channel to tracking. @@ -85,7 +88,7 @@ class VoiceChannelTracker: "last_checked": datetime.now(UTC).isoformat(), "empty_since": None, "creator_id": str(creator_id), - "text_channel_id": str(text_channel_id) if text_channel_id else None + "text_channel_id": str(text_channel_id) if text_channel_id else None, } self.save_data() logger.info(f"Added channel to tracking: {channel.name} (ID: {channel.id})") @@ -130,9 +133,13 @@ class VoiceChannelTracker: channel_name = channels[channel_key]["name"] del channels[channel_key] self.save_data() - logger.info(f"Removed channel from tracking: {channel_name} (ID: {channel_id})") + logger.info( + f"Removed channel from tracking: {channel_name} (ID: {channel_id})" + ) - def get_channels_for_cleanup(self, empty_threshold_minutes: int = 15) -> List[Dict[str, Any]]: + def get_channels_for_cleanup( + self, empty_threshold_minutes: int = 15 + ) -> List[Dict[str, Any]]: """ Get channels that should be deleted based on empty duration. @@ -153,10 +160,12 @@ class VoiceChannelTracker: # Parse empty_since timestamp empty_since_str = channel_data["empty_since"] # Handle both with and without timezone info - if empty_since_str.endswith('Z'): - empty_since_str = empty_since_str[:-1] + '+00:00' + if empty_since_str.endswith("Z"): + empty_since_str = empty_since_str[:-1] + "+00:00" - empty_since = datetime.fromisoformat(empty_since_str.replace('Z', '+00:00')) + empty_since = datetime.fromisoformat( + empty_since_str.replace("Z", "+00:00") + ) # Remove timezone info for comparison (both times are UTC) if empty_since.tzinfo: @@ -164,10 +173,14 @@ class VoiceChannelTracker: if empty_since <= cutoff_time: cleanup_candidates.append(channel_data) - logger.debug(f"Channel {channel_data['name']} ready for cleanup (empty since {empty_since})") + logger.debug( + f"Channel {channel_data['name']} ready for cleanup (empty since {empty_since})" + ) except (ValueError, TypeError) as e: - logger.warning(f"Invalid timestamp for channel {channel_data.get('name', 'unknown')}: {e}") + logger.warning( + f"Invalid timestamp for channel {channel_data.get('name', 'unknown')}: {e}" + ) return cleanup_candidates @@ -242,9 +255,11 @@ class VoiceChannelTracker: for channel_id_str in stale_entries: channel_name = channels[channel_id_str].get("name", "unknown") del channels[channel_id_str] - logger.info(f"Removed stale tracking entry: {channel_name} (ID: {channel_id_str})") + logger.info( + f"Removed stale tracking entry: {channel_name} (ID: {channel_id_str})" + ) if stale_entries: self.save_data() - return len(stale_entries) \ No newline at end of file + return len(stale_entries) diff --git a/docker-compose.yml b/docker-compose.yml index 7c39ee1..f98d698 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,8 +36,11 @@ services: # Volume mounts volumes: - # Google Sheets credentials (required) - - ${SHEETS_CREDENTIALS_HOST_PATH:-./data}:/app/data:ro + # Google Sheets credentials (read-only, file mount) + - ${SHEETS_CREDENTIALS_HOST_PATH:-./data/major-domo-service-creds.json}:/app/data/major-domo-service-creds.json:ro + + # Runtime state files (writable) - scorecards, voice channels, trade channels, soak data + - ${STATE_HOST_PATH:-./storage}:/app/storage:rw # Logs directory (persistent) - mounted to /app/logs where the application expects it - ${LOGS_HOST_PATH:-./logs}:/app/logs:rw