#!/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_m60')
_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 M60 Report"))
    sys.exit(0)


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

Generates per-pod lists of M60 miners with hardware-definable issues:
zero hash, hashboard, fan, thermal, power, 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
from miners.error_categories import ERROR_CATEGORIES

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'

# Combine temp_sensor + overtemp into one "thermal" bucket.
# Order = bucketing precedence (a miner only lands in one bucket).
HW_BUCKET_SETS = [
    ('hashboard', frozenset(ERROR_CATEGORIES['hashboard_errors'])),
    ('fan',       frozenset(ERROR_CATEGORIES['fan_errors'])),
    ('thermal',   frozenset(ERROR_CATEGORIES['temp_sensor_errors'] + ERROR_CATEGORIES['overtemp_protection'])),
    ('power',     frozenset(ERROR_CATEGORIES['power_errors'])),
]


def classify_error_bucket(error_codes_str):
    """Return the first matching hardware bucket name, or None."""
    if not error_codes_str:
        return None
    codes = []
    for raw in error_codes_str.split(','):
        c = raw.strip()
        if not c:
            continue
        try:
            codes.append(int(c))
        except ValueError:
            codes.append(c.upper())
    if not codes:
        return None
    for name, code_set in HW_BUCKET_SETS:
        for c in codes:
            if c in code_set:
                return name
    return None


