Skip to main content
Glama

OpenManager Vibe V4 MCP Server

by skyasu2
server_dashboard.html.bak104 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="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: #4a69bd; --primary-dark: #3a559d; --primary-light: rgba(74, 105, 189, 0.3); --background: #f5f6fa; --surface: #ffffff; --text: #333333; --text-white: #ffffff; --border: #e0e0e0; --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); --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: #f5f6fa; color: #333; 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(280px, 1fr)); gap: 15px; 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; } .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); } .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-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.65rem; padding: 2px 8px; border-radius: 20px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; } .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 { flex-grow: 1; height: 5px; background-color: #ecf0f1; border-radius: 5px; overflow: hidden; } .progress-bar { height: 100%; border-radius: 6px; transition: width 0.5s ease; } .progress-normal { background: linear-gradient(90deg, #2ecc71, #27ae60); } .progress-warning { background: linear-gradient(90deg, #f39c12, #e67e22); } .progress-critical { background: linear-gradient(90deg, #e74c3c, #c0392b); } .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-color: var(--surface); border-radius: var(--border-radius-md); box-shadow: var(--shadow-md); 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); } .query-result.active { display: block; animation: fadeIn 0.3s ease-out forwards; } /* 반응형 */ @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 { margin-top: 15px; display: flex; flex-wrap: wrap; gap: 8px; } .preset-tag { cursor: pointer; padding: 6px 12px; border-radius: 20px; font-size: 0.8rem; background: #f5f6fa; border: 1px solid #e9ecef; transition: all 0.2s ease; } .preset-tag:hover { background: #e9ecef; transform: translateY(-2px); } .preset-tag.tag-critical { background-color: #ffebee; color: #d32f2f; border-color: #ffcdd2; } .preset-tag.tag-warning { background-color: #fff8e1; color: #ff8f00; border-color: #ffecb3; } /* 자동 장애 보고서 스타일 개선 */ .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: column; } .preset-tag { width: 100%; text-align: center; margin-bottom: 8px; } } /* 작은 모바일 화면 */ @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.1); 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; } .modal-title i { margin-right: 10px; color: var(--primary); } .modal-body { padding: 20px; } .detail-section { margin-bottom: 20px; } .detail-section-title { font-size: 1rem; font-weight: 600; margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #f0f0f0; padding-bottom: 8px; } /* AI 자동 장애 보고서 리스트 아이템 스타일 */ .problem-item { cursor: pointer; transition: background-color 0.2s ease; } .problem-item:hover { background-color: #f8f9fa; /* 약간 밝은 배경으로 호버 효과 */ } .problem-item .problem-description { font-weight: 500; } .problem-item .problem-solution { font-size: 0.9em; color: #555; } .problem-item .problem-severity-text { /* 이미 부트스트랩 fw-bold가 적용됨 */ } .list-group-item.severity-critical { border-left: 5px solid var(--bs-danger, #dc3545); background-color: rgba(220, 53, 69, 0.05); /* 매우 연한 빨강 배경 */ } .list-group-item.severity-warning, .list-group-item.severity-error { /* Error도 Warning과 동일하게 처리 */ border-left: 5px solid var(--bs-warning, #ffc107); background-color: rgba(255, 193, 7, 0.05); /* 매우 연한 노랑 배경 */ } #aiProblemList { /* 스크롤바는 JS에서 maxHeight와 함께 overflowY: auto로 제어 */ } </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"> <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"> <input type="text" class="form-control query-input" id="queryInput" placeholder="자연어로 질문하세요... (Enter 키를 눌러 질문)"> <button class="btn btn-primary ai-query-submit" id="ai-query-submit" type="button"> <i class="fas fa-paper-plane"></i> 질문하기 </button> </div> <!-- 장애 프리셋 추가 --> <div class="preset-container"> <div class="preset-tag tag-critical" data-preset="CPU 사용률이 높은 서버 목록과 원인 분석"> <i class="fas fa-microchip me-1"></i> CPU 과부하 분석 </div> <div class="preset-tag tag-warning" data-preset="메모리 사용량이 많은 서버 목록 및 조치방안"> <i class="fas fa-memory me-1"></i> 메모리 부족 분석 </div> <div class="preset-tag tag-critical" data-preset="서비스가 중단된 서버 목록과 재시작 방법"> <i class="fas fa-exclamation-triangle me-1"></i> 서비스 중단 분석 </div> <div class="preset-tag tag-warning" data-preset="디스크 공간이 부족한 서버 분석 및 해결방안"> <i class="fas fa-hdd me-1"></i> 디스크 공간 분석 </div> <div class="preset-tag" data-preset="전체 서버 상태 요약 보고서 생성"> <i class="fas fa-file-alt me-1"></i> 전체 상태 요약 </div> </div> <div class="query-loading" id="queryLoading"> <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" id="queryResult"></div> </div> </div> </div> <!-- 자동 장애 보고서 섹션 및 서버 현황 요약을 포함하는 incident-panel을 AI 질의 영역 아래로 이동 --> <div class="incident-panel mt-4"> <!-- mt-4 클래스 추가하여 위쪽과 간격 조정 --> <!-- AI 자동 장애 보고서 패널 --> <div class="collapsible-panel" id="aiReportPanel"> <!-- 패널에 ID 추가 --> <div class="collapsible-header" onclick="toggleCollapsible(this)"> <div><i class="fas fa-exclamation-triangle me-2 text-danger"></i> AI 자동 장애 보고서</div> <!-- 제목 수정 --> <div class="collapse-toggle"><i class="fas fa-chevron-down"></i></div> </div> <div class="collapsible-body expanded"> <!-- 기본적으로 펼쳐진 상태로 시작 --> <div class="d-flex justify-content-between align-items-center mb-3"> <h6 class="mb-0">감지된 문제</h6> <button class="btn btn-sm btn-outline-primary" id="downloadAllReportsBtn"> <i class="bi bi-download"></i> 전체 보고서 다운로드 </button> </div> <div class="alert alert-info" id="aiProblemsLoading" style="display: none;"> <!-- ID 변경 및 초기 숨김 --> <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="mt-3 text-center" id="aiProblemListToggle" style="display: none;"> <button class="btn btn-sm btn-outline-secondary" id="toggleAiProblemListBtn" data-expanded="false"> 더 보기 (0개 더 있음) </button> </div> </div> </div> <!-- 서버 현황 요약 패널 (기존 incident-panel 내 다른 패널이 있다면 여기에 유지 또는 추가) --> <div class="collapsible-panel mt-3" id="serverStatusSummaryPanel"> <div class="collapsible-header" onclick="toggleCollapsible(this)"> <div><i class="fas fa-tasks me-2 text-primary"></i> 서버 현황 요약</div> <div class="collapse-toggle"><i class="fas fa-chevron-down"></i></div> </div> <div class="collapsible-body expanded"> <div id="statusSummaryContainer"> <!-- 요약 정보가 여기에 동적으로 채워집니다 (data_processor.js) --> <p>요약 정보를 불러오는 중...</p> </div> </div> </div> </div> <!-- 메인 컨텐츠 그리드에서 incident-panel 제거하고, 서버 그리드 및 필터만 남김 --> <div class="main-content-grid mt-4" id="mainContentGrid"> <!-- 좌측: 서버 그리드 및 검색 필터 --> <div> <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"> <i class="fas fa-sync-alt me-1"></i> 새로고침 </button> </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="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 class="incident-panel"> 이 부분은 위로 이동되었음 --> <!-- 이전에 여기에 있던 다른 패널들이 있다면, 위로 옮겨진 incident-panel 내부로 통합하거나, 별도 처리 필요 --> <!-- </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> <!-- 스크립트는 순서가 중요: 먼저 dummy_data_generator.js, 그 다음 ai_processor.js 로드 --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="dummy_data_generator.js"></script> <script src="ai_processor.js"></script> <script> // 전역 변수 let serverData = []; let currentSort = { column: 'hostname', ascending: true }; let charts = {}; // 페이지네이션 변수 let currentPage = 1; let pageSize = 10; let filteredData = []; // 초기화 함수 function init() { // 서버 데이터 로드 loadServerData(); // 검색 및 필터링 이벤트 리스너 추가 document.getElementById('searchInput').addEventListener('keyup', function() { // 익명 함수로 감싸서 호출 if (window.dataProcessor) { window.dataProcessor.searchQuery = this.value.toLowerCase(); window.dataProcessor.currentPage = 1; window.dataProcessor.applyFiltersAndSort(); } }); document.getElementById('statusFilter').addEventListener('change', function() { // 익명 함수로 감싸서 호출 if (window.dataProcessor) { // dataProcessor의 필터 상태를 직접 업데이트하는 로직이 필요할 수 있음 // 여기서는 applyFiltersAndSort가 내부적으로 필터 값을 읽는다고 가정 window.dataProcessor.currentFilter = this.value; // 예시: statusFilter의 value를 직접 사용 window.dataProcessor.currentPage = 1; window.dataProcessor.applyFiltersAndSort(); } }); document.getElementById('pageSize').addEventListener('change', function() { currentPage = 1; // 이 currentPage는 전역 변수를 의미하는 것으로 보임 // dataProcessor.itemsPerPage도 업데이트 필요 if(window.dataProcessor) { window.dataProcessor.itemsPerPage = parseInt(this.value); window.dataProcessor.currentPage = 1; window.dataProcessor.applyFiltersAndSort(); } }); // 페이지 이동 버튼 document.getElementById('prevPageBtn').addEventListener('click', function() { // currentPage는 전역 변수를 사용하고, dataProcessor의 currentPage와 동기화 필요 if (currentPage > 1) { currentPage--; if(window.dataProcessor) { window.dataProcessor.currentPage = currentPage; window.dataProcessor.applyFiltersAndSort(); } } }); document.getElementById('nextPageBtn').addEventListener('click', function() { // filteredData는 html 로컬 변수, dataProcessor.filteredData와 동기화 또는 직접 사용 필요 // 여기서는 dataProcessor의 상태를 기준으로 동작하도록 수정 if(window.dataProcessor && window.dataProcessor.currentPage < Math.ceil(window.dataProcessor.filteredData.length / window.dataProcessor.itemsPerPage)) { currentPage++; // 로컬 currentPage도 업데이트 window.dataProcessor.currentPage = currentPage; window.dataProcessor.applyFiltersAndSort(); } }); // 새로고침 버튼 document.getElementById('refreshBtn').addEventListener('click', loadServerData); // loadServerData는 dataProcessor.loadData() 호출로 이어질 것 // AI 질의 처리 document.getElementById('queryInput').addEventListener('keyup', function(event) { if (event.key === 'Enter') { handleQuery(this.value); // processAIQuery -> handleQuery } }); document.getElementById('ai-query-submit').addEventListener('click', function() { // 익명 함수로 감싸서 호출 handleQuery(document.getElementById('queryInput').value); // processAIQuery -> handleQuery }); // 전체 보고서 다운로드 버튼 document.getElementById('downloadAllReportsBtn').addEventListener('click', downloadAllReports); // 장애 프리셋 이벤트 추가 document.querySelectorAll('.preset-tag').forEach(tag => { tag.addEventListener('click', function() { const preset = this.dataset.preset; document.getElementById('queryInput').value = preset; handleQuery(preset); // processAIQuery -> handleQuery }); }); // 사이드바 토글 버튼 이벤트 리스너 // document.getElementById('sidebarToggle').addEventListener('click', toggleSidebar); // sidebarToggle ID가 현재 HTML에 없음 // 저장된 사이드바 상태 복원 // const savedSidebarState = localStorage.getItem('sidebar_state'); // if (savedSidebarState === 'hidden') { // toggleSidebar(); // } // 저장된 패널 상태 복원 (페이지 로드 시 각 패널 헤더에 대해 실행) document.querySelectorAll('.collapsible-header').forEach(header => { // const panelId = header.textContent.trim(); // 불안정한 방식 const panelBody = header.nextElementSibling; if (panelBody && panelBody.parentElement && panelBody.parentElement.id) { const panelId = panelBody.parentElement.id; // 부모 .collapsible-panel의 ID를 사용 const savedState = localStorage.getItem(`panel_${panelId}_state`); // 키 이름 일관성 유지 if (savedState === 'collapsed' && panelBody.classList.contains('expanded')) { // 현재 펼쳐져 있는데 저장된 상태가 'collapsed'이면 토글 toggleCollapsible(header); } else if (savedState === 'expanded' && !panelBody.classList.contains('expanded')) { // 현재 접혀 있는데 저장된 상태가 'expanded'이면 토글 toggleCollapsible(header); } } }); } // window.serverData가 생성될 때까지 대기 function checkServerData() { // 새로 추가: 초기화 시간을 추적하기 위한 정적 변수 if (typeof checkServerData.startTime === 'undefined') { checkServerData.startTime = Date.now(); } // 새로 추가: 10초 제한시간 설정 const timeoutMs = 10000; // 10초 const elapsedTime = Date.now() - checkServerData.startTime; if (window.serverData && Array.isArray(window.serverData) && window.serverData.length > 0) { serverData = window.serverData; processServerData(); document.getElementById('loadingIndicator').style.display = 'none'; } else if (elapsedTime > timeoutMs) { // 제한시간 초과 시 빈 배열로 초기화하고 계속 진행 console.warn('서버 데이터 로드 타임아웃. 빈 데이터로 초기화합니다.'); window.serverData = []; serverData = []; processServerData(); document.getElementById('loadingIndicator').style.display = 'none'; } else { // 로딩 메시지 업데이트 (사용자에게 진행상황 알림) const loadingText = document.querySelector('.loading-content div:last-child'); if (loadingText) { const dots = '.'.repeat((new Date().getSeconds() % 3) + 1); loadingText.textContent = `서버 데이터를 가져오는 중입니다${dots}`; } // 데이터가 아직 없으면 100ms 후 재시도 setTimeout(checkServerData, 100); } } // 서버 데이터 처리 function processServerData() { // applyFilters(); // 이 호출은 data_processor.js의 handleDataUpdate 내부 applyFiltersAndSort로 대체됨 updatePagination(); // 이 함수도 data_processor.js의 updatePagination으로 대체되었는지 확인 필요 renderServerTable(); // 이 함수도 data_processor.js의 updateServerGrid으로 대체되었는지 확인 필요 updateCharts(); updateTimestamp(); // updateStatusCounts(); // 상태 요약 카운트 업데이트 - 이 줄을 주석 처리 (data_processor.js의 updateGlobalStatusSummary로 대체됨) createStatusChart(); // 상태 요약 차트 생성 - 이 함수도 data_processor.js로 옮겨졌거나 대체되었는지 확인 필요 // generateIncidentReports(); // 장애 보고서 자동 생성 - 이 함수 내 generateIncidentAccordionItems 호출로 인해 오류 발생. data_processor.js의 updateProblemsList로 대체됨. } // 서버 상태 요약 카운트 업데이트 함수 function updateStatusCounts() { if (!serverData || serverData.length === 0) return; const criticalCount = serverData.filter(server => server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90 ).length; const warningCount = serverData.filter(server => (server.cpu_usage >= 70 && server.cpu_usage < 90) || (server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) || (server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90) ).length; const normalCount = serverData.length - criticalCount - warningCount; // 카운트 업데이트 document.getElementById('normalCount').textContent = normalCount; document.getElementById('warningCount').textContent = warningCount; document.getElementById('criticalCount').textContent = criticalCount; } // 상태 차트 생성 함수 function createStatusChart() { const ctx = document.getElementById('statusChart'); if (!ctx) return; // 이미 차트가 존재하면 제거 if (charts.statusChart) { charts.statusChart.destroy(); } // 상태별 서버 수 계산 const criticalCount = serverData.filter(server => server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90 ).length; const warningCount = serverData.filter(server => (server.cpu_usage >= 70 && server.cpu_usage < 90) || (server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) || (server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90) ).length; const normalCount = serverData.length - criticalCount - warningCount; // 차트 생성 charts.statusChart = new Chart(ctx, { type: 'doughnut', data: { labels: ['정상', '경고', '심각'], datasets: [{ data: [normalCount, warningCount, criticalCount], backgroundColor: ['#28a745', '#ffc107', '#dc3545'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } // 리소스 개요 차트 생성 함수 (CPU, 메모리) function updateCharts() { if (!serverData || serverData.length === 0) return; // CPU 사용량 차트 createResourceChart('cpuUsageChart', 'CPU 사용량', serverData.map(s => s.cpu_usage)); // 메모리 사용량 차트 createResourceChart('memoryUsageChart', '메모리 사용량', serverData.map(s => s.memory_usage_percent)); } // 리소스 차트 생성 함수 function createResourceChart(canvasId, label, dataValues) { const canvas = document.getElementById(canvasId); if (!canvas) return; // 기존 차트 제거 if (charts[canvasId]) { charts[canvasId].destroy(); } // 서버 이름과 값 페어로 정렬 const serverValuePairs = serverData.map((server, index) => ({ name: server.hostname, value: dataValues[index] })).sort((a, b) => b.value - a.value).slice(0, 10); // 차트 생성 charts[canvasId] = new Chart(canvas, { type: 'bar', data: { labels: serverValuePairs.map(pair => pair.name), datasets: [{ label: label, data: serverValuePairs.map(pair => pair.value), backgroundColor: serverValuePairs.map(pair => { if (pair.value >= 90) return '#dc3545'; if (pair.value >= 70) return '#ffc107'; return '#28a745'; }), borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: '사용률 (%)' } }, x: { ticks: { autoSkip: false, maxRotation: 45, minRotation: 45 } } }, plugins: { legend: { display: false } } } }); } // 필터 적용 function applyFilters() { // 상태 필터링 const statusFilter = document.getElementById('statusFilter').value; filteredData = serverData; if (statusFilter !== 'all') { filteredData = serverData.filter(server => { if (statusFilter === 'critical' && (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90)) { return true; } else if (statusFilter === 'warning' && ((server.cpu_usage >= 70 && server.cpu_usage < 90) || (server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) || (server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90))) { return true; } else if (statusFilter === 'normal' && (server.cpu_usage < 70 && server.memory_usage_percent < 70 && server.disk[0].disk_usage_percent < 70)) { return true; } return false; }); } // 검색 필터링 const searchTerm = document.getElementById('searchInput').value.toLowerCase(); if (searchTerm) { filteredData = filteredData.filter(server => server.hostname.toLowerCase().includes(searchTerm) ); } // 정렬 적용 filteredData.sort((a, b) => { let valueA = a[currentSort.column]; let valueB = b[currentSort.column]; if (currentSort.column === 'disk_usage_percent') { valueA = a.disk[0].disk_usage_percent; valueB = b.disk[0].disk_usage_percent; } if (currentSort.ascending) { return valueA > valueB ? 1 : -1; } else { return valueA < valueB ? 1 : -1; } }); // 페이지 리셋 (필터가 변경됐을 때) currentPage = 1; updatePagination(); updateServerCount(filteredData.length); } // 페이지네이션 컨트롤 업데이트 function updatePagination() { const totalPages = Math.ceil(filteredData.length / pageSize); const prevPageBtn = document.getElementById('prevPageBtn'); const nextPageBtn = document.getElementById('nextPageBtn'); prevPageBtn.disabled = currentPage === 1; nextPageBtn.disabled = currentPage === totalPages || totalPages === 0; // 페이지 정보 업데이트 const startIndex = (currentPage - 1) * pageSize + 1; const endIndex = Math.min(currentPage * pageSize, filteredData.length); document.getElementById('currentPage').textContent = filteredData.length > 0 ? `${startIndex}-${endIndex} / ${filteredData.length} 서버` : '0 / 0 서버'; } // 데이터 로드 함수 function loadServerData() { document.getElementById('loadingIndicator').style.display = 'flex'; // 최신 serverData 사용 if (window.serverData && Array.isArray(window.serverData)) { serverData = window.serverData; processServerData(); } document.getElementById('loadingIndicator').style.display = 'none'; } // window.serverData 갱신 이벤트 리스너 window.addEventListener('serverDataUpdated', function(e) { serverData = e.detail; processServerData(); }); // 사용률에 따른 색상 클래스 반환 function getUsageColorClass(usage) { if (usage >= 80) return 'usage-high'; if (usage >= 60) return 'usage-medium'; return 'usage-low'; } // 테이블 정렬 함수 function sortTable(column) { if (currentSort.column === column) { currentSort.ascending = !currentSort.ascending; } else { currentSort.column = column; currentSort.ascending = true; } applyFilters(); renderServerTable(); } // 서버 테이블 렌더링 함수 function renderServerTable() { const serverGrid = document.getElementById('serverGrid'); if (!serverGrid) return; serverGrid.innerHTML = ''; if (!filteredData || filteredData.length === 0) { serverGrid.innerHTML = `<div class="text-center py-4">표시할 서버 데이터가 없습니다.</div>`; return; } // 현재 페이지에 해당하는 서버만 표시 const startIndex = (currentPage - 1) * pageSize; const endIndex = Math.min(startIndex + pageSize, filteredData.length); const currentPageData = filteredData.slice(startIndex, endIndex); // 서버 카드 생성 (컴팩트 형태) currentPageData.forEach(server => { const statusClass = getServerStatusClass(server); const statusText = getServerStatusText(server); const serverCard = document.createElement('div'); serverCard.className = `server-card ${statusClass}`; serverCard.onclick = () => showServerDetail(server); serverCard.innerHTML = ` <div class="server-header"> <div class="server-name"><i class="fas fa-server me-1"></i>${server.hostname}</div> <div class="server-status ${statusClass}">${statusText}</div> </div> <div class="server-metrics"> <div class="metric-item"> <div class="metric-label"><i class="fas fa-microchip me-1"></i>CPU</div> <div class="metric-value">${server.cpu_usage.toFixed(1)}%</div> <div class="progress-bar-container"> <div class="progress-bar ${getProgressClass(server.cpu_usage)}" style="width: ${server.cpu_usage}%"></div> </div> </div> <div class="metric-item"> <div class="metric-label"><i class="fas fa-memory me-1"></i>메모리</div> <div class="metric-value">${server.memory_usage_percent.toFixed(1)}%</div> <div class="progress-bar-container"> <div class="progress-bar ${getProgressClass(server.memory_usage_percent)}" style="width: ${server.memory_usage_percent}%"></div> </div> </div> <div class="metric-item"> <div class="metric-label"><i class="fas fa-hdd me-1"></i>디스크</div> <div class="metric-value">${server.disk[0].disk_usage_percent.toFixed(1)}%</div> <div class="progress-bar-container"> <div class="progress-bar ${getProgressClass(server.disk[0].disk_usage_percent)}" style="width: ${server.disk[0].disk_usage_percent}%"></div> </div> </div> </div> ${server.errors.length > 0 ? `<div class="error-badge"><i class="fas fa-exclamation-triangle me-1"></i>${server.errors.length}개 오류</div>` : ''} `; serverGrid.appendChild(serverCard); }); } // 접이식 패널 토글 함수 function toggleCollapsible(header) { const body = header.nextElementSibling; const icon = header.querySelector('.collapse-toggle i'); body.classList.toggle('expanded'); icon.classList.toggle('fa-chevron-down'); icon.classList.toggle('fa-chevron-up'); // 각 패널의 확장 상태를 로컬 스토리지에 저장 // const panelId = header.textContent.trim(); // 불안정한 방식 let panelId = null; if (body && body.parentElement && body.parentElement.id) { panelId = body.parentElement.id; // 부모 .collapsible-panel의 ID를 사용 localStorage.setItem(`panel_${panelId}_state`, body.classList.contains('expanded') ? 'expanded' : 'collapsed'); } } // 사이드바 토글 함수 function toggleSidebar() { const mainContentGrid = document.getElementById('mainContentGrid'); mainContentGrid.classList.toggle('sidebar-hidden'); const toggleButton = document.getElementById('sidebarToggle'); const icon = toggleButton.querySelector('i'); if (mainContentGrid.classList.contains('sidebar-hidden')) { icon.classList.remove('fa-chevron-left'); icon.classList.add('fa-chevron-right'); } else { icon.classList.remove('fa-chevron-right'); icon.classList.add('fa-chevron-left'); } // 사이드바 상태 저장 localStorage.setItem('sidebar_state', mainContentGrid.classList.contains('sidebar-hidden') ? 'hidden' : 'visible'); } // 서비스 태그 렌더링 함수 function renderServiceTags(services) { return Object.entries(services) .map(([service, status]) => `<span class="service-tag ${status === 'running' ? 'service-running' : 'service-stopped'}">${service} (${status})</span>` ) .join(''); } // 서버 상태에 따른 클래스 반환 function getServerStatusClass(server) { if (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90) { return 'status-critical'; } else if (server.cpu_usage >= 70 || server.memory_usage_percent >= 70 || server.disk[0].disk_usage_percent >= 70) { return 'status-warning'; } else { return 'status-normal'; } } // 서버 상태 텍스트 반환 function getServerStatusText(server) { if (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90) { return '심각'; } else if (server.cpu_usage >= 70 || server.memory_usage_percent >= 70 || server.disk[0].disk_usage_percent >= 70) { return '경고'; } else { return '정상'; } } // 진행률 바 클래스 반환 function getProgressClass(value) { if (value >= 90) { return 'progress-critical'; } else if (value >= 70) { return 'progress-warning'; } else { return 'progress-normal'; } } // 서버 상세 정보 표시 함수 function showServerDetail(server) { // 모달 제목 설정 const statusClass = getServerStatusClass(server); const statusText = getServerStatusText(server); document.getElementById('modalServerName').innerHTML = ` <i class="fas fa-server"></i> ${server.hostname} <span class="badge ${statusClass === 'status-normal' ? 'bg-success' : (statusClass === 'status-warning' ? 'bg-warning' : 'bg-danger')}">${statusText}</span> `; // 시스템 정보 설정 document.getElementById('modalOS').textContent = server.os || '-'; document.getElementById('modalUptime').textContent = server.uptime || '-'; document.getElementById('modalProcessCount').textContent = server.process_count || '0'; document.getElementById('modalZombieCount').textContent = server.zombie_count || '0'; document.getElementById('modalLoadAvg').textContent = server.load_avg_1m || '0'; document.getElementById('modalLastUpdate').textContent = new Date().toLocaleString('ko-KR'); // 네트워크 정보 설정 const bytesToGB = bytes => (bytes / (1024 * 1024 * 1024)).toFixed(2); document.getElementById('modalNetInterface').textContent = server.net.interface || 'eth0'; document.getElementById('modalRxBytes').textContent = `${bytesToGB(server.net.rx_bytes)} GB`; document.getElementById('modalTxBytes').textContent = `${bytesToGB(server.net.tx_bytes)} GB`; document.getElementById('modalRxErrors').textContent = server.net.rx_errors || '0'; document.getElementById('modalTxErrors').textContent = server.net.tx_errors || '0'; // 서비스 상태 설정 const serviceStatusContainer = document.getElementById('modalServiceStatus'); serviceStatusContainer.innerHTML = ''; Object.entries(server.services).forEach(([service, status]) => { const serviceTag = document.createElement('div'); serviceTag.className = `service-status-tag ${status === 'running' ? 'service-running' : 'service-stopped'}`; serviceTag.innerHTML = `${service} <span class="status-indicator">${status}</span>`; serviceStatusContainer.appendChild(serviceTag); }); // 오류 메시지 설정 const errorsContainer = document.getElementById('modalErrorsContainer'); if (server.errors && server.errors.length > 0) { errorsContainer.className = 'alert alert-danger'; errorsContainer.innerHTML = ` <ul class="mb-0"> ${server.errors.map(error => `<li>${error}</li>`).join('')} </ul> `; } else { errorsContainer.className = 'alert alert-info'; errorsContainer.innerHTML = '현재 보고된 오류가 없습니다.'; } // 리소스 현황 차트 생성 createResourceBarChart(server); // 리소스 사용 추이 차트 생성 createResourceHistoryChart(server); // 모달 표시 const modal = new bootstrap.Modal(document.getElementById('serverDetailModal')); modal.show(); } // 리소스 현황 차트 생성 함수 function createResourceBarChart(server) { const ctx = document.getElementById('resourceBarChart'); // 기존 차트가 있으면 제거 if (charts.resourceBar) { charts.resourceBar.destroy(); } // 리소스 데이터 const data = { labels: ['CPU', '메모리', '디스크'], datasets: [{ label: '사용률 (%)', data: [ server.cpu_usage, server.memory_usage_percent, server.disk[0].disk_usage_percent ], backgroundColor: [ getResourceBarColor(server.cpu_usage), getResourceBarColor(server.memory_usage_percent), getResourceBarColor(server.disk[0].disk_usage_percent) ], borderWidth: 0 }] }; // 차트 옵션 const options = { indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, max: 100, grid: { display: true } }, y: { grid: { display: false } } } }; // 차트 생성 charts.resourceBar = new Chart(ctx, { type: 'bar', data: data, options: options }); } // 리소스 사용 추이 차트 생성 함수 function createResourceHistoryChart(server) { const ctx = document.getElementById('resourceHistoryChart'); // 기존 차트가 있으면 제거 if (charts.resourceHistory) { charts.resourceHistory.destroy(); } // 가상의 시계열 데이터 생성 (실제로는 서버에서 가져와야 함) const timePoints = []; const cpuData = []; const memoryData = []; const diskData = []; // 지난 24시간의 데이터 포인트 생성 (1시간 간격) const now = new Date(); for (let i = 24; i >= 0; i--) { const time = new Date(now.getTime() - i * 60 * 60 * 1000); timePoints.push(time.getHours() + ':' + (time.getMinutes() < 10 ? '0' : '') + time.getMinutes()); // 가상의 데이터 포인트 (실제 구현에서는 서버에서 가져와야 함) // 약간의 변동을 주기 위해 기본값에 랜덤 값을 더함 cpuData.push(Math.max(10, Math.min(100, server.cpu_usage + (Math.random() * 20 - 10)))); memoryData.push(Math.max(10, Math.min(100, server.memory_usage_percent + (Math.random() * 20 - 10)))); diskData.push(Math.max(10, Math.min(100, server.disk[0].disk_usage_percent + (Math.random() * 10 - 5)))); } // 차트 데이터 const data = { labels: timePoints, datasets: [ { label: 'CPU', data: cpuData, borderColor: '#FF6384', backgroundColor: 'rgba(255, 99, 132, 0.1)', borderWidth: 2, tension: 0.4 }, { label: '메모리', data: memoryData, borderColor: '#36A2EB', backgroundColor: 'rgba(54, 162, 235, 0.1)', borderWidth: 2, tension: 0.4 }, { label: '디스크', data: diskData, borderColor: '#4BC0C0', backgroundColor: 'rgba(75, 192, 192, 0.1)', borderWidth: 2, tension: 0.4 } ] }; // 차트 옵션 const options = { plugins: { legend: { position: 'top' } }, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: '사용률 (%)' } }, x: { title: { display: true, text: '시간' } } }, responsive: true, maintainAspectRatio: false }; // 차트 생성 charts.resourceHistory = new Chart(ctx, { type: 'line', data: data, options: options }); } // 리소스 바 색상 결정 함수 function getResourceBarColor(value) { if (value >= 90) return '#dc3545'; // 빨간색 (심각) if (value >= 70) return '#ffc107'; // 노란색 (주의) return '#28a745'; // 녹색 (정상) } // 서버 상세 정보 닫기 함수 제거 // 타임스탬프 업데이트 함수 function updateTimestamp() { const timestampEl = document.getElementById('timestamp'); if (timestampEl && serverData.length > 0) { const now = new Date(); timestampEl.textContent = `데이터 기준 시각: ${now.toLocaleString('ko-KR')}`; } } // 서버 수 업데이트 함수 function updateServerCount(count = null) { const serverCountEl = document.getElementById('serverCount'); if (serverCountEl) { serverCountEl.textContent = `${count || serverData.length} 서버`; } } // AI 질의 처리 async function handleQuery(queryText) { if (!queryText.trim()) return; const queryLoading = document.getElementById('queryLoading'); const queryResult = document.getElementById('queryResult'); // 요소가 존재하는지 확인 후 처리 if (queryLoading) queryLoading.classList.add('active'); if (queryResult) { queryResult.classList.remove('active'); queryResult.style.display = 'block'; // 항상 보이게 } try { if (typeof window.processQuery !== 'function') { throw new Error('AI 처리 함수가 로드되지 않았습니다.'); } const response = await window.processQuery(queryText); if (queryResult) { queryResult.innerHTML = response; queryResult.classList.add('active'); } } catch (error) { console.error("AI 질의 처리 오류:", error); if (queryResult) { queryResult.innerHTML = `<div class="alert alert-danger">처리 중 오류가 발생했습니다: ${error.message}</div>`; queryResult.classList.add('active'); } } finally { if (queryLoading) queryLoading.classList.remove('active'); } } // 이벤트 리스너 설정 function setupEventListeners() { // 새로고침 버튼 document.getElementById('refreshBtn').addEventListener('click', loadServerData); // 검색 입력 document.getElementById('searchInput').addEventListener('input', function() { applyFilters(); renderServerTable(); }); // 상태 필터 document.getElementById('statusFilter').addEventListener('change', function() { applyFilters(); renderServerTable(); }); // 페이지 사이즈 변경 document.getElementById('pageSize').addEventListener('change', function() { pageSize = parseInt(this.value); currentPage = 1; // 페이지 리셋 applyFilters(); renderServerTable(); }); // 이전 페이지 버튼 document.getElementById('prevPageBtn').addEventListener('click', function() { if (currentPage > 1) { currentPage--; updatePagination(); renderServerTable(); } }); // 다음 페이지 버튼 document.getElementById('nextPageBtn').addEventListener('click', function() { const totalPages = Math.ceil(filteredData.length / pageSize); if (currentPage < totalPages) { currentPage++; updatePagination(); renderServerTable(); } }); // AI 질의 입력 - Enter 키 및 버튼 클릭 지원 const queryInput = document.getElementById('queryInput'); if (queryInput) { queryInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { handleQuery(this.value); // init 함수와 동일하게 수정 } }); // 버튼 클릭도 지원 const queryBtn = document.getElementById('ai-query-submit'); if (queryBtn) { queryBtn.addEventListener('click', function() { handleQuery(queryInput.value); // init 함수와 동일하게 수정 }); } } // 보고서 생성 버튼 - generate-report-btn ID가 현재 HTML에 없음. // document.getElementById('generate-report-btn').addEventListener('click', generateIncidentReports); // 전체 보고서 다운로드 버튼 const downloadAllBtn = document.getElementById('downloadAllReportsBtn'); // download-all-reports-btn -> downloadAllReportsBtn if (downloadAllBtn) { downloadAllBtn.addEventListener('click', downloadAllReports); } // 페이지 로드 시 보고서 자동 생성 (3초 후) // setTimeout(generateIncidentReports, 3000); // generateIncidentReports는 이제 직접 사용하지 않음 // 10분마다 자동 갱신 // setInterval(loadServerData, 10 * 60 * 1000); // 데이터는 dummy_data_generator에서 업데이트 후 이벤트 발생시킴 } // 장애 보고서 관련 함수들 // 장애 보고서 생성 함수 function generateIncidentReports() { // 문제가 있는 서버 찾기 const criticalServers = serverData.filter(server => server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || (server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= 90) || (server.errors && server.errors.length > 0) ); const warningServers = serverData.filter(server => ((server.cpu_usage >= 70 && server.cpu_usage < 90) || (server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) || (server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90)) || (server.services && Object.values(server.services).includes('stopped')) ); // UI 업데이트 const incidentsLoading = document.getElementById('incidentsLoading'); // 이 ID도 현재 HTML에 없음 const incidentsEmpty = document.getElementById('incidentsEmpty'); // 이 ID도 현재 HTML에 없음 if (incidentsLoading) incidentsLoading.style.display = 'none'; if (criticalServers.length === 0 && warningServers.length === 0) { if (incidentsEmpty) incidentsEmpty.style.display = 'block'; return; } // 장애가 있으면 요약 표시 if (incidentsEmpty) incidentsEmpty.style.display = 'none'; // 아코디언 항목 생성 - 이 함수 호출로 인해 오류 발생. data_processor.js의 updateProblemsList로 대체됨. // generateIncidentAccordionItems(criticalServers, warningServers); } // 장애 아코디언 항목 생성 - 이 함수는 이제 사용되지 않음 (오류 원인) // function generateIncidentAccordionItems(criticalServers, warningServers) { // const accordion = document.getElementById('incidents-accordion'); // 존재하지 않는 ID // if (!accordion) { // console.error("'incidents-accordion' element not found. Cannot generate accordion items."); // return; // } // accordion.innerHTML = ''; // // ... (이하 로직도 data_processor.js의 updateProblemsList로 대체됨) // } // 서버 문제 감지 함수 function detectServerProblems(server) { const problems = []; // CPU 문제 if (server.cpu_usage >= 90) { problems.push({ type: 'CPU 과부하', description: `CPU 사용률이 ${server.cpu_usage.toFixed(2)}%로 매우 높음 (임계값: 90%)`, solution: '불필요한 프로세스 종료, CPU 사용량이 높은 애플리케이션 최적화, 서버 스케일업 고려' }); } else if (server.cpu_usage >= 70) { problems.push({ type: 'CPU 부하', description: `CPU 사용률이 ${server.cpu_usage.toFixed(2)}%로 높음 (임계값: 70%)`, solution: 'CPU 사용량 모니터링, 지속적인 증가 시 원인 파악 필요' }); } // 메모리 문제 if (server.memory_usage_percent >= 90) { problems.push({ type: '메모리 부족', description: `메모리 사용률이 ${server.memory_usage_percent.toFixed(2)}%로 매우 높음 (임계값: 90%)`, solution: '메모리 누수 점검, 불필요한 프로세스 종료, 메모리 증설 고려' }); } else if (server.memory_usage_percent >= 70) { problems.push({ type: '메모리 부하', description: `메모리 사용률이 ${server.memory_usage_percent.toFixed(2)}%로 높음 (임계값: 70%)`, solution: '메모리 사용량 모니터링, 캐시 설정 최적화 검토' }); } // 디스크 문제 if (server.disk[0].disk_usage_percent >= 90) { problems.push({ type: '디스크 공간 부족', description: `디스크 사용률이 ${server.disk[0].disk_usage_percent.toFixed(2)}%로 매우 높음 (임계값: 90%)`, solution: '불필요한 파일 정리, 로그 파일 압축/제거, 디스크 확장 고려' }); } else if (server.disk[0].disk_usage_percent >= 70) { problems.push({ type: '디스크 공간 주의', description: `디스크 사용률이 ${server.disk[0].disk_usage_percent.toFixed(2)}%로 높음 (임계값: 70%)`, solution: '디스크 사용량 모니터링, 대용량 파일 위치 확인' }); } // 서비스 문제 const stoppedServices = []; Object.entries(server.services).forEach(([service, status]) => { if (status === 'stopped') { stoppedServices.push(service); } }); if (stoppedServices.length > 0) { problems.push({ type: '서비스 중단', description: `${stoppedServices.length}개 서비스 중단됨: ${stoppedServices.join(', ')}`, solution: '서비스 로그 확인 후 재시작, 의존성 확인, 서비스 구성 파일 검토' }); } // 오류 메시지 if (server.errors.length > 0) { problems.push({ type: '오류 발생', description: `${server.errors.length}개의 오류 발생: ${server.errors.join(', ')}`, solution: '오류 로그 분석, 애플리케이션 재시작, 관련 구성 파일 검토' }); } return problems; } // 장애 보고서 다운로드 함수 function downloadIncidentReport(serverName) { const server = serverData.find(s => s.hostname === serverName); if (!server) return; const problems = detectServerProblems(server); // 보고서 내용 생성 let reportContent = `=== OpenManager AI - 장애 보고서 ===\n\n`; reportContent += `서버: ${server.hostname}\n`; reportContent += `시간: ${new Date().toLocaleString()}\n`; reportContent += `OS: ${server.os}\n`; reportContent += `가동시간: ${server.uptime}\n\n`; reportContent += `=== 시스템 상태 ===\n`; reportContent += `CPU 사용률: ${server.cpu_usage.toFixed(2)}%\n`; reportContent += `메모리 사용률: ${server.memory_usage_percent.toFixed(2)}%\n`; reportContent += `디스크 사용률: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n`; reportContent += `Load Average (1m): ${server.load_avg_1m}\n\n`; reportContent += `=== 감지된 문제 ===\n`; problems.forEach((problem, index) => { reportContent += `${index + 1}. ${problem.type}: ${problem.description}\n`; }); reportContent += `\n=== 권장 조치 ===\n`; problems.forEach((problem, index) => { reportContent += `${index + 1}. ${problem.solution}\n`; }); reportContent += `\n=== 서비스 상태 ===\n`; Object.entries(server.services).forEach(([service, status]) => { reportContent += `${service}: ${status}\n`; }); if (server.errors.length > 0) { reportContent += `\n=== 오류 메시지 ===\n`; server.errors.forEach((error, index) => { reportContent += `${index + 1}. ${error}\n`; }); } // 보고서 파일 다운로드 const blob = new Blob([reportContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `장애보고서_${server.hostname}_${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 전체 보고서 다운로드 function downloadAllReports() { const criticalServers = serverData.filter(server => server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90 || server.errors.length > 0 ); const warningServers = serverData.filter(server => (server.cpu_usage >= 70 && server.cpu_usage < 90) || (server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) || (server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90) || Object.values(server.services).includes('stopped') ); if (criticalServers.length === 0 && warningServers.length === 0) { alert('다운로드할 장애 보고서가 없습니다.'); return; } // 요약 보고서 생성 let summaryReport = `=== OpenManager AI - 전체 장애 요약 보고서 ===\n\n`; summaryReport += `생성 시간: ${new Date().toLocaleString()}\n`; summaryReport += `전체 서버 수: ${serverData.length}대\n`; summaryReport += `심각 상태 서버: ${criticalServers.length}대\n`; summaryReport += `경고 상태 서버: ${warningServers.length}대\n`; summaryReport += `정상 상태 서버: ${serverData.length - criticalServers.length - warningServers.length}대\n\n`; summaryReport += `=== 심각 상태 서버 목록 ===\n`; criticalServers.forEach((server, index) => { const problems = detectServerProblems(server); summaryReport += `${index + 1}. ${server.hostname}: ${problems.map(p => p.type).join(', ')}\n`; }); summaryReport += `\n=== 경고 상태 서버 목록 ===\n`; warningServers.forEach((server, index) => { const problems = detectServerProblems(server); summaryReport += `${index + 1}. ${server.hostname}: ${problems.map(p => p.type).join(', ')}\n`; }); // 개별 서버 보고서 추가 summaryReport += `\n\n============= 개별 서버 상세 보고서 =============\n\n`; // 심각 서버 먼저 criticalServers.forEach(server => { summaryReport += `\n\n=============================================\n`; summaryReport += `서버: ${server.hostname} (심각)\n`; summaryReport += `=============================================\n\n`; const problems = detectServerProblems(server); summaryReport += `CPU: ${server.cpu_usage.toFixed(2)}%, 메모리: ${server.memory_usage_percent.toFixed(2)}%, 디스크: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n\n`; summaryReport += `--- 감지된 문제 ---\n`; problems.forEach((problem, index) => { summaryReport += `${index + 1}. ${problem.type}: ${problem.description}\n`; }); summaryReport += `\n--- 권장 조치 ---\n`; problems.forEach((problem, index) => { summaryReport += `${index + 1}. ${problem.solution}\n`; }); if (server.errors.length > 0) { summaryReport += `\n--- 오류 메시지 ---\n`; server.errors.forEach((error, index) => { summaryReport += `${index + 1}. ${error}\n`; }); } }); // 경고 서버 추가 warningServers.forEach(server => { summaryReport += `\n\n=============================================\n`; summaryReport += `서버: ${server.hostname} (경고)\n`; summaryReport += `=============================================\n\n`; const problems = detectServerProblems(server); summaryReport += `CPU: ${server.cpu_usage.toFixed(2)}%, 메모리: ${server.memory_usage_percent.toFixed(2)}%, 디스크: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n\n`; summaryReport += `--- 감지된 문제 ---\n`; problems.forEach((problem, index) => { summaryReport += `${index + 1}. ${problem.type}: ${problem.description}\n`; }); summaryReport += `\n--- 권장 조치 ---\n`; problems.forEach((problem, index) => { summaryReport += `${index + 1}. ${problem.solution}\n`; }); if (server.errors.length > 0) { summaryReport += `\n--- 오류 메시지 ---\n`; server.errors.forEach((error, index) => { summaryReport += `${index + 1}. ${error}\n`; }); } }); // 전체 보고서 다운로드 const blob = new Blob([summaryReport], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `전체_장애_보고서_${new Date().toISOString().slice(0, 10)}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', init); </script> <!-- 서버 상세 정보 모달 창 --> <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> <!-- 자바스크립트 라이브러리 및 소스 파일 --> <!-- 부트스트랩 JS 및 Popper.js --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- AI 프로세서, 데이터 처리기, 더미 데이터 생성기 --> <script src="ai_processor.js"></script> <script src="data_processor.js"></script> <script src="dummy_data_generator.js"></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