137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
#!/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)
|
|
}
|