GNU/_PAGE/trading/upbit/setting.php
<?php
include_once('./_common.php');
if (!defined('_GNUBOARD_')) exit;
if (!$is_admin) exit;
/**
 * UPBIT ALL DAEMON NAME LISTER (DB Integrated & Control)
 * 경로: /home/www/GNU/_PAGE/monitoring/upbit/daemon_list.php
 * 기능: DB 정보, 실시간 리소스, PID 출력 및 실행/중단 제어 기능 통합
 */

// ==================================================
// ✅ 실시간 데이터 갱신 처리부 (AJAX 요청 - 최상단 배치) 🏛️
// ==================================================
if (isset($_GET['mode']) && $_GET['mode'] === 'realtime_update') {
    header('Content-Type: application/json; charset=utf-8');

    // DB 연결 (AJAX용)
    $db_data = null;
    try {
        require_once '/home/www/DB/db_upbit.php';
        $db_data = $pdo; 
    } catch (Exception $e) { }

    // 함수 정의 (AJAX 요청 시에도 사용 가능하도록)
    if (!function_exists('get_daemon_resources_ajax')) {
        function get_daemon_resources_ajax($filename) {
            $pattern = "php .*{$filename}";
            $cmd = "ps -eo %cpu,%mem,etimes,pid,cmd | grep " . escapeshellarg($pattern) . " | grep -v grep";
            exec($cmd, $out);
            if (empty($out)) return ['cpu' => 0, 'mem' => 0, 'uptime' => 0, 'pid' => '-', 'active' => false];
            $cols = preg_split('/\s+/', trim($out[0]));
            return [
                'cpu' => (float)($cols[0] ?? 0),
                'mem' => (float)($cols[1] ?? 0),
                'uptime' => $cols[2] ?? 0,
                'pid' => $cols[3] ?? '-', 
                'active' => true
            ];
        }
    }

    // DB 레코드 로드
    $records = [];
    if ($db_data) {
        try {
            $stmt = $db_data->query("SELECT * FROM daemon_record");
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $records[$row['d_id']] = $row; }
        } catch (Exception $e) { }
    }

    $DAEMON_ROOT = '/home/www/DATA';
    $daemon_updates = [];
    
    // 전체 합계 변수
    $total_daemon_cpu = 0;
    $total_daemon_mem = 0;
    
    // 카운트 변수
    $active_cnt = 0;
    $stopped_cnt = 0;
    $total_cnt = 0;

    if (is_dir($DAEMON_ROOT)) {
        $dir = new RecursiveDirectoryIterator($DAEMON_ROOT, FilesystemIterator::SKIP_DOTS);
        $it = new RecursiveIteratorIterator($dir);
        
        $temp_daemons = [];
        foreach ($it as $file) {
            if ($file->isFile() && strpos($file->getFilename(), 'daemon_') === 0 && substr($file->getFilename(), -4) === '.php') {
                $filename = $file->getFilename();
                $d_id = preg_replace('/\.php$/', '', $filename);
                $res = get_daemon_resources_ajax($filename); // 함수 호출
                $db_row = $records[$d_id] ?? null;
                $display_name = $db_row['d_name'] ?? preg_replace('/^daemon_/', '', $d_id);
                
                $temp_daemons[] = [
                    'd_id' => $d_id,
                    'name' => $display_name,
                    'res' => $res,
                    'db_heartbeat' => $db_row['d_heartbeat'] ?? '-'
                ];
            }
        }
        
        // 정렬
        usort($temp_daemons, function($a, $b) { return strcmp($a['name'], $b['name']); });

        foreach($temp_daemons as $i => $d) {
            $total_cnt++;
            if($d['res']['active']) $active_cnt++; else $stopped_cnt++;

            // 전체 사용량 합산
            $total_daemon_cpu += $d['res']['cpu'];
            $total_daemon_mem += $d['res']['mem'];

            // uptime 포맷팅
            $seconds = $d['res']['uptime'];
            $uptime_str = '0초';
            if ($d['res']['active'] && $seconds >= 0) {
                $days = floor($seconds / 86400);
                $hours = floor(($seconds % 86400) / 3600);
                $mins = floor(($seconds % 3600) / 60);
                $secs = $seconds % 60;
                $res_arr = [];
                if ($days > 0) $res_arr[] = $days . "일";
                if ($hours > 0) $res_arr[] = $hours . "시";
                if ($mins > 0) $res_arr[] = $mins . "분";
                $res_arr[] = $secs . "초";
                $uptime_str = implode(" ", $res_arr);
            }

            $daemon_updates[] = [
                'idx' => $i,
                'd_id' => $d['d_id'],
                'cpu' => $d['res']['cpu'],
                'mem' => $d['res']['mem'],
                'pid' => $d['res']['pid'],
                'uptime' => $uptime_str,
                'heartbeat' => $d['db_heartbeat'],
                'active' => $d['res']['active']
            ];
        }
    }

    echo json_encode([
        'daemon_total_cpu' => round($total_daemon_cpu, 1),
        'daemon_total_mem' => round($total_daemon_mem, 1),
        'daemons' => $daemon_updates,
        'summary' => [$active_cnt, $stopped_cnt, $total_cnt]
    ]);
    exit;
}

