Update performance dashboard for nightly tests (#18824)

This commit is contained in:
Kangyan-Zhou
2026-02-14 09:28:28 +08:00
committed by GitHub
parent 3299c4f9c1
commit 3a1c388b43
3 changed files with 993 additions and 259 deletions
+118 -13
View File
@@ -23,12 +23,12 @@ const metricTypes = {
};
// Chart.js default configuration for dark theme
Chart.defaults.color = '#8b949e';
Chart.defaults.borderColor = '#30363d';
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = '#1e293b';
const chartColors = [
'#58a6ff', '#3fb950', '#d29922', '#f85149', '#a371f7',
'#79c0ff', '#56d364', '#e3b341', '#ff7b72', '#bc8cff'
'#22d3ee', '#34d399', '#fbbf24', '#f87171', '#a78bfa',
'#67e8f9', '#6ee7b7', '#fcd34d', '#fca5a5', '#c4b5fd'
];
// Initialize the dashboard
@@ -53,7 +53,7 @@ async function init() {
async function loadData() {
// Try local server API first (if running server.py)
try {
const response = await fetch('/api/metrics');
const response = await fetch('/api/metrics', { headers: getAuthHeaders() });
if (response.ok) {
const data = await response.json();
if (data.length > 0 && data[0].results && data[0].results.length > 0) {
@@ -726,12 +726,13 @@ function getChartOptions(yAxisLabel) {
}
},
tooltip: {
backgroundColor: '#21262d',
borderColor: '#30363d',
backgroundColor: '#1a2332',
borderColor: 'rgba(148, 163, 184, 0.1)',
borderWidth: 1,
titleFont: { size: 13 },
bodyFont: { size: 12 },
padding: 12
titleFont: { size: 13, family: "'DM Sans', sans-serif" },
bodyFont: { size: 12, family: "'JetBrains Mono', monospace" },
padding: 14,
cornerRadius: 8
}
},
scales: {
@@ -744,7 +745,7 @@ function getChartOptions(yAxisLabel) {
}
},
grid: {
color: '#21262d'
color: 'rgba(148, 163, 184, 0.06)'
}
},
y: {
@@ -753,7 +754,7 @@ function getChartOptions(yAxisLabel) {
text: yAxisLabel
},
grid: {
color: '#21262d'
color: 'rgba(148, 163, 184, 0.06)'
}
}
}
@@ -832,5 +833,109 @@ function formatNumber(num) {
return num.toFixed(1);
}
// Authentication state
let authToken = sessionStorage.getItem('dashboard_auth_token') || null;
// Get auth headers for API requests
function getAuthHeaders() {
const headers = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
return headers;
}
// Check if server requires authentication and show/hide login accordingly
async function checkAuthAndInit() {
const loginOverlay = document.getElementById('login-overlay');
const dashboardContainer = document.getElementById('dashboard-container');
try {
const response = await fetch('/api/auth-check');
if (response.ok) {
const data = await response.json();
if (!data.auth_required) {
// No auth required - skip login, show dashboard directly
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
}
} catch (e) {
// Server not available (e.g. static hosting) - skip login
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
// Auth is required - check if we have a valid token from a previous session
if (authToken) {
try {
const testResponse = await fetch('/api/metrics', {
headers: getAuthHeaders()
});
if (testResponse.ok) {
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
} catch (e) {
// Token invalid or expired
}
// Clear invalid token
authToken = null;
sessionStorage.removeItem('dashboard_auth_token');
}
// Show login form
loginOverlay.style.display = 'flex';
dashboardContainer.style.display = 'none';
}
// Handle login form submission
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
const errorEl = document.getElementById('login-error');
const loginBtn = document.getElementById('login-btn');
errorEl.textContent = '';
loginBtn.disabled = true;
loginBtn.textContent = 'Signing in...';
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok && data.token) {
authToken = data.token;
sessionStorage.setItem('dashboard_auth_token', authToken);
document.getElementById('login-overlay').style.display = 'none';
document.getElementById('dashboard-container').style.display = 'block';
init();
} else {
errorEl.textContent = data.error || 'Invalid username or password';
}
} catch (e) {
errorEl.textContent = 'Unable to connect to server';
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Sign In';
}
return false;
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', checkAuthAndInit);
File diff suppressed because it is too large Load Diff
+157 -14
View File
@@ -12,13 +12,19 @@ Usage:
python server.py --port 8080
python server.py --host 0.0.0.0 # Allow external access
python server.py --fetch-on-start
python server.py --username admin --password secret # Enable authentication
DASHBOARD_USERNAME=admin DASHBOARD_PASSWORD=secret python server.py # Via env vars
python server.py --refresh-interval 12 # Auto-refresh data every 12 hours
"""
import argparse
import hashlib
import hmac
import http.server
import io
import json
import os
import secrets
import socketserver
import threading
import time
@@ -44,6 +50,47 @@ metrics_cache = {
CACHE_TTL = 300 # 5 minutes
REQUEST_TIMEOUT = 30 # seconds
# Authentication configuration (set via CLI flags)
auth_config = {
"enabled": False,
"username": None,
"password_hash": None, # SHA-256 hash of the password
"active_tokens": {}, # token -> expiry timestamp
}
auth_lock = threading.Lock()
AUTH_TOKEN_TTL = 3600 # 1 hour
def hash_password(password):
"""Hash a password using SHA-256 for constant-time comparison."""
return hashlib.sha256(password.encode("utf-8")).hexdigest()
def create_auth_token():
"""Create a new session token."""
token = secrets.token_hex(32)
with auth_lock:
# Clean up expired tokens
now = time.time()
auth_config["active_tokens"] = {
t: exp for t, exp in auth_config["active_tokens"].items() if exp > now
}
auth_config["active_tokens"][token] = now + AUTH_TOKEN_TTL
return token
def verify_auth_token(token):
"""Verify a session token is valid and not expired."""
if not token:
return False
with auth_lock:
expiry = auth_config["active_tokens"].get(token)
if expiry and expiry > time.time():
return True
# Remove expired token
auth_config["active_tokens"].pop(token, None)
return False
def get_github_token():
"""Get GitHub token from environment or gh CLI."""
@@ -187,12 +234,47 @@ def update_cache_async():
metrics_cache["updating"] = False
def start_periodic_refresh(interval_hours):
"""Start a background thread that refreshes the cache periodically."""
interval_seconds = interval_hours * 3600
def refresh_loop():
while True:
time.sleep(interval_seconds)
print(f"Periodic refresh triggered (every {interval_hours}h)")
update_cache_async()
thread = threading.Thread(target=refresh_loop, daemon=True)
thread.start()
print(f"Periodic refresh enabled: every {interval_hours} hours")
class DashboardHandler(http.server.SimpleHTTPRequestHandler):
"""HTTP request handler for the dashboard."""
def __init__(self, *args, directory=None, **kwargs):
super().__init__(*args, directory=directory, **kwargs)
def _send_json(self, data, status=200):
"""Send a JSON response."""
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def _check_auth(self):
"""Check if request is authenticated. Returns True if OK, sends 401 and returns False otherwise."""
if not auth_config["enabled"]:
return True
auth_header = self.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if verify_auth_token(token):
return True
self._send_json({"error": "Unauthorized"}, status=401)
return False
def do_GET(self):
parsed = urlparse(self.path)
@@ -201,13 +283,55 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
self.send_error(400, "Invalid path")
return
if parsed.path == "/api/metrics":
self.handle_metrics_api(parsed)
if parsed.path == "/api/auth-check":
self.handle_auth_check()
elif parsed.path == "/api/metrics":
if self._check_auth():
self.handle_metrics_api(parsed)
elif parsed.path == "/api/refresh":
self.handle_refresh_api()
if self._check_auth():
self.handle_refresh_api()
else:
super().do_GET()
def do_POST(self):
parsed = urlparse(self.path)
if parsed.path == "/api/login":
self.handle_login()
else:
self.send_error(404, "Not Found")
def handle_auth_check(self):
"""Tell the frontend whether authentication is required."""
self._send_json({"auth_required": auth_config["enabled"]})
def handle_login(self):
"""Validate username/password and return a session token."""
content_length = int(self.headers.get("Content-Length", 0))
if content_length == 0 or content_length > 4096:
self._send_json({"error": "Invalid request"}, status=400)
return
try:
body = json.loads(self.rfile.read(content_length))
except (json.JSONDecodeError, ValueError):
self._send_json({"error": "Invalid JSON"}, status=400)
return
username = body.get("username", "")
password = body.get("password", "")
if hmac.compare_digest(
username, auth_config["username"]
) and hmac.compare_digest(
hash_password(password), auth_config["password_hash"]
):
token = create_auth_token()
self._send_json({"token": token})
else:
self._send_json({"error": "Invalid username or password"}, status=401)
def handle_metrics_api(self, parsed):
"""Handle /api/metrics endpoint."""
# Check cache with thread safety
@@ -222,21 +346,12 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
# Trigger background update
threading.Thread(target=update_cache_async, daemon=True).start()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
self._send_json(data)
def handle_refresh_api(self):
"""Handle /api/refresh endpoint."""
threading.Thread(target=update_cache_async, daemon=True).start()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(json.dumps({"status": "refreshing"}).encode())
self._send_json({"status": "refreshing"})
def log_message(self, format, *args):
"""Custom log format."""
@@ -254,8 +369,33 @@ def main():
parser.add_argument(
"--fetch-on-start", action="store_true", help="Fetch metrics on startup"
)
parser.add_argument(
"--refresh-interval",
type=float,
default=12,
help="Auto-refresh interval in hours (default: 12, set to 0 to disable)",
)
parser.add_argument(
"--username",
default=os.environ.get("DASHBOARD_USERNAME"),
help="Username for dashboard authentication (or set DASHBOARD_USERNAME env var)",
)
parser.add_argument(
"--password",
default=os.environ.get("DASHBOARD_PASSWORD"),
help="Password for dashboard authentication (or set DASHBOARD_PASSWORD env var)",
)
args = parser.parse_args()
# Configure authentication if both username and password are provided
if args.username and args.password:
auth_config["enabled"] = True
auth_config["username"] = args.username
auth_config["password_hash"] = hash_password(args.password)
print(f"Authentication enabled for user: {args.username}")
elif args.username or args.password:
parser.error("Both --username and --password must be provided together")
# Change to dashboard directory
dashboard_dir = Path(__file__).parent
os.chdir(dashboard_dir)
@@ -264,6 +404,9 @@ def main():
print("Fetching initial metrics data...")
update_cache_async()
if args.refresh_interval > 0:
start_periodic_refresh(args.refresh_interval)
handler = lambda *a, **kw: DashboardHandler(*a, directory=str(dashboard_dir), **kw)
with socketserver.TCPServer((args.host, args.port), handler) as httpd: