#!/usr/bin/env python3
import cgi, sys
sys.path.insert(0, '/opt/ngon/apps')
from managers.auth_manager import AuthManager, generate_login_page_html

_form = cgi.FieldStorage()
_auth = AuthManager('dead_xp')
_auth_required, _should_exit, _headers = _auth.require_auth(_form)

if _should_exit:
    print("Content-Type: application/json")
    if _headers:
        print(_headers)
    print("")
    if _auth_required:
        print('{"success": false, "error": "Authentication required"}')
    else:
        print('{"success": true}')
    sys.exit(0)

if _auth_required:
    print("Content-Type: text/html")
    print("")
    print(generate_login_page_html("Dead XP Report"))
    sys.exit(0)


"""
Dead XP Report - Field work issue lists by pod
File: /var/www/html/ngon/dead_xp.py

Generates per-pod lists of miners with issues: zero hashrate, missing hashboards, offline.
Uses miner_history.db for data (works even when sites are down).

Serves HTML page on GET, JSON pod data on GET ?pod=<name>
"""

import cgi
import csv
import json
import os
import sqlite3
import sys
from datetime import datetime, timedelta

sys.path.insert(0, '/opt/ngon/apps')
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from links import generate_dropdown_css, generate_dropdown_js, generate_dropdown_html

MINER_HISTORY_DB = '/opt/ngon/data/miner_history.db'
MINER_STATUS_CSV = '/opt/ngon/data/live/miner_status.csv'
MINER_INVENTORY_CSV = '/opt/ngon/data/live/miner_inventory.csv'
MASTER_CONFIG = '/opt/ngon/config/master_config.json'

SPEC_HASHRATES = {
    'XP': 140, 'XP_B': 140,
    'JPro': 100, 'JPro_B': 100,
    'M60': 180,
}


def get_pod_order(xp_only=True):
    pod_order = []
    try:
        with open(MASTER_CONFIG) as f:
            cfg = json.load(f)
        for site_name, site_data in cfg.get('sites', {}).items():
            for group_name, group_data in site_data.get('generator_groups', {}).items():
                for pod_name, pod_data in group_data.get('pods', {}).items():
                    if xp_only and pod_data.get('miner_type', '') != 'XP':
                        continue
                    pod_order.append(pod_name)
    except Exception:
        pass
    return pod_order


def get_inventory_for_pod(pod_name):
    """Load inventory for a specific pod."""
    items = []
    try:
        # Parse pod_name into site + pod_num (e.g. "GN 4" -> "GN", "4")
        parts = pod_name.rsplit(' ', 1)
        if len(parts) != 2:
            return items
        target_site, target_pod = parts[0], parts[1]

        with open(MINER_INVENTORY_CSV) as f:
            reader = csv.DictReader(f)
            for row in reader:
                if row.get('Site', '') == target_site and row.get('Pod', '') == target_pod:
                    items.append(row)
    except Exception:
        pass
    return items


def get_snapshot_data(pod_name):
    try:
        conn = sqlite3.connect(f'file:{MINER_HISTORY_DB}?immutable=1', uri=True)
        c = conn.cursor()
        c.execute("SELECT timestamp FROM miner_snapshots WHERE pod = ? ORDER BY timestamp DESC LIMIT 1", (pod_name,))
        row = c.fetchone()
        if not row:
            conn.close()
            return None, []
        ts = row[0]
        c.execute("""SELECT position, ip, serial, miner_type, hs_rt, power, error_codes, mining_state, mac
                     FROM miner_snapshots WHERE pod = ? AND timestamp = ?
                     ORDER BY position""", (pod_name, ts))
        rows = c.fetchall()
        conn.close()
        return ts, rows
    except Exception:
        return None, []


def get_live_data(pod_name):
    miners = []
    try:
        with open(MINER_STATUS_CSV) as f:
            reader = csv.DictReader(f)
            for row in reader:
                if row.get('pod') == pod_name:
                    miners.append(row)
    except Exception:
        pass
    return miners