date_default_timezone_set('Asia/Seoul');
require_once '/home/www/GNU/_PAGE/head.php'; 

// --- [환경 설정 및 DB 연결] ---
$DAEMON_ROOT = '/home/www/DATA';
$daemons_data = [];
$msg = '';

$db_data = null;
try {
    require_once '/home/www/DB/db_upbit.php';
    $db_data = $pdo; 
} catch (Exception $e) { }

// ==================================================
// ✅ 데몬 제어 처리부 (POST)
// ==================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['action']) && $_POST['action'] === 'start' && isset($_POST['target_path'])) {
        $targetFile = $_POST['target_path'];
        $fullPath = $DAEMON_ROOT . '/' . $targetFile;
        if (is_file($fullPath)) {
            $cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
            exec($cmdRun, $o, $rc);
            $msg = "실행 완료: " . basename($targetFile);
        }
    }
    if (isset($_POST['action']) && $_POST['action'] === 'stop' && isset($_POST['target_file'])) {
        $targetFileName = $_POST['target_file'];
        $cmdKill = "pkill -f " . escapeshellarg($targetFileName);
        exec($cmdKill, $o, $rc);
        $msg = "종료 완료: " . $targetFileName;
    }
}

$keyword_map = [
    'upbit'     => ['label' => '업비트', 'color' => '#0052ff'],
    'stats'     => ['label' => '통계', 'color' => '#f59e0b'],
    'Ticker'    => ['label' => '플랫폼', 'color' => '#3b82f6'],
    'user'      => ['label' => '개인자산', 'color' => '#ec4899'],
    'Best'      => ['label' => '베스트 코인', 'color' => '#fbbf24'],
    'trading'   => ['label' => '매매', 'color' => '#10b981'],
    'day'       => ['label' => '특정일', 'color' => '#8b5cf6'],
    'multi'     => ['label' => '종합', 'color' => '#06b6d4'],
    'watchman'  => ['label' => '감시', 'color' => '#a78bfa'],
    'main'      => ['label' => '기본', 'color' => '#38bdf8'],
    'sub'       => ['label' => '하위', 'color' => '#94a3b8'],
    'direction' => ['label' => '방향', 'color' => '#f43f5e'],
    'buy'       => ['label' => '매수', 'color' => '#10b981'],
    'sell'      => ['label' => '매도', 'color' => '#ef4444'],
];

function get_all_daemon_records($pdo) {
    $records = [];
    if (!$pdo) return $records;
    try {
        $stmt = $pdo->query("SELECT * FROM daemon_record");
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $records[$row['d_id']] = $row; }
    } catch (Exception $e) { }
    return $records;
}

function format_uptime($seconds) {
    if (!$seconds || $seconds < 0) return '-';
    $days = floor($seconds / 86400);
    $hours = floor(($seconds % 86400) / 3600);
    $mins = floor(($seconds % 3600) / 60);
    $secs = $seconds % 60;
    $res = [];
    if ($days > 0) $res[] = $days . "일";
    if ($hours > 0) $res[] = $hours . "시";
    if ($mins > 0) $res[] = $mins . "분";
    $res[] = $secs . "초";
    return implode(" ", $res);
}

function get_daemon_resources($filename) {
    $pattern = "php .*{$filename}";
    $cmd = "ps -eo %cpu,%mem,etimes,pid,cmd | grep " . escapeshellarg($pattern) . " | grep -v grep";
    exec($cmd, $out);
    if (empty($out)) return ['cpu' => 0, 'mem' => 0, 'uptime' => 0, 'pid' => '-', 'active' => false];
    $cols = preg_split('/\s+/', trim($out[0]));
    return ['cpu' => (float)($cols[0] ?? 0), 'mem' => (float)($cols[1] ?? 0), 'uptime' => $cols[2] ?? 0, 'pid' => $cols[3] ?? '-', 'active' => true];
}

