#!/usr/bin/env python3

import cgi
from managers.auth_manager import AuthManager, generate_login_page_html

_form = cgi.FieldStorage()
_auth = AuthManager('gm')
_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("Miner Performance"))
    sys.exit(0)


import requests
import html
import json
import csv
import sys
sys.path.insert(0, '/opt/ngon/apps')
from managers.config_manager import config
from collections import OrderedDict, defaultdict
import re

# Hashrate thresholds for determining "good" performance
WHATSMINER_GOOD_HASHRATE = 150
ANTMINER_GOOD_HASHRATE = 90

# Load generator data from local file
gen_file_path = "/opt/ngon/data/live/generator_data.json"

def parse_position(position_str):
    """Parse position string like '0-1-01' or '2-2-13' into (pod, row, pos)"""
    parts = position_str.split('-')
    if len(parts) == 3:
        return int(parts[0]), int(parts[1]), int(parts[2])
    return None, None, None

def load_installed_miners():
    """Load the total miners installed per pod and preserve order"""
    installed = config.get_pod_miner_counts()
    site_order = []  # Track the order sites appear in the data

    # Extract site names and track order from the config data
    for site_pod in installed.keys():
        if ' ' in site_pod:
            site_name = site_pod.split(' ')[0]
        else:
            site_name = site_pod

        if site_name not in site_order:
            site_order.append(site_name)

    return installed, site_order