def categorize_miners(rows):
    zero_hash = []
    missing_one = []
    missing_two = []
    online_macs = set()
    online_count = 0

    for row in rows:
        if isinstance(row, dict):
            pos = row.get('position', '')
            ip = row.get('ip', '')
            serial = row.get('serial', '')
            mac = row.get('mac', '')
            mtype = row.get('miner_type', '')
            hs = float(row.get('hs_rt', 0) or 0)
            errors = row.get('error_codes', '') or ''
            state = row.get('mining_state', '') or ''
        else:
            if len(row) >= 9:
                pos, ip, serial, mtype, hs_rt, power, errors, state, mac = row
            else:
                pos, ip, serial, mtype, hs_rt, power, errors, state = row
                mac = ''
            hs = float(hs_rt) if hs_rt else 0
            errors = errors or ''
            state = state or ''
            mac = mac or ''

        online_count += 1
        if mac:
            online_macs.add(mac.upper())

        base_type = mtype.replace('_B', '') if mtype else 'XP'
        m_spec = SPEC_HASHRATES.get(base_type, 140)
        m_board = m_spec / 3.0

        if state == 'sleeping':
            continue

        entry = {'position': pos, 'ip': ip, 'serial': serial, 'mac': mac,
                 'type': mtype, 'hashrate': f'{hs:.1f}', 'errors': errors}

        if hs == 0:
            entry['hashrate'] = '0'
            zero_hash.append(entry)
        elif hs < m_board * 1.5:
            missing_two.append(entry)
        elif hs < m_board * 2.5:
            missing_one.append(entry)

    return zero_hash, missing_one, missing_two, online_macs, online_count


def serve_pod_data(pod_name):
    """Return JSON data for a single pod."""
    print("Content-Type: application/json\n")

    live = get_live_data(pod_name)
    if live:
        source = 'live'
        ts = datetime.now().strftime('%Y-%m-%d %H:%M')
        zero, one, two, online_macs, online_count = categorize_miners(live)
    else:
        ts, snap_rows = get_snapshot_data(pod_name)
        if snap_rows:
            source = 'snapshot'
            zero, one, two, online_macs, online_count = categorize_miners(snap_rows)
        else:
            source = 'none'
            ts = None
            zero, one, two, online_macs, online_count = [], [], [], set(), 0

    # Load first_seen from lifecycle DB
    lifecycle = {}
    try:
        conn = sqlite3.connect(f'file:{MINER_HISTORY_DB}?immutable=1', uri=True)
        c = conn.cursor()
        c.execute("SELECT mac, first_seen FROM miner_lifecycle")
        for mac_val, first_seen in c.fetchall():
            lifecycle[mac_val.upper()] = first_seen
        conn.close()
    except Exception:
        pass

    # Add first_seen to issue lists
    for lst in [zero, one, two]:
        for entry in lst:
            m = (entry.get('mac') or '').upper()
            fs = lifecycle.get(m, '')
            entry['first_online'] = fs[:16].replace('T', ' ') if fs else ''
            entry.pop('mac', None)  # don't send mac to frontend

    inv = get_inventory_for_pod(pod_name)
    offline = []
    for m in inv:
        inv_mac = m.get('MAC', '').strip().upper()
        if inv_mac and inv_mac in online_macs:
            continue
        if not inv_mac:
            # No MAC in inventory - can't match, assume offline
            offline.append({
                'position': m.get('Position', ''),
                'serial': m.get('Serial', ''),
                'mac': '',
            })
        else:
            offline.append({
                'position': m.get('Position', ''),
                'serial': m.get('Serial', ''),
                'mac': m.get('MAC', ''),
            })
    offline.sort(key=lambda x: x['position'])

    result = {
        'source': source,
        'timestamp': ts,
        'inv_count': len(inv),
        'online_count': online_count,
        'zero_hash': sorted(zero, key=lambda x: x['position']),
        'missing_one': sorted(one, key=lambda x: x['position']),
        'missing_two': sorted(two, key=lambda x: x['position']),
        'offline': offline,
        'counts': {
            'zero': len(zero),
            'one': len(one),
            'two': len(two),
            'offline': len(offline),
        }
    }
    print(json.dumps(result))