function get_daemon_tags($filename) {
    global $keyword_map;
    $tags = [];
    foreach ($keyword_map as $key => $meta) {
        if (stripos($filename, $key) !== false) $tags[] = $meta;
    }
    return $tags;
}

// 메인 리스트 로직
$db_records = get_all_daemon_records($db_data);

if (is_dir($DAEMON_ROOT)) {
    try {
        $directory = new RecursiveDirectoryIterator($DAEMON_ROOT, FilesystemIterator::SKIP_DOTS);
        $iterator = new RecursiveIteratorIterator($directory);
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $filename = $file->getFilename();
                if (strpos($filename, 'daemon_') === 0 && substr($filename, -4) === '.php') {
                    $d_id = preg_replace('/\.php$/', '', $filename);
                    $db_row = $db_records[$d_id] ?? null;
                    $tags = get_daemon_tags($filename);
                    $res = get_daemon_resources($filename);
                    $display_name = $db_row['d_name'] ?? preg_replace('/^daemon_/', '', $d_id);
                    
                    $daemons_data[] = [
                        'd_id' => $d_id,
                        'filename' => $filename,
                        'name' => $display_name,
                        'path' => $iterator->getSubPath(),
                        'subpath_file' => $iterator->getSubPathName(), 
                        'tags' => $tags,
                        'cpu'  => $res['cpu'],
                        'mem'  => $res['mem'],
                        'pid'  => $res['pid'], 
                        'uptime' => format_uptime($res['uptime']),
                        'active' => $res['active'],
                        'db_status' => $db_row['d_status'] ?? ($res['active'] ? 'RUNNING' : 'STOPPED'),
                        'db_heartbeat' => $db_row['d_heartbeat'] ?? '-',
                        'db_kind' => $db_row['d_kind'] ?? '-',
                        'db_memo' => $db_row['d_memo'] ?? '-',
                    ];
                }
            }
        }
    } catch (Exception $e) { }
}
usort($daemons_data, function($a, $b) { return strcmp($a['name'], $b['name']); });

$total_count = count($daemons_data);
$active_count = 0;
foreach($daemons_data as $dd) if($dd['active']) $active_count++;
$stopped_count = $total_count - $active_count;
?>

<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<link rel="stylesheet" type="text/css" href="./setting.css">

<div class="daemon-name-container">
    <div class="header-section">
        <div class="list-title">
            <i class="fa-solid fa-microchip"></i>
            <div>
                데몬 커맨드 센터
                <div style="font-size: 14px; color: #475569; margin-top: 5px; font-weight: 500;">실시간 모니터링 & 프로세스 제어 시스템</div>
            </div>
        </div>

        <div style="display:flex; align-items:flex-end;">
            <div class="total-status-box">
                <div class="total-chart-row">
                    <span class="total-chart-label">
                        <span>데몬 전체 CPU 합계</span>
                        <span id="txtTotalCpu" style="color:#f43f5e">0%</span>
                    </span>
                    <div id="totalDaemonCpuBar" style="height: 40px;"></div>
                </div>
                <div class="total-chart-row">
                    <span class="total-chart-label">
                        <span>데몬 전체 메모리 합계</span>
                        <span id="txtTotalMem" style="color:#8b5cf6">0%</span>
                    </span>
                    <div id="totalDaemonMemBar" style="height: 40px;"></div>
                </div>
            </div>

            <div class="summary-chart-box">
                <div class="summary-chart-label">시스템 상태 요약</div>
                <div id="summaryChart"></div>
            </div>
        </div>
    </div>

<?php if ($msg): ?>
    <div class="msg-notice"><i class="fa-solid fa-bolt"></i> <?=htmlspecialchars($msg)?></div>
<?php endif; ?>

<div class="daemon-grid">
    <?php foreach ($daemons_data as $idx => $d): ?>
        <div class="daemon-card <?= $d['active'] ? 'active' : 'stopped' ?>" id="card_<?= $d['d_id'] ?>">
            <div class="card-top">
                <div>
                    <div class="tag-area">
                        <?php if (empty($d['tags'])): ?>
                            <span class="tag-badge" style="background: #334155;">System</span>
                        <?php else: ?>
                            <?php foreach ($d['tags'] as $t): ?>
                                <span class="tag-badge" style="background: <?=$t['color']?>;"><?=htmlspecialchars($t['label'])?></span>
                            <?php endforeach; ?>
                        <?php endif; ?>
                        <span class="tag-badge" style="background: #6366f1;"><?=$d['db_kind']?></span>
                    </div>
                    <div class="daemon-display-name">
                        <div class="status-pulse"></div>
                        <?= htmlspecialchars($d['name']) ?>
                    </div>
                    <div class="daemon-file-id">UUID: <?= htmlspecialchars($d['d_id']) ?></div>
                </div>
                <div style="text-align: right;">
                    <span style="font-size: 11px; font-weight: 800; color: <?= $d['active'] ? 'var(--success)' : 'var(--danger)' ?>">
                        <?= $d['active'] ? '정상 작동' : '중지됨' ?>
                    </span>
                </div>
            </div>

            <div class="resource-charts">
                <div class="chart-item">
                    <div id="cpuChart_<?=$idx?>"></div>
                    <div class="chart-label">
                        CPU <span id="txtCpuVal_<?=$idx?>" style="color:#3b82f6; font-family:'JetBrains Mono';"><?= $d['cpu'] ?>%</span>
                    </div>
                </div>
                <div class="chart-item">
                    <div id="memChart_<?=$idx?>"></div>
                    <div class="chart-label">
                        MEM <span id="txtMemVal_<?=$idx?>" style="color:#10b981; font-family:'JetBrains Mono';"><?= $d['mem'] ?>%</span>
                    </div>
                </div>
            </div>

            <div class="info-area">
                <div class="info-box">
                    <span class="info-label">프로세스 ID</span>
                    <span class="info-val pid" id="pid_<?=$idx?>"># <?= $d['pid'] ?></span>
                </div>
                <div class="info-box">
                    <span class="info-label">가동 시간</span>
                    <span class="info-val <?= $d['active'] ? 'active' : '' ?>" id="uptime_<?=$idx?>"><?= $d['active'] ? $d['uptime'] : '0s' ?></span>
                </div>
                <div class="info-box" style="grid-column: span 2;">
                    <span class="info-label">최근 하트비트 신호</span>
                    <span class="info-val" style="color:var(--warning)" id="heartbeat_<?=$idx?>">
                        <i class="fa-solid fa-wave-square" style="font-size: 10px;"></i> <?= $d['db_heartbeat'] ?>
                    </span>
                </div>
            </div>

            <div class="control-area">
                <?php if (!$d['active']): ?>
                    <form method="post" style="width: 100%;">
                        <input type="hidden" name="action" value="start">
                        <input type="hidden" name="target_path" value="<?=htmlspecialchars($d['subpath_file'])?>">
                        <button type="submit" class="btn-ctl btn-start">
                            <i class="fa-solid fa-play"></i> 데몬 시작
                        </button>
                    </form>
                <?php else: ?>
                    <form method="post" style="width: 100%;">
                        <input type="hidden" name="action" value="stop">
                        <input type="hidden" name="target_file" value="<?=htmlspecialchars($d['filename'])?>">
                        <button type="submit" class="btn-ctl btn-stop">
                            <i class="fa-solid fa-power-off"></i> 강제 종료
                        </button>
                    </form>
                <?php endif; ?>
            </div>

            <div class="daemon-detail-box">
                <div class="detail-item">
                    <span class="detail-label">파일 경로</span>
                    <span class="detail-val">/<?= htmlspecialchars($d['path']) ?>/<?= htmlspecialchars($d['filename']) ?></span>
                </div>
                <div class="detail-item" style="margin-bottom: 0;">
                    <span class="detail-label">시스템 메모</span>
                    <span class="detail-val" style="font-family: 'Pretendard';"><?= htmlspecialchars($d['db_memo']) ?></span>
                </div>
            </div>
        </div>
    <?php endforeach; ?>
</div>
</div>

