Files
system-monitor/frontend/server/list.vue
2025-12-28 12:03:48 +09:00

516 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>