def serve_chart_data(days=7):
    """Return N-day trend data: at each pod's daily peak hashrate, count zero/missing1/missing2."""
    print("Content-Type: application/json\n")

    pod_order = get_pod_order()
    if not pod_order:
        print(json.dumps({'error': 'no pods'}))
        return

    since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    placeholders = ','.join('?' * len(pod_order))

    try:
        conn = sqlite3.connect(f'file:{MINER_HISTORY_DB}?immutable=1', uri=True)
        c = conn.cursor()

        # Step 1: Find peak-hashrate timestamp per pod per day
        # Sum hashrate excluding sleeping miners for each snapshot timestamp
        c.execute(f"""
            WITH snapshot_totals AS (
                SELECT pod, DATE(timestamp) as day, timestamp,
                       SUM(CASE WHEN mining_state != 'sleeping' OR mining_state IS NULL
                           THEN CAST(hs_rt AS REAL) ELSE 0 END) as total_hr
                FROM miner_snapshots
                WHERE pod IN ({placeholders}) AND timestamp >= ?
                GROUP BY pod, timestamp
            ),
            ranked AS (
                SELECT pod, day, timestamp, total_hr,
                       ROW_NUMBER() OVER (PARTITION BY pod, day ORDER BY total_hr DESC) as rn
                FROM snapshot_totals
            )
            SELECT pod, day, timestamp FROM ranked WHERE rn = 1
        """, pod_order + [since])

        peak_times = {}
        for pod, day, ts in c.fetchall():
            peak_times[(pod, day)] = ts

        # Step 2: For each peak timestamp, categorize miners
        result = {}  # {pod: [{day, zero, miss1, miss2}, ...]}
        for (pod, day), ts in peak_times.items():
            c.execute("""SELECT miner_type, hs_rt, mining_state
                         FROM miner_snapshots WHERE pod = ? AND timestamp = ?""", (pod, ts))
            zero = miss1 = miss2 = 0
            for mtype, hs_rt, state in c.fetchall():
                if state == 'sleeping':
                    continue
                hs = float(hs_rt) if hs_rt else 0
                base_type = mtype.replace('_B', '') if mtype else 'XP'
                m_board = SPEC_HASHRATES.get(base_type, 140) / 3.0
                if hs == 0:
                    zero += 1
                elif hs < m_board * 1.5:
                    miss2 += 1
                elif hs < m_board * 2.5:
                    miss1 += 1
            if pod not in result:
                result[pod] = []
            result[pod].append({'day': day, 'zero': zero, 'miss1': miss1, 'miss2': miss2})

        conn.close()

        # Sort each pod's days
        for pod in result:
            result[pod].sort(key=lambda x: x['day'])

        print(json.dumps({'pods': result, 'pod_order': pod_order}))

    except Exception as e:
        print(json.dumps({'error': str(e)}))


