#!/usr/bin/env python3 """ ui_state.py - State Management and Persistence Handles persistent state across application reloads with file-based storage """ import json import logging from datetime import datetime from pathlib import Path from typing import Dict, Any, Optional logger = logging.getLogger(__name__) STATE_FILE = Path(".collector_state.json") class StateManager: """Thread-safe state manager that persists across uvicorn reloads""" def __init__(self): self.state = self._load_state() def _load_state(self) -> Dict[str, Any]: """Load state from disk with integrity checks""" try: if STATE_FILE.exists(): with open(STATE_FILE, 'r') as f: state = json.load(f) # Check if state is recent (within last 60 seconds) if 'timestamp' in state: saved_time = datetime.fromisoformat(state['timestamp']) age = (datetime.utcnow() - saved_time).total_seconds() if age < 60: # Extended validity window logger.info( f"Loaded persistent state (age: {age:.1f}s): " f"collecting={state.get('is_collecting')}" ) return state else: logger.info(f"State too old ({age:.1f}s), starting fresh") except Exception as e: logger.error(f"Error loading state: {e}") return { "is_collecting": False, "websocket_collection_running": False, "timestamp": datetime.utcnow().isoformat() } def _save_state(self): """Save state to disk atomically""" try: self.state['timestamp'] = datetime.utcnow().isoformat() # Atomic write using temp file temp_file = STATE_FILE.with_suffix('.tmp') with open(temp_file, 'w') as f: json.dump(self.state, f) temp_file.replace(STATE_FILE) logger.debug(f"Saved state: {self.state}") except Exception as e: logger.error(f"Error saving state: {e}") def update(self, **kwargs): """Update state and persist""" self.state.update(kwargs) self._save_state() def get(self, key: str, default=None): """Get state value""" return self.state.get(key, default) def get_all(self) -> Dict[str, Any]: """Get all state""" return self.state.copy() # Global state manager instance state_manager = StateManager() async def get_current_status(db_manager, data_collector, config) -> Dict[str, Any]: """Get current system status - robust against reload issues""" try: # Use state manager as source of truth is_collecting = state_manager.get("is_collecting", False) # Double-check with data collector if available if data_collector and hasattr(data_collector, 'is_collecting'): actual_collecting = data_collector.is_collecting # Sync state if mismatch detected if actual_collecting != is_collecting: logger.warning( f"State mismatch detected! State: {is_collecting}, " f"Actual: {actual_collecting}" ) is_collecting = actual_collecting state_manager.update(is_collecting=actual_collecting) # Get database statistics total_records = await db_manager.get_total_records() if db_manager else 0 last_update = await db_manager.get_last_update_time() if db_manager else "Never" # Get active trading pairs active_pairs = [] if config and 'trading_pairs' in config: active_pairs = [ pair['symbol'] for pair in config['trading_pairs'] if pair.get('enabled', False) ] return { "status": "Active" if is_collecting else "Stopped", "total_records": total_records, "last_update": last_update, "active_pairs": len(active_pairs), "active_pair_list": active_pairs, "is_collecting": is_collecting } except Exception as e: logger.error(f"Error getting status: {e}") return { "status": "Error", "total_records": 0, "last_update": "Never", "active_pairs": 0, "active_pair_list": [], "is_collecting": False, "error": str(e) }