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 — reads CSV output from PVC, serves HTML tables.""" import csv import glob import html import os from http.server import BaseHTTPRequestHandler, HTTPServer OUTPUT_DIR = os.environ.get("SPARK_OUTPUT_DIR", "/spark-output") PORT = int(os.environ.get("PORT", "8080")) # Each tuple: (url-slug, display label, path relative to SPARK_OUTPUT_DIR) # Paths mirror the CronJob outputDir arg + the subdir each job writes CSV to. DATASETS = [ ("opening-book", "Opening Book (Top 1000)", "opening-book/opening_book_top1000"), ("player-stats", "Player Statistics", "player-stats/player_stats_csv"), ("cluster-archetypes", "Cluster Archetypes", "player-clusters/cluster_archetypes"), ("component-sizes", "Graph Component Sizes", "player-graph/component_sizes"), ("game-length", "Game Length Distribution", "game-length/game_length_distribution"), ("game-length-by-result", "Game Length by Result", "game-length/game_length_by_result"), ("color-advantage", "Color Advantage", "color-advantage/color_advantage"), ("elo-distribution", "ELO Distribution", "elo-distribution/elo_distribution"), ("time-control", "Time Control Analysis", "time-control/time_control_stats"), ("hourly-activity", "Hourly Activity", "daily-activity/hourly_activity"), ("weekly-activity", "Weekly Activity", "daily-activity/weekly_activity"), ("rating-mismatch", "Rating Mismatch (Upsets)", "rating-mismatch/rating_mismatch"), ("termination-stats", "Termination Types", "termination-stats/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 read_csv_parts(subdir: str) -> tuple[list[str], list[list[str]]]: files = sorted(glob.glob(f"{OUTPUT_DIR}/{subdir}/part-*.csv")) headers: list[str] = [] rows: list[list[str]] = [] for f in files: with open(f, newline="", encoding="utf-8") as fh: reader = csv.reader(fh) for i, row in enumerate(reader): if i == 0 and not headers: headers = row elif headers: rows.append(row) return headers, rows def dataset_status(subdir: str) -> tuple[int, int]: files = glob.glob(f"{OUTPUT_DIR}/{subdir}/part-*.csv") return len(files), sum(os.path.getsize(f) for f in files) def page(title: str, body: str) -> str: return ( f"
" f"" f"