GNU/_PAGE/structure/server/capacity.php
<?php
/**
 * Server Stats API Logic
 * 이 섹션은 서버 사이드에서 실행되어 실제 리소스 정보를 JSON으로 반환합니다.
 */
if (isset($_GET['action']) && $_GET['action'] === 'get_stats') {
    header('Content-Type: application/json');

    // 1. CPU Usage (%)
    $cpu_load = sys_getloadavg();
    $cpu_count = (int)shell_exec('nproc') ?: 1;
    $cpu_usage = floor(($cpu_load[0] / $cpu_count) * 100);
    if ($cpu_usage > 100) $cpu_usage = 100;

    // 2. RAM Usage
    $free = shell_exec('free -m');
    $free = (string)trim($free);
    $free_arr = explode("\n", $free);
    $mem = explode(" ", preg_replace("/\s+/", " ", $free_arr[1]));
    $mem_total = (int)($mem[1] ?? 1);
    $mem_used = (int)($mem[2] ?? 0);
    $ram_usage = floor(($mem_used / $mem_total) * 100);

    // 3. Disk Usage
    function get_disk_info($path) {
        if (!is_dir($path)) return ['total' => 1, 'used' => 0];
        $total_bytes = @disk_total_space($path);
        if ($total_bytes === false || $total_bytes == 0) {
            $df_output = shell_exec("df -P $path | tail -1 | awk '{print $2 \" \" $3}'");
            if ($df_output) {
                $parts = explode(" ", trim($df_output));
                $total_gb = floor(($parts[0] ?? 0) / 1024 / 1024);
                $used_gb = floor(($parts[1] ?? 0) / 1024 / 1024);
                return ['total' => max(1, $total_gb), 'used' => $used_gb];
            }
            return ['total' => 1, 'used' => 0];
        }
        $total = floor($total_bytes / (1024 * 1024 * 1024));
        $free = floor(@disk_free_space($path) / (1024 * 1024 * 1024));
        $used = max(0, $total - $free);
        return ['total' => max(1, $total), 'used' => $used];
    }
    $disk_root = get_disk_info('/');
    $disk_data = get_disk_info('/data');

    // 4. System Vitality
    $uptime_raw = shell_exec("cut -d. -f1 /proc/uptime") ?: 0;
    $uptime_days = floor($uptime_raw / 86400);
    $proc_count = (int)shell_exec("ps -e | wc -l");

    // 5. Database Connections
    $db_conns = (int)shell_exec("netstat -an | grep ':3306' | grep 'ESTABLISHED' | wc -l");
    if (!$db_conns) {
        $db_conns = (int)shell_exec("ss -ant | grep ':3306' | grep 'ESTAB' | wc -l");
    }
    $db_active = $db_conns ?: ((int)shell_exec("pgrep -f mariadbd | wc -l") ? 1 : 0);

    echo json_encode([
        'cpu' => $cpu_usage,
        'ram' => $ram_usage,
        'disk_root' => $disk_root,
        'disk_data' => $disk_data,
        'mem_total' => $mem_total,
        'mem_used' => $mem_used,
        'uptime_days' => $uptime_days,
        'proc_count' => $proc_count,
        'db_conns' => $db_active,
        'load_idx' => floor($cpu_load[0] * 100 / $cpu_count)
    ]);
    exit;
}
// 헤더 부분 포함
require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>코어 모니터링 시스템 V4 - 전문가 모드</title>
    
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/lucide@latest"></script>

    <style>
        @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;900&family=JetBrains+Mono:wght@400;700&family=Noto+Sans+KR:wght@300;700;900&display=swap');
        
        :root {
            --neon-cyan: #00f2ff;
            --neon-blue: #0066ff;
            --neon-purple: #bc13fe;
            --neon-rose: #ff0055;
            --neon-amber: #ffaa00;
        }

        body { 
            font-family: 'Noto Sans KR', sans-serif; 
            background-color: #00040a;
            color: #e2e8f0;
            overflow-x: hidden;
            width: 100%;
        }
        
        .orbitron { font-family: 'Orbitron', sans-serif; }
        .mono { font-family: 'JetBrains Mono', monospace; }

        .bg-grid {
            background-image: 
                linear-gradient(to right, rgba(0, 242, 255, 0.05) 1px, transparent 1px),
                linear-gradient(to bottom, rgba(0, 242, 255, 0.05) 1px, transparent 1px);
            background-size: 50px 50px;
            mask-image: radial-gradient(circle at 50% 50%, black, transparent 80%);
        }

        .glow-text-cyan { text-shadow: 0 0 10px rgba(0, 242, 255, 0.7); }
        .glow-text-purple { text-shadow: 0 0 10px rgba(188, 19, 254, 0.7); }
        .glow-text-rose { text-shadow: 0 0 10px rgba(255, 0, 85, 0.7); }
        .glow-text-amber { text-shadow: 0 0 10px rgba(255, 170, 0, 0.7); }

        @keyframes border-rotate {
            0% { border-color: rgba(0, 242, 255, 0.2); }
            50% { border-color: rgba(0, 242, 255, 0.8); }
            100% { border-color: rgba(0, 242, 255, 0.2); }
        }
        .neon-border { border-width: 2px; animation: border-rotate 4s infinite; }

        @keyframes data-pulse {
            0% { transform: scale(1); opacity: 1; }
            50% { transform: scale(1.05); opacity: 0.8; }
            100% { transform: scale(1); opacity: 1; }
        }
        .animate-data { animation: data-pulse 0.5s ease-out; }

        .spin-slow { animation: spin 15s linear infinite; }
        @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        
        @keyframes card-in {
            from { opacity: 0; transform: perspective(1000px) rotateX(10deg) translateY(30px); }
            to { opacity: 1; transform: perspective(1000px) rotateX(0deg) translateY(0); }
        }
        .animate-card { animation: card-in 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; }

        /* 페이지 스크롤바 스타일 */
        ::-webkit-scrollbar {
            width: 7px;
        }
            
        ::-webkit-scrollbar-track {
            background: #020617;
        }
            
        ::-webkit-scrollbar-thumb {
            background: rgba(99, 102, 241, 0.5);
            border-radius: 6px;
        }
            
        ::-webkit-scrollbar-thumb:hover {
            background: rgba(99, 102, 241, 0.7);
        }
    </style>
