o
    ]�j�Ÿ  ã                   @   s  d dl Z d dlZej d d¡ d dlmZmZ e  ¡ ZedƒZ	e	 
e¡\ZZZerGedƒ er3eeƒ edƒ er>edƒ nedƒ e d ¡ er\ed	ƒ edƒ eed
ƒƒ e d ¡ 	 d dl Z d dlZd dlZd dlZd dlZd dlZd dlmZmZ ej d d¡ ej ej ej e¡¡¡ d dlmZmZmZ d dlmZ dZ dZ!dZ"dZ#de$ed ƒfde$ed ƒfde$ed ed  ƒfde$ed ƒfgZ%dd„ Z&d2dd„Z'd d!„ Z(d"d#„ Z)d$d%„ Z*d&d'„ Z+d(d)„ Z,d3d+d,„Z-d-d.„ Z.d/d0„ Z/e0d1krÿe/ƒ  dS dS )4é    Nz/opt/ngon/apps)ÚAuthManagerÚgenerate_login_page_htmlÚdead_m60zContent-Type: application/jsonÚ z6{"success": false, "error": "Authentication required"}z{"success": true}zContent-Type: text/htmlzDead M60 Report)ÚdatetimeÚ	timedelta)Úgenerate_dropdown_cssÚgenerate_dropdown_jsÚgenerate_dropdown_html)ÚERROR_CATEGORIESz/opt/ngon/data/miner_history.dbz$/opt/ngon/data/live/miner_status.csvz'/opt/ngon/data/live/miner_inventory.csvz#/opt/ngon/config/master_config.jsonÚ	hashboardÚhashboard_errorsÚfanÚ
fan_errorsÚthermalÚtemp_sensor_errorsÚovertemp_protectionÚpowerÚpower_errorsc              	   C   s”   | sdS g }|   d¡D ]"}| ¡ }|sqz	| t|ƒ¡ W q ty-   | | ¡ ¡ Y qw |s2dS tD ]\}}|D ]}||v rF|    S q:q4dS )z8Return the first matching hardware bucket name, or None.Nú,)ÚsplitÚstripÚappendÚintÚ
ValueErrorÚupperÚHW_BUCKET_SETS)Úerror_codes_strÚcodesÚrawÚcÚnameÚcode_set© r#   ú/var/www/html/ngon/dead_m60.pyÚclassify_error_bucketC   s*   ÿÿÿr%   Tc           
      C   sÀ   g }zSt tƒ�}t |¡}W d   ƒ n1 sw   Y  | di ¡ ¡ D ].\}}| di ¡ ¡ D ]!\}}| di ¡ ¡ D ]\}}	| rK|	 dd¡dkrKq<| |¡ q<q0q$W |S  ty_   Y |S w )NÚsitesÚgenerator_groupsÚpodsÚ
miner_typer   ÚM60)ÚopenÚMASTER_CONFIGÚjsonÚloadÚgetÚitemsr   Ú	Exception)
Úm60_onlyÚ	pod_orderÚfÚcfgÚ	site_nameÚ	site_dataÚ
group_nameÚ
group_dataÚpod_nameÚpod_datar#   r#   r$   Úget_pod_orderY   s&   
ÿýÿÿþþr<   c                 C   sº   g }zP|   dd¡}t|ƒdkr|W S |d |d }}ttƒ�)}t |¡}|D ]}| dd¡|kr>| dd¡|kr>| |¡ q'W d  ƒ W |S 1 sKw   Y  W |S  ty\   Y |S w )	z"Load inventory for a specific pod.ú é   é   r   ÚSiter   ÚPodN)	ÚrsplitÚlenr+   ÚMINER_INVENTORY_CSVÚcsvÚ
DictReaderr/   r   r1   )r:   r0   ÚpartsÚtarget_siteÚ
target_podr4   ÚreaderÚrowr#   r#   r$   Úget_inventory_for_podi   s*   

 
€þþùþþrL   c                 C   s˜   z>t jdt› d�dd�}| ¡ }| d| f¡ | ¡ }|s&| ¡  d g fW S |d }| d| |f¡ | ¡ }| ¡  ||fW S  tyK   d g f Y S w )Núfile:ú?immutable=1T©ÚurizSSELECT timestamp FROM miner_snapshots WHERE pod = ? ORDER BY timestamp DESC LIMIT 1r   zÆSELECT position, ip, serial, miner_type, hs_rt, power, error_codes, mining_state, mac
                     FROM miner_snapshots WHERE pod = ? AND timestamp = ?
                     ORDER BY position)	Úsqlite3ÚconnectÚMINER_HISTORY_DBÚcursorÚexecuteÚfetchoneÚcloseÚfetchallr1   )r:   Úconnr    rK   ÚtsÚrowsr#   r#   r$   Úget_snapshot_data}   s$   
þ
ÿr\   c                 C   sx   g }z/t tƒ� }t |¡}|D ]}| d¡| kr| |¡ qW d   ƒ W |S 1 s*w   Y  W |S  ty;   Y |S w )NÚpod)r+   ÚMINER_STATUS_CSVrE   rF   r/   r   r1   )r:   Úminersr4   rJ   rK   r#   r#   r$   Úget_live_data‘   s"   


€þþùþþr`   c              
   C   s„  g g g g g dœ}t ƒ }d}| D ]­}t|tƒrO| dd¡}| dd¡}| dd¡}| dd¡}| dd¡}	t| d	d¡p<dƒ}
| d
d¡pEd}| dd¡pMd}n2t|ƒdkra|\	}}}}	}}}}}n|\}}}}	}}}}d}|rst|ƒnd}
|pxd}|p|d}|p€d}|d7 }|rŽ| | ¡ ¡ |dkr“q|||||	|
d›|dœ}|
dkr¯d|d< |d  |¡ qt	|ƒ}|r¼||  |¡ q|||fS )N©Úzeror   r   r   r   r   Úpositionr   ÚipÚserialÚmacr)   Úhs_rtÚerror_codesÚmining_stateé	   r>   Úsleepingz.1f)rc   rd   re   rf   ÚtypeÚhashrateÚerrorsÚ0rm   rb   )
ÚsetÚ
isinstanceÚdictr/   ÚfloatrC   Úaddr   r   r%   )r[   ÚbucketsÚonline_macsÚonline_countrK   Úposrd   re   rf   ÚmtypeÚhsrn   Ústaterg   r   ÚentryÚbucketr#   r#   r$   Úcategorize_minersž   sL   

ÿ€
r~   c                 C   s€  t dƒ t| ƒ}|rd}t ¡  d¡}t|ƒ\}}}n#t| ƒ\}}|r-d}t|ƒ\}}}nd}d}g g g g g dœ}tƒ }d}i }z)tj	d	t
› d
�dd�}	|	 ¡ }
|
 d¡ |
 ¡ D ]
\}}||| ¡ < qY|	 ¡  W n	 tyr   Y nw | ¡ D ],}|D ]'}| d¡pƒd ¡ }| |d¡}|r˜|dd…  dd¡nd|d< | dd¡ q{qwt| ƒ}g }|D ]3}| dd¡ ¡  ¡ }| dd¡ ¡ }|sÅ|sÅq¬|rÌ||v rÌq¬| | dd¡|| dd¡ ¡ dœ¡ q¬|jdd„ d� dd„ }||t|ƒ|t|d |d�t|d |d�t|d |d�t|d |d�t|d  |d�|t|d ƒt|d ƒt|d ƒt|d ƒt|d  ƒt|ƒd!œd"œ}t t |¡ƒ dS )#z"Return JSON data for a single pod.úContent-Type: application/json
Úlivez%Y-%m-%d %H:%MÚsnapshotÚnoneNra   r   rM   rN   TrO   z+SELECT mac, first_seen FROM miner_lifecyclerf   r   é   ÚTr=   Úfirst_onlineÚMACÚSerialÚPosition)rc   re   rf   c                 S   ó   | d S ©Nrc   r#   ©Úxr#   r#   r$   Ú<lambda>  ó    z serve_pod_data.<locals>.<lambda>©Úkeyc                 S   r‰   rŠ   r#   r‹   r#   r#   r$   r�     rŽ   rb   r   r   r   r   )rb   r   r   r   r   Úoffline)ÚsourceÚ	timestampÚ	inv_countrw   Ú	zero_hashr   r   r   r   r‘   Úcounts)Úprintr`   r   ÚnowÚstrftimer~   r\   rp   rQ   rR   rS   rT   rU   rX   r   rW   r1   Úvaluesr/   ÚreplaceÚpoprL   r   r   ÚsortrC   Úsortedr-   Údumps)r:   r€   r’   rZ   ru   rv   rw   Ú	snap_rowsÚ	lifecyclerY   r    Úmac_valÚ
first_seenÚlstr|   ÚmÚfsÚinvr‘   Úinv_macÚ
inv_serialÚsort_posÚresultr#   r#   r$   Úserve_pod_dataÎ   sˆ   
ÿ ü

