diff --git a/config/constants/system_health_constants.py b/config/constants/system_health_constants.py new file mode 100644 index 0000000..c6a458e --- /dev/null +++ b/config/constants/system_health_constants.py @@ -0,0 +1,19 @@ +CAPACITY_GOOD_THRESHOLD = 70 +CAPACITY_WARNING_THRESHOLD = 85 + +CPU_GOOD_THRESHOLD = 80 +CPU_WARNING_THRESHOLD = 90 + +MEMORY_GOOD_THRESHOLD = 80 +MEMORY_WARNING_THRESHOLD = 90 + +DISK_GOOD_THRESHOLD = 70 +DISK_WARNING_THRESHOLD = 85 + +DATA_FRESHNESS_RECENT_MINUTES = 5 +DATA_FRESHNESS_STALE_HOURS = 1 + +DATABASE_RECENT_ACTIVITY_HOURS = 1 +DATABASE_LARGEST_TABLES_LIMIT = 5 + +BYTE_TO_GB = 1024**3 \ No newline at end of file diff --git a/dashboard/callbacks/charts.py b/dashboard/callbacks/charts.py index 25380b1..a9f6254 100644 --- a/dashboard/callbacks/charts.py +++ b/dashboard/callbacks/charts.py @@ -18,6 +18,7 @@ from components.charts.builder import ChartBuilder from components.charts.utils import prepare_chart_data import pandas as pd import io +from utils.time_range_utils import load_time_range_options logger = get_logger("default_logger") @@ -25,14 +26,31 @@ logger = get_logger("default_logger") def calculate_time_range(time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals): """Calculate days_back and status message based on time range controls.""" try: - predefined_ranges = ['1h', '4h', '6h', '12h', '1d', '3d', '7d', '30d'] + # Load time range options dynamically + time_range_options = load_time_range_options() + predefined_ranges = [option['value'] for option in time_range_options if option['value'] not in ['custom', 'realtime']] + + time_map = {} + + # Dynamically create time_map from loaded options + for option in time_range_options: + value = option['value'] + label = option['label'] + if value.endswith('h'): + days_back_fractional = int(value[:-1]) / 24 + elif value.endswith('d'): + days_back_fractional = int(value[:-1]) + else: + continue # Skip custom and realtime, and any other unexpected values + + time_map[value] = (days_back_fractional, label) if time_range_quick in predefined_ranges: - time_map = { - '1h': (1/24, '🕐 Last 1 Hour'), '4h': (4/24, '🕐 Last 4 Hours'), '6h': (6/24, '🕐 Last 6 Hours'), - '12h': (12/24, '🕐 Last 12 Hours'), '1d': (1, '📅 Last 1 Day'), '3d': (3, '📅 Last 3 Days'), - '7d': (7, '📅 Last 7 Days'), '30d': (30, '📅 Last 30 Days') - } + # Ensure the selected time_range_quick exists in our dynamically created time_map + if time_range_quick not in time_map: + logger.warning(f"Selected time range quick option '{time_range_quick}' not found in time_map. Defaulting to 7 days.") + return 7, f"⚠️ Invalid time range selected. Defaulting to 7 days." + days_back_fractional, label = time_map[time_range_quick] mode_text = "🔒 Locked" if analysis_mode == 'locked' else "🔴 Live" status = f"{label} | {mode_text}" @@ -86,6 +104,28 @@ def register_chart_callbacks(app): def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy, time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals, relayout_data, current_figure): + """Updates the main price chart based on user selections and time range. + + Args: + symbol (str): The selected trading symbol. + timeframe (str): The selected chart timeframe (e.g., '1h', '1d'). + overlay_indicators (list): List of selected overlay indicators. + subplot_indicators (list): List of selected subplot indicators. + selected_strategy (str): The selected trading strategy. + time_range_quick (str): Quick time range selection (e.g., '7d', 'custom'). + custom_start_date (str): Custom start date for the chart. + custom_end_date (str): Custom end date for the chart. + analysis_mode (str): The current analysis mode ('locked' or 'live'). + n_intervals (int): Interval component trigger. + relayout_data (dict): Data from chart relayout events (e.g., zoom/pan). + current_figure (dict): The current chart figure data. + + Returns: + tuple: A tuple containing: + - dash.Dash.figure: The updated price chart figure. + - str: The status message for the time range. + - str: JSON string of the chart data. + """ try: triggered_id = ctx.triggered_id if triggered_id == 'interval-component' and analysis_mode == 'locked': @@ -126,6 +166,15 @@ def register_chart_callbacks(app): prevent_initial_call=True ) def auto_lock_chart_on_interaction(relayout_data, current_mode): + """Automatically locks the chart to 'analysis' mode upon user interaction (zoom/pan). + + Args: + relayout_data (dict): Data from chart relayout events (e.g., zoom/pan). + current_mode (str): The current analysis mode ('locked' or 'live'). + + Returns: + str: The new analysis mode ('locked') if interaction occurred and not already locked, else no_update. + """ if relayout_data and 'xaxis.range' in relayout_data and current_mode != 'locked': return 'locked' return no_update @@ -137,6 +186,16 @@ def register_chart_callbacks(app): State('timeframe-dropdown', 'value')] ) def update_market_stats(stored_data, symbol, timeframe): + """Updates the market statistics display based on the stored chart data. + + Args: + stored_data (str): JSON string of the chart data. + symbol (str): The selected trading symbol. + timeframe (str): The selected chart timeframe. + + Returns: + dbc.Alert or html.Div: An alert message or the market statistics component. + """ if not stored_data: return dbc.Alert("Statistics will be available once chart data is loaded.", color="info") try: @@ -158,6 +217,18 @@ def register_chart_callbacks(app): prevent_initial_call=True, ) def export_chart_data(csv_clicks, json_clicks, stored_data, symbol, timeframe): + """Exports chart data to CSV or JSON based on button clicks. + + Args: + csv_clicks (int): Number of clicks on the export CSV button. + json_clicks (int): Number of clicks on the export JSON button. + stored_data (str): JSON string of the chart data. + symbol (str): The selected trading symbol. + timeframe (str): The selected chart timeframe. + + Returns: + dict: Data for download (filename and content) or no_update. + """ triggered_id = ctx.triggered_id if not triggered_id or not stored_data: return no_update @@ -181,6 +252,14 @@ def register_chart_callbacks(app): [Input('strategy-dropdown', 'value')] ) def update_indicators_from_strategy(selected_strategy): + """Updates the overlay and subplot indicators based on the selected strategy. + + Args: + selected_strategy (str): The currently selected trading strategy. + + Returns: + tuple: A tuple containing lists of overlay indicators and subplot indicators. + """ if not selected_strategy or selected_strategy == 'basic': return [], [] try: @@ -207,6 +286,14 @@ def register_chart_callbacks(app): prevent_initial_call=True ) def clear_custom_date_range(n_clicks): + """Clears the custom date range and resets the quick time range selection. + + Args: + n_clicks (int): Number of clicks on the clear date range button. + + Returns: + tuple: A tuple containing None for start and end dates, and '7d' for quick select, or no_update. + """ if n_clicks and n_clicks > 0: return None, None, '7d' return no_update, no_update, no_update diff --git a/dashboard/callbacks/system_health.py b/dashboard/callbacks/system_health.py index 4c06f82..37e33e2 100644 --- a/dashboard/callbacks/system_health.py +++ b/dashboard/callbacks/system_health.py @@ -2,566 +2,29 @@ Enhanced system health callbacks for the dashboard. """ -import asyncio -import json -import subprocess -import psutil -from datetime import datetime, timedelta -from typing import Dict, Any, Optional, List -from dash import Output, Input, State, html, callback_context, no_update +from dash import callback_context + import dash_bootstrap_components as dbc from utils.logger import get_logger -from database.connection import DatabaseManager -from database.redis_manager import get_sync_redis_manager -logger = get_logger("system_health_callbacks") +# Import refactored callback modules +from dashboard.callbacks.system_health_modules.quick_status_callbacks import register_quick_status_callbacks +from dashboard.callbacks.system_health_modules.data_collection_callbacks import register_data_collection_callbacks +from dashboard.callbacks.system_health_modules.database_callbacks import register_database_callbacks +from dashboard.callbacks.system_health_modules.redis_callbacks import register_redis_callbacks +from dashboard.callbacks.system_health_modules.system_performance_callbacks import register_system_performance_callbacks + +logger = get_logger("default_logger") def register_system_health_callbacks(app): """Register enhanced system health callbacks with comprehensive monitoring.""" - # Quick Status Updates (Top Cards) - @app.callback( - [Output('data-collection-quick-status', 'children'), - Output('database-quick-status', 'children'), - Output('redis-quick-status', 'children'), - Output('performance-quick-status', 'children')], - Input('interval-component', 'n_intervals') - ) - def update_quick_status(n_intervals): - """Update quick status indicators.""" - try: - # Data Collection Status - dc_status = _get_data_collection_quick_status() - - # Database Status - db_status = _get_database_quick_status() - - # Redis Status - redis_status = _get_redis_quick_status() - - # Performance Status - perf_status = _get_performance_quick_status() - - return dc_status, db_status, redis_status, perf_status - - except Exception as e: - logger.error(f"Error updating quick status: {e}") - error_status = dbc.Badge("🔴 Error", color="danger", className="me-1") - return error_status, error_status, error_status, error_status + # Register callbacks from refactored modules + register_quick_status_callbacks(app) + register_data_collection_callbacks(app) + register_database_callbacks(app) + register_redis_callbacks(app) + register_system_performance_callbacks(app) - # Detailed Data Collection Service Status - @app.callback( - [Output('data-collection-service-status', 'children'), - Output('data-collection-metrics', 'children')], - [Input('interval-component', 'n_intervals'), - Input('refresh-data-status-btn', 'n_clicks')] - ) - def update_data_collection_status(n_intervals, refresh_clicks): - """Update detailed data collection service status and metrics.""" - try: - service_status = _get_data_collection_service_status() - metrics = _get_data_collection_metrics() - - return service_status, metrics - - except Exception as e: - logger.error(f"Error updating data collection status: {e}") - error_div = dbc.Alert( - f"Error: {str(e)}", - color="danger", - dismissable=True - ) - return error_div, error_div - - # Individual Collectors Status - @app.callback( - Output('individual-collectors-status', 'children'), - [Input('interval-component', 'n_intervals'), - Input('refresh-data-status-btn', 'n_clicks')] - ) - def update_individual_collectors_status(n_intervals, refresh_clicks): - """Update individual data collector health status.""" - try: - return _get_individual_collectors_status() - except Exception as e: - logger.error(f"Error updating individual collectors status: {e}") - return dbc.Alert( - f"Error: {str(e)}", - color="danger", - dismissable=True - ) - - # Database Status and Statistics - @app.callback( - [Output('database-status', 'children'), - Output('database-stats', 'children')], - Input('interval-component', 'n_intervals') - ) - def update_database_status(n_intervals): - """Update database connection status and statistics.""" - try: - db_status = _get_database_status() - db_stats = _get_database_statistics() - - return db_status, db_stats - - except Exception as e: - logger.error(f"Error updating database status: {e}") - error_alert = dbc.Alert( - f"Error: {str(e)}", - color="danger", - dismissable=True - ) - return error_alert, error_alert - - # Redis Status and Statistics - @app.callback( - [Output('redis-status', 'children'), - Output('redis-stats', 'children')], - Input('interval-component', 'n_intervals') - ) - def update_redis_status(n_intervals): - """Update Redis connection status and statistics.""" - try: - redis_status = _get_redis_status() - redis_stats = _get_redis_statistics() - - return redis_status, redis_stats - - except Exception as e: - logger.error(f"Error updating Redis status: {e}") - error_alert = dbc.Alert( - f"Error: {str(e)}", - color="danger", - dismissable=True - ) - return error_alert, error_alert - - # System Performance Metrics - @app.callback( - Output('system-performance-metrics', 'children'), - Input('interval-component', 'n_intervals') - ) - def update_system_performance(n_intervals): - """Update system performance metrics.""" - try: - return _get_system_performance_metrics() - except Exception as e: - logger.error(f"Error updating system performance: {e}") - return dbc.Alert( - f"Error: {str(e)}", - color="danger", - dismissable=True - ) - - # Data Collection Details Modal - @app.callback( - [Output("collection-details-modal", "is_open"), - Output("collection-details-content", "children")], - [Input("view-collection-details-btn", "n_clicks")], - [State("collection-details-modal", "is_open")] - ) - def toggle_collection_details_modal(n_clicks, is_open): - """Toggle and populate the collection details modal.""" - if n_clicks: - details_content = _get_collection_details_content() - return not is_open, details_content - return is_open, no_update - - # Collection Logs Modal - @app.callback( - [Output("collection-logs-modal", "is_open"), - Output("collection-logs-content", "children")], - [Input("view-collection-logs-btn", "n_clicks"), - Input("refresh-logs-btn", "n_clicks")], - [State("collection-logs-modal", "is_open")], - prevent_initial_call=True - ) - def toggle_collection_logs_modal(logs_clicks, refresh_clicks, is_open): - """Toggle and populate the collection logs modal.""" - ctx = callback_context - if not ctx.triggered: - return is_open, no_update - - triggered_id = ctx.triggered_id - if triggered_id in ["view-collection-logs-btn", "refresh-logs-btn"]: - logs_content = _get_collection_logs_content() - return True, logs_content - - return is_open, no_update - - @app.callback( - Output("collection-logs-modal", "is_open", allow_duplicate=True), - Input("close-logs-modal", "n_clicks"), - State("collection-logs-modal", "is_open"), - prevent_initial_call=True - ) - def close_logs_modal(n_clicks, is_open): - if n_clicks: - return not is_open - return is_open - - logger.info("Enhanced system health callbacks registered successfully") - - -# Helper Functions - -def _get_data_collection_quick_status() -> dbc.Badge: - """Get quick data collection status.""" - try: - is_running = _check_data_collection_service_running() - if is_running: - return dbc.Badge("Active", color="success", className="me-1") - else: - return dbc.Badge("Stopped", color="danger", className="me-1") - except: - return dbc.Badge("Unknown", color="warning", className="me-1") - - -def _get_database_quick_status() -> dbc.Badge: - """Get quick database status.""" - try: - db_manager = DatabaseManager() - db_manager.initialize() - if db_manager.test_connection(): - return dbc.Badge("Connected", color="success", className="me-1") - else: - return dbc.Badge("Error", color="danger", className="me-1") - except: - return dbc.Badge("Error", color="danger", className="me-1") - - -def _get_redis_quick_status() -> dbc.Badge: - """Get quick Redis status.""" - try: - redis_manager = get_sync_redis_manager() - redis_manager.initialize() - # This check is simplified as initialize() would raise an error on failure. - # For a more explicit check, a dedicated test_connection could be added to SyncRedisManager. - if redis_manager.client.ping(): - return dbc.Badge("Connected", color="success", className="me-1") - else: - return dbc.Badge("Error", color="danger", className="me-1") - except Exception as e: - logger.error(f"Redis quick status check failed: {e}") - return dbc.Badge("Error", color="danger", className="me-1") - - -def _get_performance_quick_status() -> dbc.Badge: - """Get quick performance status.""" - try: - cpu_percent = psutil.cpu_percent(interval=0.1) - memory = psutil.virtual_memory() - - if cpu_percent < 80 and memory.percent < 80: - return dbc.Badge("Good", color="success", className="me-1") - elif cpu_percent < 90 and memory.percent < 90: - return dbc.Badge("Warning", color="warning", className="me-1") - else: - return dbc.Badge("High", color="danger", className="me-1") - except: - return dbc.Badge("Unknown", color="secondary", className="me-1") - - -def _get_data_collection_service_status() -> html.Div: - """Get detailed data collection service status.""" - try: - is_running = _check_data_collection_service_running() - current_time = datetime.now().strftime('%H:%M:%S') - - if is_running: - status_badge = dbc.Badge("Service Running", color="success", className="me-2") - status_text = html.P("Data collection service is actively collecting market data.", className="mb-0") - details = html.Div() - else: - status_badge = dbc.Badge("Service Stopped", color="danger", className="me-2") - status_text = html.P("Data collection service is not running.", className="text-danger") - details = html.Div([ - html.P("To start the service, run:", className="mt-2 mb-1"), - html.Code("python scripts/start_data_collection.py") - ]) - - return html.Div([ - dbc.Row([ - dbc.Col(status_badge, width="auto"), - dbc.Col(html.P(f"Checked: {current_time}", className="text-muted mb-0"), width="auto") - ], align="center", className="mb-2"), - status_text, - details - ]) - except Exception as e: - return dbc.Alert(f"Error checking status: {e}", color="danger") - - -def _get_data_collection_metrics() -> html.Div: - """Get data collection metrics.""" - try: - db_manager = DatabaseManager() - db_manager.initialize() - - with db_manager.get_session() as session: - from sqlalchemy import text - candles_count = session.execute(text("SELECT COUNT(*) FROM market_data")).scalar() or 0 - tickers_count = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE data_type = 'ticker'")).scalar() or 0 - latest_market_data = session.execute(text("SELECT MAX(timestamp) FROM market_data")).scalar() - latest_raw_data = session.execute(text("SELECT MAX(timestamp) FROM raw_trades")).scalar() - - latest_data = max(d for d in [latest_market_data, latest_raw_data] if d) if any([latest_market_data, latest_raw_data]) else None - - if latest_data: - time_diff = datetime.utcnow() - (latest_data.replace(tzinfo=None) if latest_data.tzinfo else latest_data) - if time_diff < timedelta(minutes=5): - freshness_badge = dbc.Badge(f"Fresh ({time_diff.seconds // 60}m ago)", color="success") - elif time_diff < timedelta(hours=1): - freshness_badge = dbc.Badge(f"Recent ({time_diff.seconds // 60}m ago)", color="warning") - else: - freshness_badge = dbc.Badge(f"Stale ({time_diff.total_seconds() // 3600:.1f}h ago)", color="danger") - else: - freshness_badge = dbc.Badge("No data", color="secondary") - - return html.Div([ - dbc.Row([ - dbc.Col(html.Strong("Candles:")), - dbc.Col(f"{candles_count:,}", className="text-end") - ]), - dbc.Row([ - dbc.Col(html.Strong("Tickers:")), - dbc.Col(f"{tickers_count:,}", className="text-end") - ]), - dbc.Row([ - dbc.Col(html.Strong("Data Freshness:")), - dbc.Col(freshness_badge, className="text-end") - ]) - ]) - - except Exception as e: - return dbc.Alert(f"Error loading metrics: {e}", color="danger") - - -def _get_individual_collectors_status() -> html.Div: - """Get individual data collector status.""" - try: - return dbc.Alert([ - html.P("Individual collector health data will be displayed here when the data collection service is running.", className="mb-2"), - html.Hr(), - html.P("To start monitoring, run the following command:", className="mb-1"), - html.Code("python scripts/start_data_collection.py") - ], color="info") - - except Exception as e: - return dbc.Alert(f"Error checking collector status: {e}", color="danger") - - -def _get_database_status() -> html.Div: - """Get detailed database status.""" - try: - db_manager = DatabaseManager() - db_manager.initialize() - - with db_manager.get_session() as session: - from sqlalchemy import text - result = session.execute(text("SELECT version()")).fetchone() - version = result[0] if result else "Unknown" - connections = session.execute(text("SELECT count(*) FROM pg_stat_activity")).scalar() or 0 - - return html.Div([ - dbc.Row([ - dbc.Col(dbc.Badge("Database Connected", color="success"), width="auto"), - dbc.Col(f"Checked: {datetime.now().strftime('%H:%M:%S')}", className="text-muted") - ], align="center", className="mb-2"), - html.P(f"Version: PostgreSQL {version.split()[1] if 'PostgreSQL' in version else 'Unknown'}", className="mb-1"), - html.P(f"Active connections: {connections}", className="mb-0") - ]) - - except Exception as e: - return dbc.Alert(f"Error connecting to database: {e}", color="danger") - - -def _get_database_statistics() -> html.Div: - """Get database statistics.""" - try: - db_manager = DatabaseManager() - db_manager.initialize() - - with db_manager.get_session() as session: - from sqlalchemy import text - table_stats_query = """ - SELECT tablename, pg_size_pretty(pg_total_relation_size('public.'||tablename)) as size - FROM pg_tables WHERE schemaname = 'public' - ORDER BY pg_total_relation_size('public.'||tablename) DESC LIMIT 5 - """ - table_stats = session.execute(text(table_stats_query)).fetchall() - - market_data_activity = session.execute(text("SELECT COUNT(*) FROM market_data WHERE timestamp > NOW() - INTERVAL '1 hour'")).scalar() or 0 - raw_data_activity = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE timestamp > NOW() - INTERVAL '1 hour'")).scalar() or 0 - total_recent_activity = market_data_activity + raw_data_activity - - components = [ - dbc.Row([ - dbc.Col(html.Strong("Recent Activity (1h):")), - dbc.Col(f"{total_recent_activity:,} records", className="text-end") - ]), - html.Hr(className="my-2"), - html.Strong("Largest Tables:"), - ] - if table_stats: - for table, size in table_stats: - components.append(dbc.Row([ - dbc.Col(f"• {table}"), - dbc.Col(size, className="text-end text-muted") - ])) - else: - components.append(html.P("No table statistics available.", className="text-muted")) - - return html.Div(components) - - except Exception as e: - return dbc.Alert(f"Error loading database stats: {e}", color="danger") - - -def _get_redis_status() -> html.Div: - """Get detailed Redis server status.""" - try: - redis_manager = get_sync_redis_manager() - redis_manager.initialize() - - if not redis_manager.client.ping(): - raise ConnectionError("Redis server is not responding.") - - info = redis_manager.client.info() - status_badge = dbc.Badge("Connected", color="success", className="me-1") - - return html.Div([ - html.H5("Redis Status"), - status_badge, - html.P(f"Version: {info.get('redis_version', 'N/A')}"), - html.P(f"Mode: {info.get('redis_mode', 'N/A')}") - ]) - except Exception as e: - logger.error(f"Failed to get Redis status: {e}") - return html.Div([ - html.H5("Redis Status"), - dbc.Badge("Error", color="danger", className="me-1"), - dbc.Alert(f"Error: {e}", color="danger", dismissable=True) - ]) - - -def _get_redis_statistics() -> html.Div: - """Get detailed Redis statistics.""" - try: - redis_manager = get_sync_redis_manager() - redis_manager.initialize() - - if not redis_manager.client.ping(): - raise ConnectionError("Redis server is not responding.") - - info = redis_manager.client.info() - - return html.Div([ - html.H5("Redis Statistics"), - html.P(f"Connected Clients: {info.get('connected_clients', 'N/A')}"), - html.P(f"Memory Used: {info.get('used_memory_human', 'N/A')}"), - html.P(f"Total Commands Processed: {info.get('total_commands_processed', 'N/A')}") - ]) - except Exception as e: - logger.error(f"Failed to get Redis statistics: {e}") - return dbc.Alert(f"Error: {e}", color="danger", dismissable=True) - - -def _get_system_performance_metrics() -> html.Div: - """Get system performance metrics.""" - try: - cpu_percent = psutil.cpu_percent(interval=0.1) - cpu_count = psutil.cpu_count() - memory = psutil.virtual_memory() - disk = psutil.disk_usage('/') - - def get_color(percent): - if percent < 70: return "success" - if percent < 85: return "warning" - return "danger" - - return html.Div([ - html.Div([ - html.Strong("CPU Usage: "), - dbc.Badge(f"{cpu_percent:.1f}%", color=get_color(cpu_percent)), - html.Span(f" ({cpu_count} cores)", className="text-muted ms-1") - ], className="mb-2"), - dbc.Progress(value=cpu_percent, color=get_color(cpu_percent), style={"height": "10px"}, className="mb-3"), - - html.Div([ - html.Strong("Memory Usage: "), - dbc.Badge(f"{memory.percent:.1f}%", color=get_color(memory.percent)), - html.Span(f" ({memory.used / (1024**3):.1f} / {memory.total / (1024**3):.1f} GB)", className="text-muted ms-1") - ], className="mb-2"), - dbc.Progress(value=memory.percent, color=get_color(memory.percent), style={"height": "10px"}, className="mb-3"), - - html.Div([ - html.Strong("Disk Usage: "), - dbc.Badge(f"{disk.percent:.1f}%", color=get_color(disk.percent)), - html.Span(f" ({disk.used / (1024**3):.1f} / {disk.total / (1024**3):.1f} GB)", className="text-muted ms-1") - ], className="mb-2"), - dbc.Progress(value=disk.percent, color=get_color(disk.percent), style={"height": "10px"}) - ]) - - except Exception as e: - return dbc.Alert(f"Error loading performance metrics: {e}", color="danger") - - -def _get_collection_details_content() -> html.Div: - """Get detailed collection information for modal.""" - try: - return html.Div([ - html.H5("Data Collection Service Details"), - html.P("Comprehensive data collection service information would be displayed here."), - html.Hr(), - html.H6("Configuration"), - html.P("Service configuration details..."), - html.H6("Performance Metrics"), - html.P("Detailed performance analytics..."), - html.H6("Health Status"), - html.P("Individual collector health information...") - ]) - except Exception as e: - return dbc.Alert(f"Error loading details: {e}", color="danger") - - -def _get_collection_logs_content() -> str: - """Get recent collection service logs.""" - try: - # This would read from actual log files - # For now, return a placeholder - current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - return f"""[{current_time}] INFO - Data Collection Service Logs - -Recent log entries would be displayed here from the data collection service. - -This would include: -- Service startup/shutdown events -- Collector connection status changes -- Data collection statistics -- Error messages and warnings -- Performance metrics - -To view real logs, check the logs/ directory or configure log file monitoring. -""" - except Exception as e: - return f"Error loading logs: {str(e)}" - - -def _check_data_collection_service_running() -> bool: - """Check if data collection service is running.""" - try: - # Check for running processes (simplified) - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): - try: - if proc.info['cmdline']: - cmdline = ' '.join(proc.info['cmdline']) - if 'start_data_collection.py' in cmdline or 'collection_service' in cmdline: - return True - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - return False - except: - return False \ No newline at end of file + logger.info("Enhanced system health callbacks registered successfully") \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/common_health_utils.py b/dashboard/callbacks/system_health_modules/common_health_utils.py new file mode 100644 index 0000000..cd8ff47 --- /dev/null +++ b/dashboard/callbacks/system_health_modules/common_health_utils.py @@ -0,0 +1,26 @@ +from utils.logger import get_logger +from database.connection import DatabaseManager +from database.redis_manager import get_sync_redis_manager +import psutil +from datetime import datetime, timedelta +import dash_bootstrap_components as dbc +from dash import html + +logger = get_logger("default_logger") + +def _check_data_collection_service_running() -> bool: + """Check if data collection service is running.""" + try: + for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + try: + if proc.info['cmdline']: + cmdline = ' '.join(proc.info['cmdline']) + if 'start_data_collection.py' in cmdline or 'collection_service' in cmdline: + return True + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: + logger.warning(f"Access or process error checking service: {e}") + continue + return False + except Exception as e: + logger.error(f"Error checking data collection service running status: {e}") + return False \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/data_collection_callbacks.py b/dashboard/callbacks/system_health_modules/data_collection_callbacks.py new file mode 100644 index 0000000..bef7e9d --- /dev/null +++ b/dashboard/callbacks/system_health_modules/data_collection_callbacks.py @@ -0,0 +1,234 @@ +from dash import Output, Input, State, html, callback_context, no_update +import dash_bootstrap_components as dbc +from utils.logger import get_logger +from database.connection import DatabaseManager +from datetime import datetime, timedelta + +from dashboard.callbacks.system_health_modules.common_health_utils import _check_data_collection_service_running +from config.constants.system_health_constants import ( + DATA_FRESHNESS_RECENT_MINUTES, + DATA_FRESHNESS_STALE_HOURS +) + +logger = get_logger("default_logger") + + +def register_data_collection_callbacks(app): + """Register data collection status and metrics callbacks.""" + + # Detailed Data Collection Service Status + @app.callback( + [Output('data-collection-service-status', 'children'), + Output('data-collection-metrics', 'children')], + [Input('interval-component', 'n_intervals'), + Input('refresh-data-status-btn', 'n_clicks')] + ) + def update_data_collection_status(n_intervals, refresh_clicks): + """Update detailed data collection service status and metrics.""" + try: + service_status = _get_data_collection_service_status() + metrics = _get_data_collection_metrics() + + return service_status, metrics + + except Exception as e: + logger.error(f"Error updating data collection status: {e}") + error_div = dbc.Alert( + f"Error: {str(e)}", + color="danger", + dismissable=True + ) + return error_div, error_div + + # Individual Collectors Status + @app.callback( + Output('individual-collectors-status', 'children'), + [Input('interval-component', 'n_intervals'), + Input('refresh-data-status-btn', 'n_clicks')] + ) + def update_individual_collectors_status(n_intervals, refresh_clicks): + """Update individual data collector health status.""" + try: + return _get_individual_collectors_status() + except Exception as e: + logger.error(f"Error updating individual collectors status: {e}") + return dbc.Alert( + f"Error: {str(e)}", + color="danger", + dismissable=True + ) + + # Data Collection Details Modal + @app.callback( + [Output("collection-details-modal", "is_open"), + Output("collection-details-content", "children")], + [Input("view-collection-details-btn", "n_clicks")], + [State("collection-details-modal", "is_open")] + ) + def toggle_collection_details_modal(n_clicks, is_open): + """Toggle and populate the collection details modal.""" + if n_clicks: + details_content = _get_collection_details_content() + return not is_open, details_content + return is_open, no_update + + # Collection Logs Modal + @app.callback( + [Output("collection-logs-modal", "is_open"), + Output("collection-logs-content", "children")], + [Input("view-collection-logs-btn", "n_clicks"), + Input("refresh-logs-btn", "n_clicks")], + [State("collection-logs-modal", "is_open")], + prevent_initial_call=True + ) + def toggle_collection_logs_modal(logs_clicks, refresh_clicks, is_open): + """Toggle and populate the collection logs modal.""" + ctx = callback_context + if not ctx.triggered: + return is_open, no_update + + triggered_id = ctx.triggered_id + if triggered_id in ["view-collection-logs-btn", "refresh-logs-btn"]: + logs_content = _get_collection_logs_content() + return True, logs_content + + return is_open, no_update + + @app.callback( + Output("collection-logs-modal", "is_open", allow_duplicate=True), + Input("close-logs-modal", "n_clicks"), + State("collection-logs-modal", "is_open"), + prevent_initial_call=True + ) + def close_logs_modal(n_clicks, is_open): + if n_clicks: + return not is_open + return is_open + + +def _get_data_collection_service_status() -> html.Div: + """Get detailed data collection service status.""" + try: + is_running = _check_data_collection_service_running() + current_time = datetime.now().strftime('%H:%M:%S') + + if is_running: + status_badge = dbc.Badge("Service Running", color="success", className="me-2") + status_text = html.P("Data collection service is actively collecting market data.", className="mb-0") + details = html.Div() + else: + status_badge = dbc.Badge("Service Stopped", color="danger", className="me-2") + status_text = html.P("Data collection service is not running.", className="text-danger") + details = html.Div([ + html.P("To start the service, run:", className="mt-2 mb-1"), + html.Code("python scripts/start_data_collection.py") + ]) + + return html.Div([ + dbc.Row([ + dbc.Col(status_badge, width="auto"), + dbc.Col(html.P(f"Checked: {current_time}", className="text-muted mb-0"), width="auto") + ], align="center", className="mb-2"), + status_text, + details + ]) + except Exception as e: + return dbc.Alert(f"Error checking status: {e}", color="danger") + + +def _get_data_collection_metrics() -> html.Div: + """Get data collection metrics.""" + try: + db_manager = DatabaseManager() + db_manager.initialize() + + with db_manager.get_session() as session: + from sqlalchemy import text + candles_count = session.execute(text("SELECT COUNT(*) FROM market_data")).scalar() or 0 + tickers_count = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE data_type = 'ticker'")).scalar() or 0 + latest_market_data = session.execute(text("SELECT MAX(timestamp) FROM market_data")).scalar() + latest_raw_data = session.execute(text("SELECT MAX(timestamp) FROM raw_trades")).scalar() + + latest_data = max(d for d in [latest_market_data, latest_raw_data] if d) if any([latest_market_data, latest_raw_data]) else None + + if latest_data: + time_diff = datetime.utcnow() - (latest_data.replace(tzinfo=None) if latest_data.tzinfo else latest_data) + if time_diff < timedelta(minutes=DATA_FRESHNESS_RECENT_MINUTES): + freshness_badge = dbc.Badge(f"Fresh ({time_diff.seconds // 60}m ago)", color="success") + elif time_diff < timedelta(hours=DATA_FRESHNESS_STALE_HOURS): + freshness_badge = dbc.Badge(f"Recent ({time_diff.seconds // 60}m ago)", color="warning") + else: + freshness_badge = dbc.Badge(f"Stale ({time_diff.total_seconds() // 3600:.1f}h ago)", color="danger") + else: + freshness_badge = dbc.Badge("No data", color="secondary") + + return html.Div([ + dbc.Row([ + dbc.Col(html.Strong("Candles:")), + dbc.Col(f"{candles_count:,}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong("Tickers:")), + dbc.Col(f"{tickers_count:,}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong("Data Freshness:")), + dbc.Col(freshness_badge, className="text-end") + ]) + ]) + + except Exception as e: + return dbc.Alert(f"Error loading metrics: {e}", color="danger") + + +def _get_individual_collectors_status() -> html.Div: + """Get individual data collector status.""" + try: + return dbc.Alert([ + html.P("Individual collector health data will be displayed here when the data collection service is running.", className="mb-2"), + html.Hr(), + html.P("To start monitoring, run the following command:", className="mb-1"), + html.Code("python scripts/start_data_collection.py") + ], color="info") + + except Exception as e: + return dbc.Alert(f"Error checking collector status: {e}", color="danger") + + +def _get_collection_details_content() -> html.Div: + """Get detailed collection information for modal.""" + try: + return html.Div([ + html.H5("Data Collection Service Details"), + html.P("Comprehensive data collection service information would be displayed here."), + html.Hr(), + html.H6("Configuration"), + html.P("Service configuration details..."), + html.H6("Performance Metrics"), + html.P("Detailed performance analytics..."), + html.H6("Health Status"), + html.P("Individual collector health information...") + ]) + except Exception as e: + return dbc.Alert(f"Error loading details: {e}", color="danger") + + +def _get_collection_logs_content() -> str: + """Get recent collection service logs.""" + try: + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return f"""[{current_time}] INFO - Data Collection Service Logs + +Recent log entries would be displayed here from the data collection service. + +This would include: +- Service startup/shutdown events +- Collector connection status changes +- Data collection statistics +- Error messages and warnings +- Performance metrics + +To view real logs, check the logs/ directory or configure log file monitoring. +""" + except Exception as e: + return f"Error loading logs: {str(e)}" \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/database_callbacks.py b/dashboard/callbacks/system_health_modules/database_callbacks.py new file mode 100644 index 0000000..2de42cc --- /dev/null +++ b/dashboard/callbacks/system_health_modules/database_callbacks.py @@ -0,0 +1,102 @@ +from dash import Output, Input, html +import dash_bootstrap_components as dbc +from utils.logger import get_logger +from database.connection import DatabaseManager +from datetime import datetime, timedelta +from sqlalchemy import text + +from config.constants.system_health_constants import ( + DATABASE_RECENT_ACTIVITY_HOURS, + DATABASE_LARGEST_TABLES_LIMIT +) +from database.operations import get_database_operations + +logger = get_logger("default_logger") + + +def register_database_callbacks(app): + """Register database status and statistics callbacks.""" + + # Database Status and Statistics + @app.callback( + [Output('database-status', 'children'), + Output('database-stats', 'children')], + Input('interval-component', 'n_intervals') + ) + def update_database_status(n_intervals): + """Update database connection status and statistics.""" + try: + db_status = _get_database_status() + db_stats = _get_database_statistics() + + return db_status, db_stats + + except Exception as e: + logger.error(f"Error updating database status: {e}") + error_alert = dbc.Alert( + f"Error: {str(e)}", + color="danger", + dismissable=True + ) + return error_alert, error_alert + + +def _get_database_status() -> html.Div: + """Get detailed database status.""" + db_operations = get_database_operations(logger) + try: + is_connected = db_operations.health_check() + current_time = datetime.now().strftime('%H:%M:%S') + + if is_connected: + status_badge = dbc.Badge("Database Connected", color="success") + # Placeholder for version and connections, as get_stats will provide more detailed info + details_text = html.P("Details available in Database Statistics section.", className="mb-0") + else: + status_badge = dbc.Badge("Database Disconnected", color="danger") + details_text = html.P("Could not connect to the database.", className="mb-0") + + return html.Div([ + dbc.Row([ + dbc.Col(status_badge, width="auto"), + dbc.Col(html.P(f"Checked: {current_time}", className="text-muted"), width="auto") + ], align="center", className="mb-2"), + details_text + ]) + + except Exception as e: + logger.error(f"Error connecting to database: {e}") + return dbc.Alert(f"Error connecting to database: {e}", color="danger") + + +def _get_database_statistics() -> html.Div: + """Get database statistics.""" + db_operations = get_database_operations(logger) + try: + stats = db_operations.get_stats() + + if not stats.get('healthy'): + return dbc.Alert(f"Database statistics unavailable: {stats.get('error', 'Connection failed')}", color="warning") + + components = [ + dbc.Row([ + dbc.Col(html.Strong(f"Bots:")), + dbc.Col(f"{stats.get('bot_count', 'N/A')}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong(f"Candles:")), + dbc.Col(f"{stats.get('candle_count', 'N/A')}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong(f"Raw Trades:")), + dbc.Col(f"{stats.get('raw_trade_count', 'N/A')}", className="text-end") + ]), + # TODO: Integrate detailed table stats, recent activity from `database.operations` if available + # Currently, `get_stats` does not provide this granular data directly. + ] + + return html.Div(components) + + except Exception as e: + logger.error(f"Error loading database stats: {e}") + return dbc.Alert(f"Error loading database stats: {e}", color="danger") \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/quick_status_callbacks.py b/dashboard/callbacks/system_health_modules/quick_status_callbacks.py new file mode 100644 index 0000000..ca29305 --- /dev/null +++ b/dashboard/callbacks/system_health_modules/quick_status_callbacks.py @@ -0,0 +1,120 @@ +import asyncio +import json +import subprocess +import psutil +from datetime import datetime, timedelta +from typing import Dict, Any, Optional, List +from dash import Output, Input, State, html, callback_context, no_update +import dash_bootstrap_components as dbc +from utils.logger import get_logger +from database.connection import DatabaseManager +from database.redis_manager import get_sync_redis_manager + +from config.constants.system_health_constants import ( + CPU_GOOD_THRESHOLD, CPU_WARNING_THRESHOLD, + MEMORY_GOOD_THRESHOLD, MEMORY_WARNING_THRESHOLD +) + +logger = get_logger("default_logger") + + +def register_quick_status_callbacks(app): + """Register quick status callbacks (top cards).""" + + @app.callback( + [Output('data-collection-quick-status', 'children'), + Output('database-quick-status', 'children'), + Output('redis-quick-status', 'children'), + Output('performance-quick-status', 'children')], + Input('interval-component', 'n_intervals') + ) + def update_quick_status(n_intervals): + """Update quick status indicators.""" + try: + dc_status = _get_data_collection_quick_status() + db_status = _get_database_quick_status() + redis_status = _get_redis_quick_status() + perf_status = _get_performance_quick_status() + + return dc_status, db_status, redis_status, perf_status + + except Exception as e: + logger.error(f"Error updating quick status: {e}") + error_status = dbc.Badge("🔴 Error", color="danger", className="me-1") + return error_status, error_status, error_status, error_status + + +def _get_data_collection_quick_status() -> dbc.Badge: + """Get quick data collection status.""" + try: + is_running = _check_data_collection_service_running() + if is_running: + return dbc.Badge("Active", color="success", className="me-1") + else: + return dbc.Badge("Stopped", color="danger", className="me-1") + except Exception as e: + logger.error(f"Error checking data collection quick status: {e}") + return dbc.Badge("Unknown", color="warning", className="me-1") + + +def _get_database_quick_status() -> dbc.Badge: + """Get quick database status.""" + try: + db_manager = DatabaseManager() + db_manager.initialize() + if db_manager.test_connection(): + return dbc.Badge("Connected", color="success", className="me-1") + else: + return dbc.Badge("Error", color="danger", className="me-1") + except Exception as e: + logger.error(f"Error checking database quick status: {e}") + return dbc.Badge("Error", color="danger", className="me-1") + + +def _get_redis_quick_status() -> dbc.Badge: + """Get quick Redis status.""" + try: + redis_manager = get_sync_redis_manager() + redis_manager.initialize() + if redis_manager.client.ping(): + return dbc.Badge("Connected", color="success", className="me-1") + else: + return dbc.Badge("Error", color="danger", className="me-1") + except Exception as e: + logger.error(f"Redis quick status check failed: {e}") + return dbc.Badge("Error", color="danger", className="me-1") + + +def _get_performance_quick_status() -> dbc.Badge: + """Get quick performance status.""" + try: + cpu_percent = psutil.cpu_percent(interval=0.1) + memory = psutil.virtual_memory() + + if cpu_percent < CPU_GOOD_THRESHOLD and memory.percent < MEMORY_GOOD_THRESHOLD: + return dbc.Badge("Good", color="success", className="me-1") + elif cpu_percent < CPU_WARNING_THRESHOLD and memory.percent < MEMORY_WARNING_THRESHOLD: + return dbc.Badge("Warning", color="warning", className="me-1") + else: + return dbc.Badge("High", color="danger", className="me-1") + except Exception as e: + logger.error(f"Error checking performance quick status: {e}") + return dbc.Badge("Unknown", color="secondary", className="me-1") + + +def _check_data_collection_service_running() -> bool: + """Check if data collection service is running.""" + try: + for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + try: + if proc.info['cmdline']: + cmdline = ' '.join(proc.info['cmdline']) + if 'start_data_collection.py' in cmdline or 'collection_service' in cmdline: + return True + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: + logger.warning(f"Access or process error checking service: {e}") + continue + return False + except Exception as e: + logger.error(f"Error checking data collection service running status: {e}") + return False \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/redis_callbacks.py b/dashboard/callbacks/system_health_modules/redis_callbacks.py new file mode 100644 index 0000000..234b97a --- /dev/null +++ b/dashboard/callbacks/system_health_modules/redis_callbacks.py @@ -0,0 +1,82 @@ +from dash import Output, Input, html +import dash_bootstrap_components as dbc +from utils.logger import get_logger +from database.redis_manager import get_sync_redis_manager + +logger = get_logger("default_logger") + + +def register_redis_callbacks(app): + """Register Redis status and statistics callbacks.""" + + # Redis Status and Statistics + @app.callback( + [Output('redis-status', 'children'), + Output('redis-stats', 'children')], + Input('interval-component', 'n_intervals') + ) + def update_redis_status(n_intervals): + """Update Redis connection status and statistics.""" + try: + redis_status = _get_redis_status() + redis_stats = _get_redis_statistics() + + return redis_status, redis_stats + + except Exception as e: + logger.error(f"Error updating Redis status: {e}") + error_alert = dbc.Alert( + f"Error: {str(e)}", + color="danger", + dismissable=True + ) + return error_alert, error_alert + + +def _get_redis_status() -> html.Div: + """Get detailed Redis server status.""" + try: + redis_manager = get_sync_redis_manager() + redis_manager.initialize() + + if not redis_manager.client.ping(): + raise ConnectionError("Redis server is not responding.") + + info = redis_manager.client.info() + status_badge = dbc.Badge("Connected", color="success", className="me-1") + + return html.Div([ + html.H5("Redis Status"), + status_badge, + html.P(f"Version: {info.get('redis_version', 'N/A')}"), + html.P(f"Mode: {info.get('redis_mode', 'N/A')}") + ]) + except Exception as e: + logger.error(f"Failed to get Redis status: {e}") + return html.Div([ + html.H5("Redis Status"), + dbc.Badge("Error", color="danger", className="me-1"), + dbc.Alert(f"Error: {e}", color="danger", dismissable=True) + ]) + + +def _get_redis_statistics() -> html.Div: + """Get detailed Redis statistics.""" + try: + redis_manager = get_sync_redis_manager() + redis_manager.initialize() + + if not redis_manager.client.ping(): + raise ConnectionError("Redis server is not responding.") + + info = redis_manager.client.info() + + return html.Div([ + html.H5("Redis Statistics"), + html.P(f"Connected Clients: {info.get('connected_clients', 'N/A')}"), + html.P(f"Memory Used: {info.get('used_memory_human', 'N/A')}"), + html.P(f"Total Commands Processed: {info.get('total_commands_processed', 'N/A')}") + ]) + except Exception as e: + logger.error(f"Failed to get Redis statistics: {e}") + return dbc.Alert(f"Error: {e}", color="danger", dismissable=True) \ No newline at end of file diff --git a/dashboard/callbacks/system_health_modules/system_performance_callbacks.py b/dashboard/callbacks/system_health_modules/system_performance_callbacks.py new file mode 100644 index 0000000..4022ced --- /dev/null +++ b/dashboard/callbacks/system_health_modules/system_performance_callbacks.py @@ -0,0 +1,75 @@ +from dash import Output, Input, html +import dash_bootstrap_components as dbc +from utils.logger import get_logger +import psutil + +from config.constants.system_health_constants import ( + CAPACITY_GOOD_THRESHOLD, CAPACITY_WARNING_THRESHOLD, + CPU_GOOD_THRESHOLD, CPU_WARNING_THRESHOLD, + MEMORY_GOOD_THRESHOLD, MEMORY_WARNING_THRESHOLD, + DISK_GOOD_THRESHOLD, DISK_WARNING_THRESHOLD, + BYTE_TO_GB +) + +logger = get_logger("default_logger") + + +def register_system_performance_callbacks(app): + """Register system performance metrics callbacks.""" + + # System Performance Metrics + @app.callback( + Output('system-performance-metrics', 'children'), + Input('interval-component', 'n_intervals') + ) + def update_system_performance(n_intervals): + """Update system performance metrics.""" + try: + return _get_system_performance_metrics() + except Exception as e: + logger.error(f"Error updating system performance: {e}") + return dbc.Alert( + f"Error: {str(e)}", + color="danger", + dismissable=True + ) + + +def _get_system_performance_metrics() -> html.Div: + """Get system performance metrics.""" + try: + cpu_percent = psutil.cpu_percent(interval=0.1) + cpu_count = psutil.cpu_count() + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + def get_color(percent): + if percent < CAPACITY_GOOD_THRESHOLD: return "success" + if percent < CAPACITY_WARNING_THRESHOLD: return "warning" + return "danger" + + return html.Div([ + html.Div([ + html.Strong("CPU Usage: "), + dbc.Badge(f"{cpu_percent:.1f}%", color=get_color(cpu_percent)), + html.Span(f" ({cpu_count} cores)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=cpu_percent, color=get_color(cpu_percent), style={"height": "10px"}, className="mb-3"), + + html.Div([ + html.Strong("Memory Usage: "), + dbc.Badge(f"{memory.percent:.1f}%", color=get_color(memory.percent)), + html.Span(f" ({memory.used / BYTE_TO_GB:.1f} / {memory.total / BYTE_TO_GB:.1f} GB)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=memory.percent, color=get_color(memory.percent), style={"height": "10px"}, className="mb-3"), + + html.Div([ + html.Strong("Disk Usage: "), + dbc.Badge(f"{disk.percent:.1f}%", color=get_color(disk.percent)), + html.Span(f" ({disk.used / BYTE_TO_GB:.1f} / {disk.total / BYTE_TO_GB:.1f} GB)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=disk.percent, color=get_color(disk.percent), style={"height": "10px"}) + ]) + + except Exception as e: + return dbc.Alert(f"Error loading performance metrics: {e}", color="danger") \ No newline at end of file diff --git a/utils/time_range_utils.py b/utils/time_range_utils.py index 12cb76e..7c95475 100644 --- a/utils/time_range_utils.py +++ b/utils/time_range_utils.py @@ -4,6 +4,19 @@ import logging logger = logging.getLogger(__name__) +DEFAULT_TIME_RANGE_OPTIONS = [ + {"label": "🕐 Last 1 Hour", "value": "1h"}, + {"label": "🕐 Last 4 Hours", "value": "4h"}, + {"label": "🕐 Last 6 Hours", "value": "6h"}, + {"label": "🕐 Last 12 Hours", "value": "12h"}, + {"label": "📅 Last 1 Day", "value": "1d"}, + {"label": "📅 Last 3 Days", "value": "3d"}, + {"label": "📅 Last 7 Days", "value": "7d"}, + {"label": "📅 Last 30 Days", "value": "30d"}, + {"label": "📅 Custom Range", "value": "custom"}, + {"label": "🔴 Real-time", "value": "realtime"} +] + def load_time_range_options(): """Loads time range options from a JSON file. @@ -21,43 +34,10 @@ def load_time_range_options(): return json.load(f) except FileNotFoundError: logger.error(f"Time range options file not found at {file_path}. Using default.") - return [ - {"label": "🕐 Last 1 Hour", "value": "1h"}, - {"label": "🕐 Last 4 Hours", "value": "4h"}, - {"label": "🕐 Last 6 Hours", "value": "6h"}, - {"label": "🕐 Last 12 Hours", "value": "12h"}, - {"label": "📅 Last 1 Day", "value": "1d"}, - {"label": "📅 Last 3 Days", "value": "3d"}, - {"label": "📅 Last 7 Days", "value": "7d"}, - {"label": "📅 Last 30 Days", "value": "30d"}, - {"label": "📅 Custom Range", "value": "custom"}, - {"label": "🔴 Real-time", "value": "realtime"} - ] + return DEFAULT_TIME_RANGE_OPTIONS except json.JSONDecodeError: logger.error(f"Error decoding JSON from {file_path}. Using default.") - return [ - {"label": "🕐 Last 1 Hour", "value": "1h"}, - {"label": "🕐 Last 4 Hours", "value": "4h"}, - {"label": "🕐 Last 6 Hours", "value": "6h"}, - {"label": "🕐 Last 12 Hours", "value": "12h"}, - {"label": "📅 Last 1 Day", "value": "1d"}, - {"label": "📅 Last 3 Days", "value": "3d"}, - {"label": "📅 Last 7 Days", "value": "7d"}, - {"label": "📅 Last 30 Days", "value": "30d"}, - {"label": "📅 Custom Range", "value": "custom"}, - {"label": "🔴 Real-time", "value": "realtime"} - ] + return DEFAULT_TIME_RANGE_OPTIONS except Exception as e: logger.error(f"An unexpected error occurred while loading time range options: {e}. Using default.") - return [ - {"label": "🕐 Last 1 Hour", "value": "1h"}, - {"label": "🕐 Last 4 Hours", "value": "4h"}, - {"label": "🕐 Last 6 Hours", "value": "6h"}, - {"label": "🕐 Last 12 Hours", "value": "12h"}, - {"label": "📅 Last 1 Day", "value": "1d"}, - {"label": "📅 Last 3 Days", "value": "3d"}, - {"label": "📅 Last 7 Days", "value": "7d"}, - {"label": "📅 Last 30 Days", "value": "30d"}, - {"label": "📅 Custom Range", "value": "custom"}, - {"label": "🔴 Real-time", "value": "realtime"} - ] \ No newline at end of file + return DEFAULT_TIME_RANGE_OPTIONS \ No newline at end of file diff --git a/utils/timeframe_utils.py b/utils/timeframe_utils.py index 90b3ae1..8393fbe 100644 --- a/utils/timeframe_utils.py +++ b/utils/timeframe_utils.py @@ -4,6 +4,19 @@ import logging logger = logging.getLogger(__name__) +DEFAULT_TIMEFRAME_OPTIONS = [ + {'label': '1 Second', 'value': '1s'}, + {'label': '5 Seconds', 'value': '5s'}, + {'label': '15 Seconds', 'value': '15s'}, + {'label': '30 Seconds', 'value': '30s'}, + {'label': '1 Minute', 'value': '1m'}, + {'label': '5 Minutes', 'value': '5m'}, + {'label': '15 Minutes', 'value': '15m'}, + {'label': '1 Hour', 'value': '1h'}, + {'label': '4 Hours', 'value': '4h'}, + {'label': '1 Day', 'value': '1d'}, +] + def load_timeframe_options(): """Loads timeframe options from a JSON file. @@ -21,43 +34,10 @@ def load_timeframe_options(): return json.load(f) except FileNotFoundError: logger.error(f"Timeframe options file not found at {file_path}. Using default timeframes.") - return [ - {'label': '1 Second', 'value': '1s'}, - {'label': '5 Seconds', 'value': '5s'}, - {'label': '15 Seconds', 'value': '15s'}, - {'label': '30 Seconds', 'value': '30s'}, - {'label': '1 Minute', 'value': '1m'}, - {'label': '5 Minutes', 'value': '5m'}, - {'label': '15 Minutes', 'value': '15m'}, - {'label': '1 Hour', 'value': '1h'}, - {'label': '4 Hours', 'value': '4h'}, - {'label': '1 Day', 'value': '1d'}, - ] + return DEFAULT_TIMEFRAME_OPTIONS except json.JSONDecodeError: logger.error(f"Error decoding JSON from {file_path}. Using default timeframes.") - return [ - {'label': '1 Second', 'value': '1s'}, - {'label': '5 Seconds', 'value': '5s'}, - {'label': '15 Seconds', 'value': '15s'}, - {'label': '30 Seconds', 'value': '30s'}, - {'label': '1 Minute', 'value': '1m'}, - {'label': '5 Minutes', 'value': '5m'}, - {'label': '15 Minutes', 'value': '15m'}, - {'label': '1 Hour', 'value': '1h'}, - {'label': '4 Hours', 'value': '4h'}, - {'label': '1 Day', 'value': '1d'}, - ] + return DEFAULT_TIMEFRAME_OPTIONS except Exception as e: logger.error(f"An unexpected error occurred while loading timeframes: {e}. Using default timeframes.") - return [ - {'label': '1 Second', 'value': '1s'}, - {'label': '5 Seconds', 'value': '5s'}, - {'label': '15 Seconds', 'value': '15s'}, - {'label': '30 Seconds', 'value': '30s'}, - {'label': '1 Minute', 'value': '1m'}, - {'label': '5 Minutes', 'value': '5m'}, - {'label': '15 Minutes', 'value': '15m'}, - {'label': '1 Hour', 'value': '1h'}, - {'label': '4 Hours', 'value': '4h'}, - {'label': '1 Day', 'value': '1d'}, - ] \ No newline at end of file + return DEFAULT_TIMEFRAME_OPTIONS \ No newline at end of file