def serve_page():
    """Serve the HTML page."""
    print("Content-Type: text/html\n")

    pod_order = get_pod_order()
    pod_order_json = json.dumps(pod_order)

    # dropdown_css = generate_dropdown_css()
    # dropdown_js = generate_dropdown_js()
    # dropdown_html = generate_dropdown_html(AuthManager.get_user_access())

    print(f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dead XP Report</title>
    <style>
        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
        body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #111; color: #fff; min-height: 100vh; }}

        .page-header {{
            background: rgba(255,255,255,0.05); padding: 20px 30px; margin: 20px; border-radius: 8px;
            border: 1px solid rgba(255,255,255,0.1);
        }}
        .page-header h1 {{ font-size: 2em; color: #00ff00; margin: 0; display: inline-block; }}
        .page-header .subtitle {{ color: #888; margin-top: 4px; font-size: 0.95rem; }}

        .controls {{
            margin: 0 20px 20px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
        }}

        .pod-btn {{
            padding: 8px 16px; border-radius: 4px; font-size: 0.9rem; cursor: pointer;
            background: #2a2a2a; border: 1px solid #3a3a3a; color: #ccc; transition: all 0.15s;
            user-select: none;
        }}
        .pod-btn:hover {{ border-color: #555; }}
        .pod-btn.active {{
            background: rgba(0,255,0,0.15); border-color: rgba(0,255,0,0.4); color: #00ff00;
        }}
        .pod-btn.loading {{
            opacity: 0.6; pointer-events: none;
        }}

        .refresh-btn {{
            padding: 8px 16px; border-radius: 4px; font-size: 0.9rem; cursor: pointer;
            background: rgba(74, 158, 255, 0.15); border: 1px solid rgba(74, 158, 255, 0.4);
            color: #4a9eff; transition: all 0.15s;
        }}
        .refresh-btn:hover {{ background: rgba(74, 158, 255, 0.25); color: #fff; }}

        .content {{ margin: 0 20px 20px; }}

        .data-source {{
            color: #888; font-size: 0.85rem; margin-bottom: 12px; padding: 8px 12px;
            background: rgba(255,255,255,0.03); border-radius: 4px; border: 1px solid #2a2a2a;
        }}
        .data-source .live {{ color: #00ff00; }}
        .data-source .snapshot {{ color: #ff6600; }}

        .summary-row {{
            display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px;
        }}
        .summary-badge {{
            padding: 10px 20px; border-radius: 6px; font-size: 0.9rem; font-weight: 600;
            background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
        }}
        .summary-badge.red {{ background: rgba(255,68,68,0.15); border-color: rgba(255,68,68,0.3); color: #ff4444; }}
        .summary-badge.orange {{ background: rgba(255,102,0,0.15); border-color: rgba(255,102,0,0.3); color: #ff6600; }}
        .summary-badge.yellow {{ background: rgba(255,170,0,0.15); border-color: rgba(255,170,0,0.3); color: #ffaa00; }}
        .summary-badge.grey {{ background: rgba(136,136,136,0.15); border-color: rgba(136,136,136,0.3); color: #888; }}

        .tabs {{
            display: flex; gap: 0; margin-bottom: 0; border-bottom: 2px solid #2a2a2a;
        }}
        .tab {{
            padding: 10px 20px; cursor: pointer; font-size: 0.9rem; color: #888;
            border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s;
            user-select: none;
        }}
        .tab:hover {{ color: #ccc; }}
        .tab.active {{ color: #00ff00; border-bottom-color: #00ff00; }}
        .tab .tab-count {{ font-size: 0.8rem; margin-left: 4px; opacity: 0.7; }}

        .tab-content {{ display: none; }}
        .tab-content.active {{ display: block; }}

        .table-wrap {{
            max-height: calc(100vh - 360px); overflow-y: auto;
            scrollbar-width: thin; scrollbar-color: #666 transparent;
            border: 1px solid #2a2a2a; border-radius: 0 0 6px 6px;
        }}
        table {{
            width: 100%; border-collapse: collapse; font-size: 0.9rem;
        }}
        thead th {{
            position: sticky; top: 0; background: #1a1a1a; padding: 10px 14px;
            text-align: left; font-size: 0.8rem; text-transform: uppercase;
            color: #888; letter-spacing: 0.5px; border-bottom: 1px solid #2a2a2a;
            user-select: none; white-space: nowrap;
        }}
        tbody tr {{ border-bottom: 1px solid #1e1e1e; transition: background 0.1s; }}
        tbody tr:hover {{ background: rgba(255,255,255,0.03); }}
        td {{ padding: 8px 14px; vertical-align: middle; }}
        td.mono {{ font-family: 'Courier New', monospace; font-size: 0.85rem; }}
        td.errors {{ font-size: 0.8rem; color: #ff6600; max-width: 300px; word-break: break-all; }}
        td.hashrate {{ font-weight: 600; color: #ff6600; }}
        td.hashrate.zero {{ color: #ff4444; }}

        .no-data {{ padding: 40px; text-align: center; color: #555; font-size: 1rem; }}
        .no-selection {{ padding: 60px; text-align: center; color: #555; font-size: 1.1rem; }}
        .loading-msg {{ padding: 60px; text-align: center; color: #4a9eff; font-size: 1.1rem; }}

        .chart-container {{
            background: rgba(255,255,255,0.03); border: 1px solid #2a2a2a; border-radius: 6px;
            padding: 20px; margin-bottom: 16px;
        }}
        .chart-container h3 {{ color: #ccc; margin-bottom: 12px; font-size: 1rem; font-weight: 600; }}
        .chart-wrap {{ position: relative; height: 300px; }}
        .chart-grid {{
            display: grid; grid-template-columns: 1fr; gap: 16px;
        }}
        .chart-totals {{
            margin-bottom: 16px; padding: 12px 16px;
            background: rgba(255,255,255,0.03); border: 1px solid #2a2a2a; border-radius: 6px;
            color: #888; font-size: 0.9rem;
        }}
        .chart-totals span {{ font-weight: 600; margin-right: 20px; }}
    </style>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
</head>
<body>
<div class="page-header">
    <h1>Dead XP Report</h1>
    <div class="subtitle">Field work issue lists by pod - zero hash, missing hashboards, offline</div>
</div>

<div class="controls" id="pod-buttons">
    <span id="right-btns" style="margin-left:auto;display:flex;gap:8px;align-items:center;">
        <select id="chart-days" style="padding:7px 10px;border-radius:4px;font-size:0.9rem;background:#2a2a2a;border:1px solid rgba(255,170,0,0.4);color:#ffaa00;cursor:pointer;">
            <option value="7">7 Days</option>
            <option value="14">14 Days</option>
            <option value="30">30 Days</option>
        </select>
        <button onclick="chartAll()" style="padding:8px 16px;border-radius:4px;font-size:0.9rem;cursor:pointer;background:rgba(255,170,0,0.15);border:1px solid rgba(255,170,0,0.4);color:#ffaa00;transition:all 0.15s;" id="chart-btn">Chart All</button>
        <button onclick="exportCSV()" style="padding:8px 16px;border-radius:4px;font-size:0.9rem;cursor:pointer;background:rgba(0,255,0,0.15);border:1px solid rgba(0,255,0,0.4);color:#00ff00;transition:all 0.15s;display:none;" id="export-btn">Export CSV</button>
        <button class="refresh-btn" onclick="refreshPod()">Refresh</button>
    </span>
</div>

<div class="content" id="content">
    <div class="no-selection">Select a pod above to view issues</div>
</div>

<script>
var podOrder = {pod_order_json};
var activePod = null;
var activeTab = 'zero';
var podCache = {{}};

function init() {{
    var container = document.getElementById('pod-buttons');
    var rightBtns = container.querySelector('#right-btns');
    for (var i = 0; i < podOrder.length; i++) {{
        var pod = podOrder[i];
        var btn = document.createElement('button');
        btn.className = 'pod-btn';
        btn.setAttribute('data-pod', pod);
        btn.textContent = pod;
        btn.onclick = (function(p) {{ return function() {{ selectPod(p); }}; }})(pod);
        container.insertBefore(btn, rightBtns);
    }}
}}

function selectPod(pod) {{
    activePod = pod;
    activeTab = 'zero';
    var btns = document.querySelectorAll('.pod-btn');
    for (var i = 0; i < btns.length; i++) {{
        btns[i].classList.toggle('active', btns[i].getAttribute('data-pod') === pod);
    }}
    document.getElementById('export-btn').style.display = 'inline-block';
    loadPod(pod);
}}

function refreshPod() {{
    if (activePod) {{
        delete podCache[activePod];
        loadPod(activePod);
    }} else {{
        location.reload();
    }}
}}

function loadPod(pod) {{
    if (podCache[pod]) {{
        renderPod(pod, podCache[pod]);
        return;
    }}
    document.getElementById('content').innerHTML = '<div class="loading-msg">Loading ' + esc(pod) + '...</div>';
    var btn = document.querySelector('.pod-btn[data-pod="' + pod + '"]');
    if (btn) btn.classList.add('loading');

    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'dead_xp.py?pod=' + encodeURIComponent(pod));
    xhr.onload = function() {{
        if (btn) btn.classList.remove('loading');
        if (xhr.status === 200) {{
            var data = JSON.parse(xhr.responseText);
            podCache[pod] = data;
            if (activePod === pod) renderPod(pod, data);
        }} else {{
            document.getElementById('content').innerHTML = '<div class="no-data">Error loading data</div>';
        }}
    }};
    xhr.onerror = function() {{
        if (btn) btn.classList.remove('loading');
        document.getElementById('content').innerHTML = '<div class="no-data">Error loading data</div>';
    }};
    xhr.send();
}}

function renderPod(pod, d) {{
    var content = document.getElementById('content');
    var sourceHtml = '';
    if (d.source === 'live') {{
        sourceHtml = 'Data: <span class="live">LIVE</span> | ' + d.online_count + ' online / ' + d.inv_count + ' inventory';
    }} else if (d.source === 'snapshot') {{
        sourceHtml = 'Data: <span class="snapshot">SNAPSHOT</span> from ' + d.timestamp + ' | ' + d.online_count + ' in snapshot / ' + d.inv_count + ' inventory';
    }} else {{
        sourceHtml = 'Data: <span class="snapshot">NO DATA</span> | ' + d.inv_count + ' in inventory (all offline)';
    }}

    var h = '';
    h += '<div class="data-source">' + sourceHtml + '</div>';
    h += '<div class="summary-row">';
    h += '<div class="summary-badge red">Zero Hash: ' + d.counts.zero + '</div>';
    h += '<div class="summary-badge orange">Missing 2 Boards: ' + d.counts.two + '</div>';
    h += '<div class="summary-badge yellow">Missing 1 Board: ' + d.counts.one + '</div>';
    h += '<div class="summary-badge grey">Offline: ' + d.counts.offline + '</div>';
    h += '</div>';
    h += '<div class="tabs">';
    h += '<div class="tab' + (activeTab==='zero'?' active':'') + '" onclick="switchTab(&quot;zero&quot;)">Zero Hash<span class="tab-count"> ' + d.counts.zero + '</span></div>';
    h += '<div class="tab' + (activeTab==='two'?' active':'') + '" onclick="switchTab(&quot;two&quot;)">Missing 2 Boards<span class="tab-count"> ' + d.counts.two + '</span></div>';
    h += '<div class="tab' + (activeTab==='one'?' active':'') + '" onclick="switchTab(&quot;one&quot;)">Missing 1 Board<span class="tab-count"> ' + d.counts.one + '</span></div>';
    h += '<div class="tab' + (activeTab==='offline'?' active':'') + '" onclick="switchTab(&quot;offline&quot;)">Offline<span class="tab-count"> ' + d.counts.offline + '</span></div>';
    h += '</div>';
    h += '<div class="tab-content' + (activeTab==='zero'?' active':'') + '" id="tab-zero">' + renderIssueTable(d.zero_hash) + '</div>';
    h += '<div class="tab-content' + (activeTab==='two'?' active':'') + '" id="tab-two">' + renderIssueTable(d.missing_two) + '</div>';
    h += '<div class="tab-content' + (activeTab==='one'?' active':'') + '" id="tab-one">' + renderIssueTable(d.missing_one) + '</div>';
    h += '<div class="tab-content' + (activeTab==='offline'?' active':'') + '" id="tab-offline">' + renderOfflineTable(d.offline) + '</div>';
    content.innerHTML = h;
}}

function switchTab(tab) {{
    activeTab = tab;
    var tabs = document.querySelectorAll('.tab');
    for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
    var contents = document.querySelectorAll('.tab-content');
    for (var i = 0; i < contents.length; i++) contents[i].classList.remove('active');
    event.target.closest('.tab').classList.add('active');
    document.getElementById('tab-' + tab).classList.add('active');
}}

function renderIssueTable(miners) {{
    if (!miners || miners.length === 0) return '<div class="no-data">None</div>';
    var html = '<div class="table-wrap"><table><thead><tr>';
    html += '<th>Position</th><th>IP</th><th>Serial</th><th>Type</th><th>Hashrate</th><th>First Online</th><th>Errors</th>';
    html += '</tr></thead><tbody>';
    for (var i = 0; i < miners.length; i++) {{
        var m = miners[i];
        var hrClass = parseFloat(m.hashrate) === 0 ? 'hashrate zero' : 'hashrate';
        html += '<tr>';
        html += '<td class="mono">' + esc(m.position) + '</td>';
        html += '<td class="mono">' + esc(m.ip) + '</td>';
        html += '<td class="mono">' + esc(m.serial) + '</td>';
        html += '<td>' + esc(m.type) + '</td>';
        html += '<td class="' + hrClass + '">' + esc(m.hashrate) + '</td>';
        html += '<td class="mono">' + esc(m.first_online || '') + '</td>';
        html += '<td class="errors">' + esc(m.errors) + '</td>';
        html += '</tr>';
    }}
    html += '</tbody></table></div>';
    return html;
}}

function renderOfflineTable(miners) {{
    if (!miners || miners.length === 0) return '<div class="no-data">None</div>';
    var html = '<div class="table-wrap"><table><thead><tr>';
    html += '<th>Position</th><th>Serial</th><th>MAC</th>';
    html += '</tr></thead><tbody>';
    for (var i = 0; i < miners.length; i++) {{
        var m = miners[i];
        html += '<tr>';
        html += '<td class="mono">' + esc(m.position) + '</td>';
        html += '<td class="mono">' + esc(m.serial) + '</td>';
        html += '<td class="mono">' + esc(m.mac || '') + '</td>';
        html += '</tr>';
    }}
    html += '</tbody></table></div>';
    return html;
}}

function esc(s) {{
    if (!s) return '';
    var d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
}}

function exportCSV() {{
    if (!activePod || !podCache[activePod]) return;
    var d = podCache[activePod];
    var rows = [];
    var filename = activePod.replace(/ /g, '_') + '_' + activeTab + '.csv';

    if (activeTab === 'offline') {{
        rows.push('Position,Serial,MAC');
        var items = d.offline || [];
        for (var i = 0; i < items.length; i++) {{
            var m = items[i];
            rows.push(csvRow([m.position, m.serial, m.mac || '']));
        }}
    }} else {{
        rows.push('Position,IP,Serial,Type,Hashrate,First Online,Errors');
        var items = activeTab === 'zero' ? d.zero_hash : activeTab === 'two' ? d.missing_two : d.missing_one;
        items = items || [];
        for (var i = 0; i < items.length; i++) {{
            var m = items[i];
            rows.push(csvRow([m.position, m.ip, m.serial, m.type, m.hashrate, m.first_online || '', m.errors]));
        }}
    }}

    var blob = new Blob([rows.join('\\n')], {{type: 'text/csv'}});
    var a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
}}

function csvRow(fields) {{
    return fields.map(function(f) {{
        f = (f || '').toString();
        if (f.indexOf(',') >= 0 || f.indexOf('"') >= 0) return '"' + f.replace(/"/g, '""') + '"';
        return f;
    }}).join(',');
}}

var chartInstances = [];

function chartAll() {{
    // Deselect any active pod
    activePod = null;
    var btns = document.querySelectorAll('.pod-btn');
    for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
    document.getElementById('export-btn').style.display = 'none';

    var chartDays = document.getElementById('chart-days').value;
    document.getElementById('content').innerHTML = '<div class="loading-msg">Loading ' + chartDays + '-day chart data...</div>';
    var chartBtn = document.getElementById('chart-btn');
    chartBtn.style.opacity = '0.6';
    chartBtn.style.pointerEvents = 'none';

    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'dead_xp.py?chart_all=1&days=' + chartDays);
    xhr.onload = function() {{
        chartBtn.style.opacity = '1';
        chartBtn.style.pointerEvents = 'auto';
        if (xhr.status === 200) {{
            var data = JSON.parse(xhr.responseText);
            if (data.error) {{
                document.getElementById('content').innerHTML = '<div class="no-data">Error: ' + esc(data.error) + '</div>';
            }} else {{
                renderCharts(data);
            }}
        }} else {{
            document.getElementById('content').innerHTML = '<div class="no-data">Error loading chart data</div>';
        }}
    }};
    xhr.onerror = function() {{
        chartBtn.style.opacity = '1';
        chartBtn.style.pointerEvents = 'auto';
        document.getElementById('content').innerHTML = '<div class="no-data">Error loading chart data</div>';
    }};
    xhr.send();
}}

function renderCharts(data) {{
    // Destroy old charts
    for (var i = 0; i < chartInstances.length; i++) chartInstances[i].destroy();
    chartInstances = [];

    var pods = data.pods;
    var podOrder = data.pod_order;

    // Collect all unique days across all pods, sorted
    var daySet = {{}};
    for (var pod in pods) {{
        for (var i = 0; i < pods[pod].length; i++) {{
            daySet[pods[pod][i].day] = true;
        }}
    }}
    var days = Object.keys(daySet).sort();
    var dayLabels = days.map(function(d) {{
        var parts = d.split('-');
        return parts[1] + '/' + parts[2];
    }});

    // Build per-pod lookup: pod -> {{day -> {{zero, miss1, miss2}}}}
    var podData = {{}};
    for (var pod in pods) {{
        podData[pod] = {{}};
        for (var i = 0; i < pods[pod].length; i++) {{
            var rec = pods[pod][i];
            podData[pod][rec.day] = rec;
        }}
    }}

    // Compute daily totals across all pods
    var totals = days.map(function(day) {{
        var z = 0, m1 = 0, m2 = 0;
        for (var pod in podData) {{
            var rec = podData[pod][day];
            if (rec) {{ z += rec.zero; m1 += rec.miss1; m2 += rec.miss2; }}
        }}
        return {{zero: z, miss1: m1, miss2: m2}};
    }});

    var h = '';

    // Totals summary bar
    if (totals.length > 0) {{
        var latest = totals[totals.length - 1];
        var first = totals[0];
        h += '<div class="chart-totals">';
        h += 'Latest: ';
        h += '<span style="color:#ff4444">Zero Hash: ' + latest.zero + '</span>';
        h += '<span style="color:#ff6600">Missing 2: ' + latest.miss2 + '</span>';
        h += '<span style="color:#ffaa00">Missing 1: ' + latest.miss1 + '</span>';
        if (totals.length > 1) {{
            var dz = latest.zero - first.zero;
            var d1 = latest.miss1 - first.miss1;
            var d2 = latest.miss2 - first.miss2;
            var total_change = dz + d1 + d2;
            var arrow = total_change <= 0 ? '\\u2193' : '\\u2191';
            var color = total_change <= 0 ? '#00ff00' : '#ff4444';
            h += '<span style="color:' + color + '"> | Total change: ' + arrow + ' ' + Math.abs(total_change) + '</span>';
        }}
        h += '</div>';
    }}

    // Total trend chart
    h += '<div class="chart-container">';
    var chartDays = document.getElementById('chart-days').value;
    h += '<h3>All XP Pods - ' + chartDays + ' Day Trend (at daily peak hashrate)</h3>';
    h += '<div class="chart-wrap"><canvas id="chart-totals"></canvas></div>';
    h += '</div>';

    // Per-pod charts in grid
    h += '<div class="chart-grid" style="grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));">';
    for (var p = 0; p < podOrder.length; p++) {{
        var pod = podOrder[p];
        if (!podData[pod]) continue;
        h += '<div class="chart-container">';
        h += '<h3>' + esc(pod) + '</h3>';
        h += '<div class="chart-wrap"><canvas id="chart-pod-' + p + '"></canvas></div>';
        h += '</div>';
    }}
    h += '</div>';

    document.getElementById('content').innerHTML = h;

    // Render total chart
    var totalCtx = document.getElementById('chart-totals').getContext('2d');
    chartInstances.push(new Chart(totalCtx, {{
        type: 'bar',
        data: {{
            labels: dayLabels,
            datasets: [
                {{
                    label: 'Zero Hash',
                    data: totals.map(function(t) {{ return t.zero; }}),
                    backgroundColor: 'rgba(255, 68, 68, 0.7)',
                    borderColor: '#ff4444',
                    borderWidth: 1
                }},
                {{
                    label: 'Missing 2 Boards',
                    data: totals.map(function(t) {{ return t.miss2; }}),
                    backgroundColor: 'rgba(255, 102, 0, 0.7)',
                    borderColor: '#ff6600',
                    borderWidth: 1
                }},
                {{
                    label: 'Missing 1 Board',
                    data: totals.map(function(t) {{ return t.miss1; }}),
                    backgroundColor: 'rgba(255, 170, 0, 0.7)',
                    borderColor: '#ffaa00',
                    borderWidth: 1
                }}
            ]
        }},
        options: {{
            responsive: true,
            maintainAspectRatio: false,
            plugins: {{
                legend: {{ labels: {{ color: '#ccc' }} }}
            }},
            scales: {{
                x: {{
                    stacked: true,
                    ticks: {{ color: '#888' }},
                    grid: {{ color: '#2a2a2a' }}
                }},
                y: {{
                    stacked: true,
                    beginAtZero: true,
                    ticks: {{ color: '#888' }},
                    grid: {{ color: '#2a2a2a' }}
                }}
            }}
        }}
    }}));

    // Compute common Y-axis max across all per-pod charts (max stacked total)
    var globalMax = 0;
    for (var p = 0; p < podOrder.length; p++) {{
        var pod = podOrder[p];
        if (!podData[pod]) continue;
        for (var di = 0; di < days.length; di++) {{
            var r = podData[pod][days[di]];
            if (r) {{
                var stacked = r.zero + r.miss1 + r.miss2;
                if (stacked > globalMax) globalMax = stacked;
            }}
        }}
    }}
    // Round up to nearest nice number for axis
    var yMax = globalMax <= 5 ? globalMax + 1 : Math.ceil(globalMax * 1.1);

    // Render per-pod charts
    for (var p = 0; p < podOrder.length; p++) {{
        var pod = podOrder[p];
        if (!podData[pod]) continue;
        var canvas = document.getElementById('chart-pod-' + p);
        if (!canvas) continue;
        var ctx = canvas.getContext('2d');

        var zeroData = days.map(function(d) {{ var r = podData[pod][d]; return r ? r.zero : null; }});
        var miss2Data = days.map(function(d) {{ var r = podData[pod][d]; return r ? r.miss2 : null; }});
        var miss1Data = days.map(function(d) {{ var r = podData[pod][d]; return r ? r.miss1 : null; }});

        chartInstances.push(new Chart(ctx, {{
            type: 'bar',
            data: {{
                labels: dayLabels,
                datasets: [
                    {{
                        label: 'Zero Hash',
                        data: zeroData,
                        backgroundColor: 'rgba(255, 68, 68, 0.7)',
                        borderColor: '#ff4444',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Missing 2',
                        data: miss2Data,
                        backgroundColor: 'rgba(255, 102, 0, 0.7)',
                        borderColor: '#ff6600',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Missing 1',
                        data: miss1Data,
                        backgroundColor: 'rgba(255, 170, 0, 0.7)',
                        borderColor: '#ffaa00',
                        borderWidth: 1
                    }}
                ]
            }},
            options: {{
                responsive: true,
                maintainAspectRatio: false,
                plugins: {{
                    legend: {{ display: false }}
                }},
                scales: {{
                    x: {{
                        stacked: true,
                        ticks: {{ color: '#888' }},
                        grid: {{ color: '#2a2a2a' }}
                    }},
                    y: {{
                        stacked: true,
                        beginAtZero: true,
                        max: yMax,
                        ticks: {{ color: '#888', stepSize: 1 }},
                        grid: {{ color: '#2a2a2a' }}
                    }}
                }}
            }}
        }}));
    }}
}}

init();
</script>
</body>
</html>""")


def main():
    params = cgi.FieldStorage()
    pod = params.getfirst('pod')

    if params.getfirst('chart_all'):
        days = int(params.getfirst('days', 7))
        if days not in (7, 14, 30):
            days = 7
        serve_chart_data(days)
    elif pod:
        serve_pod_data(pod)
    else:
        serve_page()


if __name__ == '__main__':
    main()
