시스템 모니터
This commit is contained in:
246
frontend/index.vue
Normal file
246
frontend/index.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<SidebarNav />
|
||||
|
||||
<div class="main-content">
|
||||
<header class="main-header">
|
||||
<h1 class="page-title">📊 대시보드</h1>
|
||||
<div class="header-info">
|
||||
<span class="current-time">{{ currentTime }}</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-body">
|
||||
<DashboardControl
|
||||
:interval="interval"
|
||||
:auto-refresh="autoRefresh"
|
||||
:last-fetch-time="lastFetchTime"
|
||||
:fetch-state="fetchState"
|
||||
@update:interval="updateInterval"
|
||||
@update:auto-refresh="updateAutoRefresh"
|
||||
@fetch-at="fetchAt"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<!-- 서버 현황 (좌측 90%) -->
|
||||
<div class="server-section">
|
||||
<ServerPortlet
|
||||
v-if="serverDashboard"
|
||||
:servers="serverDashboard.servers"
|
||||
:summary="serverDashboard.summary"
|
||||
@navigate="navigateTo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 네트워크 상태 (우측 10%) -->
|
||||
<div class="network-section">
|
||||
<NetworkPortlet
|
||||
type="pubnet"
|
||||
title="Public"
|
||||
icon="🌐"
|
||||
:status="pubnetStatus"
|
||||
/>
|
||||
<NetworkPortlet
|
||||
type="privnet"
|
||||
title="Private"
|
||||
icon="🔒"
|
||||
:status="privnetStatus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="connection-status" :class="{ connected: wsConnected }">
|
||||
{{ wsConnected ? '🟢 연결됨' : '🔴 연결 끊김' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
|
||||
// 초기값 상수
|
||||
const DEFAULT_INTERVAL = 1
|
||||
const DEFAULT_AUTO_REFRESH = true
|
||||
const MIN_LOADING_TIME = 500
|
||||
const SUCCESS_DISPLAY_TIME = 800
|
||||
|
||||
// 상태
|
||||
const interval = ref(DEFAULT_INTERVAL)
|
||||
const autoRefresh = ref(DEFAULT_AUTO_REFRESH)
|
||||
const wsConnected = ref(false)
|
||||
const currentTime = ref('')
|
||||
const lastFetchTime = ref<Date | null>(null)
|
||||
const fetchState = ref<'idle' | 'loading' | 'success'>('idle')
|
||||
|
||||
// 로딩 타이밍 관리
|
||||
let loadingStartTime = 0
|
||||
|
||||
// 데이터
|
||||
const pubnetStatus = ref<any>(null)
|
||||
const privnetStatus = ref<any>(null)
|
||||
const serverDashboard = ref<any>(null)
|
||||
|
||||
// WebSocket
|
||||
let ws: WebSocket | null = null
|
||||
let timeInterval: ReturnType<typeof window.setInterval> | null = null
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const min = String(date.getMinutes()).padStart(2, '0')
|
||||
const s = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${y}-${m}-${d} ${h}:${min}:${s}`
|
||||
}
|
||||
|
||||
function updateCurrentTime() {
|
||||
currentTime.value = formatTime(new Date())
|
||||
}
|
||||
|
||||
function sendMessage(msg: object) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
console.log('[WS] Sending:', JSON.stringify(msg))
|
||||
ws.send(JSON.stringify(msg))
|
||||
}
|
||||
}
|
||||
|
||||
function handleDataReceived(data: any) {
|
||||
lastFetchTime.value = new Date()
|
||||
|
||||
if (data.pubnet) {
|
||||
pubnetStatus.value = data.pubnet.status
|
||||
}
|
||||
|
||||
if (data.privnet) {
|
||||
privnetStatus.value = data.privnet.status
|
||||
}
|
||||
}
|
||||
|
||||
function finishLoading() {
|
||||
fetchState.value = 'success'
|
||||
setTimeout(() => {
|
||||
fetchState.value = 'idle'
|
||||
}, SUCCESS_DISPLAY_TIME)
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/_ws`
|
||||
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WS] Connected')
|
||||
wsConnected.value = true
|
||||
|
||||
ws!.send(JSON.stringify({ type: 'set_interval', interval: DEFAULT_INTERVAL }))
|
||||
ws!.send(JSON.stringify({ type: 'set_auto_refresh', enabled: DEFAULT_AUTO_REFRESH }))
|
||||
ws!.send(JSON.stringify({ type: 'refresh' }))
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
|
||||
if (msg.type === 'status' || msg.type === 'historical') {
|
||||
handleDataReceived(msg.data)
|
||||
|
||||
if (fetchState.value === 'loading') {
|
||||
const elapsed = Date.now() - loadingStartTime
|
||||
const remaining = Math.max(0, MIN_LOADING_TIME - elapsed)
|
||||
|
||||
setTimeout(() => {
|
||||
finishLoading()
|
||||
}, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === 'server') {
|
||||
serverDashboard.value = msg.data
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WS] Parse error:', err)
|
||||
fetchState.value = 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[WS] Disconnected')
|
||||
wsConnected.value = false
|
||||
fetchState.value = 'idle'
|
||||
|
||||
setTimeout(() => {
|
||||
if (autoRefresh.value) {
|
||||
connectWebSocket()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WS] Error:', error)
|
||||
fetchState.value = 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
function updateInterval(min: number) {
|
||||
interval.value = min
|
||||
sendMessage({ type: 'set_interval', interval: min })
|
||||
}
|
||||
|
||||
function updateAutoRefresh(enabled: boolean) {
|
||||
autoRefresh.value = enabled
|
||||
sendMessage({ type: 'set_auto_refresh', enabled })
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
sendMessage({ type: 'refresh' })
|
||||
}
|
||||
|
||||
function fetchAt(datetime: string) {
|
||||
fetchState.value = 'loading'
|
||||
loadingStartTime = Date.now()
|
||||
sendMessage({ type: 'fetch_at', datetime })
|
||||
}
|
||||
|
||||
function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connectWebSocket()
|
||||
updateCurrentTime()
|
||||
timeInterval = window.setInterval(updateCurrentTime, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
}
|
||||
if (timeInterval) {
|
||||
window.clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.app-layout { display: flex; height: 100vh; background: var(--bg-primary); }
|
||||
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.main-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); }
|
||||
.page-title { margin: 0; font-size: 20px; font-weight: 600; color: var(--text-primary); }
|
||||
.header-info { display: flex; align-items: center; gap: 16px; }
|
||||
.current-time { font-size: 14px; color: var(--text-muted); font-family: monospace; }
|
||||
.main-body { flex: 1; padding: 20px 24px; overflow-y: auto; }
|
||||
|
||||
.dashboard-layout { display: flex; gap: 16px; height: 100%; }
|
||||
.server-section { flex: 9; min-width: 0; }
|
||||
.network-section { flex: 1; min-width: 130px; max-width: 160px; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.connection-status { position: fixed; bottom: 16px; right: 16px; padding: 8px 12px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; font-size: 12px; color: var(--text-muted); }
|
||||
.connection-status.connected { color: #16a34a; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user