def load_inventory():
    """Load inventory data to know which positions should have miners"""
    inventory = {}
    mac_to_location = {}  # For lookup by MAC address

    try:
        with open('/opt/ngon/data/live/miner_inventory.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                position = row['Position']
                site = row['Site']
                pod_designation = row['Pod']
                serial = row['Serial'].strip() if row['Serial'] else ''
                mac = row['MAC'].strip() if row['MAC'] else ''

                # Create pod key
                if site in ['Haley', 'John']:
                    pod_key = site
                else:
                    pod_key = f"{site} {pod_designation}"

                pod, row_num, pos_num = parse_position(position)

                if pod is not None:
                    if pod_key not in inventory:
                        inventory[pod_key] = {}
                    if row_num not in inventory[pod_key]:
                        inventory[pod_key][row_num] = {}

                    # Check if this is an empty position (no serial/MAC)
                    is_empty = not serial and not mac

                    inventory[pod_key][row_num][pos_num] = {
                        'serial': serial,
                        'mac': mac,
                        'installed': not is_empty
                    }

                    # Only store MAC to location mapping if there's actually a miner there
                    if not is_empty and mac:
                        mac_to_location[mac] = {
                            'pod_key': pod_key,
                            'row': row_num,
                            'pos': pos_num
                        }

    except FileNotFoundError:
        pass
    except Exception as e:
        pass
    return inventory, mac_to_location

def load_miners(mac_to_location):
    """Load current miner status and map to correct locations via MAC"""
    miners = {}
    try:
        with open('/opt/ngon/data/live/miner_status.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                mac = row['mac']

                # Look up location by MAC address
                if mac in mac_to_location:
                    location = mac_to_location[mac]
                    pod_key = location['pod_key']
                    row_num = location['row']
                    pos_num = location['pos']

                    if pod_key not in miners:
                        miners[pod_key] = {}
                    if row_num not in miners[pod_key]:
                        miners[pod_key][row_num] = {}

                    hashrate = float(row['hs_rt']) if row['hs_rt'] else 0
                    error_codes = row['error_codes'].strip() if row['error_codes'] else ''
                    has_errors = bool(error_codes)

                    miners[pod_key][row_num][pos_num] = {
                        'serial': row['serial'],
                        'mac': mac,
                        'hashrate': hashrate,
                        'power': row['power'],
                        'errors': has_errors,
                        'error_codes': error_codes,
                        'miner_type': row['miner_type'],
                        'online': True
                    }

    except FileNotFoundError:
        pass
    return miners

def load_sleeping_miners():
    """Load sleeping miners from miner_status.csv based on mining_state column"""
    sleeping_miners = set()
    try:
        with open('/opt/ngon/data/live/miner_status.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                mining_state = row.get('mining_state', '').strip().lower()
                if mining_state == 'sleeping':
                    mac = row.get('mac', '').strip().lower()  # Normalize to lowercase
                    if mac:
                        sleeping_miners.add(mac)
    except (FileNotFoundError, Exception):
        pass
    return sleeping_miners

def calculate_pod_hashrate(pod_key, inventory, miners):
    """Calculate total hashrate for a pod"""
    total_hashrate = 0
    
    if pod_key not in miners:
        return 0
        
    for row_num in miners[pod_key]:
        for pos_num in miners[pod_key][row_num]:
            # Check if position exists in inventory and has a miner installed
            has_inventory_entry = (pod_key in inventory and
                                 row_num in inventory[pod_key] and
                                 pos_num in inventory[pod_key][row_num])
            
            if has_inventory_entry and inventory[pod_key][row_num][pos_num]['installed']:
                miner = miners[pod_key][row_num][pos_num]
                total_hashrate += miner['hashrate']
    
    return total_hashrate

def calculate_site_hashrate(site_name, sites, inventory, miners):
    """Calculate total hashrate for a site"""
    total_hashrate = 0
    site_pods = sites.get(site_name, [])
    
    for pod_key in site_pods:
        total_hashrate += calculate_pod_hashrate(pod_key, inventory, miners)
    
    return total_hashrate

def get_miner_color(pod_key, row_num, pos_num, inventory, miners, sleeping_miners, miner_type_specs):
    """Determine the color for a specific miner position"""

    # Check if position exists in inventory
    has_inventory_entry = (pod_key in inventory and
                          row_num in inventory[pod_key] and
                          pos_num in inventory[pod_key][row_num])

    if not has_inventory_entry:
        return 'grey'  # Position doesn't exist in inventory at all

    inventory_entry = inventory[pod_key][row_num][pos_num]

    # Check if there's actually a miner installed at this position
    if not inventory_entry['installed']:
        return 'grey'  # No miner installed (empty position)

    # Check if miner is online in miners.csv
    has_miner_data = (pod_key in miners and
                     row_num in miners[pod_key] and
                     pos_num in miners[pod_key][row_num])

    if not has_miner_data:
        return 'white'  # Miner in inventory but offline (not in miners.csv)

    miner = miners[pod_key][row_num][pos_num]
    hashrate = miner['hashrate']
    has_errors = bool(miner['error_codes'])
    miner_type = miner['miner_type']
    mac = miner.get('mac', '').lower()  # Normalize to lowercase
    
    # Check if miner is sleeping first - if in sleeping CSV, always blue
    if mac in sleeping_miners:
        return 'blue-error' if has_errors else 'blue'

    # If not sleeping, determine color based on hashrate
    if hashrate == 0:
        base_color = 'red'  # Zero hashrate and not sleeping
    else:
        # Get spec hashrate for this miner type
        spec_hashrate = None
        if miner_type in miner_type_specs:
            spec_hashrate = miner_type_specs[miner_type].get('spec_hashrate')
        
        # If we don't have spec data, use 80% threshold as fallback
        if spec_hashrate:
            good_threshold = spec_hashrate * 0.8  # 80% of spec = good
        else:
            # Fallback to old logic if spec not found
            if 'whatsminer' in miner_type.lower():
                good_threshold = WHATSMINER_GOOD_HASHRATE
            elif 'antminer' in miner_type.lower() or 'jpro' in miner_type.lower():
                good_threshold = ANTMINER_GOOD_HASHRATE
            else:
                good_threshold = WHATSMINER_GOOD_HASHRATE

        # Determine base color based on hashrate
        if hashrate >= good_threshold:
            base_color = 'green'
        else:
            base_color = 'yellow'  # Low hashrate but not zero

    # Add error indicator if present
    if has_errors:
        return f'{base_color}-error'
    else:
        return base_color

def generate_pod_grid(pod_key, inventory, miners, sleeping_miners, miner_type_specs):
    """Generate HTML for a single pod's miner grid"""

    # Find all rows and positions for this pod
    all_rows = set()
    all_positions = set()

    if pod_key in inventory:
        for row_num in inventory[pod_key]:
            all_rows.add(row_num)
            for pos_num in inventory[pod_key][row_num]:
                all_positions.add(pos_num)

    if pod_key in miners:
        for row_num in miners[pod_key]:
            all_rows.add(row_num)
            for pos_num in miners[pod_key][row_num]:
                all_positions.add(pos_num)

    if not all_rows or not all_positions:
        return '<div class="pod-grid">No data</div>'

    min_row, max_row = min(all_rows), max(all_rows)
    min_pos, max_pos = min(all_positions), max(all_positions)

    html = '<div class="pod-grid">\n'

    for row in range(min_row, max_row + 1):
        html += '  <div class="pod-row">\n'
        for pos in range(min_pos, max_pos + 1):
            color = get_miner_color(pod_key, row, pos, inventory, miners, sleeping_miners, miner_type_specs)
            title = f"Row {row}, Pos {pos}"

            # Add additional info for tooltip
            if (pod_key in miners and row in miners[pod_key] and pos in miners[pod_key][row]):
                miner = miners[pod_key][row][pos]
                mac = miner.get('mac', '').lower()  # Normalize to lowercase
                is_sleeping = mac in sleeping_miners
                title += f" - {miner['miner_type']} - {miner['hashrate']}TH/s"
                if is_sleeping:
                    title += " - SLEEPING"
                if miner['error_codes']:  # Show error codes only if they exist
                    title += f" - Error: {miner['error_codes']}"
            elif (pod_key in inventory and row in inventory[pod_key] and pos in inventory[pod_key][row]):
                title += " - Offline"
            else:
                title += " - Empty"

            html += f'    <div class="miner-square {color}" data-tooltip="{title}"></div>\n'
        html += '  </div>\n'

    html += '</div>\n'
    return html

def organize_by_sites(inventory, miners):
    """Organize pods by site"""
    sites = defaultdict(list)

    # Get all unique pod keys
    all_pods = set(inventory.keys()) | set(miners.keys())

    for pod_key in all_pods:
        # Extract site name
        if ' ' in pod_key:
            site_name = pod_key.split(' ')[0]
        else:
            site_name = pod_key  # Single pod sites like Haley, John

        sites[site_name].append(pod_key)

    # Sort pods within each site
    for site in sites:
        sites[site].sort()

    return dict(sites)

# Grab gen data from file
try:
    with open(gen_file_path, 'r') as f:
        generator_data = json.load(f)
except Exception as e:
    generator_data = []
    print(f"<!-- Error loading generator data: {e} -->")

# Load gens to pods mapping
gen_to_pod = {}
pod_order = OrderedDict()

# Get pod to generators mapping from config manager
pod_to_generators = config.get_generator_to_pod_mapping()
for pod_name, gen_ids in pod_to_generators.items():
    if pod_name in ("Out of Service", "Spares"):
        continue
    pod_order[pod_name] = gen_ids
    for gen_id in gen_ids:
        gen_to_pod[gen_id] = pod_name

# Load site name mapping for generators  
# Get generator to site mapping from config manager
generator_sites = config.get_generator_sites()
site_name_mapping = {}

# Build site_id to site_name mapping by processing generator data
# This will be populated when we iterate through generator_data below

# Load generator grouping information
gen_groups = config.get_generator_groups()
pod_to_group = {}

# Map each pod to its group
for group_name, pod_names in gen_groups.items():
    for pod_name in pod_names:
        pod_to_group[pod_name] = group_name

# Organize generator data by site and pod
site_pod_generators = {}
gen_lookup = {g.get("unit_id", ""): g for g in generator_data}

for pod_name, gen_ids in pod_order.items():
    for gen_id in gen_ids:
        gen = gen_lookup.get(gen_id)
        if not gen:
            continue
        
        # Get site name from generator_sites mapping or fallback to extracting from gen data
        site = generator_sites.get(gen_id)
        if not site:
            # Fallback: try to extract site name from site_id in generator data
            site_id_raw = gen.get("site_id", "UNKNOWN")
            site_id = str(site_id_raw).replace("CSITE-", "").strip()
            site = site_name_mapping.get(site_id, "Unknown Site")
            # Store this mapping for future use
            if site != "Unknown Site":
                site_name_mapping[site_id] = site

        # Handle different pod naming patterns
        if " " in pod_name:
            # Generator has "Alpha 1" format - match to "Alpha 1"
            site_pod_key = f"{site} {pod_name.split()[-1]}"
        else:
            # Generator has just site name like "Ely", "Haley", "John", "JW", "Osprey"
            # For now, store under the generator pod name and we'll map it in display
            site_pod_key = pod_name

        if site_pod_key not in site_pod_generators:
            site_pod_generators[site_pod_key] = []
        site_pod_generators[site_pod_key].append(gen)

def generate_generator_group_box(group_name, pod_keys, generators):
    """Generate HTML for a single generator group box"""
    if not generators:
        return ""
    
    html = f'''
    <div class="generator-group-box">
      <div class="generator-group-header" onclick="showGroupCharts(&quot;{group_name}&quot;, {json.dumps([g.get('unit_id') for g in generators]).replace('"', '&quot;')})">Generators</div>
      <div class="generator-group-content">
    '''
    
    for gen in generators:
        gen_id = gen.get("unit_id", "???")
        short_id = gen_id.split("-")[-1] if "-" in gen_id else gen_id[-3:]
        run_status = str(gen.get("run_status", "")).lower()
        dot_class = "online" if run_status == "running" else "offline"
        
        html += f'''
        <div class="gen-entry" onclick="showGenDetails('{gen_id}')">
          <span class="shape dot {dot_class}"></span> {short_id}
        </div>
        '''
    
    html += '''
      </div>
    </div>
    '''
    
    return html

# Modified site generation in the main HTML function
def generate_site_html_with_grouped_generators(site_name, sites, inventory, miners, sleeping_miners, miner_type_specs):
    """Generate HTML for a site with properly grouped generators and visual pod grouping"""
    
    site_pods = sites.get(site_name, [])
    site_hashrate = calculate_site_hashrate(site_name, sites, inventory, miners)
    
    # Determine which pods are grouped and which are standalone
    grouped_pods = set()
    pod_groups = []  # List of (group_name, pods_in_group)
    standalone_pods = []
    
    # First, find all grouped pods
    for group_name, pod_names in gen_groups.items():
        if group_name.startswith(site_name + " "):  # e.g., "Alpha A"
            group_display_name = group_name.split(" ", 1)[1]  # Extract "A" from "Alpha A"
            
            # Find pods in this site that belong to this group
            pods_in_group = [pod for pod in pod_names if pod in site_pods]
            
            if pods_in_group:
                pod_groups.append((group_display_name, pods_in_group))
                # Mark these pods as grouped
                for pod in pods_in_group:
                    grouped_pods.add(pod)
    
    # Find standalone pods (not in any group)
    for pod_key in site_pods:
        if pod_key not in grouped_pods:
            standalone_pods.append(pod_key)
    
    # Format site title with hashrate (convert TH to PH)
    site_title = f"{site_name} - {site_hashrate/1000:.2f}&nbsp;PH/s"
    
    html = f'<div class="site-block">'
    html += f'  <div class="site-title">{site_title}</div>'
    html += f'  <div class="pods-container">'
    
    # Generate standalone pods (each in their own box with generators)
    for pod_key in standalone_pods:
        pod_hashrate = calculate_pod_hashrate(pod_key, inventory, miners)
        pod_title = f"{pod_key} - {pod_hashrate/1000:.2f}&nbsp;PH/s"
        
        html += f'    <div class="pod-group-box standalone-pod">'
        html += f'      <div class="pod-containers-in-group">'
        html += f'        <div class="pod-container">'
        html += f'          <div class="pod-title" onclick="showPodModal(\'{pod_key}\')">{pod_title}</div>'
        html += f'          {generate_pod_grid(pod_key, inventory, miners, sleeping_miners, miner_type_specs)}'
        html += f'        </div>'
        html += f'      </div>'
        
        # Add generators for standalone pod
        generators = site_pod_generators.get(pod_key, [])
        if generators:
            pod_name = pod_key.split(' ')[-1] if ' ' in pod_key else pod_key
            html += generate_generator_group_box(pod_name, [pod_key], generators)
        
        html += f'    </div>'
    
    # Generate grouped pods (multiple pods in one box with shared generators)
    for group_name, pods_in_group in pod_groups:
        html += f'    <div class="pod-group-box grouped-pods">'
        html += f'      <div class="pod-containers-in-group">'
        
        # Add all pods in the group
        for pod_key in pods_in_group:
            pod_hashrate = calculate_pod_hashrate(pod_key, inventory, miners)
            pod_title = f"{pod_key} - {pod_hashrate/1000:.2f}&nbsp;PH/s"
            
            html += f'        <div class="pod-container">'
            html += f'          <div class="pod-title" onclick="showPodModal(\'{pod_key}\')">{pod_title}</div>'
            html += f'          {generate_pod_grid(pod_key, inventory, miners, sleeping_miners, miner_type_specs)}'
            html += f'        </div>'
        
        html += f'      </div>'
        
        # Add shared generators for the group (collect from all pods in group)
        all_generators = []
        for pod_name in pods_in_group:
            if pod_name in site_pod_generators:
                all_generators.extend(site_pod_generators[pod_name])
        
        if all_generators:
            html += generate_generator_group_box(group_name, pods_in_group, all_generators)
        
        html += f'    </div>'
    
    html += f'  </div>'
    
    # Add site-level generators if any (like Ely, JW, etc.)
    # Skip if this site has a standalone pod with the same name (to avoid duplication for John, Haley, etc.)
    has_matching_standalone_pod = site_name in standalone_pods
    site_level_generators = site_pod_generators.get(site_name, [])
    if site_level_generators and not has_matching_standalone_pod:
        html += f'  <div class="site-generator-groups">'
        html += generate_generator_group_box("Site", [site_name], site_level_generators)
        html += f'  </div>'
    
    html += f'</div>'
    
    return html
    
def calculate_total_hashrate(sites, inventory, miners):
    """Calculate total hashrate across all sites"""
    total_hashrate = 0
    for site_name in sites:
        total_hashrate += calculate_site_hashrate(site_name, sites, inventory, miners)
    return total_hashrate

def calculate_total_generator_power():
    """Calculate total MW from all generators"""
    total_power = 0
    for gen in generator_data:
        power = gen.get("power_avg")
        if power is not None:
            try:
                # Convert to float and add to total (assuming power_avg is in MW)
                total_power += float(power)
            except (ValueError, TypeError):
                continue
    return total_power

def calculate_total_miners_hashing(inventory, miners):
    """Calculate total number of miners that are hashing (hashrate > 0) - matches index.py logic exactly"""
    total_hashing = 0
    miners_csv_path = "/opt/ngon/data/live/miner_status.csv"
    
    try:
        import csv
        with open(miners_csv_path, 'r') as file:
            reader = csv.DictReader(file)
            for row in reader:
                pod = row.get('pod', '').strip()
                if not pod:  # Skip miners without pod data
                    continue
                
                try:
                    hs_rt = float(row.get('hs_rt', 0))
                except (ValueError, TypeError):
                    hs_rt = 0
                
                if hs_rt > 0:
                    total_hashing += 1
    except FileNotFoundError:
        pass
    
    return total_hashing

def calculate_total_miners_zero_hashrate(inventory, miners):
    """Calculate total number of miners that are online but have zero hashrate - matches index.py logic exactly"""
    total_zero = 0
    miners_csv_path = "/opt/ngon/data/live/miner_status.csv"
    
    try:
        import csv
        with open(miners_csv_path, 'r') as file:
            reader = csv.DictReader(file)
            for row in reader:
                pod = row.get('pod', '').strip()
                if not pod:  # Skip miners without pod data
                    continue
                
                try:
                    hs_rt = float(row.get('hs_rt', 0))
                except (ValueError, TypeError):
                    hs_rt = 0
                
                if hs_rt == 0:
                    total_zero += 1
    except FileNotFoundError:
        pass
    
    return total_zero

def calculate_total_miners_offline(inventory, miners):
    """Calculate total number of miners that are offline - matches index.py logic exactly"""
    # Load installed miners mapping (same as index.py)
    installed, _ = load_installed_miners()
    total_miners_installed = sum(installed.values())
    
    # Count total online miners (those in miners.csv) - same logic as index.py
    total_online = 0
    miners_csv_path = "/opt/ngon/data/live/miner_status.csv"
    
    try:
        import csv
        with open(miners_csv_path, 'r') as file:
            reader = csv.DictReader(file)
            for row in reader:
                pod = row.get('pod', '').strip()
                if not pod:  # Skip miners without pod data
                    continue
                total_online += 1
    except FileNotFoundError:
        pass
    
    return total_miners_installed - total_online

def generate_html():
    """Generate the complete HTML page"""

    try:
        installed, site_order = load_installed_miners()
        inventory, mac_to_location = load_inventory()
        miners = load_miners(mac_to_location)
        sleeping_miners = load_sleeping_miners()
        sites = organize_by_sites(inventory, miners)
        
        # Load miner type specs
        from managers.config_manager import get_miner_type_specs
        miner_type_specs = get_miner_type_specs()
        
        # Calculate totals
        total_hashrate = calculate_total_hashrate(sites, inventory, miners)
        total_power = calculate_total_generator_power()
        total_miners_hashing = calculate_total_miners_hashing(inventory, miners)
        total_miners_zero_hashrate = calculate_total_miners_zero_hashrate(inventory, miners)
        total_miners_offline = calculate_total_miners_offline(inventory, miners)

        # Start building HTML
        html_parts = []

        html_parts.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
  <title>God Mode - Pods & Generators v2</title>
  <meta http-equiv="refresh" content="300">
  <link rel="stylesheet" type="text/css" href="/status/styles.css">
  <style>
    body {{ overflow-y: auto; overflow-x: hidden; }}
    .container {{
      display: flex;
      flex-direction: column;
      height: 100vh;
      padding: 10px;
    }}
    .header-section {{
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin: 10px 0 20px 0;
    }}
    h1 {{
      margin: 0;
      font-size: 2.2em;
    }}
    .totals-section {{
      display: flex;
      gap: 30px;
      font-size: 1.1em;
      color: #4a9eff;
    }}
    .total-item {{
      text-align: center;
    }}
    .total-value {{
      font-weight: bold;
      font-size: 1.3em;
    }}
    .total-label {{
      font-size: 0.9em;
      color: #cccccc;
    }}
    .sites-wrapper {{
      flex: 1;
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      align-content: flex-start;
      overflow-y: auto;
    }}
    .site-block {{
      background-color: rgba(255,255,255,0.05);
      padding: 10px;
      border-radius: 8px;
      display: inline-block;
      vertical-align: top;
    }}
    .site-title {{
      font-size: 1.1em;
      font-weight: bold;
      margin-bottom: 8px;
      color: #ffffff;
      text-align: center;
    }}
    .pods-container {{
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: flex-start;
    }}
    .pod-container {{
      background-color: rgba(255,255,255,0.02);
      padding: 8px;
      border-radius: 6px;
      border: 1px solid rgba(255,255,255,0.1);
      display: inline-block;
      min-width: fit-content;
      width: auto;
    }}
    .pod-title {{
      font-weight: bold;
      margin-bottom: 6px;
      text-align: center;
      color: #ffffff;
      font-size: 0.85em;
      cursor: pointer;
    }}
    .pod-grid {{
      display: flex;
      flex-direction: column;
      gap: 4px;
      align-items: center;
    }}
    .pod-row {{
      display: flex;
      gap: 4px;
      line-height: 0;
    }}
    .miner-square {{
      width: 4px;
      height: 4px;
      display: block;
      margin: 0;
      padding: 0;
      line-height: 0;
      font-size: 0;
      flex-shrink: 0;
      cursor: pointer;
    }}
    .green {{ background-color: green; }}
    .red {{ background-color: red; }}
    .blue {{ background-color: blue; }}
    .yellow {{ background-color: yellow; color: black; }}
    .orange {{ background-color: orange; }}
    .white {{ background-color: white; }}
    .grey {{ background-color: #666; }}

    .green-error {{
      background-color: green;
      box-shadow: inset 0 0 0 1px orange;
    }}
    .red-error {{
      background-color: red;
      box-shadow: inset 0 0 0 1px orange;
    }}
    .blue-error {{
      background-color: blue;
      box-shadow: inset 0 0 0 1px orange;
    }}
    .yellow-error {{
      background-color: yellow;
      color: black;
      box-shadow: inset 0 0 0 1px orange;
    }}

    .modal {{
      position: fixed;
      z-index: 1000;
      padding-top: 100px;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      overflow: auto;
      background-color: rgba(0,0,0,0.7);
    }}
    .modal-content {{
      background-color: #222;
      margin: auto;
      padding: 20px;
      border: 1px solid #888;
      width: 95%;
      max-width: 95vw;
      color: #ccc;
      height: 85vh;
      max-height: 85vh;
      overflow: hidden;
      font-family: monospace;
      white-space: pre-wrap;
      display: flex;
      gap: 20px;
      position: relative;
      box-sizing: border-box;
    }}
    .close {{
      color: #aaa;
      position: absolute;
      top: 10px;
      right: 20px;
      font-size: 24px;
      font-weight: bold;
      cursor: pointer;
    }}
    .close:hover {{ color: #fff; }}
    .gen-table td {{ padding-right: 20px; padding-bottom: 4px; }}

    .pod-modal-content {{
      width: 80%;
      max-width: none;
      padding: 20px;
      text-align: center;
      height: 70vh;
      max-height: 70vh;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }}
    .pod-modal-title {{
      font-size: 2em;
      margin-bottom: 15px;
      color: #ffffff;
    }}
    .pod-modal-grid {{
      display: flex;
      flex-direction: column;
      gap: 4px;
      width: fit-content;
      justify-content: center;
      align-items: center;
    }}
    .pod-modal-row {{
      display: flex;
      gap: 4px;
      line-height: 0;
    }}
    .pod-modal-square {{
      display: block;
      margin: 0;
      padding: 0;
      line-height: 0;
      font-size: 0;
      flex-shrink: 0;
      cursor: pointer;
    }}

    .pod-modal-square.green-error {{
      box-shadow: inset 0 0 0 2px orange;
    }}
    .pod-modal-square.red-error {{
      box-shadow: inset 0 0 0 2px orange;
    }}
    .pod-modal-square.blue-error {{
      box-shadow: inset 0 0 0 2px orange;
    }}

    .mobile-tooltip {{
      background-color: rgba(0,0,0,0.9);
      color: white;
      padding: 10px 15px;
      border-radius: 8px;
      font-size: 14px;
      z-index: 10000;
      display: none;
      max-width: 90%;
      text-align: center;
      border: 1px solid rgba(255,255,255,0.3);
      pointer-events: none;
      position: fixed;
    }}

    @media (max-width: 768px) {{
      .mobile-tooltip {{
        font-size: 12px;
        padding: 8px 12px;
        border-radius: 6px;
        max-width: 80%;
      }}

      .sites-wrapper {{
        flex-direction: column;
      }}
      .site-block {{
        width: 100%;
      }}
      .pods-container {{
        flex-direction: column;
      }}
      .pod-container {{
        max-width: none;
        width: 100%;
      }}

      .miner-square {{
        min-width: 15px;
        min-height: 15px;
        margin: 1px;
      }}

      body {{
        touch-action: manipulation;
      }}
    }}
    .site-generator-groups {{
      margin-top: 10px;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: center;
    }}

    .generator-group-box {{
      background-color: rgba(255,255,255,0.03);
      border: 1px solid rgba(76, 158, 255, 0.3);
      border-radius: 6px;
      padding: 8px;
      min-width: 120px;
    }}

    .generator-group-header {{
      font-weight: bold;
      font-size: 0.85em;
      color: #4a9eff;
      cursor: pointer;
      margin-bottom: 6px;
      text-align: center;
      padding-bottom: 4px;
      border-bottom: 1px solid rgba(76, 158, 255, 0.2);
    }}

    .generator-group-content {{
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      justify-content: center;
    }}

    .generator-group-box .gen-entry {{
      font-size: 0.7em;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 2px;
      padding: 2px 4px;
      border-radius: 3px;
      background-color: rgba(255,255,255,0.05);
    }}

    .generator-group-box .gen-entry:hover {{
      background-color: rgba(255,255,255,0.1);
    }}
    
    .pod-group-box {{
      background-color: rgba(255,255,255,0.02);
      border: 2px solid rgba(76, 158, 255, 0.4);
      border-radius: 8px;
      padding: 10px;
      margin: 8px;
      display: inline-block;
      vertical-align: top;
    }}
    
    .standalone-pod {{
      border-color: rgba(255, 255, 255, 0.3);
    }}
    
    .grouped-pods {{
      border-color: rgba(76, 158, 255, 0.6);
    }}
    
    .pod-containers-in-group {{
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 10px;
    }}
    
    .pod-containers-in-group .pod-container {{
      background-color: rgba(255,255,255,0.02);
      padding: 8px;
      border-radius: 6px;
      border: 1px solid rgba(255,255,255,0.1);
      display: inline-block;
      min-width: fit-content;
      width: auto;
    }}
    
    .site-generator-groups {{
      margin-top: 10px;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: center;
    }}
    
    .generator-group-box {{
      background-color: rgba(255,255,255,0.03);
      border: 1px solid rgba(76, 158, 255, 0.3);
      border-radius: 6px;
      padding: 8px;
      min-width: 120px;
    }}
    
    .generator-group-header {{
      font-weight: bold;
      font-size: 0.85em;
      color: #4a9eff;
      cursor: pointer;
      margin-bottom: 6px;
      text-align: center;
      padding-bottom: 4px;
      border-bottom: 1px solid rgba(76, 158, 255, 0.2);
    }}
    
    .generator-group-content {{
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      justify-content: center;
    }}
    
    .generator-group-box .gen-entry {{
      font-size: 0.7em;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 2px;
      padding: 2px 4px;
      border-radius: 3px;
      background-color: rgba(255,255,255,0.05);
    }}
    
    .generator-group-box .gen-entry:hover {{
      background-color: rgba(255,255,255,0.1);
    }}
  </style>
</head>
<body>
  <div id="mobile-tooltip" class="mobile-tooltip"></div>
  <div class="container">
    <div class="header-section">
      <h1>God Mode - Pods & Generators</h1>
      <div class="totals-section">
        <div class="total-item">
          <div class="total-value">{total_hashrate/1000:.1f} PH/s</div>
          <div class="total-label">Total Hashrate</div>
        </div>
        <div class="total-item">
          <div class="total-value">{total_power/1000:.1f} MW</div>
          <div class="total-label">Total Power</div>
        </div>
        <div class="total-item">
          <div class="total-value">{total_miners_hashing}</div>
          <div class="total-label">Miners Hashing</div>
        </div>
        <div class="total-item">
          <div class="total-value">{total_miners_zero_hashrate}</div>
          <div class="total-label">Online, Zero Hashrate</div>
        </div>
        <div class="total-item">
          <div class="total-value">{total_miners_offline}</div>
          <div class="total-label">Offline</div>
        </div>
      </div>
    </div>
    <div class="sites-wrapper">""")

        # Add sites
        for site_name in site_order:
            if site_name in sites:
                html_parts.append(generate_site_html_with_grouped_generators(site_name, sites, inventory, miners, sleeping_miners, miner_type_specs))
    
        # Add modals
        html_parts.append('    </div>')
        html_parts.append('  </div>')

        # Pod modal
        html_parts.append('  <div id="podModal" class="modal" style="display:none;">')
        html_parts.append('    <div class="modal-content pod-modal-content">')
        html_parts.append('      <span class="close" onclick="document.getElementById(\'podModal\').style.display=\'none\';">&times;</span>')
        html_parts.append('      <div id="podModalTitle" class="pod-modal-title"></div>')
        html_parts.append('      <div id="podModalStats" style="text-align: center; margin: -10px 0 5px 0; font-size: 13px; color: #ccc;"></div>')
        html_parts.append('      <div id="podModalLegend" style="text-align: center; margin: 0 0 10px 0; font-size: 14px; color: #ccc;"></div>')
        html_parts.append('      <div id="podModalGrid"></div>')
        html_parts.append('    </div>')
        html_parts.append('  </div>')

        # Generator modal
        html_parts.append('  <div id="genModal" class="modal" style="display:none;">')
        html_parts.append('    <div class="modal-content">')
        html_parts.append('      <span class="close" onclick="document.getElementById(\'genModal\').style.display=\'none\';">&times;</span>')
        html_parts.append('      <div id="modalData" style="min-width: 280px; max-width: 350px; flex-shrink: 0;"></div>')
        html_parts.append('      <div style="flex: 1; display: flex; flex-direction: column; min-width: 0;">')
        html_parts.append('        <div style="flex: 1; min-height: 0; margin-bottom: 10px;">')
        html_parts.append('          <canvas id="powerChart" style="width: 100%; height: 100%;"></canvas>')
        html_parts.append('        </div>')
        html_parts.append('        <div style="flex: 1; min-height: 0;">')
        html_parts.append('          <canvas id="gasChart" style="width: 100%; height: 100%;"></canvas>')
        html_parts.append('        </div>')
        html_parts.append('      </div>')
        html_parts.append('    </div>')
        html_parts.append('  </div>')

        # Add JavaScript
        inventory_json = json.dumps({k: {str(r): {str(p): v for p, v in row.items()} for r, row in pod.items()} for k, pod in inventory.items()})
        miners_json = json.dumps({k: {str(r): {str(p): v for p, v in row.items()} for r, row in pod.items()} for k, pod in miners.items()})

        html_parts.append('  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>')
        html_parts.append('  <script>')
        html_parts.append(f'    const podInventory = {inventory_json};')
        html_parts.append(f'    const podMiners = {miners_json};')
        # Normalize sleeping miners MAC addresses to lowercase for JavaScript
        normalized_sleeping_miners = [mac.lower() for mac in sleeping_miners]
        html_parts.append(f'    const sleepingMiners = {json.dumps(normalized_sleeping_miners)};')
        html_parts.append(f'    const minerTypeSpecs = {json.dumps(miner_type_specs)};')
        html_parts.append(f'    const WHATSMINER_THRESHOLD = {WHATSMINER_GOOD_HASHRATE};')
        html_parts.append(f'    const ANTMINER_THRESHOLD = {ANTMINER_GOOD_HASHRATE};')

        # Generator data for JavaScript
        html_parts.append('    const genData = {')
        for pod_key, generators in site_pod_generators.items():
            for gen in generators:
                gen_id = gen.get("unit_id", "???")
                safe_json = json.dumps(gen, indent=2).replace("</", "<\\/")
                escaped_json = json.dumps(safe_json)
                html_parts.append(f'      "{gen_id}": {escaped_json},')
        html_parts.append('    };')

        # Pod to generators mapping - handle both exact matches and site-only matches
        html_parts.append('    const podGenerators = {')
        for pod_key, generators in site_pod_generators.items():
            if generators:
                unit_ids = [g["unit_id"] for g in generators]
                html_parts.append(f'      "{pod_key}": {json.dumps(unit_ids)},')
        html_parts.append('    };')

        # Site to generators mapping for site-level generators
        html_parts.append('    const siteGenerators = {')

        # First, add existing site-level generators (like Ely, JW, etc.)
        for pod_key, generators in site_pod_generators.items():
            if generators and " " not in pod_key:  # Site-level generators (no space in key)
                unit_ids = [g["unit_id"] for g in generators]
                html_parts.append(f'      "{pod_key}": {json.dumps(unit_ids)},')

        # Then, add grouped generators
        site_grouped_generators = {}
        for group_name, pod_names in gen_groups.items():
            if " " in group_name:  # e.g., "Alpha A"
                site_name = group_name.split(' ')[0]  # Extract "Alpha" from "Alpha A"

                # Collect all generators from the pods in this group
                all_unit_ids = []
                for pod_name in pod_names:
                    if pod_name in site_pod_generators:
                        unit_ids = [g["unit_id"] for g in site_pod_generators[pod_name]]
                        all_unit_ids.extend(unit_ids)

                if all_unit_ids:
                    if site_name not in site_grouped_generators:
                        site_grouped_generators[site_name] = []
                    site_grouped_generators[site_name].extend(all_unit_ids)

        # Add the grouped generators to the JavaScript object
        for site_name, unit_ids in site_grouped_generators.items():
            # Remove duplicates and add to mapping
            unique_ids = list(set(unit_ids))
            html_parts.append(f'      "{site_name}": {json.dumps(unique_ids)},')

        html_parts.append('    };')

        # Output group information for JavaScript
        html_parts.append('    const genGroups = ' + json.dumps(gen_groups) + ';')
        html_parts.append('    const podToGroup = ' + json.dumps(pod_to_group) + ';')

        html_parts.append('    let chartInstances = {};')

        # Add all the JavaScript functions from the original pods.py plus generator functions
        js_code = '''
    function showMobileTooltip(text, event) {
      const tooltip = document.getElementById('mobile-tooltip');
      tooltip.textContent = text;

      const rect = event.target.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      
      // Position tooltip above the clicked element
      let x = rect.left + rect.width / 2;
      let y = rect.top - 60;
      
      // Adjust position if tooltip would go off screen
      if (y < 0) {
        y = rect.bottom + 10; // Show below if no space above
      }
      
      // Ensure tooltip doesn't go off the sides
      const tooltipWidth = 200; // Approximate width
      if (x - tooltipWidth/2 < 10) {
        x = 10 + tooltipWidth/2;
      } else if (x + tooltipWidth/2 > viewportWidth - 10) {
        x = viewportWidth - 10 - tooltipWidth/2;
      }

      tooltip.style.position = 'fixed';
      tooltip.style.left = x + 'px';
      tooltip.style.top = y + 'px';
      tooltip.style.transform = 'translateX(-50%)';
      tooltip.style.zIndex = '10000';
      tooltip.style.display = 'block';

      setTimeout(() => {
        tooltip.style.display = 'none';
      }, 3000);
    }

    function showPodModal(podKey) {
      const modal = document.getElementById('podModal');
      const title = document.getElementById('podModalTitle');
      const stats = document.getElementById('podModalStats');
      const legend = document.getElementById('podModalLegend');
      const grid = document.getElementById('podModalGrid');

      // Calculate pod hashrate and stats
      const podHashrate = calculatePodHashrate(podKey);
      const podStats = calculatePodStats(podKey);
      
      // Set title
      title.innerHTML = `${podKey} - ${(podHashrate/1000).toFixed(2)}&nbsp;PH/s`;
      
      // Set stats
      stats.innerHTML = `Installed: ${podStats.installed}/${podStats.total} | Online: ${podStats.online}/${podStats.installed} | Hashing: ${podStats.hashing} | Sleeping: ${podStats.sleeping} | Low Hash: ${podStats.lowHash} | Zero Hash: ${podStats.zeroHash}`;
      
      // Set legend
      legend.innerHTML = `
        <div style="white-space: nowrap;">
          <span class="green" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Hashing
          <span style="margin: 0 8px;"></span>
          <span class="red" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Not Hashing
          <span style="margin: 0 8px;"></span>
          <span class="blue" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Sleeping
          <span style="margin: 0 8px;"></span>
          <span class="yellow" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Low Hash
          <span style="margin: 0 8px;"></span>
          <span class="white" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px; border: 1px solid #999;"></span>Offline
          <span style="margin: 0 8px;"></span>
          <span class="grey" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Empty
          <span style="margin: 0 8px;"></span>
          <span class="green-error" style="display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 4px;"></span>Has Errors
        </div>
      `;
      
      grid.innerHTML = generatePodModalGrid(podKey);
      modal.style.display = 'block';
    }

    function calculatePodHashrate(podKey) {
      let totalHashrate = 0;
      
      if (!podMiners[podKey]) {
        return 0;
      }
      
      for (const rowStr in podMiners[podKey]) {
        for (const posStr in podMiners[podKey][rowStr]) {
          // Check if position exists in inventory and has a miner installed
          const hasInventoryEntry = podInventory[podKey] && 
                                   podInventory[podKey][rowStr] && 
                                   podInventory[podKey][rowStr][posStr];
          
          if (hasInventoryEntry && podInventory[podKey][rowStr][posStr].installed) {
            const miner = podMiners[podKey][rowStr][posStr];
            totalHashrate += parseFloat(miner.hashrate) || 0;
          }
        }
      }
      
      return totalHashrate;
    }

    function calculatePodStats(podKey) {
      const inventory = podInventory[podKey] || {};
      const miners = podMiners[podKey] || {};
      
      const allRows = new Set();
      const allPositions = new Set();
      
      // Collect all row/position combinations
      Object.keys(inventory).forEach(row => {
        allRows.add(parseInt(row));
        Object.keys(inventory[row]).forEach(pos => {
          allPositions.add(parseInt(pos));
        });
      });
      
      Object.keys(miners).forEach(row => {
        allRows.add(parseInt(row));
        Object.keys(miners[row]).forEach(pos => {
          allPositions.add(parseInt(pos));
        });
      });
      
      if (allRows.size === 0 || allPositions.size === 0) {
        return { total: 0, installed: 0, online: 0, hashing: 0, sleeping: 0, lowHash: 0, zeroHash: 0 };
      }
      
      const minRow = Math.min(...allRows);
      const maxRow = Math.max(...allRows);
      const minPos = Math.min(...allPositions);
      const maxPos = Math.max(...allPositions);
      
      let totalSlots = 0;
      let installedCount = 0;
      let onlineCount = 0;
      let hashingCount = 0;
      let sleepingCount = 0;
      let lowHashCount = 0;
      let zeroHashCount = 0;
      
      for (let row = minRow; row <= maxRow; row++) {
        for (let pos = minPos; pos <= maxPos; pos++) {
          const rowStr = row.toString();
          const posStr = pos.toString();
          
          const hasInventory = inventory[rowStr] && inventory[rowStr][posStr];
          const hasMinerData = miners[rowStr] && miners[rowStr][posStr];
          
          if (hasInventory) {
            totalSlots++;
            if (inventory[rowStr][posStr].installed) {
              installedCount++;
              if (hasMinerData) {
                onlineCount++;
                const miner = miners[rowStr][posStr];
                const mac = (miner.mac || '').toLowerCase(); // Normalize to lowercase
                const hashrate = parseFloat(miner.hashrate) || 0;
                const minerType = (miner.miner_type || '').toLowerCase();
                
                // Check if sleeping first (same logic as color function)
                if (sleepingMiners.includes(mac)) {
                  sleepingCount++;
                } else if (hashrate === 0) {
                  zeroHashCount++;
                } else {
                  // Get dynamic threshold based on miner type spec
                  let goodThreshold;
                  if (minerTypeSpecs[minerType] && minerTypeSpecs[minerType].spec_hashrate) {
                    goodThreshold = minerTypeSpecs[minerType].spec_hashrate * 0.8; // 80% of spec
                  } else {
                    // Fallback to old logic if spec not found
                    goodThreshold = minerType.includes('whatsminer') ? WHATSMINER_THRESHOLD : 
                               (minerType.includes('antminer') || minerType.includes('jpro')) ? ANTMINER_THRESHOLD : 
                               WHATSMINER_THRESHOLD;
                  }
                  
                  if (hashrate >= goodThreshold) {
                    hashingCount++;
                  } else {
                    lowHashCount++;
                  }
                }
              }
            }
          }
        }
      }
      
      return {
        total: totalSlots,
        installed: installedCount,
        online: onlineCount,
        hashing: hashingCount,
        sleeping: sleepingCount,
        lowHash: lowHashCount,
        zeroHash: zeroHashCount
      };
    }

    function generatePodModalGrid(podKey) {
      const inventory = podInventory[podKey] || {};
      const miners = podMiners[podKey] || {};

      const allRows = new Set();
      const allPositions = new Set();

      Object.keys(inventory).forEach(row => {
        allRows.add(parseInt(row));
        Object.keys(inventory[row]).forEach(pos => {
          allPositions.add(parseInt(pos));
        });
      });

      Object.keys(miners).forEach(row => {
        allRows.add(parseInt(row));
        Object.keys(miners[row]).forEach(pos => {
          allPositions.add(parseInt(pos));
        });
      });

      if (allRows.size === 0 || allPositions.size === 0) {
        return '<div>No data</div>';
      }

      const minRow = Math.min(...allRows);
      const maxRow = Math.max(...allRows);
      const minPos = Math.min(...allPositions);
      const maxPos = Math.max(...allPositions);

      const numRows = maxRow - minRow + 1;
      const numCols = maxPos - minPos + 1;

      const availableWidth = window.innerWidth * 0.7;
      const availableHeight = window.innerHeight * 0.4;

      const maxSquareWidth = Math.floor(availableWidth / numCols) - 2;
      const maxSquareHeight = Math.floor(availableHeight / numRows) - 2;

      const squareSize = Math.max(6, Math.min(maxSquareWidth, maxSquareHeight, 30));

      let html = '<div class="pod-modal-grid">';

      for (let row = minRow; row <= maxRow; row++) {
        html += '<div class="pod-modal-row">';
        for (let pos = minPos; pos <= maxPos; pos++) {
          const color = getMinerColor(podKey, row, pos, inventory, miners);
          const tooltip = getMinerTooltip(podKey, row, pos, inventory, miners);
          html += `<div class="pod-modal-square ${color}" data-tooltip="${tooltip}" style="width:${squareSize}px;height:${squareSize}px;"></div>`;
        }
        html += '</div>';
      }

      html += '</div>';
      return html;
    }

    function getMinerColor(podKey, row, pos, inventory, miners) {
      const rowStr = row.toString();
      const posStr = pos.toString();

      if (!inventory[rowStr] || !inventory[rowStr][posStr]) {
        return 'grey';
      }

      if (!inventory[rowStr][posStr].installed) {
        return 'grey';
      }

      if (!miners[rowStr] || !miners[rowStr][posStr]) {
        return 'white';
      }

      const miner = miners[rowStr][posStr];
      const hashrate = parseFloat(miner.hashrate) || 0;
      const hasErrors = miner.error_codes && miner.error_codes.trim();
      const minerType = (miner.miner_type || '').toLowerCase();
      const mac = (miner.mac || '').toLowerCase(); // Normalize to lowercase

      // Check if miner is sleeping first - if in sleeping CSV, always blue
      if (sleepingMiners.includes(mac)) {
        return hasErrors ? 'blue-error' : 'blue';
      }

      // If not sleeping, determine color based on hashrate
      let baseColor;
      if (hashrate === 0) {
        baseColor = 'red'; // Zero hashrate and not sleeping
      } else {
        // Get dynamic threshold based on miner type spec
        let goodThreshold;
        if (minerTypeSpecs[minerType] && minerTypeSpecs[minerType].spec_hashrate) {
          goodThreshold = minerTypeSpecs[minerType].spec_hashrate * 0.8; // 80% of spec
        } else {
          // Fallback to old logic if spec not found
          goodThreshold = minerType.includes('whatsminer') ? WHATSMINER_THRESHOLD : 
                         (minerType.includes('antminer') || minerType.includes('jpro')) ? ANTMINER_THRESHOLD : 
                         WHATSMINER_THRESHOLD;
        }

        if (hashrate >= goodThreshold) {
          baseColor = 'green';
        } else {
          baseColor = 'yellow';  // Low hashrate but not zero
        }
      }

      return hasErrors ? baseColor + '-error' : baseColor;
    }

    function getMinerTooltip(podKey, row, pos, inventory, miners) {
      const rowStr = row.toString();
      const posStr = pos.toString();

      let tooltip = `Row ${row}, Pos ${pos}`;

      if (miners[rowStr] && miners[rowStr][posStr]) {
        const miner = miners[rowStr][posStr];
        tooltip += ` - ${miner.miner_type} - ${miner.hashrate}TH/s`;
        if (miner.error_codes && miner.error_codes.trim()) {
          tooltip += ` - Error: ${miner.error_codes}`;
        }
      } else if (inventory[rowStr] && inventory[rowStr][posStr]) {
        tooltip += ' - Offline';
      } else {
        tooltip += ' - Empty';
      }

      return tooltip;
    }

    function convertTimezone(dateString) {
      if (!dateString || typeof dateString !== 'string') return dateString;
      
      // Check if it's a timestamp with -06:00 timezone
      if (dateString.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-06:00$/)) {
        const date = new Date(dateString);
        const now = new Date();
        
        // Determine if we're in CDT (March 2nd Sunday to November 1st Sunday)
        const year = now.getFullYear();
        const marchSecondSunday = new Date(year, 2, 1);
        marchSecondSunday.setDate(marchSecondSunday.getDate() + (7 - marchSecondSunday.getDay()) + 7);
        const novemberFirstSunday = new Date(year, 10, 1);
        novemberFirstSunday.setDate(novemberFirstSunday.getDate() + (7 - novemberFirstSunday.getDay()));
        
        const isDST = now >= marchSecondSunday && now < novemberFirstSunday;
        
        if (isDST) {
          // Convert CST (-06:00) to CDT (-05:00) by adding 1 hour
          date.setHours(date.getHours() + 1);
        }
        
        // Format as readable date and time (MM/DD/YYYY HH:MM:SS)
        return date.toLocaleString("en-US", {
          month: '2-digit',
          day: '2-digit',
          year: 'numeric',
          hour: '2-digit',
          minute: '2-digit',
          second: '2-digit',
          hour12: false
        });
      }
      
      return dateString;
    }

    function showGenDetails(id) {
      const modal = document.getElementById("genModal");
      const modalData = document.getElementById("modalData");
      const gen = genData[id];

      if (!gen) {
        modalData.innerHTML = "<p>No generator data.</p>";
      } else {
        const obj = JSON.parse(gen);
        let rows = "";
        for (let key in obj) {
          if (obj[key] === null || obj[key] === "") continue;
          const label = key.replace(/_/g, " ").replace(/\\b\\w/g, c => c.toUpperCase());
          const value = convertTimezone(obj[key]);
          rows += `<tr><td>${label}</td><td>${value}</td></tr>`;
        }
        modalData.innerHTML = `<table class="gen-table">${rows}</table>`;
      }

      if (chartInstances["power"]) {
        chartInstances["power"].destroy();
        chartInstances["power"] = null;
      }
      if (chartInstances["gas"]) {
        chartInstances["gas"].destroy();
        chartInstances["gas"] = null;
      }

      document.getElementById("powerChart").style.display = "none";
      document.getElementById("gasChart").style.display = "none";
      modal.style.display = "block";

      fetchChart(id, 'power');
      fetchChart(id, 'gas');
    }

    async function fetchChart(id, type) {
      const gen = JSON.parse(genData[id]);
      const unitId = gen.unit_id;
      const days = 1;
      const field = type === "power" ? "power_avg" : "ng_pressure";

      try {
        const res = await fetch(`/api/gen_data?unit_id=${unitId}&field=${field}&days=${days}`);
        const result = await res.json();
        const labels = result.data.map(row => new Date(row.time).toLocaleString());
        const values = result.data.map(row => row.value);

        const canvasId = type === "power" ? "powerChart" : "gasChart";
        const canvas = document.getElementById(canvasId);
        if (!canvas) return console.error(`Canvas ${canvasId} not found`);

        canvas.style.display = "block";
        const ctx = canvas.getContext("2d");

        if (chartInstances[type]) chartInstances[type].destroy();
        chartInstances[type] = new Chart(ctx, {
          type: "line",
          data: {
            labels: labels,
            datasets: [{
              label: field.replace(/_/g, " ") + ` (${unitId})`,
              data: values,
              borderWidth: 1,
              pointRadius: 2,
              pointHoverRadius: 4,
              fill: true,
              tension: 0.3
            }]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            scales: {
              x: {
                display: true,
                title: { display: true, text: "Time"},
                ticks: {
                  callback: function(value, index, ticks) {
                    const date = new Date(this.getLabelForValue(value));
                    return date.toLocaleString("en-US", {
                      day: '2-digit',
                      month: '2-digit',
                      year: '2-digit',
                      hour: '2-digit',
                      minute: '2-digit',
                      hour12: false
                    }).replace(',', '');
                  }
                }
              },
              y: {
                display: true,
                title: { display: true, text: field.replace(/_/g, " ")}
              }
            }
          }
        });
      } catch (err) {
        console.error("Error fetching chart data:", err);
      }
    }

    async function showPodCharts(podKey) {
      const unitIds = podGenerators[podKey];
      if (!unitIds || unitIds.length === 0) return;

      const modal = document.getElementById("genModal");
      document.getElementById("modalData").innerHTML = `<h3>${podKey} Generators</h3>`;
      modal.style.display = "block";

      if (chartInstances["power"]) chartInstances["power"].destroy();
      if (chartInstances["gas"]) chartInstances["gas"].destroy();

      const days = 1;
      const powerDatasets = [];
      const gasDatasets = [];
      let labels = [];

      for (let i = 0; i < unitIds.length; i++) {
        const unitId = unitIds[i];
        try {
          for (const type of ["power_avg", "ng_pressure"]) {
            const res = await fetch(`/api/gen_data?unit_id=${unitId}&field=${type}&days=${days}`);
            const result = await res.json();

            if (i === 0) {
              labels = result.data.map(row => new Date(row.time).toLocaleString());
            }

            const values = result.data.map(row => row.value);
            const dataset = {
              label: `${type.replace(/_/g, " ")} (${unitId})`,
              data: values,
              borderWidth: 1,
              pointRadius: 2,
              pointHoverRadius: 4,
              fill: true,
              tension: 0.3
            };

            if (type === "power_avg") powerDatasets.push(dataset);
            else gasDatasets.push(dataset);
          }
        } catch (err) {
          console.error("Error fetching for", unitId, err);
        }
      }

      chartInstances["power"] = new Chart(document.getElementById("powerChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: powerDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "Power Avg" }}
          }
        }
      });

      chartInstances["gas"] = new Chart(document.getElementById("gasChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: gasDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "NG Pressure" }}
          }
        }
      });

      document.getElementById("powerChart").style.display = "block";
      document.getElementById("gasChart").style.display = "block";
    }

    async function showSiteCharts(siteName) {
      const unitIds = siteGenerators[siteName];
      if (!unitIds || unitIds.length === 0) return;

      const modal = document.getElementById("genModal");
      document.getElementById("modalData").innerHTML = `<h3>${siteName} Site Generators</h3>`;
      modal.style.display = "block";

      if (chartInstances["power"]) chartInstances["power"].destroy();
      if (chartInstances["gas"]) chartInstances["gas"].destroy();

      const days = 1;
      const powerDatasets = [];
      const gasDatasets = [];
      let labels = [];

      for (let i = 0; i < unitIds.length; i++) {
        const unitId = unitIds[i];
        try {
          for (const type of ["power_avg", "ng_pressure"]) {
            const res = await fetch(`/api/gen_data?unit_id=${unitId}&field=${type}&days=${days}`);
            const result = await res.json();

            if (i === 0) {
              labels = result.data.map(row => new Date(row.time).toLocaleString());
            }

            const values = result.data.map(row => row.value);
            const dataset = {
              label: `${type.replace(/_/g, " ")} (${unitId})`,
              data: values,
              borderWidth: 1,
              pointRadius: 2,
              pointHoverRadius: 4,
              fill: true,
              tension: 0.3
            };

            if (type === "power_avg") powerDatasets.push(dataset);
            else gasDatasets.push(dataset);
          }
        } catch (err) {
          console.error("Error fetching for", unitId, err);
        }
      }

      chartInstances["power"] = new Chart(document.getElementById("powerChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: powerDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "Power Avg" }}
          }
        }
      });

      chartInstances["gas"] = new Chart(document.getElementById("gasChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: gasDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "NG Pressure" }}
          }
        }
      });

      document.getElementById("powerChart").style.display = "block";
      document.getElementById("gasChart").style.display = "block";
    }
    
    async function showGroupCharts(groupName, unitIds) {
      if (!unitIds || unitIds.length === 0) return;

      const modal = document.getElementById("genModal");
      document.getElementById("modalData").innerHTML = `<h3>${groupName} Group Generators</h3>`;
      modal.style.display = "block";

      if (chartInstances["power"]) chartInstances["power"].destroy();
      if (chartInstances["gas"]) chartInstances["gas"].destroy();

      const days = 1;
      const powerDatasets = [];
      const gasDatasets = [];
      let labels = [];

      for (let i = 0; i < unitIds.length; i++) {
        const unitId = unitIds[i];
        try {
          for (const type of ["power_avg", "ng_pressure"]) {
            const res = await fetch(`/api/gen_data?unit_id=${unitId}&field=${type}&days=${days}`);
            const result = await res.json();

            if (i === 0) {
              labels = result.data.map(row => new Date(row.time).toLocaleString());
            }

            const values = result.data.map(row => row.value);
            const dataset = {
              label: `${type.replace(/_/g, " ")} (${unitId})`,
              data: values,
              borderWidth: 1,
              pointRadius: 2,
              pointHoverRadius: 4,
              fill: true,
              tension: 0.3
            };

            if (type === "power_avg") powerDatasets.push(dataset);
            else gasDatasets.push(dataset);
          }
        } catch (err) {
          console.error("Error fetching for", unitId, err);
        }
      }

      // CREATE POWER CHART
      chartInstances["power"] = new Chart(document.getElementById("powerChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: powerDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "Power Avg" }}  // FIXED: was "NG Pressure"
          }
        }
      });

      // CREATE GAS CHART 
      chartInstances["gas"] = new Chart(document.getElementById("gasChart").getContext("2d"), {
        type: "line",
        data: { labels, datasets: gasDatasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          scales: {
            x: {
              display: true,
              title: { display: true, text: "Time"},
              ticks: {
                callback: function(value, index, ticks) {
                  const date = new Date(this.getLabelForValue(value));
                  return date.toLocaleString("en-US", {
                    day: '2-digit',
                    month: '2-digit',
                    year: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  }).replace(',', '');
                }
              }
            },
            y: { title: { display: true, text: "NG Pressure" }}
          }
        }
      });

  document.getElementById("powerChart").style.display = "block";
  document.getElementById("gasChart").style.display = "block";
}    
    document.addEventListener('DOMContentLoaded', function() {
      const squares = document.querySelectorAll('.miner-square, .pod-modal-square');
      squares.forEach(square => {
        square.addEventListener('click', function(e) {
          const tooltipText = this.getAttribute('data-tooltip') || this.title;
          showMobileTooltip(tooltipText, e);
        });
      });
    });

    window.addEventListener('click', function(event) {
      const podModal = document.getElementById('podModal');
      const genModal = document.getElementById('genModal');
      if (event.target === podModal) {
        podModal.style.display = 'none';
      }
      if (event.target === genModal) {
        genModal.style.display = 'none';
      }
    });

    window.addEventListener('resize', function() {
      const modal = document.getElementById('podModal');
      if (modal.style.display === 'block') {
        const title = document.getElementById('podModalTitle');
        const currentPodKey = title.textContent.split(' - ')[0]; // Extract pod name before hashrate
        const grid = document.getElementById('podModalGrid');
        grid.innerHTML = generatePodModalGrid(currentPodKey);
      }
    });

    const observer = new MutationObserver(function(mutations) {
      const squares = document.querySelectorAll('.pod-modal-square');
      squares.forEach(square => {
        square.addEventListener('click', function(e) {
          const tooltipText = this.getAttribute('data-tooltip') || this.title;
          showMobileTooltip(tooltipText, e);
        });
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });
        '''

        html_parts.append(js_code)
        html_parts.append('  </script>')
        html_parts.append('</body>')
        html_parts.append('</html>')

        return '\n'.join(html_parts)

    except Exception as e:
        return f"<html><body><h1>Error in generate_html</h1><p>{str(e)}</p></body></html>"

if __name__ == "__main__":
    try:
        # Output HTTP headers
        print("Content-Type: text/html")
        print()  # Empty line to separate headers from content

        # Generate and output HTML
        html_content = generate_html()
        print(html_content)

    except Exception as e:
        print("Content-Type: text/html")
        print()
        import traceback
        print(f"<html><body><h1>Error</h1><pre>{traceback.format_exc()}</pre></body></html>")
