Files
vmis/frontend/admin-portal/servers.html
VMIS Developer b5eb5652b9 fix: add base href /admin/ to admin portal HTML files
Without base href, relative asset paths (css/, js/, img/) resolve to
root path when accessed via /admin (no trailing slash), causing CSS/JS
to be served by NC instead of vmis-backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:20:22 +08:00

255 lines
9.6 KiB
HTML

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<base href="/admin/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>伺服器狀態 — VMIS Admin</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.3/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="css/style.css">
<link rel="icon" href="img/logo.png">
</head>
<body>
<!-- Sidebar -->
<nav id="sidebar">
<a class="brand" href="index.html"><img src="img/logo.png" alt="logo" style="height:28px;object-fit:contain;vertical-align:middle;" class="me-2">VMIS Admin</a>
<div class="pt-2">
<div class="nav-section">租戶服務</div>
<a class="nav-link" href="tenants.html"><i class="bi bi-building"></i> 租戶管理</a>
<a class="nav-link" href="accounts.html"><i class="bi bi-people"></i> 帳號管理</a>
<div class="nav-section">基礎設施</div>
<a class="nav-link active" href="servers.html"><i class="bi bi-hdd-network"></i> 伺服器狀態</a>
<a class="nav-link" href="system-status.html"><i class="bi bi-activity"></i> 系統狀態</a>
<div class="nav-section">排程</div>
<a class="nav-link" href="schedules.html"><i class="bi bi-clock"></i> 排程管理</a>
<a class="nav-link" href="schedule-logs.html"><i class="bi bi-list-ul"></i> 執行紀錄</a>
<div class="nav-section">系統</div>
<a class="nav-link" href="settings.html"><i class="bi bi-gear"></i> 系統設定</a>
</div>
</nav>
<!-- Main -->
<div id="main">
<div id="topbar">
<span class="fw-semibold text-secondary"><i class="bi bi-hdd-network me-1"></i>伺服器狀態</span>
<div class="ms-auto">
<button class="btn btn-primary btn-sm" onclick="openCreate()">
<i class="bi bi-plus-lg me-1"></i>新增伺服器
</button>
</div>
</div>
<div id="content">
<div class="card shadow-sm">
<div class="card-body p-0">
<table id="tbl" class="table table-hover mb-0 w-100">
<thead>
<tr>
<th>排序</th>
<th>主機名稱</th>
<th>IP 位址</th>
<th>說明</th>
<th class="text-center">狀態</th>
<th>回應時間</th>
<th style="min-width:160px">可用率 30天</th>
<th style="min-width:160px">可用率 90天</th>
<th style="min-width:160px">可用率 365天</th>
<th class="text-center">啟用</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="modal fade" id="formModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="formTitle">新增伺服器</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="f-id">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">主機名稱 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="f-name" placeholder="home">
</div>
<div class="col-md-4">
<label class="form-label">排序</label>
<input type="number" class="form-control" id="f-sort" value="0" min="0">
</div>
<div class="col-12">
<label class="form-label">IP 位址 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="f-ip" placeholder="10.1.0.254">
</div>
<div class="col-12">
<label class="form-label">說明</label>
<input type="text" class="form-control" id="f-desc" placeholder="核心服務主機">
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="f-active" checked>
<label class="form-check-label" for="f-active">啟用(納入排程檢查)</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveForm()">儲存</button>
</div>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header"><h6 class="modal-title">確認</h6></div>
<div class="modal-body" id="confirm-body"></div>
<div class="modal-footer">
<button id="confirm-cancel" class="btn btn-secondary btn-sm">取消</button>
<button id="confirm-ok" class="btn btn-danger btn-sm">確定</button>
</div>
</div>
</div>
</div>
<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index:9999"></div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js"></script>
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.bootstrap5.min.js"></script>
<script src="js/api.js"></script>
<script>
let dt;
function rtHtml(ms) {
if (ms === null || ms === undefined) return '<span class="text-muted">—</span>';
return `<span class="text-success">${ms.toFixed(1)} ms</span>`;
}
async function loadTable() {
let rows;
try { rows = await apiFetch('/servers'); } catch (e) { toast('無法載入伺服器資料:' + e.message, 'error'); return; }
const data = rows.map(s => {
const av = s.availability || {};
return [
s.sort_order,
`<strong>${s.name}</strong>`,
`<code>${s.ip_address}</code>`,
`<small class="text-muted">${s.description || '—'}</small>`,
lightHtml(s.last_result),
rtHtml(s.last_response_time),
availBar(av.availability_30d),
availBar(av.availability_90d),
availBar(av.availability_365d),
s.is_active
? '<span class="badge bg-success-subtle text-success">啟用</span>'
: '<span class="badge bg-secondary-subtle text-secondary">停用</span>',
`<button class="btn btn-outline-primary btn-sm me-1" onclick='editRow(${JSON.stringify(s)})'><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteRow(${s.id},'${s.name}')"><i class="bi bi-trash"></i></button>`,
];
});
if (dt) {
dt.clear().rows.add(data).draw();
} else {
dt = $('#tbl').DataTable({
data,
columns: [
{ type: 'num' }, {}, { className: 'font-monospace' }, {},
{ className: 'text-center', orderable: false },
{},
{ orderable: false },
{ orderable: false },
{ orderable: false },
{ className: 'text-center' },
{ className: 'text-center', orderable: false },
],
language: { url: 'https://cdn.datatables.net/plug-ins/2.0.3/i18n/zh-HANT.json' },
pageLength: 25,
order: [[0, 'asc']],
});
}
}
function clearForm() {
document.getElementById('f-id').value = '';
document.getElementById('f-name').value = '';
document.getElementById('f-ip').value = '';
document.getElementById('f-desc').value = '';
document.getElementById('f-sort').value = 0;
document.getElementById('f-active').checked = true;
}
function openCreate() {
clearForm();
document.getElementById('formTitle').textContent = '新增伺服器';
new bootstrap.Modal(document.getElementById('formModal')).show();
}
function editRow(s) {
clearForm();
document.getElementById('formTitle').textContent = '編輯伺服器';
document.getElementById('f-id').value = s.id;
document.getElementById('f-name').value = s.name;
document.getElementById('f-ip').value = s.ip_address;
document.getElementById('f-desc').value = s.description || '';
document.getElementById('f-sort').value = s.sort_order;
document.getElementById('f-active').checked = s.is_active;
new bootstrap.Modal(document.getElementById('formModal')).show();
}
async function saveForm() {
const id = document.getElementById('f-id').value;
const payload = {
name: document.getElementById('f-name').value.trim(),
ip_address: document.getElementById('f-ip').value.trim(),
description: document.getElementById('f-desc').value.trim() || null,
sort_order: parseInt(document.getElementById('f-sort').value),
is_active: document.getElementById('f-active').checked,
};
try {
if (id) {
await apiFetch(`/servers/${id}`, { method: 'PUT', body: JSON.stringify(payload) });
toast('伺服器已更新');
} else {
await apiFetch('/servers', { method: 'POST', body: JSON.stringify(payload) });
toast('伺服器已新增');
}
bootstrap.Modal.getInstance(document.getElementById('formModal')).hide();
loadTable();
} catch (e) {
toast(e.message, 'error');
}
}
async function deleteRow(id, name) {
const ok = await confirm(`確定要刪除伺服器「${name}」?`);
if (!ok) return;
try {
await apiFetch(`/servers/${id}`, { method: 'DELETE' });
toast('伺服器已刪除');
loadTable();
} catch (e) {
toast(e.message, 'error');
}
}
loadTable();
</script>
</body>
</html>