MQL5-Google-Onedrive/scripts/web_dashboard.py

384 lines
13 KiB
Python
Raw Permalink Normal View History

import os
import sys
import logging
import platform
import time
import threading
from datetime import datetime
from flask import Flask, render_template_string, jsonify
import markdown
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Cache storage: filepath -> (mtime, html_content)
_content_cache = {}
# Thread-local storage for Markdown instances to avoid re-initialization overhead
_md_local = threading.local()
# Constants for paths to avoid re-calculating on every request
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..'))
README_PATH = os.path.join(REPO_ROOT, 'README.md')
VERIFICATION_PATH = os.path.join(REPO_ROOT, 'VERIFICATION.md')
VERSION_PATH = os.path.join(REPO_ROOT, 'VERSION')
def get_version():
"""Read version from VERSION file."""
try:
if os.path.exists(VERSION_PATH):
with open(VERSION_PATH, 'r') as f:
return f.read().strip()
except Exception:
pass
return "1.0.0"
APP_VERSION = get_version()
# HTML Template
DASHBOARD_HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GenX FX | Trading Automation Dashboard</title>
<style>
:root {
--primary: #4f46e5;
--primary-hover: #4338ca;
--success: #10b981;
--bg: #f9fafb;
--card-bg: #ffffff;
--text-main: #111827;
--text-muted: #6b7280;
--border: #e5e7eb;
}
* { box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, sans-serif;
line-height: 1.5;
background-color: var(--bg);
color: var(--text-main);
margin: 0;
padding: 0;
}
.container { max-width: 1100px; margin: 0 auto; padding: 2rem; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.header h1 { font-size: 1.5rem; margin: 0; color: var(--primary); }
.version { font-size: 0.875rem; color: var(--text-muted); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.card h2 { font-size: 1.125rem; margin-top: 0; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.status-badge {
background: var(--success);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.nav {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
z-index: 10;
padding: 0.75rem 0;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border);
}
.nav-inner { max-width: 1100px; margin: 0 auto; padding: 0 2rem; display: flex; gap: 1.5rem; }
.nav a { text-decoration: none; color: var(--text-muted); font-weight: 500; font-size: 0.875rem; transition: color 0.2s; }
.nav a:hover { color: var(--primary); }
.stats-list { list-style: none; padding: 0; margin: 0; }
.stats-item { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.stats-item:last-child { border-bottom: none; }
.stats-label { color: var(--text-muted); font-size: 0.875rem; }
.stats-value { font-weight: 600; font-size: 0.875rem; }
.markdown-content img { max-width: 100%; height: auto; border-radius: 0.5rem; }
.markdown-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.markdown-content th, .markdown-content td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); }
.markdown-content th { background: #f9fafb; }
footer { text-align: center; padding: 3rem 0; color: var(--text-muted); font-size: 0.875rem; }
.skip-link {
position: absolute;
top: -100px;
left: 0;
background: var(--primary);
color: white;
padding: 1rem;
z-index: 100;
transition: top 0.2s;
}
.skip-link:focus { top: 0; }
@media (max-width: 640px) {
.container { padding: 1rem; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="nav">
<div class="nav-inner">
<a href="#status">Status</a>
<a href="#verification">Verification</a>
<a href="#readme">Documentation</a>
</div>
</div>
<div class="container" id="main-content">
<div class="header">
<h1>GenX FX Trading Automation</h1>
<div class="version">v{{ version }}</div>
</div>
<div class="grid">
<div id="status" class="card">
<h2>System Status <span class="status-badge">ONLINE</span></h2>
<ul class="stats-list">
<li class="stats-item">
<span class="stats-label">Platform</span>
<span class="stats-value">{{ platform }}</span>
</li>
<li class="stats-item">
<span class="stats-label">Uptime</span>
<span class="stats-value" id="uptime">Loading...</span>
</li>
<li class="stats-item">
<span class="stats-label">Fly.io App</span>
<span class="stats-value">mql5-automation</span>
</li>
<li class="stats-item">
<span class="stats-label">Telegram Bot</span>
<span class="stats-value">@GenX_FX_bot</span>
</li>
</ul>
</div>
<div class="card">
<h2>Resources</h2>
<ul class="stats-list">
<li class="stats-item">
<span class="stats-label">Python Version</span>
<span class="stats-value">{{ python_version }}</span>
</li>
<li class="stats-item">
<span class="stats-label">Last Refresh</span>
<span class="stats-value">{{ last_refresh }}</span>
</li>
</ul>
</div>
<div class="card">
<h2>API Endpoints</h2>
<ul class="stats-list">
<li class="stats-item">
<span class="stats-label">Version API</span>
<span class="stats-value"><a href="/api/version" style="color: var(--primary);">/api/version</a></span>
</li>
<li class="stats-item">
<span class="stats-label">System Info</span>
<span class="stats-value"><a href="/api/system_info" style="color: var(--primary);">/api/system_info</a></span>
</li>
<li class="stats-item">
<span class="stats-label">Files API</span>
<span class="stats-value"><a href="/api/files" style="color: var(--primary);">/api/files</a></span>
</li>
</ul>
</div>
</div>
<div id="verification" class="card markdown-content" style="margin-bottom: 2rem;">
<h2>Verification Status</h2>
{{ html_verification|safe }}
</div>
<div id="readme" class="card markdown-content">
<h2>Documentation</h2>
{{ html_readme|safe }}
</div>
<footer>
<p>&copy; {{ year }} GenX FX Trading System. All rights reserved.</p>
</footer>
</div>
<script>
const startTime = Date.now();
function updateUptime() {
const now = Date.now();
const diff = Math.floor((now - startTime) / 1000);
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = diff % 60;
document.getElementById('uptime').textContent =
`${hours}h ${minutes}m ${seconds}s`;
}
setInterval(updateUptime, 1000);
updateUptime();
</script>
</body>
</html>
"""
# Global to store compiled template
DASHBOARD_TEMPLATE = None
def get_cached_markdown(filepath):
"""
Returns the markdown content of a file converted to HTML, using a cache
that invalidates based on file modification time.
"""
try:
stat_result = os.stat(filepath)
except OSError:
return None
try:
mtime = stat_result.st_mtime
if filepath in _content_cache:
cached_mtime, cached_html = _content_cache[filepath]
if cached_mtime == mtime:
return cached_html
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if not hasattr(_md_local, 'md'):
_md_local.md = markdown.Markdown(extensions=['tables', 'fenced_code'])
_md_local.md.reset()
html_content = _md_local.md.convert(content)
_content_cache[filepath] = (mtime, html_content)
return html_content
except Exception as e:
logger.error(f"Error reading/converting {filepath}: {e}")
return None
@app.route('/health')
def health_check():
"""Lightweight health check for load balancers."""
return jsonify({
"status": "healthy",
"timestamp": time.time(),
"version": APP_VERSION
})
@app.route('/api/version')
def api_version():
"""Returns the application version."""
return jsonify({
"version": APP_VERSION,
"name": "GenX FX Trading Automation"
})
@app.route('/api/system_info')
def system_info():
"""Returns basic system information."""
return jsonify({
"platform": platform.platform(),
"python_version": sys.version,
"processor": platform.processor(),
"hostname": platform.node(),
"time": datetime.now().isoformat()
})
@app.route('/api/files')
def list_important_files():
"""Lists important files in the repository."""
important_files = []
for root, dirs, files in os.walk(REPO_ROOT):
# Skip hidden directories and common build artifacts
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['node_modules', '__pycache__', 'dist', 'build']]
for file in files:
if file.endswith(('.mq5', '.mqh', '.py', '.sh', '.ps1', '.md')):
rel_path = os.path.relpath(os.path.join(root, file), REPO_ROOT)
important_files.append(rel_path)
return jsonify({
"count": len(important_files),
"files": sorted(important_files)[:100] # Limit to 100 files
})
@app.after_request
def add_security_headers(response):
"""Add security headers to every response."""
csp = "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'"
response.headers['Content-Security-Policy'] = csp
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
@app.route('/')
def dashboard():
global DASHBOARD_TEMPLATE
try:
html_readme = get_cached_markdown(README_PATH) or "<p>README.md not found.</p>"
html_verification = get_cached_markdown(VERIFICATION_PATH) or "<p>VERIFICATION.md not found.</p>"
if DASHBOARD_TEMPLATE is None:
DASHBOARD_TEMPLATE = app.jinja_env.from_string(DASHBOARD_HTML)
return DASHBOARD_TEMPLATE.render(
html_readme=html_readme,
html_verification=html_verification,
year=datetime.now().year,
version=APP_VERSION,
platform=platform.system(),
python_version=platform.python_version(),
last_refresh=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
)
except Exception as e:
logger.error(f"Error rendering dashboard: {e}", exc_info=True)
return "Internal Server Error", 500
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
print(f"Starting web dashboard on port {port}...")
app.run(host='0.0.0.0', port=port)