시스템 모니터

This commit is contained in:
2025-12-28 12:03:48 +09:00
parent dbae6649bc
commit a871ec8008
73 changed files with 21354 additions and 1 deletions

515
frontend/server/list.vue Normal file
View File

@@ -0,0 +1,515 @@
<template>
<div class="app-layout">
<SidebarNav />
<div class="main-content">
<header class="main-header">
<h1 class="page-title">🖥 Server Targets</h1>
<div class="header-info">
<span class="current-time">{{ currentTime }}</span>
<ThemeToggle />
</div>
</header>
<main class="main-body">
<!-- 스케줄러 상태 -->
<section class="section">
<h2>스케줄러 상태</h2>
<div class="scheduler-card">
<div class="status-item">
<span class="label">상태</span>
<span :class="['value', schedulerStatus.is_running ? 'active' : 'inactive']">
{{ schedulerStatus.is_running ? '실행 중' : '중지됨' }}
</span>
</div>
<div class="status-item">
<span class="label">활성 타이머</span>
<span class="value">{{ schedulerStatus.active_timers }} / {{ schedulerStatus.total_targets }}</span>
</div>
<div class="scheduler-controls">
<button class="btn btn-start" @click="startScheduler" :disabled="schedulerStatus.is_running">
시작
</button>
<button class="btn btn-stop" @click="stopScheduler" :disabled="!schedulerStatus.is_running">
중지
</button>
</div>
</div>
</section>
<!-- 서버 목록 관리 -->
<section class="section">
<h2>서버 목록 관리</h2>
<!-- 추가 -->
<div class="add-form">
<input
v-model="newTarget.server_name"
type="text"
placeholder="서버명"
class="input-field"
style="width: 120px;"
/>
<input
v-model="newTarget.server_ip"
type="text"
placeholder="IP"
class="input-field"
style="width: 130px;"
/>
<input
v-model="newTarget.glances_url"
type="text"
placeholder="Glances URL"
class="input-field url-input"
/>
<input
v-model.number="newTarget.collect_interval"
type="number"
placeholder="주기(초)"
class="input-field"
style="width: 80px;"
min="10"
/>
<label class="checkbox-label">
<input type="checkbox" v-model="newTarget.is_active" />
활성
</label>
<button class="btn btn-add" @click="addTarget" :disabled="!canAdd">
추가
</button>
</div>
<!-- 목록 -->
<div v-if="targets.length === 0" class="no-data">
등록된 서버가 없습니다.
</div>
<div v-else>
<p class="total-count"> {{ targets.length }}</p>
<table class="target-table">
<thead>
<tr>
<th style="width: 50px;">ID</th>
<th style="width: 100px;">서버명</th>
<th style="width: 120px;">IP</th>
<th>Glances URL</th>
<th style="width: 70px;">주기()</th>
<th style="width: 50px;">상태</th>
<th style="width: 120px;">관리</th>
</tr>
</thead>
<tbody>
<tr v-for="target in targets" :key="target.target_id">
<td>{{ target.target_id }}</td>
<td>
<input v-if="editingId === target.target_id" v-model="editTarget.server_name" type="text" class="edit-input" />
<span v-else>{{ target.server_name }}</span>
</td>
<td>
<input v-if="editingId === target.target_id" v-model="editTarget.server_ip" type="text" class="edit-input" />
<span v-else>{{ target.server_ip }}</span>
</td>
<td>
<input v-if="editingId === target.target_id" v-model="editTarget.glances_url" type="text" class="edit-input" />
<span v-else class="url-cell">{{ target.glances_url }}</span>
</td>
<td>
<input v-if="editingId === target.target_id" v-model.number="editTarget.collect_interval" type="number" class="edit-input" min="10" />
<span v-else>{{ target.collect_interval }}</span>
</td>
<td>
<label v-if="editingId === target.target_id" class="checkbox-label">
<input type="checkbox" v-model="editTarget.is_active" />
</label>
<span v-else>{{ target.is_active ? '' : '' }}</span>
</td>
<td>
<div v-if="editingId === target.target_id" class="action-buttons">
<button class="btn btn-save" @click="saveEdit">저장</button>
<button class="btn btn-cancel" @click="cancelEdit">취소</button>
</div>
<div v-else class="action-buttons">
<button class="btn btn-edit" @click="startEdit(target)">수정</button>
<button class="btn btn-delete" @click="deleteTarget(target.target_id)">삭제</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
</template>
<script setup lang="ts">
interface ServerTarget {
target_id: number
server_name: string
server_ip: string
glances_url: string
is_active: number
collect_interval: number
}
interface SchedulerStatus {
is_running: boolean
active_timers: number
total_targets: number
}
const targets = ref<ServerTarget[]>([])
const currentTime = ref('')
const editingId = ref<number | null>(null)
const schedulerStatus = ref<SchedulerStatus>({
is_running: false,
active_timers: 0,
total_targets: 0
})
const newTarget = ref({
server_name: '',
server_ip: '',
glances_url: '',
is_active: true,
collect_interval: 60
})
const editTarget = ref({
server_name: '',
server_ip: '',
glances_url: '',
is_active: true,
collect_interval: 60
})
const canAdd = computed(() =>
newTarget.value.server_name.trim() &&
newTarget.value.server_ip.trim() &&
newTarget.value.glances_url.trim()
)
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 m = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${y}-${M}-${d} ${h}:${m}:${s}`
}
async function fetchSchedulerStatus() {
try {
const res = await $fetch('/api/server/status')
schedulerStatus.value = res as SchedulerStatus
} catch (err) {
console.error('Failed to fetch scheduler status:', err)
}
}
async function startScheduler() {
try {
await $fetch('/api/server/scheduler/start', { method: 'POST' })
await fetchSchedulerStatus()
} catch (err) {
console.error('Failed to start scheduler:', err)
}
}
async function stopScheduler() {
try {
await $fetch('/api/server/scheduler/stop', { method: 'POST' })
await fetchSchedulerStatus()
} catch (err) {
console.error('Failed to stop scheduler:', err)
}
}
async function fetchTargets() {
try {
const res = await $fetch('/api/server/targets')
targets.value = (res as ServerTarget[]) || []
} catch (err) {
console.error('Failed to fetch targets:', err)
}
}
async function addTarget() {
if (!canAdd.value) return
try {
await $fetch('/api/server/targets', {
method: 'POST',
body: newTarget.value
})
newTarget.value = { server_name: '', server_ip: '', glances_url: '', is_active: true, collect_interval: 60 }
await fetchTargets()
await fetchSchedulerStatus()
} catch (err) {
console.error('Failed to add target:', err)
}
}
function startEdit(target: ServerTarget) {
editingId.value = target.target_id
editTarget.value = {
server_name: target.server_name,
server_ip: target.server_ip,
glances_url: target.glances_url,
is_active: !!target.is_active,
collect_interval: target.collect_interval || 60
}
}
function cancelEdit() {
editingId.value = null
}
async function saveEdit() {
if (!editingId.value) return
try {
await $fetch(`/api/server/targets/${editingId.value}`, {
method: 'PUT',
body: editTarget.value
})
editingId.value = null
await fetchTargets()
await fetchSchedulerStatus()
} catch (err) {
console.error('Failed to save target:', err)
}
}
async function deleteTarget(id: number) {
if (!confirm('정말 삭제하시겠습니까?')) return
try {
await $fetch(`/api/server/targets/${id}`, {
method: 'DELETE'
})
await fetchTargets()
await fetchSchedulerStatus()
} catch (err) {
console.error('Failed to delete target:', err)
}
}
let timeInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
currentTime.value = formatTime(new Date())
timeInterval = setInterval(() => {
currentTime.value = formatTime(new Date())
}, 1000)
fetchTargets()
fetchSchedulerStatus()
// 스케줄러 상태 주기적 갱신
setInterval(fetchSchedulerStatus, 5000)
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
})
</script>
<style scoped>
.app-layout {
display: flex;
min-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;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.header-info {
display: flex;
align-items: center;
gap: 16px;
}
.current-time {
font-family: monospace;
color: var(--text-muted);
}
.main-body {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.section h2 {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px 0;
color: var(--text-primary);
}
.scheduler-card {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
}
.status-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.status-item .label {
font-size: 12px;
color: var(--text-muted);
}
.status-item .value {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.status-item .value.active { color: #22c55e; }
.status-item .value.inactive { color: #ef4444; }
.scheduler-controls {
display: flex;
gap: 8px;
margin-left: auto;
}
.add-form {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.input-field {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}
.input-field.url-input {
flex: 1;
min-width: 200px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-primary);
font-size: 14px;
}
.no-data {
text-align: center;
padding: 40px;
color: var(--text-muted);
}
.total-count {
margin-bottom: 10px;
color: var(--text-muted);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: var(--bg-primary);
color: var(--text-muted);
font-weight: 500;
}
td {
color: var(--text-primary);
}
.url-cell {
word-break: break-all;
}
.edit-input {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 13px;
}
.action-buttons {
display: flex;
gap: 6px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: opacity 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-add { background: var(--btn-primary-bg); color: #fff; }
.btn-add:hover:not(:disabled) { background: var(--btn-primary-hover); }
.btn-start { background: #22c55e; color: #fff; }
.btn-stop { background: #ef4444; color: #fff; }
.btn-edit { background: #3b82f6; color: #fff; }
.btn-save { background: #22c55e; color: #fff; }
.btn-cancel { background: #6b7280; color: #fff; }
.btn-delete { background: #ef4444; color: #fff; }
</style>