def get_pod_order(m60_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 m60_only and pod_data.get('miner_type', '') != 'M60':
                        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):
    buckets = {'zero': [], 'hashboard': [], 'fan': [], 'thermal': [], 'power': []}
    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())

        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'
            buckets['zero'].append(entry)
            continue

        bucket = classify_error_bucket(errors)
        if bucket:
            buckets[bucket].append(entry)

    return buckets, 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')
        buckets, online_macs, online_count = categorize_miners(live)
    else:
        ts, snap_rows = get_snapshot_data(pod_name)
        if snap_rows:
            source = 'snapshot'
            buckets, online_macs, online_count = categorize_miners(snap_rows)
        else:
            source = 'none'
            ts = None
            buckets = {'zero': [], 'hashboard': [], 'fan': [], 'thermal': [], 'power': []}
            online_macs = set()
            online_count = 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 buckets.values():
        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()
        inv_serial = m.get('Serial', '').strip()
        # Skip empty slots — only count positions that actually have a miner assigned
        if not inv_mac and not inv_serial:
            continue
        if inv_mac and inv_mac in online_macs:
            continue
        offline.append({
            'position': m.get('Position', ''),
            'serial': inv_serial,
            'mac': m.get('MAC', '').strip(),
        })
    offline.sort(key=lambda x: x['position'])

    sort_pos = lambda x: x['position']
    result = {
        'source': source,
        'timestamp': ts,
        'inv_count': len(inv),
        'online_count': online_count,
        'zero_hash': sorted(buckets['zero'], key=sort_pos),
        'hashboard': sorted(buckets['hashboard'], key=sort_pos),
        'fan': sorted(buckets['fan'], key=sort_pos),
        'thermal': sorted(buckets['thermal'], key=sort_pos),
        'power': sorted(buckets['power'], key=sort_pos),
        'offline': offline,
        'counts': {
            'zero': len(buckets['zero']),
            'hashboard': len(buckets['hashboard']),
            'fan': len(buckets['fan']),
            'thermal': len(buckets['thermal']),
            'power': len(buckets['power']),
            '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 by hardware bucket
        result = {}  # {pod: [{day, zero, hashboard, fan, thermal, power}, ...]}
        for (pod, day), ts in peak_times.items():
            c.execute("""SELECT hs_rt, error_codes, mining_state
                         FROM miner_snapshots WHERE pod = ? AND timestamp = ?""", (pod, ts))
            counts = {'zero': 0, 'hashboard': 0, 'fan': 0, 'thermal': 0, 'power': 0}
            for hs_rt, errors, state in c.fetchall():
                if state == 'sleeping':
                    continue
                hs = float(hs_rt) if hs_rt else 0
                if hs == 0:
                    counts['zero'] += 1
                    continue
                bucket = classify_error_bucket(errors or '')
                if bucket:
                    counts[bucket] += 1
            if pod not in result:
                result[pod] = []
            result[pod].append({'day': day, **counts})

        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 M60 Report</title>
    <style>
        {dropdown_css}

        * {{ box-sizing: border-box; }}
        body {{
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #1a1a1a; color: #e0e0e0; min-height: 100vh;
            margin: 0; padding: 0; line-height: 1.4;
        }}

        /* Floating-box header (matches status/gen_manager) */
        .header {{
            background-color: rgba(255,255,255,0.05) !important;
            border: 1px solid rgba(255,255,255,0.1) !important;
            border-radius: 8px !important;
            padding: 30px !important;
            margin: 20px !important;
            border-bottom: none !important;
        }}
        .header h1.dropdown-title {{
            font-size: 2.2em !important;
            font-weight: 600 !important;
            margin: 3px 0 0 0 !important;
            line-height: 1.2 !important;
            color: #00ff00 !important;
        }}
        .header .subtitle {{ color: #888; margin-top: 6px; font-size: 14px; }}

        .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="header">
    <div class="dropdown">
        <h1 class="dropdown-title">NGON Mining - Dead M60 Report</h1>
        <div class="dropdown-content">{dropdown_html}</div>
    </div>
    <div class="subtitle">Field work issue lists by pod - zero hash and hardware errors (hashboard, fan, thermal, power), plus 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_m60.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">Hashboard: ' + d.counts.hashboard + '</div>';
    h += '<div class="summary-badge yellow">Fan: ' + d.counts.fan + '</div>';
    h += '<div class="summary-badge orange">Thermal: ' + d.counts.thermal + '</div>';
    h += '<div class="summary-badge yellow">Power: ' + d.counts.power + '</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==='hashboard'?' active':'') + '" onclick="switchTab(&quot;hashboard&quot;)">Hashboard<span class="tab-count"> ' + d.counts.hashboard + '</span></div>';
    h += '<div class="tab' + (activeTab==='fan'?' active':'') + '" onclick="switchTab(&quot;fan&quot;)">Fan<span class="tab-count"> ' + d.counts.fan + '</span></div>';
    h += '<div class="tab' + (activeTab==='thermal'?' active':'') + '" onclick="switchTab(&quot;thermal&quot;)">Thermal<span class="tab-count"> ' + d.counts.thermal + '</span></div>';
    h += '<div class="tab' + (activeTab==='power'?' active':'') + '" onclick="switchTab(&quot;power&quot;)">Power<span class="tab-count"> ' + d.counts.power + '</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==='hashboard'?' active':'') + '" id="tab-hashboard">' + renderIssueTable(d.hashboard) + '</div>';
    h += '<div class="tab-content' + (activeTab==='fan'?' active':'') + '" id="tab-fan">' + renderIssueTable(d.fan) + '</div>';
    h += '<div class="tab-content' + (activeTab==='thermal'?' active':'') + '" id="tab-thermal">' + renderIssueTable(d.thermal) + '</div>';
    h += '<div class="tab-content' + (activeTab==='power'?' active':'') + '" id="tab-power">' + renderIssueTable(d.power) + '</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 srcMap = {{ zero: d.zero_hash, hashboard: d.hashboard, fan: d.fan, thermal: d.thermal, power: d.power }};
        var items = srcMap[activeTab] || [];
        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_m60.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 t = {{zero: 0, hashboard: 0, fan: 0, thermal: 0, power: 0}};
        for (var pod in podData) {{
            var rec = podData[pod][day];
            if (rec) {{
                t.zero += rec.zero || 0;
                t.hashboard += rec.hashboard || 0;
                t.fan += rec.fan || 0;
                t.thermal += rec.thermal || 0;
                t.power += rec.power || 0;
            }}
        }}
        return t;
    }});

    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: ' + latest.zero + '</span>';
        h += '<span style="color:#ff6600">Hashboard: ' + latest.hashboard + '</span>';
        h += '<span style="color:#ffaa00">Fan: ' + latest.fan + '</span>';
        h += '<span style="color:#aa44ff">Thermal: ' + latest.thermal + '</span>';
        h += '<span style="color:#4a9eff">Power: ' + latest.power + '</span>';
        if (totals.length > 1) {{
            var sumLatest = latest.zero + latest.hashboard + latest.fan + latest.thermal + latest.power;
            var sumFirst = first.zero + first.hashboard + first.fan + first.thermal + first.power;
            var total_change = sumLatest - sumFirst;
            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 M60 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: 'Hashboard',
                    data: totals.map(function(t) {{ return t.hashboard; }}),
                    backgroundColor: 'rgba(255, 102, 0, 0.7)',
                    borderColor: '#ff6600',
                    borderWidth: 1
                }},
                {{
                    label: 'Fan',
                    data: totals.map(function(t) {{ return t.fan; }}),
                    backgroundColor: 'rgba(255, 170, 0, 0.7)',
                    borderColor: '#ffaa00',
                    borderWidth: 1
                }},
                {{
                    label: 'Thermal',
                    data: totals.map(function(t) {{ return t.thermal; }}),
                    backgroundColor: 'rgba(170, 68, 255, 0.7)',
                    borderColor: '#aa44ff',
                    borderWidth: 1
                }},
                {{
                    label: 'Power',
                    data: totals.map(function(t) {{ return t.power; }}),
                    backgroundColor: 'rgba(74, 158, 255, 0.7)',
                    borderColor: '#4a9eff',
                    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||0) + (r.hashboard||0) + (r.fan||0) + (r.thermal||0) + (r.power||0);
                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 seriesAt = function(key) {{
            return days.map(function(d) {{ var r = podData[pod][d]; return r ? (r[key] || 0) : null; }});
        }};

        chartInstances.push(new Chart(ctx, {{
            type: 'bar',
            data: {{
                labels: dayLabels,
                datasets: [
                    {{
                        label: 'Zero',
                        data: seriesAt('zero'),
                        backgroundColor: 'rgba(255, 68, 68, 0.7)',
                        borderColor: '#ff4444',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Hashboard',
                        data: seriesAt('hashboard'),
                        backgroundColor: 'rgba(255, 102, 0, 0.7)',
                        borderColor: '#ff6600',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Fan',
                        data: seriesAt('fan'),
                        backgroundColor: 'rgba(255, 170, 0, 0.7)',
                        borderColor: '#ffaa00',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Thermal',
                        data: seriesAt('thermal'),
                        backgroundColor: 'rgba(170, 68, 255, 0.7)',
                        borderColor: '#aa44ff',
                        borderWidth: 1
                    }},
                    {{
                        label: 'Power',
                        data: seriesAt('power'),
                        backgroundColor: 'rgba(74, 158, 255, 0.7)',
                        borderColor: '#4a9eff',
                        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>
<script>{dropdown_js}</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()
