apiVersion: v1 kind: ConfigMap metadata: name: spark-analytics-webview labels: app.kubernetes.io/name: spark-analytics app.kubernetes.io/part-of: nowchess data: serve.py: | #!/usr/bin/env python3 """Spark analytics results viewer โ queries PostgreSQL analytics tables, serves HTML tables.""" import html import os import re import psycopg2 import psycopg2.extras from http.server import BaseHTTPRequestHandler, HTTPServer PORT = int(os.environ.get("PORT", "8080")) _url = os.environ.get("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess") _m = re.match(r"jdbc:postgresql://([^:/]+):(\d+)/(.+)", _url) DB_HOST = _m.group(1) if _m else "localhost" DB_PORT = int(_m.group(2)) if _m else 5432 DB_NAME = _m.group(3) if _m else "nowchess" DB_USER = os.environ.get("NOWCHESS_DB_USER", "nowchess") DB_PASS = os.environ.get("NOWCHESS_DB_PASS", "") # Each tuple: (url-slug, display label, postgres table name) DATASETS = [ ("opening-book", "Opening Book (Top 1000)", "analytics_opening_stats"), ("player-stats", "Player Statistics", "analytics_player_stats"), ("cluster-archetypes", "Cluster Archetypes", "analytics_cluster_archetypes"), ("component-sizes", "Graph Component Sizes", "analytics_component_sizes"), ("game-length", "Game Length Distribution", "analytics_game_length_distribution"), ("game-length-by-result", "Game Length by Result", "analytics_game_length_by_result"), ("color-advantage", "Color Advantage", "analytics_color_advantage"), ("elo-distribution", "ELO Distribution", "analytics_elo_distribution"), ("time-control", "Time Control Analysis", "analytics_time_control_stats"), ("hourly-activity", "Hourly Activity", "analytics_hourly_activity"), ("weekly-activity", "Weekly Activity", "analytics_weekly_activity"), ("rating-mismatch", "Rating Mismatch (Upsets)", "analytics_rating_mismatch"), ("termination-stats", "Termination Types", "analytics_termination_stats"), ] CSS = """ * { box-sizing: border-box; } body { font-family: 'Segoe UI', sans-serif; margin: 0; background: #0d1117; color: #c9d1d9; } header { background: #161b22; border-bottom: 1px solid #30363d; padding: 1rem 2rem; } header h1 { margin: 0; color: #58a6ff; font-size: 1.25rem; font-weight: 600; } header span { color: #8b949e; font-size: 0.875rem; margin-left: 0.5rem; } main { padding: 1.5rem 2rem; } h2 { color: #e6edf3; font-size: 1rem; font-weight: 600; margin: 0 0 1rem; } .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; } .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; } .card h3 { margin: 0 0 0.5rem; font-size: 0.9rem; color: #58a6ff; } .card a { text-decoration: none; color: inherit; display: block; } .card a:hover .card-label { text-decoration: underline; } .badge { display: inline-block; background: #21262d; border: 1px solid #30363d; border-radius: 12px; padding: 1px 8px; font-size: 0.75rem; color: #8b949e; } .badge.ready { border-color: #238636; color: #3fb950; } .back { color: #58a6ff; text-decoration: none; font-size: 0.875rem; } .back:hover { text-decoration: underline; } .meta { color: #8b949e; font-size: 0.8rem; margin: 0.5rem 0 1rem; } table { width: 100%; border-collapse: collapse; font-size: 0.8rem; } thead th { background: #161b22; position: sticky; top: 0; padding: 6px 12px; text-align: left; color: #8b949e; border-bottom: 1px solid #30363d; font-weight: 600; white-space: nowrap; } tbody td { padding: 5px 12px; border-bottom: 1px solid #21262d; } tbody tr:hover td { background: #161b22; } .table-wrap { overflow-x: auto; border: 1px solid #30363d; border-radius: 6px; } .notice { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1.5rem; color: #8b949e; } """ def get_conn(): return psycopg2.connect(host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASS) def table_row_count(table: str) -> int: try: with get_conn() as conn: with conn.cursor() as cur: cur.execute(f"SELECT COUNT(*) FROM {table}") return cur.fetchone()[0] except Exception: return -1 def fetch_rows(table: str, limit: int = 10_000): with get_conn() as conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(f"SELECT * FROM {table} LIMIT %s", (limit,)) rows = cur.fetchall() if not rows: return [], [] headers = list(rows[0].keys()) data = [ [str(row[h]) if row[h] is not None else "" for h in headers] for row in rows ] return headers, data def page(title: str, body: str) -> str: return ( f"
" f"" f"