ý




úõr¬   é   c              
   C   sô  t dƒ tƒ }|st t ddi¡ƒ dS t ¡ t| d�  d¡}d dt	|ƒ ¡}z²t
jd	t› d
�dd�}| ¡ }| d|› d�||g ¡ i }| ¡ D ]\}}}	|	|||f< qLi }
| ¡ D ]]\\}}}	| d||	f¡ ddddddœ}| ¡ D ]/\}}}|dkr‚qx|rˆt|ƒnd}|dkr—|d  d7  < qxt|p›dƒ}|r§||  d7  < qx||
vr°g |
|< |
|  d|i|¥¡ q^| ¡  |
D ]}|
| jdd„ d� qÂt t |
|dœ¡ƒ W dS  tyù } zt t dt|ƒi¡ƒ W Y d}~dS d}~ww )zYReturn N-day trend data: at each pod's daily peak hashrate, count zero/missing1/missing2.r   Úerrorzno podsN)Údaysz%Y-%m-%dr   ú?rM   rN   TrO   aM  
            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 (as  ) 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
        zuSELECT hs_rt, error_codes, mining_state
                         FROM miner_snapshots WHERE pod = ? AND timestamp = ?r   ra   rk   rb   r>   r   Údayc                 S   r‰   )Nr±   r#   r‹   r#   r#   r$   r�   _  rŽ   z"serve_chart_data.<locals>.<lambda>r�   )r(   r3   )r—   r<   r-   rŸ   r   r˜   r   r™   ÚjoinrC   rQ   rR   rS   rT   rU   rX   r0   rs   r%   r   rW   r�   r1   Ústr)r¯   r3   ÚsinceÚplaceholdersrY   r    Ú
peak_timesr]   r±   rZ   r«   r–   rg   rn   r{   rz   r}   Úer#   r#   r$   Úserve_chart_data   s\   úñÿ€$€ÿr¸   c               
   C   sV   t dƒ tƒ } t | ¡}tƒ }tƒ }tt ¡ ƒ}t d|› d|› d|› d|› d�	ƒ dS )zServe the HTML page.zContent-Type: text/html
zÅ<!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>
        a  

        * { 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">aj  </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 = aÄM  ;
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>z</script>
</body>
</html>N)	r—   r<   r-   rŸ   r   r	   r
   r   Úget_user_access)r3   Úpod_order_jsonÚdropdown_cssÚdropdown_jsÚdropdown_htmlr#   r#   r$   Ú
serve_pageg  s:   
ù  �ñ & �Ú      � � � � �ér¾   c                  C   s^   t  ¡ } |  d¡}|  d¡r"t|  dd¡ƒ}|dvrd}t|ƒ d S |r*t|ƒ d S tƒ  d S )Nr]   Ú	chart_allr¯   r­   )r­   é   é   )ÚcgiÚFieldStorageÚgetfirstr   r¸   r¬   r¾   )Úparamsr]   r¯   r#   r#   r$   Úmain	  s   


rÆ   Ú__main__)T)r­   )1rÂ   ÚsysÚpathÚinsertÚmanagers.auth_managerr   r   rÃ   Ú_formÚ_authÚrequire_authÚ_auth_requiredÚ_should_exitÚ_headersr—   ÚexitrE   r-   ÚosrQ   r   r   r   ÚdirnameÚabspathÚ__file__Úlinksr   r	   r
   Úminers.error_categoriesr   rS   r^   rD   r,   Ú	frozensetr   r%   r<   rL   r\   r`   r~   r¬   r¸   r¾   rÆ   Ú__name__r#   r#   r#   r$   Ú<module>   sv   


ü
0
RG     '
ÿ