Initial commit
This commit is contained in:
136
ui_state.py
Normal file
136
ui_state.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/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)
|
||||
}
|
Reference in New Issue
Block a user