Initial commit

This commit is contained in:
2025-10-05 13:10:12 +01:00
commit 32ef7401e3
14 changed files with 8442 additions and 0 deletions

136
ui_state.py Normal file
View 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)
}