GNU/_PAGE/monitoring/upbit/daemon_container/daemon.php
<?php
include_once('./_common.php');
if (!defined('_GNUBOARD_')) exit;
if (!$is_admin) exit;
/**
 * UPBIT DAEMON MONITORING PAGE
 * 경로: /home/www/UPBIT/monitoring/daemon.php
 * 대상: /home/www/UPBIT/daemon/ 하위 모든 디렉토리 포함
 */

date_default_timezone_set('Asia/Seoul');

// 데몬이 위치한 기본 디렉토리
$DAEMON_DIR = '/home/www/DATA/UPBIT/container';
$msg = '';

// ==========================
// DB 연결
// ==========================
require '/home/www/DB/db_upbit.php';
$pdo = $db_upbit;

// ==========================
// [함수] 테이블 코멘트 가져오기 (KIND 대체)
// ==========================
function get_table_comment($file) {
    global $pdo;
    $table_name = preg_replace('/\.php$/', '', basename($file));
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) return '';

    try {
        $stmt = $pdo->prepare("SHOW TABLE STATUS LIKE :table");
        $stmt->execute([':table' => $table_name]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row['Comment'] ?? ''; 
    } catch (Exception $e) {
        return '';
    }
}

// [함수] 코멘트 파싱 (언더바 기준 분리 및 뱃지 디자인용)
function parse_comment_tokens($comment) {
    if ($comment === null || $comment === '') return [null, null, null];
    $parts = explode('_', $comment);
    $count = count($parts);

    if ($count === 1) return [$parts[0], null, null];
    if ($count === 2) return [$parts[0], null, $parts[1]];
    
    $first = $parts[0];
    $last  = $parts[$count - 1];
    $middle = implode('_', array_slice($parts, 1, -1));
    return [$first, $middle, $last];
}

function is_valid_daemon_file($file) {
    return (bool)preg_match('/^daemon_[a-zA-Z0-9_\-]+\.php$/', basename($file));
}

/**
 * 프로세스 생존 확인 함수 (Full Path 기준)
 */
function find_proc($subPath) {
    global $DAEMON_DIR;
    $fullPath = $DAEMON_DIR . '/' . $subPath;
    
    // 절대 경로를 포함하여 검색해야 오작동이 없습니다.
    $pattern = "php .*{$fullPath}";
    $cmd = "ps -eo user,pid,cmd | grep " . escapeshellarg($pattern) . " | grep -v grep";
    $out = [];
    exec($cmd, $out);
    if (empty($out)) return null;

    $cols = preg_split('/\s+/', trim($out[0]), 3);
    return [
        'user' => $cols[0] ?? '-',
        'pid'  => $cols[1] ?? '-',
        'cmd'  => $cols[2] ?? '',
        'raw'  => $out[0],
    ];
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // START (부활)
    if (isset($_POST['start'])) {
        $subPath = $_POST['start'];
        $fullPath = $DAEMON_DIR . '/' . $subPath;
        if (is_valid_daemon_file($subPath) && is_file($fullPath)) {
            if (!find_proc($subPath)) {
                // 절대 경로로 실행
                $cmdRun = "nohup php " . escapeshellarg($fullPath) . " > /dev/null 2>&1 &";
                exec($cmdRun);
                $msg = "STARTED : " . basename($subPath);
            } else {
                $msg = "ALREADY RUNNING : " . basename($subPath);
            }
        }
    }
    // STOP (중지 로직 오류 수정 및 강화)
    if (isset($_POST['stop'])) {
        $subPath = $_POST['stop'];
        $fullPath = $DAEMON_DIR . '/' . $subPath;
        if (is_valid_daemon_file($subPath)) {
            // pkill -f 는 정규표현식 패턴으로 프로세스를 찾습니다.
            // 실행 시 사용한 'php <절대경로>' 형식을 정확히 조준합니다.
            $pattern = "php .*{$fullPath}";
            $cmdKill = "pkill -f " . escapeshellarg($pattern);
            exec($cmdKill);
            
            // 종료 확인을 위해 잠시 대기 (0.3초)
            usleep(300000); 
            
            if (!find_proc($subPath)) {
                $msg = "STOPPED : " . basename($subPath);
            } else {
                // 일반 종료 실패 시 강제 종료(SIGKILL) 시도
                $cmdKillForce = "pkill -9 -f " . escapeshellarg($pattern);
                exec($cmdKillForce);
                usleep(200000);
                
                if (!find_proc($subPath)) {
                    $msg = "FORCE STOPPED : " . basename($subPath);
                } else {
                    $msg = "FAILED TO STOP : " . basename($subPath);
                }
            }
        }
    }
    // UPDATE COMMENT
    if (isset($_POST['update_comment_btn'])) {
        $file = $_POST['target_file'];
        $new_comment = trim($_POST['new_comment']);
        $table_name = preg_replace('/\.php$/', '', basename($file));
        if (preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) {
            try {
                $sql = "ALTER TABLE `{$table_name}` COMMENT = " . $pdo->quote($new_comment);
                $pdo->exec($sql);
                $msg = "COMMENT UPDATED : {$table_name}";
            } catch (Exception $e) {}
        }
    }
}

