GNU/_PAGE/monitoring/upbit/demon_platform.php
<?php
// ==================================================================
// 설정 및 DB 연결
// ==================================================================
error_reporting(E_ALL);
ini_set('display_errors', 1);

try {
    require '/home/www/DB/db_upbit.php';
    $pdo = $db_upbit;
} catch (Exception $e) {
    die("데이터베이스 연결 실패: " . $e->getMessage());
}
/** @var PDO $pdo */

$pdo_gnu = null;
try {
    require '/home/www/DB/db_gnu.php';
    if (isset($db_gnu) && $db_gnu instanceof PDO) {
        $pdo_gnu = $db_gnu;
    } elseif (isset($pdo_gnu) && $pdo_gnu instanceof PDO) {
        $pdo_gnu = $pdo_gnu;
    } elseif (isset($pdo) && $pdo instanceof PDO) {
        $pdo_gnu = $pdo;
    }
} catch (Exception $e) {
    $pdo_gnu = null;
}
/** @var PDO|null $pdo_gnu */

// ==================================================================
// 헬퍼 함수 (기존 코드 유지)
// ==================================================================
function getTimeDiff($datetime) {
    if (!$datetime) return 'N/A';
    try {
        $now = new DateTime();
        $ago = new DateTime($datetime);
        $diff = $now->diff($ago);
        if ($diff->d > 0) return $diff->d . '일 전';
        if ($diff->h > 0) return $diff->h . '시간 전';
        if ($diff->i > 0) return $diff->i . '분 전';
        return $diff->s . '초 전';
    } catch (Exception $e) { return '형식 오류'; }
}

function isHeartbeatDead($datetime) {
    if (!$datetime) return true;
    $now = new DateTime();
    $ago = new DateTime($datetime);
    $diff = $now->getTimestamp() - $ago->getTimestamp();
    return $diff > 300; 
}

function getBybitBestSymbols($pdo_gnu) {
    static $symbols = null;

    if ($symbols !== null) {
        return $symbols;
    }

    $symbols = [];
    if (!($pdo_gnu instanceof PDO)) {
        return $symbols;
    }

    try {
        $sql = "SELECT wr_subject FROM g5_write_daemon_best_bybit WHERE (x2_run = 1 OR x2_run = '1')";
        $stmt = $pdo_gnu->query($sql);
        $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);

        foreach ($rows as $symbol) {
            $symbol = strtoupper(trim((string)$symbol));
            if ($symbol === '') {
                continue;
            }
            if (strpos($symbol, 'USDT') === false && strpos($symbol, '-') === false) {
                $symbol .= 'USDT';
            }
            if (!in_array($symbol, $symbols, true)) {
                $symbols[] = $symbol;
            }
        }
    } catch (Exception $e) {
        return [];
    }

    return $symbols;
}

function getDaemonMetrics($pdo, $pdo_gnu, $daemon_id) {
    $default = [
        'total_count_raw' => 0,
        'total_count_str' => '-',
        'last_trade' => '데이터 없음',
        'trade_diff' => ''
    ];

    $config_map = [
        'daemon_upbit_Ticker' => [
            'table' => 'daemon_upbit_Ticker',
            'date_column' => 'collected_at',
            'where_sql' => "WHERE market = ?",
            'params' => ['KRW-BCH']
        ],
        'daemon_upbit_Best' => [
            'table' => 'daemon_upbit_Ticker',
            'date_column' => 'collected_at',
            'where_sql' => "WHERE market = ?",
            'params' => ['KRW-BTC']
        ],
        'daemon_upbit_Ticker_user' => [
            'table' => 'daemon_upbit_Ticker_user',
            'date_column' => 'collected_at',
            'where_sql' => '',
            'params' => []
        ],
        'daemon_bybit_Ticker' => [
            'table' => 'daemon_bybit_Ticker',
            'date_column' => 'updated_at',
            'where_sql' => '',
            'params' => []
        ]
    ];

    if ($daemon_id === 'daemon_bybit_Best') {
        $symbols = getBybitBestSymbols($pdo_gnu);
        if (!$symbols) {
            return $default;
        }

        $placeholders = implode(', ', array_fill(0, count($symbols), '?'));
        $config = [
            'table' => 'daemon_bybit_Ticker',
            'date_column' => 'updated_at',
            'where_sql' => "WHERE symbol IN ($placeholders)",
            'params' => $symbols
        ];
    } else {
        if (!isset($config_map[$daemon_id])) {
            return $default;
        }
        $config = $config_map[$daemon_id];
    }

    try {
        $sql = "SELECT COUNT(*) AS total_cnt, MAX({$config['date_column']}) AS last_trade FROM {$config['table']} {$config['where_sql']}";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($config['params']);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$row) {
            return $default;
        }

        $total_count_raw = (int)($row['total_cnt'] ?? 0);
        $last_trade = $row['last_trade'] ?: '데이터 없음';

        return [
            'total_count_raw' => $total_count_raw,
            'total_count_str' => $total_count_raw > 0 ? number_format($total_count_raw) : '0',
            'last_trade' => $last_trade,
            'trade_diff' => $row['last_trade'] ? getTimeDiff($row['last_trade']) : ''
        ];
    } catch (Exception $e) {
        return $default;
    }
}

// ==================================================================
// 실시간 데이터를 위한 API 모드 (기존 로직 유지)
// ==================================================================
if (isset($_GET['api'])) {
    header('Content-Type: application/json');
    $target_ids = ['daemon_upbit_Ticker', 'daemon_upbit_Ticker_user', 'daemon_upbit_Best', 'daemon_bybit_Ticker', 'daemon_bybit_Best'];
    $in_clause = "'" . implode("','", $target_ids) . "'";
    $sql = "SELECT * FROM daemon_record WHERE d_id IN ($in_clause) ORDER BY FIELD(d_id, $in_clause)";
    $stmt = $pdo->query($sql);
    $daemons = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $result = [];
    foreach ($daemons as $daemon) {
        $is_dead = isHeartbeatDead($daemon['d_heartbeat']);
        $metrics = getDaemonMetrics($pdo, $pdo_gnu, $daemon['d_id']);

        $result[] = [
            'd_id' => $daemon['d_id'],
            'd_status' => $daemon['d_status'],
            'is_dead' => $is_dead,
            'total_count' => $metrics['total_count_str'],
            'total_count_raw' => $metrics['total_count_raw'],
            'last_trade' => $metrics['last_trade'],
            'trade_diff' => $metrics['trade_diff'],
            'heartbeat_diff' => getTimeDiff($daemon['d_heartbeat']),
            'd_pid' => $daemon['d_pid'] ?: '-',
            'd_kill_flag' => (bool)$daemon['d_kill_flag']
        ];
    }
    echo json_encode($result);
    exit;
}

// 초기 데이터 로드
$target_ids = ['daemon_upbit_Ticker', 'daemon_upbit_Ticker_user', 'daemon_upbit_Best', 'daemon_bybit_Ticker', 'daemon_bybit_Best'];
$in_clause = "'" . implode("','", $target_ids) . "'";
$sql = "SELECT * FROM daemon_record WHERE d_id IN ($in_clause) ORDER BY FIELD(d_id, $in_clause)";
$stmt = $pdo->query($sql);
$daemons = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 헤더 부분 포함
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>핵심 데몬 가동 현황</title>
    <!-- 아이콘과 폰트만 외부 호출 허용 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
        
        /* CSS 변수 설정 */
        :root {
            --bg-body: #f1f5f9;
            --bg-card: #ffffff;
            --bg-metric: #f8fafc;
            --text-main: #1e293b;
            --text-sub: #64748b;
            --indigo: #6366f1;
            --border-card: #e2e8f0;
        }

        .dark {
            --bg-body: #0a0f1d;
            --bg-card: #161b2c;
            --bg-metric: rgba(13, 17, 29, 0.5);
            --text-main: #f1f5f9;
            --text-sub: #94a3b8;
            --indigo: #818cf8;
            --border-card: rgba(30, 41, 59, 0.5);
        }

        body { 
            font-family: 'Pretendard', sans-serif; 
            font-style: normal !important; 
            background-color: var(--bg-body);
            margin: 0;
            padding: 0px 0px 40px 0px; /* px-[60px] 요구사항 반영 */
            overflow-x: hidden;
            transition: background-color 0.3s;
        }

        * { font-style: normal !important; box-sizing: border-box; }

        .max-w-full { width: 100vw; padding: 30px 50px 0px 50px; }
        .page-enter { animation: enter 0.6s cubic-bezier(0.16, 1, 0.3, 1); }
        @keyframes enter { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } }

        /* Header Styles */
        header { margin-bottom: 20px; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; gap: 1.5rem; }
        @media (min-width: 768px) { header { flex-direction: row; } }
        
        header h1 { font-size: 2.25rem; font-weight: 900; color: var(--text-main); margin: 0; letter-spacing: -0.05em; }
        header h1 span { color: #6366f1; }
        .header-desc { font-size: 1.125rem; color: var(--text-sub); margin-top: 0.75rem; font-weight: 500; display: flex; align-items: center; }
        #update-timer { margin-left: 0.75rem; font-size: 0.875rem; background: #e2e8f0; color: #64748b; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-family: monospace; }
        .dark #update-timer { background: #1e293b; color: #94a3b8; }

        /* Grid System */
        .grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
        @media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }

        /* Card Styles */
        .daemon-card { 
            position: relative; 
            display: flex; 
            flex-direction: column; 
            height: 100%; 
            background-color: var(--bg-card); 
            border-radius: 1.5rem; 
            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); 
            transition: all 0.3s; 
            border: 1px solid var(--border-card);
            overflow: hidden;
        }
        .daemon-card:hover { transform: translateY(-5px); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); }

        .kill-flag-overlay { 
            position: absolute; top: 0; right: 0; background: #dc2626; color: white; 
            font-size: 0.75rem; font-weight: 700; padding: 0.5rem 1rem; 
            border-bottom-left-radius: 1rem; z-index: 10; 
        }

        .card-body { padding: 2.5rem; flex-grow: 1; }
        .card-head-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; }
        .card-info-group { display: flex; align-items: center; gap: 1.5rem; }

        .icon-box { 
            width: 4rem; height: 4rem; border-radius: 1rem; 
            display: flex; align-items: center; justify-content: center; 
            font-size: 1.5rem; background: #1e293b; color: #94a3b8; border: 1px solid #334155;
        }

        .card-title { font-size: 1.5rem; font-weight: 800; color: var(--text-main); margin: 0; line-height: 1.2; }
        .card-id { font-size: 0.875rem; color: var(--text-sub); font-family: monospace; margin-top: 0.25rem; }

        .status-badge { padding: 0.5rem 1.25rem; border-radius: 0.75rem; font-size: 0.875rem; font-weight: 900; }

        /* Metric Box */
        .metric-box { background-color: var(--bg-metric); border-radius: 1rem; padding: 1.5rem; border: 1px solid var(--border-card); margin-bottom: 1.5rem; }
        .metric-label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
        .metric-label { font-size: 0.875rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; }
        .mode-badge { font-size: 0.75rem; font-weight: 900; padding: 0.25rem 0.75rem; border-radius: 0.5rem; border: 1px solid transparent; }

        .total-cnt { font-size: 2.25rem; font-weight: 900; color: var(--indigo); letter-spacing: -0.05em; }
        .cnt-unit { font-size: 1.125rem; font-weight: 700; color: var(--text-sub); margin-left: 0.5rem; }

        /* Details */
        .details-container { display: flex; flex-direction: column; gap: 1.5rem; }
        .detail-label-row { display: flex; justify-content: space-between; align-items: end; margin-bottom: 0.5rem; }
        .detail-label { font-size: 0.75rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; }
        .detail-diff { font-size: 0.75rem; font-weight: 900; color: var(--indigo); }
        .detail-value-box { font-family: monospace; font-size: 1rem; font-weight: 600; color: var(--text-main); background: var(--bg-metric); padding: 0.75rem 1rem; border-radius: 0.75rem; border: 1px solid var(--border-card); }

        .meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 0.5rem; }
        .meta-label { font-size: 0.75rem; font-weight: 700; color: var(--text-sub); text-transform: uppercase; display: block; margin-bottom: 0.25rem; }
        .meta-value { font-family: monospace; font-size: 0.875rem; font-weight: 700; color: var(--indigo); }
        .meta-value.gray { color: var(--text-sub); }

        /* Card Footer */
        .card-footer { background-color: var(--bg-metric); padding: 1.25rem 2.5rem; border-top: 1px solid var(--border-card); display: flex; justify-content: space-between; align-items: center; }
        .hb-row { display: flex; align-items: center; gap: 0.75rem; color: var(--text-sub); font-weight: 700; font-size: 0.875rem; }
        .hb-value { color: #22d3ee; font-weight: 900; font-size: 1.25rem; letter-spacing: -0.025em; }
        .start-time { font-family: monospace; font-size: 0.75rem; color: var(--text-sub); font-weight: 700; opacity: 0.7; }

        /* Buttons */
        .btn-container { margin-top: 25px; display: flex; flex-direction: column; align-items: center; font-size: 18px; }
        .btn-primary { 
            background-color: #4f46e5; color: white; font-weight: 600; padding: 0.75rem 2rem; 
            border-radius: 9999px; text-decoration: none; box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
            transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem;
        }
        .btn-primary:hover { background-color: #4338ca; transform: scale(1.05); }

        /* Utility */
        .blink { animation: blinker 1.5s linear infinite; }
        @keyframes blinker { 50% { opacity: 0; } }
        .update-flash { animation: flash 1s ease-out; }
        @keyframes flash { 0% { background-color: rgba(99, 102, 241, 0.2); } 100% { background-color: transparent; } }

    </style>
</head>
<body class="bg-slate-100">

    <div class="max-w-full page-enter">
        <!-- Header -->
        <header>
            <div>
                <h1><span>핵심</span> 데몬 가동 현황</h1>
                <div class="header-desc">
                    실시간 데이터 수집 프로세스 모니터링 시스템
                    <span id="update-timer">상태 확인 중...</span>
                </div>
            </div>
        </header>

        <!-- Cards Grid -->
        <div class="grid">
            <?php foreach ($daemons as $daemon): ?>
                <?php $target_id = $daemon['d_id']; ?>
                <div id="card-<?php echo $target_id; ?>" class="daemon-card">
                    
                    <div id="kill-flag-<?php echo $target_id; ?>" class="kill-flag-overlay blink" style="display:none;">
                        종료 프로세스 감지
                    </div>

                    <div class="card-body">
                        <div class="card-head-row">
                            <div class="card-info-group">
                                <div id="icon-box-<?php echo $target_id; ?>" class="icon-box">
                                    <i class="fa-solid fa-circle-notch fa-spin"></i>
                                </div>
                                <div>
                                    <h3 class="card-title"><?php echo explode('_', $target_id, 3)[2]; ?></h3>
                                    <p class="card-id"><?php echo htmlspecialchars($target_id); ?></p>
                                </div>
                            </div>
                            <span id="status-badge-<?php echo $target_id; ?>" class="status-badge" style="background:#f1f5f9; color:#64748b;">
                                ...
                            </span>
                        </div>

                        <div class="details-container">
                            <div id="metric-box-<?php echo $target_id; ?>" class="metric-box">
                                <div class="metric-label-row">
                                    <span class="metric-label">누적 수집 데이터</span>
                                    <span id="mode-badge-<?php echo $target_id; ?>" class="mode-badge">분석 중</span>
                                </div>
                                <div>
                                    <span id="total-cnt-<?php echo $target_id; ?>" class="total-cnt">...</span>
                                    <span class="cnt-unit">건</span>
                                </div>
                            </div>

                            <div>
                                <div class="detail-label-row">
                                    <span class="detail-label">최종 동기화 시점</span>
                                    <span id="diff-<?php echo $target_id; ?>" class="detail-diff">...</span>
                                </div>
                                <div id="last-trade-<?php echo $target_id; ?>" class="detail-value-box">...</div>
                            </div>

                            <div class="meta-grid">
                                <div>
                                    <span class="meta-label">프로세스 ID</span>
                                    <span id="pid-<?php echo $target_id; ?>" class="meta-value">...</span>
                                </div>
                                <div>
                                    <span class="meta-label">서버 위치</span>
                                    <span class="meta-value gray"><?php echo $daemon['d_ip'] ?? '-'; ?></span>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="card-footer">
                        <div class="hb-row">
                            <i class="fa-regular fa-clock"></i>
                            <span>생존 신호:</span>
                            <span id="hb-<?php echo $target_id; ?>" class="hb-value">...</span>
                        </div>
                        <div class="start-time">
                            STARTED: <?php echo substr($daemon['d_start_time'], 5, 11); ?>
                        </div>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>

        <div class="btn-container">
            <a href="/home/www/GNU/_PAGE/monitoring/upbit/daemon_market/daemon.php" class="btn-primary">
                <i class="fa-solid fa-list-check"></i>
                데몬 리스트 전체보기
            </a>
        </div>
    </div>

    <script>
        // 실시간 데이터를 업데이트하는 함수 (기존 로직 100% 유지)
        async function updateData() {
            try {
                const response = await fetch('?api=1');
                const data = await response.json();
                
                data.forEach(d => {
                    const id = d.d_id;
                    const cntElem = document.getElementById(`total-cnt-${id}`);
                    const badge = document.getElementById(`status-badge-${id}`);
                    const iconBox = document.getElementById(`icon-box-${id}`);
                    const killOverlay = document.getElementById(`kill-flag-${id}`);
                    const modeBadge = document.getElementById(`mode-badge-${id}`);
                    
                    if(cntElem.innerText !== d.total_count) {
                        cntElem.innerText = d.total_count;
                        document.getElementById(`metric-box-${id}`).classList.add('update-flash');
                        setTimeout(() => document.getElementById(`metric-box-${id}`).classList.remove('update-flash'), 1000);
                    }
                    
                    if (d.total_count_raw === 1) {
                        modeBadge.innerText = "데이터 덮어쓰기";
                        modeBadge.style.background = "rgba(249, 115, 22, 0.1)";
                        modeBadge.style.color = "#f97316";
                        modeBadge.style.borderColor = "rgba(249, 115, 22, 0.2)";
                    } else {
                        modeBadge.innerText = "데이터 쌓아쓰기";
                        modeBadge.style.background = "rgba(59, 130, 246, 0.1)";
                        modeBadge.style.color = "#3b82f6";
                        modeBadge.style.borderColor = "rgba(59, 130, 246, 0.2)";
                    }

                    document.getElementById(`last-trade-${id}`).innerText = d.last_trade;
                    document.getElementById(`diff-${id}`).innerText = d.trade_diff;
                    document.getElementById(`pid-${id}`).innerText = d.d_pid;
                    document.getElementById(`hb-${id}`).innerText = d.heartbeat_diff;
                    
                    killOverlay.style.display = d.d_kill_flag ? 'block' : 'none';

                    let bStyle = "", tColor = "", label = "", icon = "";

                    if (d.d_status === 'RUNNING') {
                        if (d.is_dead) {
                            bStyle = "rgba(249, 115, 22, 0.1)"; tColor = "#f97316";
                            label = "응답 지연"; icon = '<i class="fa-solid fa-triangle-exclamation" style="color:#f97316;"></i>';
                        } else {
                            bStyle = "#22c55e"; tColor = "#ffffff";
                            label = "가동 중"; icon = '<i class="fa-solid fa-bolt" style="color:#10b981;"></i>';
                        }
                    } else if (d.d_status === 'STOPPED') {
                        bStyle = "rgba(239, 68, 68, 0.1)"; tColor = "#ef4444";
                        label = "중지됨"; icon = '<i class="fa-solid fa-power-off" style="color:#ef4444;"></i>';
                    } else {
                        bStyle = "#334155"; tColor = "#94a3b8";
                        label = "오류"; icon = '<i class="fa-solid fa-bug" style="color:#94a3b8;"></i>';
                    }

                    badge.style.background = bStyle;
                    badge.style.color = tColor;
                    badge.innerText = label;
                    iconBox.innerHTML = icon;
                });
            } catch (e) { console.error("Update fail", e); }
        }

        let countdown = 5;
        setInterval(() => {
            countdown--;
            if(countdown <= 0) {
                updateData();
                countdown = 5;
            }
            document.getElementById('update-timer').innerText = `NEXT SYNC IN ${countdown}s`;
        }, 1000);

        updateData();

        // 테마 감지
        if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
            document.documentElement.classList.add('dark');
        }
    </script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>