<script>
    // 개별 데몬 차트 (원형)
    const circleOptions = (value, color) => ({
        series: [value],
        chart: { height: 140, type: 'radialBar', animations: { enabled: true, easing: 'easeinout', speed: 800 } },
        plotOptions: {
            radialBar: {
                hollow: { size: '50%' },
                dataLabels: { name: { show: false }, value: { offsetY: 5, fontSize: '14px', fontWeight: '900', color: '#fff', formatter: (val) => val + '%' } },
                track: { background: '#1e293b' }
            }
        },
        colors: [color], stroke: { lineCap: 'round' }, tooltip: { enabled: true, theme: 'dark' }
    });

    // 전체 데몬 합계 차트 (막대 Bar 100% 기준)
    const totalBarOptions = (value, color, maxVal=100) => ({
        series: [{ data: [value] }],
        chart: { type: 'bar', height: 40, sparkline: { enabled: true }, animations: { enabled: true } },
        plotOptions: {
            bar: { horizontal: true, barHeight: '100%', borderRadius: 4, colors: { backgroundBarColors: ['#1e293b'] } }
        },
        colors: [color],
        xaxis: { min: 0, max: maxVal }, // 100% 기준
        tooltip: { theme: 'dark', x: { show: false }, y: { formatter: function(val) { return val + "%" } } }
    });

    const charts = {};
    let totalCpuBar, totalMemBar, summaryChart;

    document.addEventListener('DOMContentLoaded', function() {
        // 1. 데몬 전체 합계 막대 차트 초기화
        totalCpuBar = new ApexCharts(document.querySelector("#totalDaemonCpuBar"), totalBarOptions(0, '#f43f5e', 100));
        totalCpuBar.render();
        totalMemBar = new ApexCharts(document.querySelector("#totalDaemonMemBar"), totalBarOptions(0, '#8b5cf6', 100));
        totalMemBar.render();

        // 2. 개별 데몬 차트 초기화
        <?php foreach ($daemons_data as $idx => $d): ?>
            charts['cpu_<?=$idx?>'] = new ApexCharts(document.querySelector("#cpuChart_<?=$idx?>"), circleOptions(<?=$d['cpu']?>, '#3b82f6'));
            charts['cpu_<?=$idx?>'].render();
            charts['mem_<?=$idx?>'] = new ApexCharts(document.querySelector("#memChart_<?=$idx?>"), circleOptions(<?=$d['mem']?>, '#10b981'));
            charts['mem_<?=$idx?>'].render();
        <?php endforeach; ?>

        // 3. 요약 차트
        const summaryOptions = {
            series: [{ name: '데몬 상태', data: [<?=$active_count?>, <?=$stopped_count?>, <?=$total_count?>] }],
            chart: { type: 'bar', height: 120, toolbar: {show: false}, animations: {enabled: true} },
            plotOptions: { bar: { borderRadius: 6, horizontal: true, distributed: true, barHeight: '60%' } },
            dataLabels: { enabled: true, style: { fontWeight: '900' } },
            xaxis: { categories: ['가동중', '중지됨', '전체'], labels: { show: false }, axisBorder: { show: false } },
            yaxis: { labels: { style: { colors: '#64748b', fontWeight: '800' } } },
            grid: { show: false }, colors: ['#10b981', '#ef4444', '#3b82f6'], legend: { show: false },
            tooltip: { theme: 'dark', style: { fontSize: '12px' } }
        };
        summaryChart = new ApexCharts(document.querySelector("#summaryChart"), summaryOptions);
        summaryChart.render();

        // 4. 실시간 갱신 시작 (5초)
        setInterval(updateDashboard, 5000);
        updateDashboard();
    });

    function updateDashboard() {
        fetch('?mode=realtime_update', { cache: "no-cache" })
            .then(res => res.json())
            .then(data => {
                // 전체 데몬 합계 막대 & 텍스트 업데이트
                totalCpuBar.updateSeries([{ data: [data.daemon_total_cpu] }]);
                totalMemBar.updateSeries([{ data: [data.daemon_total_mem] }]);
                document.getElementById('txtTotalCpu').innerText = data.daemon_total_cpu + '%';
                document.getElementById('txtTotalMem').innerText = data.daemon_total_mem + '%';
                
                // 요약 업데이트
                summaryChart.updateSeries([{ data: data.summary }]);

                // 개별 차트 및 텍스트 업데이트
                data.daemons.forEach(d => {
                    const idx = d.idx;
                    if (charts['cpu_' + idx]) charts['cpu_' + idx].updateSeries([d.cpu]);
                    if (charts['mem_' + idx]) charts['mem_' + idx].updateSeries([d.mem]);
                    
                    document.getElementById('txtCpuVal_' + idx).innerText = d.cpu + '%';
                    document.getElementById('txtMemVal_' + idx).innerText = d.mem + '%';

                    const pidEl = document.getElementById('pid_' + idx);
                    if (pidEl) pidEl.innerText = '# ' + d.pid;
                    
                    const uptimeEl = document.getElementById('uptime_' + idx);
                    if (uptimeEl) {
                        uptimeEl.innerText = d.active ? d.uptime : '0초';
                        uptimeEl.className = 'info-val ' + (d.active ? 'active' : '');
                    }
                    
                    const hbEl = document.getElementById('heartbeat_' + idx);
                    if (hbEl) hbEl.innerHTML = '<i class="fa-solid fa-wave-square" style="font-size: 10px;"></i> ' + d.heartbeat;
                });
            })
            .catch(err => console.error(err));
    }
</script>

<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>