// 파일 스캔
$daemons = [];
if (is_dir($DAEMON_DIR)) {
    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($DAEMON_DIR));
    foreach ($iterator as $info) {
        if ($info->isFile()) {
            $filename = $info->getFilename();
            if (is_valid_daemon_file($filename)) {
                $daemons[] = $iterator->getSubPathName();
            }
        }
    }
}
sort($daemons);

function get_status($subPath) {
    $proc = find_proc($subPath);
    if ($proc) return ['status' => 'RUNNING', 'pid' => $proc['pid'], 'user' => $proc['user']];
    return ['status' => 'STOPPED', 'pid' => '-', 'user' => '-'];
}

require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>UPBIT DAEMON MONITORING</title>
<style>
:root {
    --bg-main: #0f172a;
    --bg-card: #1e293b;
    --border-color: #334155;
    --text-main: #f1f5f9;
    --text-dim: #94a3b8;
    --primary: #3b82f6;
    --success: #10b981;
    --danger: #ef4444;
    --warning: #f59e0b;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    font-family: 'Pretendard', sans-serif;
    background: var(--bg-main); color: var(--text-main);
    padding: 0; min-height: 100vh;
    line-height: 1.5;
}

/* 상단 영역 보정 */
.header-area {
    display: flex; justify-content: space-between; align-items: flex-end;
    margin: 40px 40px 25px 40px;
}
h2 { font-size: 30px; font-weight: 700; color: #fff; letter-spacing: -0.5px; margin: 0; padding-left: 20px; }
h2 i { color: var(--danger); margin-right: 10px; }

/* 메뉴 간격 및 행간 보정 */
.nav-links { display: flex; gap: 10px; margin-top: 15px; }
.btn-nav {
    display: inline-block;
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-dim);
    border: 1px solid var(--border-color);
    text-decoration: none;
    padding: 7px 14px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 600;
    transition: all 0.2s ease-in-out;
}
.btn-nav:hover {
    background: rgba(59, 130, 246, 0.12);
    border-color: var(--primary);
    color: #fff;
    transform: translateY(-1px);
}

.search-container { position: relative; }
.search-input {
    background: var(--bg-card); border: 1px solid var(--border-color); color: #fff;
    padding: 10px 15px 10px 42px; border-radius: 5px; font-size: 14px; width: 280px;
    outline: none; transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-input:focus { border-color: var(--primary); width: 340px; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); }
.search-container::before {
    content: '🔍'; position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
    font-size: 15px; opacity: 0.6;
}

.notice {
    background: rgba(59, 130, 246, 0.08); border-left: 4px solid var(--primary);
    padding: 14px 22px; margin: 0 40px 25px 40px; border-radius: 4px; color: var(--primary); font-size: 14px; font-weight: 500;
}

/* 테이블 영역 */
table {
    width: calc(100% - 80px); border-collapse: separate; border-spacing: 0;
    background: var(--bg-card); border-radius: 6px; overflow: hidden;
    border: 1px solid var(--border-color); margin: 0 40px 40px 40px;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
}
th {
    background: #111827; padding: 16px 20px; text-align: left;
    color: var(--text-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 1.2px;
    border-bottom: 1px solid var(--border-color);
}
td { padding: 14px 20px; border-bottom: 1px solid var(--border-color); font-size: 13.5px; vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255, 255, 255, 0.015); }

