Files
vmis/frontend/admin-portal/schedules.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

179 lines
6.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="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" 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 active" 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-clock me-1"></i>排程管理</span>
</div>
<div id="content">
<div id="cards" class="row g-3"></div>
</div>
</div>
<!-- Edit Cron Modal -->
<div class="modal fade" id="cronModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">修改 Cron</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="c-id">
<label class="form-label">Cron 表達式 <small class="text-muted">(六碼:秒 分 時 日 月 週)</small></label>
<input type="text" class="form-control font-monospace" id="c-cron" placeholder="0 */3 * * * *">
<div class="form-text">範例:<code>0 */3 * * * *</code> = 每3分鐘<code>0 0 8 * * *</code> = 每日08:00</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="saveCron()">儲存</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="js/api.js"></script>
<script>
const STATUS_BADGE = {
Waiting: '<span class="badge bg-secondary">Waiting</span>',
Going: '<span class="badge bg-warning text-dark"><i class="bi bi-arrow-repeat spin me-1"></i>Going</span>',
};
const RESULT_BADGE = {
ok: '<span class="badge bg-success">ok</span>',
error: '<span class="badge bg-danger">error</span>',
};
function cardHtml(s) {
const statusBadge = STATUS_BADGE[s.status] || s.status;
const resultBadge = s.last_status ? (RESULT_BADGE[s.last_status] || s.last_status) : '<span class="text-muted">—</span>';
return `
<div class="col-md-4">
<div class="card stat-card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h6 class="mb-0">${s.name}</h6>
${statusBadge}
</div>
<div class="mb-2">
<small class="text-muted">Cron</small><br>
<code class="fs-6">${s.cron_timer}</code>
</div>
<div class="row g-2 text-sm mb-3">
<div class="col-6">
<small class="text-muted d-block">上次執行</small>
<span class="small">${fmtDt(s.last_run_at)}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">下次執行</small>
<span class="small">${fmtDt(s.next_run_at)}</span>
</div>
<div class="col-6">
<small class="text-muted d-block">上次結果</small>
${resultBadge}
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm flex-fill"
onclick="openCron(${s.id},'${s.cron_timer}')">
<i class="bi bi-pencil me-1"></i>Cron
</button>
<button class="btn btn-outline-primary btn-sm flex-fill"
onclick="runNow(${s.id},'${s.name}')"
${s.status === 'Going' ? 'disabled' : ''}>
<i class="bi bi-play-fill me-1"></i>立即執行
</button>
<a class="btn btn-outline-secondary btn-sm" href="schedule-logs.html?schedule_id=${s.id}">
<i class="bi bi-list-ul"></i>
</a>
</div>
</div>
</div>
</div>`;
}
async function loadCards() {
let schedules;
try { schedules = await apiFetch('/schedules'); } catch (e) { toast('無法載入排程資料:' + e.message, 'error'); return; }
document.getElementById('cards').innerHTML = schedules.map(cardHtml).join('');
}
function openCron(id, cron) {
document.getElementById('c-id').value = id;
document.getElementById('c-cron').value = cron;
new bootstrap.Modal(document.getElementById('cronModal')).show();
}
async function saveCron() {
const id = document.getElementById('c-id').value;
const cron_timer = document.getElementById('c-cron').value.trim();
try {
await apiFetch(`/schedules/${id}`, {
method: 'PUT',
body: JSON.stringify({ cron_timer }),
});
toast('Cron 已更新');
bootstrap.Modal.getInstance(document.getElementById('cronModal')).hide();
loadCards();
} catch (e) {
toast(e.message, 'error');
}
}
async function runNow(id, name) {
try {
await apiFetch(`/schedules/${id}/run`, { method: 'POST' });
toast(`${name}」已觸發執行`);
setTimeout(loadCards, 1000);
} catch (e) {
toast(e.message, 'error');
}
}
// Add spin animation
const style = document.createElement('style');
style.textContent = `.spin { animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } }`;
document.head.appendChild(style);
loadCards();
// Auto refresh every 10s to catch status changes
setInterval(loadCards, 10000);
</script>
</body>
</html>