Backend: - schedule_tenant: NC 新容器自動 pgsql 安裝 (_nc_db_check 全新容器處理) - schedule_tenant: NC 初始化加入 Redis + APCu memcache 設定 (修正 OIDC invalid_state) - schedule_tenant: 新租戶 KC realm 自動設定 accessCodeLifespan=600s (修正 authentication_expired) - schedule_account: NC Mail 帳號自動設定 (nc_mail_result/nc_mail_done_at) - schedule_account: NC 台灣國定假日行事曆自動訂閱 (CalDAV MKCALENDAR) - nextcloud_client: 新增 subscribe_calendar() CalDAV 訂閱方法 - settings: 新增系統設定 API (site_title/version/timezone/SSO/Keycloak) - models/result: 新增 nc_mail_result, nc_mail_done_at 欄位 - alembic: 遷移 002(system_settings) 003(keycloak_admin) 004(nc_mail_result) Frontend (Admin Portal): - 新增完整管理後台 (index/tenants/accounts/servers/schedules/logs/settings/system-status) - api.js: Keycloak JS Adapter SSO 整合 (PKCE/S256, fallback KC JS 來源, 自動 token 更新) - index.html: Promise.allSettled 取代 Promise.all,防止單一 API 失敗影響整頁 - 所有頁面加入 try/catch + toast 錯誤處理 - 新增品牌 LOGO 與 favicon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
6.8 KiB
HTML
178 lines
6.8 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<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>
|