button { padding: 7px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }
button.start { background: var(--success); color: #fff; }
button.start:hover { background: #0d9488; }
button.stop { background: var(--danger); color: #fff; }
button.stop:hover { background: #dc2626; }
button.btn-edit { background: var(--primary); color: #fff; }

.badge-kind { padding: 3px 8px; border-radius: 4px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; }
.badge-kind-first { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
.badge-kind-last { background: rgba(168, 85, 247, 0.15); color: #a78bfa; border: 1px solid rgba(168, 85, 247, 0.3); }
.kind-center-text { font-weight: 600; color: #94a3b8; font-size: 11.5px; margin: 0 6px; }

.status-running { color: var(--success); font-weight: 800; display: flex; align-items: center; letter-spacing: 0.5px; }
.status-running::before { 
    content: ''; display: inline-block; width: 7px; height: 7px; background: var(--success); 
    border-radius: 50%; margin-right: 10px; box-shadow: 0 0 10px var(--success); 
}

/* 하단 섹션 */
.footer-section { margin: 0 40px 40px 40px; padding: 30px; background: var(--bg-card); border-radius: 6px; border: 1px solid var(--border-color); }
.footer-warning { background: rgba(245, 158, 11, 0.06); border-left: 4px solid var(--warning); padding: 18px 24px; border-radius: 4px; margin-bottom: 30px; }
.footer-warning strong { color: var(--warning); display: block; margin-bottom: 6px; font-size: 15px; }
.footer-warning p { font-size: 13.5px; color: var(--text-dim); line-height: 1.6; }

/* 9개 항목 한 행 출력 설정 */
.collection-stats { 
    display: grid; 
    grid-template-columns: repeat(9, 1fr); /* 9개 고정 */
    gap: 10px; /* 간격 축소 */
}
.stat-card { 
    background: rgba(0, 0, 0, 0.25); 
    padding: 15px 5px; /* 좌우 패딩 축소 */
    border-radius: 5px; 
    border: 1px solid var(--border-color); 
    transition: 0.2s; 
    text-align: center; /* 가운데 정렬 */
}
.stat-card:hover { border-color: var(--primary); transform: translateY(-2px); }
.stat-card h4 { font-size: 10px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; white-space: nowrap; }
.stat-card .time-val { font-size: 15px; font-weight: 800; color: var(--primary); white-space: nowrap; }
.stat-card .desc { font-size: 10px; color: #4b5563; margin-top: 6px; white-space: nowrap; }

/* 모달 */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 2000; backdrop-filter: blur(6px); }
.modal-overlay.active { display: flex; }
.modal-content { background: var(--bg-card); width: 380px; padding: 30px; border-radius: 6px; border: 1px solid var(--border-color); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); }
.modal-input { width: 100%; background: #0f172a; border: 1px solid var(--border-color); color: #fff; padding: 12px; border-radius: 5px; font-size: 14px; margin: 18px 0; outline: none; transition: 0.2s; }
.modal-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
</style>
</head>
<body>

<div class="header-area">
    <div class="header-left">
        <h2><i class="fa-solid fa-satellite"></i> CONTAINER MONITORING - CONTAINER : HISTORY</h2>
        <div class="nav-links">
            <a href="/GNU/_PAGE/monitoring/upbit/daemon_market/daemon.php" class="btn-nav">마켓&통계 데몬</a>
            <a href="/GNU/_PAGE/monitoring/upbit/daemon_trading/daemon.php" class="btn-nav">매매 데몬</a>
            <a href="/GNU/_PAGE/monitoring/upbit/daemon_watchman/daemon.php" class="btn-nav">감시/부활 데몬</a>
        </div>
    </div>
    <div class="search-container">
        <input type="text" id="daemonSearch" class="search-input" placeholder="데몬 파일명 또는 KIND 검색...">
    </div>
</div>

<?php if ($msg): ?>
<div class="notice"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>

<table>
<thead>
<tr>
    <th>KIND (TABLE COMMENT)</th>
    <th>DAEMON FILE</th>
    <th>STATUS</th>
    <th>PID</th>
    <th>PROC USER</th>
    <th>START</th>
    <th>STOP</th>
    <th>EDIT</th>
</tr>
</thead>
<tbody id="daemonTableBody">
<?php foreach ($daemons as $d):
    $st = get_status($d);
    $comment = get_table_comment($d);
    list($k_f, $k_c, $k_l) = parse_comment_tokens($comment);
?>
<tr>
    <td>
        <?php if($k_f): ?><span class="badge-kind badge-kind-first"><?=htmlspecialchars($k_f)?></span><?php endif; ?>
        <?php if($k_c): ?><span class="kind-center-text"><?=htmlspecialchars($k_c)?></span><?php endif; ?>
        <?php if($k_l): ?><span class="badge-kind badge-kind-last"><?=htmlspecialchars($k_l)?></span><?php endif; ?>
        <?php if(!$k_f && !$k_c && !$k_l): ?><span style="color:#4b5563; font-style: italic;">(No Comment)</span><?php endif; ?>
    </td>
    <td style="font-family: 'JetBrains Mono', monospace; color:#60a5fa; font-size: 13px;"><?=htmlspecialchars($d)?></td>
    <td>
        <?php if($st['status'] === 'RUNNING'): ?>
            <span class="status-running">RUNNING</span>
        <?php else: ?>
            <span style="color:var(--danger); font-weight:800; opacity: 0.8;">STOPPED</span>
        <?php endif; ?>
    </td>
    <td><code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px;"><?=htmlspecialchars($st['pid'])?></code></td>
    <td><?=htmlspecialchars($st['user'])?></td>
    <td>
        <form method="post"><input type="hidden" name="start" value="<?=htmlspecialchars($d)?>"><button class="start">START</button></form>
    </td>
    <td>
        <?php if($st['status'] === 'RUNNING'): ?>
        <form method="post"><input type="hidden" name="stop" value="<?=htmlspecialchars($d)?>"><button class="stop">STOP</button></form>
        <?php endif; ?>
    </td>
    <td>
        <button class="btn-edit" onclick="openModal('<?=htmlspecialchars($d)?>', '<?=htmlspecialchars(addslashes($comment))?>')">EDIT</button>
    </td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

<div class="footer-section">
    <div class="footer-warning">
        <strong>⚠️ 데몬 수동 관리 안내</strong>
        <p>현재 자동 부활 데몬이 존재하지 않습니다. 프로세스 사망 시 본 대시보드를 통한 <strong>수동 부활(START)</strong>이 필요하므로 주기적으로 상태를 확인하십시오.</p>
    </div>

    <h3 style="margin-bottom:18px; font-size:15px; color:#fff; font-weight: 700;">📊 1년치 데이터 수집 예상 시간 (5분 주기/200개 호출)</h3>
    <div class="collection-stats">
        <div class="stat-card"><h4>월봉 (Month)</h4><div class="time-val">약 5분</div><div class="desc">12개 데이터 (1회)</div></div>
        <div class="stat-card"><h4>주봉 (Week)</h4><div class="time-val">약 5분</div><div class="desc">52개 데이터 (1회)</div></div>
        <div class="stat-card"><h4>24시간 (일봉)</h4><div class="time-val">약 10분</div><div class="desc">365개 데이터 (2회)</div></div>
        <div class="stat-card"><h4>4시간 봉</h4><div class="time-val">약 55분</div><div class="desc">2,190개 데이터 (11회)</div></div>
        <div class="stat-card"><h4>1시간 봉</h4><div class="time-val">약 3.7시간</div><div class="desc">8,760개 데이터 (44회)</div></div>
        <div class="stat-card"><h4>30분 봉</h4><div class="time-val">약 7.3시간</div><div class="desc">17,520개 데이터 (88회)</div></div>
        <div class="stat-card"><h4>15분 봉</h4><div class="time-val">약 14.6시간</div><div class="desc">35,040개 데이터 (176회)</div></div>
        <div class="stat-card"><h4>5분 봉</h4><div class="time-val">약 1.8일</div><div class="desc">105,120개 데이터 (526회)</div></div>
        <div class="stat-card"><h4>1분 봉</h4><div class="time-val">약 9.1일</div><div class="desc">525,600개 데이터 (2,628회)</div></div>
    </div>
</div>

<div class="modal-overlay" id="editModal">
    <div class="modal-content">
        <h3 style="color:#fff; font-size:17px; font-weight: 700;">EDIT TABLE COMMENT</h3>
        <form method="post">
            <input type="hidden" name="target_file" id="modalFile">
            <input type="text" name="new_comment" id="modalComment" class="modal-input" placeholder="Enter new comment (e.g. Upbit_Trade_1m)">
            <div class="modal-actions">
                <button type="button" onclick="closeModal()" style="background:#4b5563; color:#fff;">CANCEL</button>
                <button name="update_comment_btn" class="btn-submit" style="background:var(--primary); color:#fff;">SAVE</button>
            </div>
        </form>
    </div>
</div>

<script>
document.getElementById('daemonSearch').addEventListener('keyup', function() {
    const filter = this.value.toLowerCase();
    const rows = document.querySelectorAll('#daemonTableBody tr');
    rows.forEach(row => {
        row.style.display = row.textContent.toLowerCase().includes(filter) ? '' : 'none';
    });
});

function openModal(file, comment) {
    document.getElementById('modalFile').value = file;
    document.getElementById('modalComment').value = comment;
    document.getElementById('editModal').classList.add('active');
    setTimeout(() => document.getElementById('modalComment').focus(), 100);
}
function closeModal() { document.getElementById('editModal').classList.remove('active'); }
</script>
</body>
</html>