#!/usr/bin/env python3
"""Plucker - Web-based miner detection tool."""
import cgi
print("Content-Type: text/html\n")
print("""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLUCKER - Miner Detection</title>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
<link rel="stylesheet" href="/shared/miner_view.css">
<script src="/shared/miner_view.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    font-family: 'Consolas', 'Monaco', monospace;
    background: #1a1a1a;
    color: #d4d4d4;
    min-height: 100vh;
}

/* Header */
.header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 20px;
    background: #242424;
    border-bottom: 1px solid #333;
}
.header h1 {
    font-size: 22px;
    color: #569cd6;
    letter-spacing: 2px;
}
.status-badge {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
}
.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #ff4444;
}
.status-dot.connected { background: #6a9955; }
.status-dot.connecting { background: #d7ba7d; animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

/* Controls */
.controls {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 20px;
    background: #1e1e1e;
    border-bottom: 1px solid #2a2a2a;
}
.controls input, .controls select {
    font-family: inherit;
    font-size: 13px;
    padding: 6px 12px;
    background: #2d2d2d;
    border: 1px solid #444;
    color: #d4d4d4;
    border-radius: 4px;
}
.controls input { width: 180px; }
.controls select { min-width: 140px; }
.controls input:focus, .controls select:focus { outline: none; border-color: #569cd6; }
.controls button {
    font-family: inherit;
    font-size: 13px;
    padding: 6px 16px;
    background: #2d2d2d;
    border: 1px solid #444;
    color: #d4d4d4;
    border-radius: 4px;
    cursor: pointer;
}
.controls button:hover { background: #3d3d3d; border-color: #569cd6; }
.controls .sep {
    width: 1px;
    height: 24px;
    background: #444;
}
.controls .label {
    font-size: 12px;
    color: #888;
}
.controls .hint {
    font-size: 11px;
    color: #666;
    margin-left: auto;
}
.loc-input {
    width: 50px !important;
    text-align: center;
}
.loc-dash {
    color: #666;
    font-size: 14px;
}

/* Suppressed (chronic broadcaster) panel */
.suppressed {
    margin: 0 20px;
    background: #2a1f0e;
    border: 1px solid #5a4416;
    border-radius: 6px;
    font-size: 12px;
    color: #d7ba7d;
    overflow: hidden;
}
.suppressed-header {
    padding: 8px 12px;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 8px;
}
.suppressed-header:hover { background: #33270f; }
.suppressed-toggle { font-size: 10px; transition: transform 0.15s; }
.suppressed-toggle.open { transform: rotate(90deg); }
.suppressed-label { font-weight: 600; }
.suppressed-body {
    padding: 4px 8px 8px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 4px;
}
.suppressed-row {
    padding: 5px 8px;
    background: #1f1809;
    border: 1px solid #4a3812;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    gap: 8px;
    white-space: nowrap;
    overflow: hidden;
}
.suppressed-row:hover { background: #3a2c10; border-color: #7a5e1e; }
.suppressed-row .sr-loc { color: #e0c98a; font-weight: 600; }
.suppressed-row .sr-net { color: #9a8a5a; overflow: hidden; text-overflow: ellipsis; }

/* Detection feed */
.feed {
    padding: 16px 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.feed-empty {
    text-align: center;
    padding: 80px 20px;
    color: #555;
    font-size: 14px;
}
.feed-empty .big { font-size: 18px; color: #666; margin-bottom: 8px; }

/* Miner card */
.card {
    background: #242424;
    border: 1px solid #333;
    border-radius: 6px;
    overflow: hidden;
    animation: slideIn 0.3s ease;
}
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }

.card.scanning { border-color: #d7ba7d; }
.card.complete { border-color: #444; }
.card.error { border-color: #f44747; }

.card-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px;
    background: #2a2a2a;
    cursor: pointer;
}
.card-header:hover { background: #303030; }

.card-ip {
    font-size: 15px;
    font-weight: bold;
    color: #569cd6;
    min-width: 140px;
}
.card-mac { font-size: 12px; color: #888; min-width: 160px; }
.card-location { font-size: 12px; color: #d7ba7d; }
.card-model { font-size: 12px; color: #6a9955; }
.card-hashrate { font-size: 12px; color: #00ff00; }
.card-actions {
    display: flex;
    gap: 6px;
    margin-left: 8px;
}
.card-actions button {
    font-family: inherit;
    font-size: 11px;
    padding: 2px 10px;
    border-radius: 3px;
    border: 1px solid #555;
    background: #333;
    color: #d4d4d4;
    cursor: pointer;
}
.card-actions button:hover { border-color: #569cd6; background: #3d3d3d; }
.card-actions button.btn-reboot { border-color: #d7ba7d55; }
.card-actions button.btn-reboot:hover { border-color: #d7ba7d; color: #d7ba7d; }
.card-actions button.btn-pull { border-color: #f4474755; }
.card-actions button.btn-pull:hover { border-color: #f44747; color: #f44747; }
.card-status-badge {
    font-size: 11px;
    padding: 2px 8px;
    border-radius: 3px;
    margin-left: auto;
}
.badge-detected { background: #569cd633; color: #569cd6; }
.badge-scanning { background: #d7ba7d33; color: #d7ba7d; }
.badge-complete { background: #6a995533; color: #6a9955; }
.badge-error { background: #f4474733; color: #f44747; }
.card-time { font-size: 11px; color: #666; min-width: 60px; text-align: right; }

.card-body {
    display: none;
    padding: 0;
}
.card.expanded .card-body { display: block; }

/* Scan result sections */
.scan-msg {
    padding: 16px;
    text-align: center;
    color: #888;
    font-size: 13px;
}
.scan-msg.error { color: #f44747; }

/* Plucker card chart (shorter than modal chart) */
.chart-container {
    padding: 12px 14px;
    background: #1e1e1e;
    border-bottom: 1px solid #2a2a2a;
}
.chart-container canvas {
    width: 100% !important;
    height: 120px !important;
}
.chart-label {
    font-size: 11px;
    color: #888;
    margin-bottom: 4px;
}

/* Raw UDP section */
.raw-section {
    padding: 10px 14px;
    background: #1e1e1e;
    border-top: 1px solid #2a2a2a;
}
.raw-section summary {
    font-size: 11px;
    color: #666;
    cursor: pointer;
}
.raw-section pre {
    font-size: 11px;
    color: #888;
    margin-top: 6px;
    white-space: pre-wrap;
    word-break: break-all;
}

/* Toast */
.toast {
    position: fixed;
    bottom: 20px;
    right: 20px;
    padding: 10px 16px;
    background: #2d2d2d;
    border: 1px solid #444;
    border-radius: 4px;
    font-size: 13px;
    z-index: 1000;
    animation: fadeInOut 3s forwards;
}
@keyframes fadeInOut {
    0% { opacity: 0; transform: translateY(10px); }
    10% { opacity: 1; transform: translateY(0); }
    80% { opacity: 1; }
    100% { opacity: 0; }
}
</style>
</head>
<body>

<div class="header">
    <h1>PLUCKER</h1>
    <div class="status-badge">
        <span id="statusText">Connecting...</span>
        <div id="statusDot" class="status-dot connecting"></div>
    </div>
</div>

<div class="controls">
    <span class="label">IP</span>
    <input type="text" id="manualIp" placeholder="IP address" onkeydown="if(event.key==='Enter')manualScan()">
    <button onclick="manualScan()">Scan</button>
    <div class="sep"></div>
    <span class="label">Location</span>
    <select id="podSelect"><option value="">Pod</option></select>
    <input type="text" id="locRow" class="loc-input" placeholder="Row" maxlength="2">
    <span class="loc-dash">-</span>
    <input type="text" id="locIndex" class="loc-input" placeholder="Idx" maxlength="3" onkeydown="if(event.key==='Enter')locationScan()">
    <button onclick="locationScan()">Find</button>
    <div class="sep"></div>
    <button onclick="clearFeed()">Clear</button>
    <span class="hint">Listening for Whatsminer IP Found broadcasts on UDP port 8888</span>
</div>

<div class="suppressed" id="suppressed" style="display:none">
    <div class="suppressed-header" onclick="toggleSuppressed()">
        <span class="suppressed-toggle" id="suppressedToggle">&#9656;</span>
        <span class="suppressed-label" id="suppressedTitle">Suppressed</span>
    </div>
    <div class="suppressed-body" id="suppressedBody" style="display:none"></div>
</div>

<div class="feed" id="feed">
    <div class="feed-empty" id="feedEmpty">
        <div class="big">Waiting for miners...</div>
        <div>Press the IP Found button on a miner, or enter an IP above to scan manually</div>
    </div>
</div>

<script>
// Use proxied path through Apache (works with HTTPS)
const API_BASE = window.location.origin;
const SOCKET_PATH = '/plucker/socket.io/';
let socket = null;
let cards = {};  // ip -> card element
let cardData = {};  // ip -> scan data

function connect() {
    setStatus('Connecting...', 'connecting');

    socket = io(API_BASE, {
        path: SOCKET_PATH,
        transports: ['websocket', 'polling'],
        reconnection: true,
        reconnectionDelay: 2000,
    });

    socket.on('connect', () => {
        setStatus('Connected - Listening', 'connected');
    });

    socket.on('disconnect', () => {
        setStatus('Disconnected', 'disconnected');
    });

    socket.on('connect_error', () => {
        setStatus('Connection error', 'disconnected');
    });

    socket.on('connect', () => {
        // Pull the current suppressed list on (re)connect
        fetch(API_BASE + '/api/plucker/status')
            .then(r => r.json())
            .then(d => renderMuted(d.muted || []))
            .catch(() => {});
    });

    socket.on('miner_detected', (data) => {
        addDetection(data);
    });

    socket.on('muted_update', (data) => {
        renderMuted(data.muted || []);
    });

    socket.on('scan_started', (data) => {
        updateCardStatus(data.ip, 'scanning');
    });

    socket.on('scan_result', (data) => {
        showScanResult(data);
    });

    socket.on('scan_error', (data) => {
        showScanError(data.ip, data.error);
    });
}

function setStatus(text, state) {
    document.getElementById('statusText').textContent = text;
    const dot = document.getElementById('statusDot');
    dot.className = 'status-dot';
    if (state === 'connected') dot.classList.add('connected');
    else if (state === 'connecting') dot.classList.add('connecting');
}

function toggleSuppressed() {
    const body = document.getElementById('suppressedBody');
    const tog = document.getElementById('suppressedToggle');
    const open = body.style.display === 'none';
    body.style.display = open ? 'grid' : 'none';
    tog.classList.toggle('open', open);
}

function renderMuted(list) {
    const bar = document.getElementById('suppressed');
    const title = document.getElementById('suppressedTitle');
    const body = document.getElementById('suppressedBody');
    if (!list || !list.length) {
        bar.style.display = 'none';
        body.innerHTML = '';
        return;
    }
    bar.style.display = '';
    title.textContent = `Suppressed — ${list.length} stuck IP-report${list.length === 1 ? '' : 's'} (click to view)`;

    // Sort by pod then position so they group sensibly
    list.sort((a, b) => (a.pod || 'zzz').localeCompare(b.pod || 'zzz') ||
                        (a.position || '').localeCompare(b.position || ''));

    body.innerHTML = '';
    list.forEach(m => {
        const loc = m.pod ? (m.pod + (m.position ? ' · ' + m.position : '')) : 'unknown loc';
        const row = document.createElement('div');
        row.className = 'suppressed-row';
        row.title = 'Scan ' + m.ip + (m.mac ? ' (' + m.mac + ')' : '');
        row.innerHTML = `<span class="sr-loc">${loc}</span><span class="sr-net">${m.ip}</span>`;
        row.onclick = () => scanIp(m.ip);
        body.appendChild(row);

        // Drop any lingering feed card for a now-muted device
        if (cards[m.ip]) {
            cards[m.ip].remove();
            delete cards[m.ip];
            delete cardData[m.ip];
        }
    });
}

function addDetection(det) {
    document.getElementById('feedEmpty').style.display = 'none';
    const feed = document.getElementById('feed');

    // If card exists for this IP, update it
    if (cards[det.ip]) {
        updateExistingCard(det);
        return;
    }

    const card = document.createElement('div');
    card.className = 'card';
    card.id = 'card-' + det.ip.replace(/\\./g, '-');

    const time = new Date(det.timestamp).toLocaleTimeString();

    card.innerHTML = `
        <div class="card-header" onclick="toggleCard('${det.ip}')">
            <span class="card-ip">${det.ip}</span>
            <span class="card-mac">${det.mac || 'detecting...'}</span>
            <span class="card-location" id="loc-${det.ip.replace(/\\./g, '-')}"></span>
            <span class="card-model" id="model-${det.ip.replace(/\\./g, '-')}"></span>
            <span class="card-hashrate" id="hash-${det.ip.replace(/\\./g, '-')}"></span>
            <span class="card-actions" id="actions-${det.ip.replace(/\\./g, '-')}" style="display:none">
                <button class="btn-reboot" onclick="event.stopPropagation();rebootMiner('${det.ip}')">Reboot</button>
                <button class="btn-pull" onclick="event.stopPropagation();pullMiner('${det.ip}')">Pull</button>
            </span>
            <span class="card-status-badge badge-detected" id="badge-${det.ip.replace(/\\./g, '-')}">DETECTED</span>
            <span class="card-time">${time}</span>
        </div>
        <div class="card-body" id="body-${det.ip.replace(/\\./g, '-')}">
            <div class="scan-msg">Scanning miner...</div>
        </div>
        <div class="raw-section">
            <details>
                <summary>Raw UDP (${det.bytes} bytes, port ${det.port} ${det.source})</summary>
                <pre>${det.raw_text}\\n\\nHex: ${det.raw_hex}</pre>
            </details>
        </div>
    `;

    feed.insertBefore(card, feed.children[1] || null);  // After feedEmpty
    cards[det.ip] = card;

    // Auto-expand the newest card
    card.classList.add('expanded');
}

function updateExistingCard(det) {
    const card = cards[det.ip];
    if (!card) return;
    const time = new Date(det.timestamp).toLocaleTimeString();
    const id = det.ip.replace(/\\./g, '-');
    const badge = document.getElementById('badge-' + id);
    if (badge) {
        badge.textContent = 'DETECTED';
        badge.className = 'card-status-badge badge-detected';
    }
    // Move card to top
    const feed = document.getElementById('feed');
    feed.insertBefore(card, feed.children[1] || null);
}

let scanTimeouts = {};  // ip -> timeout id

function updateCardStatus(ip, status) {
    const id = ip.replace(/\\./g, '-');
    const card = cards[ip];
    if (!card) return;

    const badge = document.getElementById('badge-' + id);
    if (status === 'scanning') {
        card.className = 'card scanning expanded';
        if (badge) { badge.textContent = 'SCANNING'; badge.className = 'card-status-badge badge-scanning'; }
        const body = document.getElementById('body-' + id);
        if (body) body.innerHTML = '<div class="scan-msg">Detecting firmware and collecting data...</div>';

        // Start scan timeout - show error if no result within 15 seconds
        if (scanTimeouts[ip]) clearTimeout(scanTimeouts[ip]);
        scanTimeouts[ip] = setTimeout(() => {
            delete scanTimeouts[ip];
            // Only fire if card is still in scanning state
            if (card.classList.contains('scanning')) {
                showScanError(ip, 'Miner could not be reached (timed out)');
            }
        }, 15000);
    }
}

function toggleCard(ip) {
    const card = cards[ip];
    if (card) card.classList.toggle('expanded');
}

function showScanResult(data) {
    const ip = data.ip;
    if (scanTimeouts[ip]) { clearTimeout(scanTimeouts[ip]); delete scanTimeouts[ip]; }
    const id = ip.replace(/\\./g, '-');
    const card = cards[ip];
    if (!card) return;

    cardData[ip] = data;
    card.className = 'card complete expanded';

    const badge = document.getElementById('badge-' + id);
    if (badge) { badge.textContent = 'COMPLETE'; badge.className = 'card-status-badge badge-complete'; }

    // Update header quick info
    const modelEl = document.getElementById('model-' + id);
    if (modelEl && data.model) modelEl.textContent = data.model;

    const hashEl = document.getElementById('hash-' + id);
    if (hashEl) {
        const hr = getHashrateDisplay(data);
        if (hr) hashEl.textContent = hr;
    }

    // Update MAC if we got it from scan
    if (data.mac) {
        const macEl = card.querySelector('.card-mac');
        if (macEl) macEl.textContent = data.mac;
    }

    // Update location in header
    if (data.pod || data.position) {
        const locEl = document.getElementById('loc-' + id);
        if (locEl) {
            let locText = data.pod || '';
            if (data.position) {
                // Strip pod number prefix from position: "2-3-06" -> "3-06"
                const pos = data.position.replace(/^\d+-/, '');
                locText += (locText ? ' ' : '') + pos;
            }
            locEl.textContent = locText;
        }
    }

    // Show action buttons
    const actionsEl = document.getElementById('actions-' + id);
    if (actionsEl) actionsEl.style.display = 'flex';

    // Build result body with chart placeholder
    const body = document.getElementById('body-' + id);
    if (body) {
        const chartId = 'chart-' + id;
        body.innerHTML = '<div class="chart-container"><div class="chart-label">24h Hashrate</div><canvas id="' + chartId + '"></canvas></div>' + buildResultHTML(data);

        // Fetch hashrate history if we have a MAC
        const mac = data.mac;
        if (mac) loadHashrateChart(mac, chartId);
    }
}

function showScanError(ip, error) {
    if (scanTimeouts[ip]) { clearTimeout(scanTimeouts[ip]); delete scanTimeouts[ip]; }
    const id = ip.replace(/\\./g, '-');
    const card = cards[ip];
    if (!card) return;

    card.className = 'card error expanded';
    const badge = document.getElementById('badge-' + id);
    if (badge) { badge.textContent = 'ERROR'; badge.className = 'card-status-badge badge-error'; }

    const body = document.getElementById('body-' + id);
    if (body) body.innerHTML = `<div class="scan-msg error">Scan failed: ${error}</div>`;
}

// ---------------------------------------------------------------------------
// Format helpers (delegates to shared MinerView)
// ---------------------------------------------------------------------------
function getHashrateDisplay(d) {
    if (d.firmware_type === 'whatsminer') {
        return MinerView.fmtHash(d.hashrate_mhs_5s || d.hashrate_mhs_avg, 'MH/s');
    }
    return MinerView.fmtHash(d.hashrate_ghs_5s || d.hashrate_ghs_15s || d.hashrate_ghs_avg, 'GH/s');
}

// ---------------------------------------------------------------------------
// Build result HTML
// ---------------------------------------------------------------------------
function buildResultHTML(d) {
    return MinerView.buildDetailHTML(d);
}

// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
function scanIp(ip) {
    if (!ip) return;

    // Create a card for the scan (or reuse existing) and kick off a scan.
    // Shared by manual IP entry and the suppressed-list click-to-inspect.
    addDetection({
        ip: ip,
        mac: null,
        port: 'manual',
        source: 'manual',
        raw_hex: '',
        raw_text: 'Manual scan',
        timestamp: new Date().toISOString(),
        bytes: 0,
    });
    updateCardStatus(ip, 'scanning');

    // Request scan via WebSocket
    if (socket && socket.connected) {
        socket.emit('request_scan', {ip: ip});
    } else {
        // Fallback to HTTP
        fetch('/api/plucker/scan', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({ip: ip}),
        }).catch(e => showScanError(ip, e.message));
    }
}

function manualScan() {
    const input = document.getElementById('manualIp');
    const ip = input.value.trim();
    if (!ip) return;
    scanIp(ip);
    input.value = '';
}

function clearFeed() {
    cards = {};
    cardData = {};
    const feed = document.getElementById('feed');
    feed.innerHTML = '<div class="feed-empty" id="feedEmpty"><div class="big">Waiting for miners...</div><div>Press the IP Found button on a miner, or enter an IP above to scan manually</div></div>';
}

// ---------------------------------------------------------------------------
// Miner actions
// ---------------------------------------------------------------------------
function rebootMiner(ip) {
    if (!confirm('Reboot miner at ' + ip + '?')) return;

    fetch('/api/plucker/reboot', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ ip: ip }),
    })
    .then(r => r.json())
    .then(data => {
        const id = ip.replace(/\./g, '-');
        const badge = document.getElementById('badge-' + id);
        if (badge) { badge.textContent = 'REBOOTING'; badge.className = 'card-status-badge badge-scanning'; }
    })
    .catch(e => alert('Reboot failed: ' + e.message));
}

function pullMiner(ip) {
    const data = cardData[ip];
    if (!data) { alert('No scan data for this miner'); return; }
    if (!data.serial && !data.mac) { alert('No serial or MAC for this miner — cannot pull.'); return; }

    if (!confirm('Pull ' + (data.model || 'miner') + ' / ' + (data.serial || data.mac) + ' from inventory?')) return;

    // The inventory API authenticates via the ngon_auth cookie (same-origin
    // request sends it automatically) and identifies the operator from it —
    // no initials needed.
    fetch('/api/plucker/pull', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            serial: data.serial || '',
            mac: data.mac || ''
        }),
    })
    .then(async r => {
        const result = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(result.error || ('HTTP ' + r.status));
        return result;
    })
    .then(result => {
        const id = ip.replace(/\./g, '-');
        const badge = document.getElementById('badge-' + id);
        if (badge) { badge.textContent = 'PULLED'; badge.className = 'card-status-badge badge-error'; }
    })
    .catch(e => alert('Pull failed: ' + e.message));
}

// ---------------------------------------------------------------------------
// Hashrate chart (delegates to shared MinerView)
// ---------------------------------------------------------------------------
function loadHashrateChart(mac, canvasId) {
    // Plucker creates its own canvas in the card, so don't pass container
    // (MinerView would replace container contents and lose the chart-label)
    MinerView.loadHistoryChart(mac, canvasId);
}

// ---------------------------------------------------------------------------
// Location lookup
// ---------------------------------------------------------------------------
function loadPods() {
    fetch('/api/plucker/pods')
        .then(r => r.json())
        .then(pods => {
            const sel = document.getElementById('podSelect');
            pods.forEach(p => {
                const opt = document.createElement('option');
                opt.value = p;
                opt.textContent = p;
                sel.appendChild(opt);
            });
        })
        .catch(e => console.error('Failed to load pods:', e));
}

function locationScan() {
    const pod = document.getElementById('podSelect').value;
    const row = document.getElementById('locRow').value.trim();
    const idx = document.getElementById('locIndex').value.trim();

    if (!pod) { alert('Select a pod'); return; }
    if (!row || !idx) { alert('Enter row and index'); return; }

    // Pad index to 2 digits if needed
    const paddedIdx = idx.padStart(2, '0');
    const location = row + '-' + paddedIdx;

    fetch(`/api/plucker/lookup?pod=${encodeURIComponent(pod)}&location=${encodeURIComponent(location)}`)
        .then(r => r.json())
        .then(data => {
            if (data.found) {
                const m = data.miner;
                // Create detection card and auto-scan
                addDetection({
                    ip: m.ip,
                    mac: m.mac || null,
                    port: 'lookup',
                    source: `${pod} ${location}`,
                    raw_hex: '',
                    raw_text: `Location lookup: ${pod} pos ${data.position}`,
                    timestamp: new Date().toISOString(),
                    bytes: 0,
                });
                updateCardStatus(m.ip, 'scanning');
                if (socket && socket.connected) {
                    socket.emit('request_scan', {ip: m.ip});
                } else {
                    fetch('/api/plucker/scan', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({ip: m.ip}),
                    }).catch(e => showScanError(m.ip, e.message));
                }
            } else {
                alert(`No miner found at ${pod} ${data.position}`);
            }
        })
        .catch(e => alert('Lookup failed: ' + e.message));
}

// Start
loadPods();
connect();
</script>
</body>
</html>""")
