Skip to main content
Glama

OpenManager Vibe V4 MCP Server

by skyasu2
server_dashboard.html63.9 kB
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OpenManager AI - 서버 모니터링 대시보드</title> <link rel="stylesheet" href="css/modern-style.css"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"> <!-- 부트스트랩 CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- 부트스트랩 아이콘 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> <!-- Font Awesome 아이콘 추가 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <!-- 차트 라이브러리 --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> /* CSS 변수 정의 */ :root { --primary: #4e73df; --primary-dark: #2e59d9; --primary-light: rgba(78, 115, 223, 0.1); --danger: #e74a3b; --warning: #f6c23e; --success: #1cc88a; --background: #f8f9fc; --surface: #ffffff; --text: #5a5c69; --text-muted: #858796; --border: #e3e6f0; --shadow-sm: 0 .125rem .25rem rgba(58, 59, 69, .2); --shadow-md: 0 .5rem 1rem rgba(58, 59, 69, .15); --shadow-lg: 0 1rem 3rem rgba(58, 59, 69, .175); --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --border-radius-sm: 4px; --border-radius-md: 12px; --border-radius-lg: 20px; } body { font-family: 'Noto Sans KR', sans-serif; background-color: var(--background); color: var(--text); margin: 0; padding: 0; } .dashboard-container { max-width: 1400px; margin: 0 auto; padding: 20px; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .logo { font-size: 1.8rem; font-weight: 700; color: #4a69bd; } /* 서버 카드 스타일 개선 */ .server-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 20px; } .server-card { background-color: #fff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); padding: 15px; transition: all 0.3s ease; border: 1px solid rgba(0, 0, 0, 0.05); position: relative; overflow: hidden; background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.7) 100%); backdrop-filter: blur(10px); cursor: pointer; height: auto; min-height: 140px; display: flex; flex-direction: column; } .server-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); border-color: rgba(0, 0, 0, 0.1); background-image: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(245, 247, 250, 0.9) 100%); } .server-card:hover::after { content: '클릭하여 자세히 보기'; position: absolute; bottom: 0; left: 0; right: 0; background-color: rgba(74, 105, 189, 0.9); color: white; padding: 5px; font-size: 0.75rem; text-align: center; transform: translateY(0); transition: transform 0.3s ease; border-radius: 0 0 12px 12px; } /* 서버 카드 눌림 효과 */ .server-card:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .server-card::before { content: ''; position: absolute; top: 0; left: 0; height: 4px; width: 100%; background: linear-gradient(90deg, #4a69bd, #2c3e50); } /* 상태에 따른 카드 스타일 */ .server-card.status-warning::before { background: linear-gradient(90deg, #ff8f00, #ffa000); } .server-card.status-critical::before { background: linear-gradient(90deg, #d32f2f, #b71c1c); } /* 상태에 따른 추가 스타일 */ .server-card.status-critical { border-left: 3px solid #d32f2f; } .server-card.status-warning { border-left: 3px solid #ff8f00; } .server-card.status-normal { border-left: 3px solid #4caf50; } /* 컴팩트 서버 카드 레이아웃 */ .server-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; } .server-name { font-size: 0.95rem; font-weight: 600; color: #2c3e50; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; } .server-status { font-size: 0.7rem; padding: 3px 8px; border-radius: 30px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: all 0.2s; } .server-status:hover { transform: scale(1.05); } .status-normal { background-color: #e8f5e9; color: #2e7d32; } .status-warning { background-color: #fff8e1; color: #ff8f00; } .status-critical { background-color: #ffebee; color: #d32f2f; } /* 컴팩트 메트릭 스타일 */ .server-metrics { display: flex; flex-direction: column; gap: 8px; } .metric-item { display: flex; align-items: center; margin-bottom: 5px; } .metric-label { font-size: 0.7rem; color: #7f8c8d; margin-right: 10px; min-width: 80px; display: flex; align-items: center; } .metric-label::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; background-color: #4a69bd; } .metric-item:nth-child(2) .metric-label::before { background-color: #3498db; } .metric-item:nth-child(3) .metric-label::before { background-color: #2ecc71; } .metric-item:nth-child(4) .metric-label::before { background-color: #9b59b6; } .metric-value { font-size: 0.8rem; font-weight: 600; color: #2c3e50; margin-right: 10px; min-width: 45px; } /* 프로그레스 바 스타일 개선 */ .progress-bar-container { height: 6px; border-radius: 3px; overflow: hidden; background-color: #f1f1f1; margin-top: 3px; position: relative; } .progress-bar { border-radius: 3px; transition: width 0.5s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); height: 100%; } .progress-normal { background: linear-gradient(90deg, #2ecc71, #27ae60); } .progress-warning { background: linear-gradient(90deg, #f39c12, #e67e22); } .progress-critical { background: linear-gradient(90deg, #e74c3c, #c0392b); } /* 메트릭 값 텍스트 스타일 개선 */ .detail-value { font-size: 0.85rem; font-weight: 600; color: #2c3e50; margin-right: 10px; min-width: 45px; transition: color 0.3s ease; } .detail-value.text-success { color: #27ae60 !important; text-shadow: 0 0 1px rgba(39, 174, 96, 0.2); } .detail-value.text-warning { color: #e67e22 !important; text-shadow: 0 0 1px rgba(230, 126, 34, 0.2); } .detail-value.text-danger { color: #c0392b !important; text-shadow: 0 0 1px rgba(192, 57, 43, 0.2); } .main-content-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; } @media (max-width: 1100px) { .main-content-grid { grid-template-columns: 1fr; } } .dashboard-section { background-color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); padding: 20px; margin-bottom: 20px; } .section-header { font-size: 1.2rem; font-weight: 600; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; } .filter-container { background-color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); padding: 20px; margin-bottom: 20px; } .chart-container { height: 300px; position: relative; } /* AI 질의 컨테이너 */ .ai-query-container { background: linear-gradient(to right, rgba(78, 115, 223, 0.05), rgba(78, 115, 223, 0.01)); border-left: 4px solid var(--primary); padding: var(--spacing-lg); margin-bottom: var(--spacing-lg); transition: transform 0.3s ease; } .ai-query-container:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); } .query-input { width: 100%; padding: 12px 15px; border-radius: var(--border-radius-md); border: 1px solid var(--border); font-size: 16px; transition: all 0.3s ease; } .query-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); } .query-loading { display: none; text-align: center; margin: var(--spacing-md) 0; } .query-loading.active { display: block; } .query-result { display: none; background-color: var(--background); border-radius: var(--border-radius-md); padding: var(--spacing-md); margin-top: var(--spacing-md); white-space: pre-line; border: 1px solid var(--border); position: relative; padding-top: 30px; } .query-result.active { display: block; animation: fadeIn 0.3s ease-out forwards; } /* 닫기 버튼 스타일 */ #closeQueryResult { transition: all 0.2s; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } #closeQueryResult:hover { transform: scale(1.1); background-color: #f8f9fa; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .close-button-container { border-bottom: 1px solid #eee; padding-bottom: 10px; } /* 반응형 */ @media (max-width: 768px) { .header { flex-direction: column; padding: 10px; } .logo { margin-bottom: 10px; } .nav-menu { width: 100%; justify-content: center; } .nav-item { margin: 0 5px; padding: 5px 10px; font-size: 14px; } } .error-badge { display: inline-block; margin-top: 10px; padding: 3px 8px; background-color: #ffebee; color: #d32f2f; border-radius: 4px; font-size: 0.8rem; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.3); } 70% { box-shadow: 0 0 0 5px rgba(211, 47, 47, 0); } 100% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0); } } .service-status-container { display: flex; flex-wrap: wrap; gap: 8px; } .service-status-tag { padding: 5px 10px; border-radius: 4px; font-size: 0.85rem; display: inline-flex; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: all 0.2s ease; } .service-status-tag:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .service-status-tag.service-running { background-color: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; } .service-status-tag.service-stopped { background-color: #ffebee; color: #d32f2f; border: 1px solid #ffcdd2; } .status-indicator { margin-left: 5px; font-size: 0.75rem; opacity: 0.8; } /* 장애 프리셋 스타일 */ .preset-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; margin-bottom: 15px; } .preset-tag { padding: 10px 15px; border-radius: 8px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.25s; background: white; box-shadow: var(--shadow-sm); display: flex; align-items: center; border: 1px solid #e3e6f0; } .preset-tag:hover { box-shadow: var(--shadow-md); transform: translateY(-3px); } .preset-tag .badge { margin-left: 8px; font-size: 0.7rem; font-weight: 600; padding: 4px 6px; border-radius: 4px; transition: all 0.2s; } .preset-tag:hover .badge { transform: scale(1.1); } .preset-tag.tag-critical { border-left: 4px solid var(--danger); background-color: rgba(231, 74, 59, 0.05); } .preset-tag.tag-warning { border-left: 4px solid var(--warning); background-color: rgba(246, 194, 62, 0.05); } .preset-tag.tag-normal { border-left: 4px solid var(--success); background-color: rgba(28, 200, 138, 0.05); } .preset-tag.active { transform: translateY(2px); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .preset-tag:active { transform: translateY(2px); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .badge.bg-danger { background-color: var(--danger) !important; } .badge.bg-warning { background-color: var(--warning) !important; color: #212529; } .badge.bg-success { background-color: var(--success) !important; } /* 자동 장애 보고서 스타일 개선 */ .auto-report-container { border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; margin-top: 20px; background-color: #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.05); } .auto-report-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; border-bottom: 1px solid #f0f0f0; margin-bottom: 15px; } /* 추가적인 반응형 디자인 개선 */ /* FHD (1920x1080) */ @media (min-width: 1920px) { .container-fluid { max-width: 1800px; margin: 0 auto; } .server-grid { grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); } } /* QHD (2560x1440) */ @media (min-width: 2560px) { .container-fluid { max-width: 2400px; margin: 0 auto; } .server-grid { grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); } .main-content-grid { grid-template-columns: 3fr 1fr; } } /* 태블릿 디바이스 */ @media (max-width: 992px) { .main-content-grid { grid-template-columns: 1fr; } .server-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } } /* 모바일 디바이스 */ @media (max-width: 768px) { .server-grid { grid-template-columns: 1fr; } .header { flex-direction: column; text-align: center; } .nav-menu { margin-top: 10px; } .section-header { flex-direction: column; align-items: flex-start; } .section-header button { margin-top: 10px; margin-left: 0; } .card-body .row { flex-direction: column; } .preset-container { flex-direction: row; overflow-x: auto; padding-bottom: 10px; margin-bottom: 5px; -webkit-overflow-scrolling: touch; } .preset-tag { flex: 0 0 auto; white-space: nowrap; } } /* 작은 모바일 화면 */ @media (max-width: 480px) { .container-fluid { padding: 10px; } h1.display-5 { font-size: 1.8rem; } .dashboard-section { padding: 15px; } .server-card { padding: 15px; } .pagination-container { flex-direction: column; align-items: center; } #prevPageBtn, #nextPageBtn { margin: 5px 0; } } /* 자동 장애 보고서 반응형 조정 */ .auto-report-container { max-height: 70vh; overflow-y: auto; } @media (max-width: 768px) { .auto-report-container { max-height: 50vh; } } @media (min-width: 1600px) { .auto-report-container { max-height: 90vh; } } /* 서비스 태그 스타일 추가 */ .services-container { margin-top: 15px; display: flex; flex-wrap: wrap; gap: 8px; } .service-tag { padding: 5px 10px; border-radius: 15px; font-size: 0.75rem; display: inline-flex; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: all 0.2s ease; } .service-tag:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .service-tag.service-running { background-color: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; } .service-tag.service-stopped { background-color: #ffebee; color: #d32f2f; border: 1px solid #ffcdd2; } /* 반응형 그리드 개선 */ @media (min-width: 1400px) { .server-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } } @media (min-width: 1700px) { .server-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } } @media (min-width: 2000px) { .server-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } } /* 오른쪽 장애 패널 개선 */ .incident-panel { position: sticky; top: 20px; max-height: calc(100vh - 40px); overflow-y: auto; } .collapsible-panel { border: 1px solid #e0e0e0; border-radius: 12px; margin-bottom: 15px; overflow: hidden; } .collapsible-header { background-color: #f8f9fa; padding: 12px 15px; font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e0e0e0; } .collapsible-header:hover { background-color: #f0f2f5; } .collapsible-body { padding: 15px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } .collapsible-body.expanded { max-height: 500px; } .collapse-toggle { color: #6c757d; transition: transform 0.3s ease; } .collapse-toggle.expanded { transform: rotate(180deg); } /* 사이드바 토글 버튼 */ .sidebar-toggle { position: fixed; right: 0; top: 50%; transform: translateY(-50%); background-color: var(--primary); color: white; border: none; border-radius: 4px 0 0 4px; width: 30px; height: 60px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); z-index: 100; } .main-content-grid { position: relative; transition: grid-template-columns 0.3s ease; } .main-content-grid.sidebar-hidden { grid-template-columns: 1fr 0fr; } .main-content-grid.sidebar-hidden .incident-panel { display: none; } /* 서비스 태그 간소화 */ .services-container { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; } .service-tag { padding: 3px 6px; border-radius: 4px; font-size: 0.65rem; display: inline-flex; align-items: center; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } /* 모달 스타일 개선 */ .modal-content { border: none; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); overflow: hidden; } .modal-header { border-bottom: 1px solid #f0f0f0; padding: 15px 20px; background-color: #f8f9fa; } .modal-title { font-weight: 600; display: flex; align-items: center; gap: 10px; } .modal-title i { color: var(--primary); } .modal-body { padding: 20px; } .modal-backdrop.show { opacity: 0.7; } /* 모달 진입/퇴장 애니메이션 */ .modal.fade .modal-dialog { transition: transform 0.3s ease-out; } .modal.fade.show .modal-dialog { transform: translate(0, 0); } .modal.fade:not(.show) .modal-dialog { transform: translate(0, -50px); } /* 차트 컨테이너에 Border와 배경 추가 */ .chart-container { background-color: #f8f9fa; border-radius: 8px; padding: 10px; border: 1px solid #e9ecef; } /* 모달 내 테이블 스타일 개선 */ .modal-body .table { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .modal-body .table th { background-color: #f8f9fa; font-weight: 500; vertical-align: middle; } .modal-body .table td { vertical-align: middle; padding: 10px; } /* AI 자동 장애 보고서 스타일 개선 */ #aiReportPanel { margin-bottom: 20px; box-shadow: var(--shadow-md); border-radius: 0.35rem; overflow: hidden; border: none; transition: all 0.3s ease; } #aiReportPanel:hover { box-shadow: var(--shadow-lg); transform: translateY(-5px); } #aiReportPanel .card-header { background-color: #f8f9fa; border-bottom: 1px solid #eaecef; padding: 15px 20px; transition: background-color 0.3s ease; } #aiReportPanel:hover .card-header { background-color: #e9ecef; } #aiProblemList { max-height: none; overflow-y: visible; } .problem-item { position: relative; padding-right: 40px; transition: all 0.3s ease; border-left: 5px solid transparent; overflow: hidden; } .problem-item:before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.03); transform: translateX(-100%); transition: transform 0.3s ease; z-index: 0; } .problem-item:hover:before { transform: translateX(0); } .problem-item:hover { background-color: rgba(255, 255, 255, 0.9); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); transform: translateY(-2px); border-left-width: 8px; } /* 심각도별 색상 */ .problem-item.severity-critical { border-left-color: #d32f2f; } .problem-item.severity-warning, .problem-item.severity-error { border-left-color: #ff8f00; } .problem-item.severity-normal { border-left-color: #4caf50; } /* 문제 설명 스타일 */ .problem-description { font-weight: 600; color: #2c3e50; position: relative; z-index: 1; } /* 해결책 스타일 */ .problem-solution { font-size: 0.85rem; color: #546e7a; position: relative; z-index: 1; } /* 심각도 텍스트 스타일 */ .problem-severity-text { color: #2c3e50; } .severity-critical .problem-severity-text { color: #d32f2f; } .severity-warning .problem-severity-text, .severity-error .problem-severity-text { color: #ff8f00; } /* 검색 아이콘 (자세히 보기 힌트) */ .problem-hint-icon { position: absolute; right: 15px; top: 50%; transform: translateY(-50%); color: #b0bec5; transition: all 0.3s ease; z-index: 1; } .problem-item:hover .problem-hint-icon { color: #4e73df; transform: translateY(-50%) scale(1.2); } /* 클릭 힌트 툴팁 */ .problem-item:hover::after { content: '클릭하여 상세 보고서 보기'; position: absolute; bottom: 0; right: 0; background-color: rgba(74, 105, 189, 0.9); color: white; padding: 3px 8px; font-size: 0.7rem; border-radius: 4px 0 0 0; z-index: 2; } /* 커서 스타일 */ .problem-item { cursor: pointer; } /* 리스트 애니메이션 */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .problem-item { animation: fadeInUp 0.5s ease forwards; animation-delay: calc(var(--item-index) * 0.1s); opacity: 0; } </style> </head> <body> <div class="loading" id="loadingIndicator"> <div class="loading-content"> <div class="spinner-border text-primary mb-3" role="status"> <span class="visually-hidden">로딩 중...</span> </div> <div>데이터를 가져오는 중입니다...</div> </div> </div> <header class="header"> <div class="logo"> <i class="fas fa-server"></i> OpenManager AI </div> <nav class="nav-menu"> <a href="index.html" class="nav-item">소개</a> <a href="server_dashboard.html" class="nav-item active">서버 모니터링</a> </nav> </header> <div class="container-fluid mt-4"> <div class="row mb-4"> <div class="col"> <h1 class="display-5 mb-0">서버 모니터링 대시보드</h1> <p class="text-muted" id="timestamp">데이터 기준 시각: 로딩 중...</p> </div> </div> <!-- AI 자동 요약 알림 영역 추가 --> <div class="row mb-4"> <div class="col-12"> <div id="statusSummaryAlert" class="alert d-flex align-items-center" role="alert" style="display: none;"> <div class="me-3 status-icon"> <!-- 아이콘은 상태에 따라 JS에서 동적으로 변경됨 --> </div> <div class="status-message"> <!-- 상태 메시지는 JS에서 동적으로 변경됨 --> </div> </div> </div> </div> <!-- AI 질의 영역 (전체 너비) --> <div class="row mb-4"> <div class="col-12"> <div class="ai-query-container"> <h5 class="mb-3"><i class="fas fa-robot me-2"></i>AI 서버 분석</h5> <p class="text-muted mb-3">서버 상태나 성능에 대해 자연어로 질문해보세요. (예: "CPU 사용률이 높은 서버는?", "메모리 상태 알려줘")</p> <div class="input-group mb-3"> <span class="input-group-text bg-primary text-white"><i class="fas fa-search"></i></span> <input type="text" class="form-control query-input shadow-sm" id="queryInput" placeholder="자연어로 질문하세요... (Enter 키를 눌러 질문)"> <button class="btn btn-primary ai-query-submit shadow-sm" id="ai-query-submit" type="button"> <i class="fas fa-paper-plane me-1"></i> 질문하기 </button> </div> <div class="preset-container mb-3"> <button class="preset-tag example-query-btn" type="button">CPU 사용률이 높은 서버는?</button> <button class="preset-tag example-query-btn" type="button">최근 장애 상황은?</button> <button class="preset-tag example-query-btn" type="button">응답 속도가 느린 서버는?</button> </div> <div class="query-loading" id="queryLoading" style="display:none;"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">분석 중...</span> </div> <p class="mt-2">AI가 서버 데이터를 분석 중입니다...</p> </div> <div class="query-result shadow-sm position-relative" id="queryResult" style="display:none;"> <div class="d-flex justify-content-between align-items-center mb-3 close-button-container"> <span class="fw-bold text-primary">AI 분석 결과</span> <button type="button" id="closeQueryResult" class="btn btn-sm btn-light border rounded-circle"> <i class="fas fa-times"></i> </button> </div> <div id="queryResultContent"></div> </div> <div id="queryHistory" class="mt-4"> <!-- 질문/응답 히스토리 --> </div> </div> </div> </div> <!-- AI 자동 장애 보고서 (전체 너비) - 순서 변경 (2번째 위치로) --> <div class="row mb-4"> <div class="col-12"> <div class="panel card" id="aiReportPanel"> <div class="card-header bg-light d-flex justify-content-between align-items-center"> <div><i class="fas fa-exclamation-triangle me-2 text-danger"></i> AI 자동 장애 보고서</div> <button class="btn btn-sm btn-outline-primary" id="downloadAllReportsBtn"> <i class="bi bi-download"></i> 전체 보고서 다운로드 </button> </div> <div class="card-body"> <div class="alert alert-info mb-3"> <i class="fas fa-info-circle me-2"></i> 각 문제 항목을 <strong>클릭</strong>하면 해당 문제의 <strong>상세 보고서</strong>를 확인할 수 있습니다. <small class="d-block mt-1">보고서는 .txt 파일로 저장하여 관리할 수 있습니다.</small> </div> <div class="alert alert-info" id="aiProblemsLoading" style="display: none;"> <div class="spinner-border spinner-border-sm text-primary me-2" role="status"> <span class="visually-hidden">로딩 중...</span> </div> AI가 문제를 분석 중입니다... </div> <div id="aiProblemsEmpty" style="display: none;" class="text-center py-4"> <i class="fas fa-check-circle text-success" style="font-size: 2rem;"></i> <p class="mt-3 mb-0\">현재 감지된 문제가 없습니다.</p> </div> <ul class="list-group" id="aiProblemList"> <!-- AI 문제 항목들이 여기에 추가됩니다 --> </ul> <div class="d-flex justify-content-center mt-3"> <div class="problem-pagination"> <!-- 문제 페이지네이션이 여기에 추가됩니다 --> </div> </div> </div> </div> </div> </div> <!-- 서버 그리드와 서버 현황 요약 섹션 --> <div class="row mb-4"> <!-- 서버 그리드 (전체 너비) --> <div class="col-12"> <div class="card search-container mb-4"> <div class="card-body"> <div class="row"> <div class="col-md-6 mb-3 mb-md-0"> <div class="input-group"> <input type="text" class="form-control" id="searchInput" placeholder="호스트명 검색..."> <button class="btn btn-refresh" type="button" id="refreshBtn"> <span class="refresh-content"> <i class="fas fa-sync-alt me-1"></i> 새로고침 </span> <span class="loading-content" style="display:none"> <span class="spinner-border spinner-border-sm" role="status"></span> 로딩중... </span> </button> <div class="refresh-tooltip">데이터 새로고침 (서버 문제 발생 시 유용)</div> </div> </div> <div class="col-md-3 mb-3 mb-md-0"> <select class="form-select" id="statusFilter"> <option value="all">모든 상태</option> <option value="critical">Critical</option> <option value="warning">Warning</option> <option value="normal">Normal</option> </select> </div> <div class="col-md-3"> <select class="form-select" id="pageSize"> <option value="10">10개씩 보기</option> <option value="20">20개씩 보기</option> <option value="50">50개씩 보기</option> </select> </div> </div> </div> </div> <!-- 서버 카드 안내 --> <div class="alert alert-info mb-3"> <i class="fas fa-info-circle me-2"></i> 각 서버 카드를 <strong>클릭</strong>하면 해당 서버의 상세 정보를 확인할 수 있습니다. <small class="d-block mt-1">카드에 마우스를 올리면 추가 정보를 확인할 수 있습니다.</small> </div> <!-- 서버 카드 그리드 --> <div class="server-grid" id="serverGrid"></div> <!-- 페이징 --> <div class="d-flex justify-content-between align-items-center mb-4"> <div class="text-muted" id="serverCount">전체 0 서버 중 0-0 표시 중</div> <div class="pagination-container"> <button class="btn btn-sm btn-outline-secondary" id="prevPageBtn"> <i class="fas fa-chevron-left"></i> 이전 </button> <span class="mx-3" id="currentPage">1 / 1</span> <button class="btn btn-sm btn-outline-secondary" id="nextPageBtn"> 다음 <i class="fas fa-chevron-right"></i> </button> </div> </div> </div> </div> <!-- 서버 현황 요약 (전체 너비) --> <div class="row"> <div class="col-12"> <div class="panel card mb-4" id="serverStatusSummaryPanel"> <div class="card-header bg-light"> <div><i class="fas fa-tasks me-2 text-primary"></i> 서버 현황 요약</div> </div> <div class="card-body"> <div id="statusSummaryContainer"> <!-- 요약 정보가 여기에 동적으로 채워집니다 (data_processor.js) --> <p>요약 정보를 불러오는 중...</p> </div> </div> </div> </div> </div> <!-- 서버 상세 정보 모달 --> <div class="modal fade" id="serverDetailModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="modalServerName">서버 상세 정보</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="row mb-4"> <!-- 시스템 정보 --> <div class="col-md-6"> <h6 class="mb-3">시스템 정보</h6> <table class="table table-sm"> <tbody id="systemInfoTable"> <tr> <th>OS</th> <td id="modalOS">-</td> </tr> <tr> <th>가동 시간</th> <td id="modalUptime">-</td> </tr> <tr> <th>프로세스 수</th> <td id="modalProcessCount">-</td> </tr> <tr> <th>좀비 프로세스</th> <td id="modalZombieCount">-</td> </tr> <tr> <th>로드 평균 (1분)</th> <td id="modalLoadAvg">-</td> </tr> <tr> <th>마지막 업데이트</th> <td id="modalLastUpdate">-</td> </tr> </tbody> </table> </div> <!-- 리소스 현황 --> <div class="col-md-6"> <h6 class="mb-3">리소스 현황</h6> <div class="chart-container"> <canvas id="resourceBarChart"></canvas> </div> </div> </div> <div class="row mb-4"> <!-- 네트워크 정보 --> <div class="col-md-6"> <h6 class="mb-3">네트워크 정보</h6> <table class="table table-sm"> <tbody id="networkInfoTable"> <tr> <th>인터페이스</th> <td id="modalNetInterface">-</td> </tr> <tr> <th>수신 바이트</th> <td id="modalRxBytes">-</td> </tr> <tr> <th>송신 바이트</th> <td id="modalTxBytes">-</td> </tr> <tr> <th>수신 오류</th> <td id="modalRxErrors">-</td> </tr> <tr> <th>송신 오류</th> <td id="modalTxErrors">-</td> </tr> </tbody> </table> </div> <!-- 서비스 상태 --> <div class="col-md-6"> <h6 class="mb-3">서비스 상태</h6> <div id="modalServiceStatus" class="service-status-container"> <!-- 서비스 상태 태그들이 여기에 추가됨 --> </div> </div> </div> <!-- 오류 메시지 --> <div class="row mb-4"> <div class="col-12"> <h6 class="mb-3">오류 메시지</h6> <div id="modalErrorsContainer" class="alert alert-info"> 현재 보고된 오류가 없습니다. </div> </div> </div> <!-- 24시간 리소스 사용 추이 --> <div class="row"> <div class="col-12"> <h6 class="mb-3">24시간 리소스 사용 추이</h6> <div class="chart-container" style="height: 300px;"> <canvas id="resourceHistoryChart"></canvas> </div> </div> </div> </div> </div> </div> </div> </div> <!-- 필요한 자바스크립트 함수 정의 --> <script> // 패널 토글 기능 제거 </script> <!-- 자바스크립트 라이브러리 및 소스 파일 --> <!-- 스크립트는 순서가 중요: 부트스트랩, dummy_data_generator.js, ai_processor.js, data_processor.js 순서로 로드 --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> // 초기 글로벌 변수 설정 window.serverData = window.serverData || []; window.loadStatus = { dummyGenerator: false, aiProcessor: false, dataProcessor: false }; </script> <!-- React 앱 내부에서 이미 모듈이 import되고 있으므로 script 태그 제거 --> <!-- 스크립트 로드 오류 처리 --> <script> // 스크립트 로드 검증 및 오류 처리 document.addEventListener('DOMContentLoaded', function() { console.log('DOM이 로드되었습니다. 스크립트 상태 확인을 시작합니다.'); // 스크립트 로드 상태 확인 checkScriptStatus(0); // 첫 번째 시도 // 스크립트 상태 확인 및 필요한 초기화 수행 (최대 3번 시도) function checkScriptStatus(attempt) { const maxAttempts = 3; if (attempt >= maxAttempts) { console.error(`스크립트 로드 최대 시도 횟수(${maxAttempts}회)를 초과했습니다.`); createFallbackInitialization(); return; } console.log(`스크립트 로드 상태 확인 중... (시도 ${attempt + 1}/${maxAttempts})`); console.log('로드 상태:', window.loadStatus); // 모든 스크립트가 로드되었는지 확인 if (window.loadStatus.dummyGenerator && window.loadStatus.aiProcessor && window.loadStatus.dataProcessor) { console.log('모든 스크립트가 정상적으로 로드되었습니다.'); initializeDashboard(); return; } // 어떤 스크립트가 누락되었는지 확인 const missingScripts = []; // React 앱에서는 모듈 로딩 방식이 변경되었으므로 이 검사가 필요없음 // if (!window.loadStatus.dummyGenerator) missingScripts.push('dummy_data_generator.js'); // if (!window.loadStatus.aiProcessor) missingScripts.push('ai_processor.js'); // if (!window.loadStatus.dataProcessor) missingScripts.push('data_processor.js'); if (missingScripts.length > 0) { console.warn(`다음 스크립트가 로드되지 않았습니다: ${missingScripts.join(', ')}`); } // 500ms 후 다시 시도 setTimeout(() => checkScriptStatus(attempt + 1), 500); } // 대시보드 초기화 함수 function initializeDashboard() { console.log('대시보드 초기화를 시작합니다.'); // 데이터 프로세서 확인 및 초기화 if (typeof DataProcessor === 'function' && !window.dataProcessor) { console.log('DataProcessor 인스턴스를 생성합니다.'); window.dataProcessor = new DataProcessor(); } else if (window.dataProcessor) { console.log('기존 DataProcessor 인스턴스가 사용됩니다.'); } else { console.error('DataProcessor 클래스를 찾을 수 없습니다.'); createFallbackInitialization(); } // AI 프로세서 확인 및 초기화 if (typeof AIProcessor === 'function' && !window.aiProcessor) { console.log('AIProcessor 인스턴스를 생성합니다.'); window.aiProcessor = new AIProcessor(); } // 서버 데이터가 없으면 더미 데이터 생성기 실행 if (!window.serverData || window.serverData.length === 0) { createDummyData(); } } // 최후의 백업 초기화 function createFallbackInitialization() { console.warn('백업 초기화 프로세스를 시작합니다.'); // 1. 더미 데이터 생성 createDummyData(); // 2. DataProcessor 최소 구현 if (!window.dataProcessor && document.getElementById('serverGrid')) { console.warn('DataProcessor 최소 구현을 시도합니다.'); // 서버 그리드와 로딩 인디케이터 참조 const serverGrid = document.getElementById('serverGrid'); const loadingIndicator = document.getElementById('loadingIndicator'); // 최소한의 DataProcessor 구현 window.dataProcessor = { serverData: window.serverData || [], filteredData: window.serverData || [], showLoading: function() { if (loadingIndicator) loadingIndicator.style.display = 'block'; if (serverGrid) serverGrid.style.opacity = '0.3'; }, hideLoading: function() { if (loadingIndicator) loadingIndicator.style.display = 'none'; if (serverGrid) serverGrid.style.opacity = '1'; }, handleDataUpdate: function(data) { this.serverData = [...data]; this.filteredData = [...data]; this.renderServerGrid(); this.hideLoading(); }, renderServerGrid: function() { if (!serverGrid) return; serverGrid.innerHTML = ''; if (this.serverData.length === 0) { serverGrid.innerHTML = '<div class="alert alert-info">서버 데이터가 없습니다.</div>'; return; } // 간단한 서버 카드 생성 및 추가 this.serverData.forEach(server => { const card = document.createElement('div'); card.className = 'server-card'; const status = server.cpu_usage >= 90 || server.memory_usage_percent >= 90 ? 'critical' : server.cpu_usage >= 70 || server.memory_usage_percent >= 70 ? 'warning' : 'normal'; card.innerHTML = ` <div class="server-header"> <div class="server-name">${server.hostname || 'Unknown Server'}</div> <div class="server-status status-${status}">${status === 'critical' ? '심각' : status === 'warning' ? '경고' : '정상'}</div> </div> <div class="server-details"> <div class="detail-item"> <div class="detail-label">CPU 사용량</div> <div class="detail-value">${server.cpu_usage || 0}%</div> </div> <div class="detail-item"> <div class="detail-label">메모리</div> <div class="detail-value">${server.memory_usage_percent || 0}%</div> </div> </div> `; serverGrid.appendChild(card); }); } }; // 로딩 상태 업데이트 및 데이터 렌더링 window.dataProcessor.showLoading(); window.dataProcessor.handleDataUpdate(window.serverData || []); } } // 더미 데이터 생성 함수 function createDummyData() { console.log('백업 더미 데이터 생성을 시도합니다.'); if (typeof generateDummyData === 'function') { console.log('기존 generateDummyData 함수를 사용하여 더미 데이터를 생성합니다.'); window.serverData = generateDummyData(10); return; } console.warn('generateDummyData 함수가 없습니다. 기본 더미 데이터를 생성합니다.'); window.generateDummyData = function(count) { console.log('기본 더미 데이터 생성 중...', count, '서버'); const servers = []; for (let i = 1; i <= count; i++) { // 약 30%의 확률로 문제 있는 서버 생성 const hasProblem = Math.random() < 0.3; const problemLevel = hasProblem ? (Math.random() < 0.3 ? 'critical' : 'warning') : 'normal'; const cpuUsage = problemLevel === 'critical' ? Math.floor(Math.random() * 10) + 90 : // 90-99% problemLevel === 'warning' ? Math.floor(Math.random() * 20) + 70 : // 70-89% Math.floor(Math.random() * 50) + 10; // 10-59% const memoryUsage = problemLevel === 'critical' ? Math.floor(Math.random() * 10) + 90 : // 90-99% problemLevel === 'warning' ? Math.floor(Math.random() * 20) + 70 : // 70-89% Math.floor(Math.random() * 50) + 10; // 10-59% servers.push({ hostname: `server-${i}`, os: 'Linux', uptime: '3 days, 12:30:15', cpu_usage: cpuUsage, memory_usage_percent: memoryUsage, memory_total: '16GB', memory_used: '8GB', disk: [{ mount: '/', disk_total: '500GB', disk_used: '300GB', disk_usage_percent: Math.floor(Math.random() * 100) }], load_avg_1m: (Math.random() * 5).toFixed(2), load_avg_5m: (Math.random() * 4).toFixed(2), load_avg_15m: (Math.random() * 3).toFixed(2), process_count: Math.floor(Math.random() * 200) + 50, zombie_count: Math.floor(Math.random() * 3), timestamp: new Date().toISOString(), net: { interface: 'eth0', rx_bytes: Math.floor(Math.random() * 1000000), tx_bytes: Math.floor(Math.random() * 1000000), rx_errors: Math.floor(Math.random() * 10), tx_errors: Math.floor(Math.random() * 10) }, services: { 'nginx': problemLevel === 'critical' ? 'stopped' : 'running', 'mysql': Math.random() > 0.9 ? 'stopped' : 'running', 'redis': Math.random() > 0.9 ? 'stopped' : 'running' }, errors: problemLevel !== 'normal' ? [problemLevel === 'critical' ? 'Critical: 서버 응답 없음' : '경고: 높은 부하 감지'] : [] }); } // 전역 변수에 저장 window.serverData = servers; // 이벤트 발생시키기 try { const event = new CustomEvent('serverDataUpdated', { detail: servers }); window.dispatchEvent(event); console.log('서버 데이터 업데이트 이벤트가 발생되었습니다.'); } catch (e) { console.error('이벤트 발생 중 오류:', e); } return servers; }; // 즉시 더미 데이터 생성 window.serverData = window.generateDummyData(10); } }); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/skyasu2/openmanager-vibe-v4'

If you have feedback or need assistance with the MCP directory API, please join our Discord server