Skip to main content
Glama
+page.svelte16 kB
<script> import { onMount } from 'svelte'; let incidents = []; let systemHealth = []; let performanceMetrics = []; let alertRules = []; let loading = true; let error = null; let selectedTimeRange = '24h'; let showIncidentModal = false; let newIncident = { title: '', description: '', severity: 'medium', incident_type: 'outage' }; onMount(async () => { await loadOperationalData(); }); async function loadOperationalData() { try { loading = true; await Promise.all([ loadIncidents(), loadSystemHealth(), loadPerformanceMetrics(), loadAlertRules() ]); } catch (e) { error = '운영 데이터 로딩 중 오류: ' + e.message; } finally { loading = false; } } async function loadIncidents() { try { const response = await fetch('/api/incidents?sort_by=created_desc&limit=10'); if (response.ok) { const data = await response.json(); incidents = Array.isArray(data) ? data : []; } } catch (e) { console.error('인시던트 로딩 중 오류:', e); incidents = []; } } async function loadSystemHealth() { try { const response = await fetch('/api/system-health'); if (response.ok) { const healthData = await response.json(); systemHealth = healthData.environments || []; } } catch (e) { console.error('시스템 상태 로딩 중 오류:', e); systemHealth = []; } } async function loadPerformanceMetrics() { try { const response = await fetch(`/api/performance-metrics?time_range=${selectedTimeRange}`); if (response.ok) { const metricsData = await response.json(); performanceMetrics = metricsData.metrics || []; } } catch (e) { console.error('성능 메트릭 로딩 중 오류:', e); performanceMetrics = []; } } async function loadAlertRules() { try { const response = await fetch('/api/alert-rules'); if (response.ok) { const data = await response.json(); alertRules = Array.isArray(data) ? data : (data.alert_rules || []); } } catch (e) { console.error('알림 규칙 로딩 중 오류:', e); alertRules = []; } } function createIncident() { showIncidentModal = true; // 폼 초기화 newIncident = { title: '', description: '', severity: 'medium', incident_type: 'outage' }; } async function submitIncident() { if (!newIncident.title.trim() || !newIncident.description.trim()) { alert('제목과 설명을 입력해주세요.'); return; } try { const response = await fetch('/api/incidents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newIncident.title, description: newIncident.description, severity: newIncident.severity, incident_type: newIncident.incident_type, status: 'open' }) }); if (response.ok) { await loadIncidents(); showIncidentModal = false; alert('인시던트가 생성되었습니다'); } else { alert('인시던트 생성 중 오류가 발생했습니다'); } } catch (e) { alert('인시던트 생성 중 오류: ' + e.message); } } function getStatusColor(status) { const colors = { 'open': 'bg-red-100 text-red-800', 'investigating': 'bg-yellow-100 text-yellow-800', 'identified': 'bg-blue-100 text-blue-800', 'monitoring': 'bg-purple-100 text-purple-800', 'resolved': 'bg-green-100 text-green-800' }; return colors[status] || 'bg-gray-100 text-gray-800'; } function getSeverityColor(severity) { const colors = { 'critical': 'bg-red-500 text-white', 'high': 'bg-orange-500 text-white', 'medium': 'bg-yellow-500 text-white', 'low': 'bg-green-500 text-white' }; return colors[severity] || 'bg-gray-500 text-white'; } function getHealthStatusColor(status) { const colors = { 'healthy': 'text-green-600', 'warning': 'text-yellow-600', 'critical': 'text-red-600', 'unknown': 'text-gray-600' }; return colors[status] || 'text-gray-600'; } function formatDate(dateString) { if (!dateString) return '-'; return new Date(dateString).toLocaleString('ko-KR'); } function calculateUptime(percentage) { return percentage ? `${percentage.toFixed(2)}%` : 'N/A'; } $: activeIncidents = Array.isArray(incidents) ? incidents.filter(i => i.status !== 'resolved') : []; $: criticalAlerts = Array.isArray(alertRules) ? alertRules.filter(a => a.severity === 'critical' && a.enabled) : []; </script> <svelte:head> <title>운영 대시보드 - WorkflowMCP</title> </svelte:head> <div class="container mx-auto px-4 py-8"> <div class="flex justify-between items-center mb-6"> <h1 class="text-3xl font-bold text-gray-900">운영 대시보드</h1> <div class="flex gap-2"> <button on:click={createIncident} class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg" > 인시던트 생성 </button> <button on:click={loadOperationalData} class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg" > 새로고침 </button> </div> </div> {#if loading} <div class="flex justify-center items-center py-8"> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <span class="ml-2">로딩 중...</span> </div> {:else if error} <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> {error} </div> {:else} <!-- Overview Cards --> <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> <div class="bg-white p-6 rounded-lg shadow"> <h3 class="text-sm font-medium text-gray-500">활성 인시던트</h3> <p class="text-2xl font-bold text-red-600">{activeIncidents.length}</p> </div> <div class="bg-white p-6 rounded-lg shadow"> <h3 class="text-sm font-medium text-gray-500">시스템 상태</h3> <p class="text-2xl font-bold text-green-600"> {systemHealth.filter(h => h.status === 'healthy').length}/{systemHealth.length} </p> <p class="text-sm text-gray-500">정상 환경</p> </div> <div class="bg-white p-6 rounded-lg shadow"> <h3 class="text-sm font-medium text-gray-500">평균 업타임</h3> <p class="text-2xl font-bold text-blue-600"> {calculateUptime(systemHealth.reduce((acc, h) => acc + (h.uptime_percentage || 0), 0) / systemHealth.length)} </p> </div> <div class="bg-white p-6 rounded-lg shadow"> <h3 class="text-sm font-medium text-gray-500">중요 알림 규칙</h3> <p class="text-2xl font-bold text-yellow-600">{criticalAlerts.length}</p> <p class="text-sm text-gray-500">활성화됨</p> </div> </div> <!-- Active Incidents --> <div class="bg-white rounded-lg shadow mb-8"> <div class="px-6 py-4 border-b border-gray-200"> <h2 class="text-xl font-semibold text-gray-900">활성 인시던트</h2> </div> <div class="p-6"> {#if activeIncidents.length === 0} <p class="text-gray-500 text-center py-4">현재 활성 인시던트가 없습니다</p> {:else} <div class="space-y-4"> {#each activeIncidents.slice(0, 5) as incident} <div class="border-l-4 border-red-400 pl-4 py-2"> <div class="flex justify-between items-start"> <div> <h3 class="font-medium text-gray-900">{incident.title}</h3> <p class="text-sm text-gray-600 mt-1">{incident.description}</p> <div class="flex items-center gap-2 mt-2"> <span class="px-2 py-1 rounded-full text-xs font-medium {getSeverityColor(incident.severity)}"> {incident.severity} </span> <span class="px-2 py-1 rounded-full text-xs font-medium {getStatusColor(incident.status)}"> {incident.status} </span> <span class="text-xs text-gray-500">{formatDate(incident.created_at)}</span> </div> </div> <button class="text-blue-600 hover:text-blue-800 text-sm" on:click={() => window.location.href = `/incidents/${incident.id}`} > 상세보기 </button> </div> </div> {/each} </div> {/if} </div> </div> <!-- System Health --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"> <div class="bg-white rounded-lg shadow"> <div class="px-6 py-4 border-b border-gray-200"> <h2 class="text-xl font-semibold text-gray-900">시스템 상태</h2> </div> <div class="p-6"> {#if systemHealth.length === 0} <p class="text-gray-500 text-center py-4">시스템 상태 데이터가 없습니다</p> {:else} <div class="space-y-4"> {#each systemHealth as health} <div class="flex justify-between items-center"> <div> <span class="font-medium">{health.environment_id}</span> <div class="text-sm text-gray-500"> 업타임: {calculateUptime(health.uptime_percentage)} | 응답시간: {health.response_time || 'N/A'}ms </div> </div> <span class="font-medium {getHealthStatusColor(health.status)}"> {health.status || 'unknown'} </span> </div> {/each} </div> {/if} </div> </div> <div class="bg-white rounded-lg shadow"> <div class="px-6 py-4 border-b border-gray-200"> <div class="flex justify-between items-center"> <h2 class="text-xl font-semibold text-gray-900">성능 메트릭</h2> <select bind:value={selectedTimeRange} on:change={loadPerformanceMetrics} class="border border-gray-300 rounded px-2 py-1 text-sm"> <option value="1h">1시간</option> <option value="6h">6시간</option> <option value="24h">24시간</option> <option value="7d">7일</option> <option value="30d">30일</option> </select> </div> </div> <div class="p-6"> {#if performanceMetrics.length === 0} <p class="text-gray-500 text-center py-4">성능 메트릭 데이터가 없습니다</p> {:else} <div class="space-y-3"> {#each performanceMetrics.slice(0, 8) as metric} <div class="flex justify-between items-center"> <div> <span class="font-medium">{metric.metric_name}</span> <span class="text-sm text-gray-500">({metric.environment_id})</span> </div> <span class="text-right"> <span class="font-bold">{metric.aggregated_value?.toFixed(2) || 'N/A'}</span> <span class="text-sm text-gray-500 ml-1">{metric.unit || ''}</span> </span> </div> {/each} </div> {/if} </div> </div> </div> <!-- Recent Activity & Navigation --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="bg-white rounded-lg shadow"> <div class="px-6 py-4 border-b border-gray-200"> <h2 class="text-xl font-semibold text-gray-900">최근 활동</h2> </div> <div class="p-6"> <div class="space-y-3"> {#each incidents.slice(0, 5) as incident} <div class="flex justify-between items-center text-sm"> <div> <span class="font-medium">{incident.title}</span> <span class="text-gray-500">- {incident.status}</span> </div> <span class="text-gray-400">{formatDate(incident.created_at)}</span> </div> {:else} <p class="text-gray-500 text-center py-4">최근 활동이 없습니다</p> {/each} </div> </div> </div> <div class="bg-white rounded-lg shadow"> <div class="px-6 py-4 border-b border-gray-200"> <h2 class="text-xl font-semibold text-gray-900">빠른 액세스</h2> </div> <div class="p-6"> <div class="grid grid-cols-2 gap-4"> <button on:click={() => window.location.href = '/incidents'} class="p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left" > <h3 class="font-medium text-gray-900">인시던트 관리</h3> <p class="text-sm text-gray-500 mt-1">모든 인시던트 보기</p> </button> <button on:click={() => window.location.href = '/deployments'} class="p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left" > <h3 class="font-medium text-gray-900">배포 센터</h3> <p class="text-sm text-gray-500 mt-1">배포 상태 확인</p> </button> <button on:click={() => window.location.href = '/environments'} class="p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left" > <h3 class="font-medium text-gray-900">환경 관리</h3> <p class="text-sm text-gray-500 mt-1">환경 상태 보기</p> </button> <button on:click={() => window.location.href = '/alerts'} class="p-4 border border-gray-300 rounded-lg hover:bg-gray-50 text-left" > <h3 class="font-medium text-gray-900">알림 규칙</h3> <p class="text-sm text-gray-500 mt-1">알림 설정 관리</p> </button> </div> </div> </div> </div> {/if} </div> <!-- 인시던트 생성 모달 --> {#if showIncidentModal} <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> <div class="flex items-center justify-between p-6 border-b border-gray-200"> <h3 class="text-lg font-semibold text-gray-900">새 인시던트 생성</h3> <button on:click={() => showIncidentModal = false} class="text-gray-400 hover:text-gray-600" > <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> </svg> </button> </div> <div class="p-6 space-y-4"> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> 제목 * </label> <input type="text" bind:value={newIncident.title} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="인시던트 제목을 입력해주세요" /> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> 설명 * </label> <textarea bind:value={newIncident.description} rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="인시던트 상세 설명을 입력해주세요" ></textarea> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> 심각도 </label> <select bind:value={newIncident.severity} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="low">Low (낮음)</option> <option value="medium">Medium (보통)</option> <option value="high">High (높음)</option> <option value="critical">Critical (심각)</option> </select> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> 인시던트 유형 </label> <select bind:value={newIncident.incident_type} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="outage">Outage (장애)</option> <option value="performance">Performance (성능)</option> <option value="security">Security (보안)</option> <option value="data">Data (데이터)</option> <option value="deployment">Deployment (배포)</option> </select> </div> </div> <div class="flex justify-end gap-3 p-6 border-t border-gray-200"> <button on:click={() => showIncidentModal = false} class="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50" > 취소 </button> <button on:click={submitIncident} class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700" > 인시던트 생성 </button> </div> </div> </div> {/if}

Latest Blog Posts

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/foswmine/workflow-mcp'

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