</head>
<body class="selection:bg-cyan-500/30 text-left">
    <div id="root"></div>

    <script type="text/babel">
        const { useState, useEffect, useMemo } = React;

        const Icon = ({ name, size = 20, className = "" }) => {
            useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [name]);
            return <i data-lucide={name} style={{ width: size, height: size }} className={className}></i>;
        };

        const StatusGauge = ({ data, type = "disk" }) => {
            const [isExpanded, setIsExpanded] = useState(false);
            const [pulse, setPulse] = useState(false);
            
            const percentage = type === "disk" 
                ? Math.floor((data.used / (data.total || 1)) * 100)
                : Math.floor(data.usage);

            const isCritical = percentage >= 90;
            const isWarning = percentage >= 80 && percentage < 90;
            
            const colors = {
                disk: { hex: "#00f2ff", class: "cyan-400", bg: "bg-cyan-500", glow: "glow-text-cyan" },
                cpu: { hex: "#bc13fe", class: "purple-500", bg: "bg-purple-600", glow: "glow-text-purple" },
                ram: { hex: "#0066ff", class: "blue-500", bg: "bg-blue-600", glow: "glow-text-cyan" },
                health: { hex: "#10b981", class: "emerald-500", bg: "bg-emerald-600", glow: "glow-text-cyan" },
                db: { hex: "#ff0055", class: "rose-500", bg: "bg-rose-600", glow: "glow-text-rose" }
            };

            const theme = colors[type] || colors.disk;
            let currentColorClass = theme.class;
            let currentGlowClass = theme.glow;
            let statusLabel = "정상";

            if (isCritical) { 
                currentColorClass = "rose-500"; 
                currentGlowClass = "glow-text-rose";
                statusLabel = "임계치 초과"; 
            } else if (isWarning) { 
                currentColorClass = "amber-500"; 
                statusLabel = "부하 주의"; 
            }

            useEffect(() => {
                setPulse(true);
                const timer = setTimeout(() => setPulse(false), 500);
                return () => clearTimeout(timer);
            }, [percentage]);

            const radius = 60;
            const circumference = 2 * Math.PI * radius;
            const strokeDashoffset = circumference - (Math.min(100, percentage) / 100) * circumference;

            return (
                <div className="animate-card group">
                    <div 
                        onClick={() => setIsExpanded(!isExpanded)}
                        className={`relative cursor-pointer bg-slate-950/85 backdrop-blur-md rounded-2xl p-8 border border-white/10 transition-all duration-500 hover:scale-[1.01] active:scale-95 ${isCritical ? 'shadow-[0_0_40px_rgba(255,0,85,0.2)]' : 'shadow-[0_0_20px_rgba(0,242,255,0.05)]'}`}
                    >
                        <div className={`absolute top-0 left-0 w-12 h-12 border-t-4 border-l-4 border-${currentColorClass}/40 rounded-tl-2xl opacity-0 group-hover:opacity-100 transition-opacity`}></div>
                        
                        <div className="flex justify-between items-start mb-8 text-left">
                            <div className="flex items-center gap-5">
                                <div className={`relative p-5 rounded-2xl bg-black border border-${currentColorClass}/30`}>
                                    <Icon name={data.icon || "activity"} size={32} className={`text-${currentColorClass}`} />
                                    <div className={`absolute -inset-1 rounded-2xl bg-${currentColorClass}/10 animate-pulse`}></div>
                                </div>
                                <div className="text-left">
                                    <h3 className={`text-2xl font-black tracking-tight group-hover:text-white transition-colors ${pulse ? 'animate-data' : ''}`}>{data.name}</h3>
                                    <p className="text-sm mono text-slate-400 uppercase tracking-widest mt-1">{data.subtitle}</p>
                                </div>
                            </div>
                            <div className={`px-3 py-1 rounded-lg text-xs font-black border border-${currentColorClass}/40 bg-black text-${currentColorClass} orbitron tracking-widest`}>
                                {type === "health" || type === "db" ? "활성화" : statusLabel}
                            </div>
                        </div>

                        <div className="relative flex justify-center items-center py-4 mb-8">
                            <div className="relative w-52 h-52">
                                <div className="absolute inset-0 border-[6px] border-slate-900 rounded-full"></div>
                                <div className="absolute inset-3 border border-dashed border-slate-800 rounded-full spin-slow"></div>
                                
                                <svg className="w-full h-full transform -rotate-90" viewBox="0 0 160 160">
                                    <circle className="text-slate-900" strokeWidth="8" stroke="currentColor" fill="transparent" r={radius} cx="80" cy="80" />
                                    <circle
                                        className={`text-${currentColorClass} transition-all duration-1000 cubic-bezier(0.34, 1.56, 0.64, 1)`}
                                        strokeWidth="10"
                                        strokeDasharray={circumference}
                                        strokeDashoffset={strokeDashoffset}
                                        strokeLinecap="butt"
                                        stroke="currentColor"
                                        fill="transparent"
                                        r={radius}
                                        cx="80"
                                        cy="80"
                                        style={{ filter: `drop-shadow(0 0 12px ${theme.hex})` }}
                                    />
                                </svg>
                                
                                <div className="absolute inset-0 flex flex-col items-center justify-center text-center px-4 overflow-hidden">
                                    <span className={`text-6xl font-black orbitron tracking-tighter transition-all duration-300 ${pulse ? 'scale-105' : ''} ${isCritical ? 'text-rose-500 glow-text-rose' : `text-white ${currentGlowClass}`}`}>
                                        {percentage}<span className="text-2xl font-light opacity-50 ml-1">{type === "health" || type === "db" ? "" : "%"}</span>
                                    </span>
                                    {type === "health" && <span className="text-xs font-bold text-slate-500 uppercase mt-2 tracking-widest">가동 일수</span>}
                                    {type === "db" && <span className="text-xs font-bold text-slate-500 uppercase mt-2 tracking-widest">현재 접속</span>}
                                </div>
                            </div>
                        </div>

                        {/* 가로 막대 차트 */}
                        <div className="mb-8 px-2 text-left">
                            <div className="flex justify-between items-center text-xs font-black orbitron mb-3 tracking-widest text-slate-400">
                                <span>자원 로드율</span>
                                <span className={`text-${currentColorClass}`}>{percentage}{type === "health" || type === "db" ? "pt" : "%"}</span>
                            </div>
                            <div className="w-full h-3.5 bg-slate-900 rounded-full overflow-hidden p-1 border border-white/5 shadow-inner">
                                <div 
                                    className={`h-full ${isCritical ? 'bg-rose-600 shadow-[0_0_15px_rgba(255,0,85,0.6)]' : theme.bg + ' shadow-[0_0_15px_rgba(0,242,255,0.4)]'} rounded-full transition-all duration-1000 ease-out`} 
                                    style={{ width: `${Math.min(100, percentage)}%` }}
                                ></div>
                            </div>
                        </div>

                        <div className="grid grid-cols-2 gap-4 mono text-left">
                            <div className="bg-slate-900/50 p-5 rounded-2xl border border-white/5">
                                <div className="text-slate-500 text-xs mb-2 font-bold uppercase tracking-widest text-left">
                                    {type === "disk" ? "사용 용량" : type === "health" ? "프로세스" : type === "db" ? "총 연결" : "현재 부하"}
                                </div>
                                <div className={`text-2xl font-bold text-white text-left`}>
                                    {type === "disk" ? Math.floor(data.used) : type === "health" ? data.procs : type === "db" ? data.usage : percentage}
                                    <span className="text-xs font-normal text-slate-500 ml-1.5 uppercase">
                                        {type === "disk" ? "GB" : type === "health" ? "개" : type === "db" ? "명" : "%"}
                                    </span>
                                </div>
                            </div>
                            <div className="bg-slate-900/50 p-5 rounded-2xl border border-white/5">
                                <div className="text-slate-500 text-xs mb-2 font-bold uppercase tracking-widest text-left">
                                    {type === "disk" ? "전체 용량" : "상태"}
                                </div>
                                <div className={`text-2xl font-bold text-white text-left`}>
                                    {type === "disk" ? Math.floor(data.total) : "안정적"}
                                    <span className="text-xs font-normal text-slate-500 ml-1.5 uppercase">
                                        {type === "disk" ? "GB" : ""}
                                    </span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            );
        };

        const App = () => {
            const [lastUpdated, setLastUpdated] = useState(new Date());
            const [isSimulated, setIsSimulated] = useState(false);
            
            const [systemStats, setSystemStats] = useState({
                cpu: { name: 'CPU 사용률', subtitle: '시스템 부하', usage: 0, icon: 'cpu' },
                ram: { name: '메모리', subtitle: 'RAM 사용량', usage: 0, usedRaw: 0, totalRaw: 0, icon: 'layers' },
                health: { name: '시스템 상태', subtitle: '가동 시간 정보', usage: 0, procs: 0, icon: 'heart-pulse' },
                db: { name: '데이터베이스', subtitle: 'MariaDB 연결', usage: 0, icon: 'database' }
            });

            const [volumes, setVolumes] = useState([
                { id: 'vol_root', name: '루트 디스크', subtitle: '시스템 파티션', mount: '/', total: 1, used: 0, type: 'SSD_FLASH', icon: 'hard-drive' },
                { id: 'vol_data', name: '데이터 볼륨', subtitle: '데이터 파티션', mount: '/data', total: 1, used: 0, type: 'BLOCK_STR', icon: 'database' }
            ]);

            const simulateStats = () => {
                setSystemStats(prev => ({
                    cpu: { ...prev.cpu, usage: Math.max(5, Math.min(100, (prev.cpu.usage || 5) + (Math.random() * 10 - 5))) },
                    ram: { ...prev.ram, usage: Math.max(30, Math.min(95, (prev.ram.usage || 45) + (Math.random() * 2 - 1))) },
                    health: { ...prev.health, usage: 14, procs: 156 },
                    db: { ...prev.db, usage: 42 }
                }));
                setVolumes(vols => vols.map(v => ({
                    ...v,
                    total: v.total <= 1 ? (v.id === 'vol_root' ? 49 : 196) : v.total,
                    used: Math.max(1, Math.min(v.total, (v.used || (v.id === 'vol_root' ? 9 : 82)) + (Math.random() * 0.1 - 0.05)))
                })));
                setLastUpdated(new Date());
            };

            const fetchStats = async () => {
                if (window.location.protocol === 'blob:' || window.location.hostname === '') {
                    setIsSimulated(true);
                    simulateStats();
                    return;
                }

                try {
                    const response = await fetch(`${window.location.origin}${window.location.pathname}?action=get_stats`);
                    if (!response.ok) throw new Error("API Offline");
                    
                    const data = await response.json();
                    
                    setSystemStats({
                        cpu: { ...systemStats.cpu, usage: data.cpu },
                        ram: { ...systemStats.ram, usage: data.ram, usedRaw: data.mem_used, totalRaw: data.mem_total },
                        health: { ...systemStats.health, usage: data.uptime_days, procs: data.proc_count },
                        db: { ...systemStats.db, usage: data.db_conns }
                    });
                    
                    setVolumes([
                        { ...volumes[0], total: data.disk_root.total, used: data.disk_root.used },
                        { ...volumes[1], total: data.disk_data.total, used: data.disk_data.used }
                    ]);
                    
                    setLastUpdated(new Date());
                    setIsSimulated(false);
                } catch (error) {
                    setIsSimulated(true);
                    simulateStats();
                }
            };

            useEffect(() => {
                fetchStats();
                const interval = setInterval(fetchStats, 5000); 
                return () => clearInterval(interval);
            }, []);

            return (
                <div className="min-h-screen py-12 relative overflow-hidden flex flex-col items-center w-full">
                    <div className="bg-grid absolute inset-0 z-0"></div>
                    
                    <div className="w-full px-[50px] relative z-10 text-left">
                        <header className="mb-16 flex flex-col lg:flex-row lg:items-end justify-between gap-8 text-left">
                            <div className="space-y-4 text-left">
                                <div className="flex items-center gap-3">
                                    <div className="px-3 py-1 bg-cyan-500 text-black text-xs font-black orbitron rounded-md">올드보이-노드</div>
                                    <span className="mono text-sm text-cyan-400 uppercase tracking-widest font-bold">시스템 연결 완료</span>
                                </div>
                                <h1 className="text-5xl font-black tracking-tighter orbitron text-white uppercase text-left">
                                    System Diagnosis
                                </h1>
                            </div>
                            
                            <div className="flex items-center gap-8 bg-slate-900/40 p-6 rounded-2xl border border-white/10 backdrop-blur-xl">
                                <div className="text-right">
                                    <p className="text-xs mono text-slate-400 uppercase mb-2 tracking-widest font-bold text-right">
                                        {isSimulated ? "시뮬레이션 모드 실행 중" : "최근 업데이트 정보"}
                                    </p>
                                    <p className="text-2xl font-black orbitron text-cyan-400 tracking-tighter text-right">
                                        {lastUpdated.toLocaleTimeString('ko-KR', { hour12: false })}
                                    </p>
                                </div>
                            </div>
                        </header>

                        {/* 모니터링 매트릭스 (3열 그리드) */}
                        <div className="mb-16 text-left">
                            <div className="flex items-center gap-4 mb-10">
                                <div className="h-0.5 flex-1 bg-gradient-to-r from-transparent via-cyan-500/40 to-transparent"></div>
                                <h2 className="orbitron text-sm font-black text-cyan-400 tracking-[0.5em] uppercase px-4">시스템 모니터링 매트릭스</h2>
                                <div className="h-0.5 flex-1 bg-gradient-to-r from-transparent via-cyan-500/40 to-transparent"></div>
                            </div>
                            
                            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
                                {/* 핵심 자원 단위 */}
                                <StatusGauge type="cpu" data={systemStats.cpu} />
                                <StatusGauge type="ram" data={systemStats.ram} />
                                
                                {/* 스토리지 파티션 */}
                                <StatusGauge type="disk" data={volumes[0]} />
                                <StatusGauge type="disk" data={volumes[1]} />
                                
                                {/* 데이터베이스 및 생존성 */}
                                <StatusGauge type="db" data={systemStats.db} />
                                <StatusGauge type="health" data={systemStats.health} />
                            </div>
                        </div>

                        <footer className="mt-24 pt-10 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-6 opacity-60">
                            <div className="flex gap-12 orbitron text-xs tracking-[0.4em] uppercase font-bold text-left">
                                <span>보안 관리자 노드</span>
                                <span>뉴럴 로직 V4.2.0</span>
                            </div>
                            <div className="flex items-center gap-3">
                                <span className={`w-3 h-3 rounded-full animate-pulse ${isSimulated ? 'bg-amber-500' : 'bg-cyan-500'}`}></span>
                                <span className="mono text-xs font-bold uppercase tracking-widest">{isSimulated ? '시뮬레이션' : '서버 연결 중'}</span>
                            </div>
                        </footer>
                    </div>
                </div>
            );
        };

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(<App />);
    </script>
</body>
</html>
<?php require_once '/home/www/GNU/_PAGE/tail.php';?>