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

326 lines
14 KiB
HTML
Raw Permalink 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" 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 active" 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-gear me-1"></i>系統設定</span>
<div class="ms-auto text-muted small" id="last-update"></div>
</div>
<div id="content">
<!-- 初始化提示 -->
<div id="init-alert" class="alert alert-warning d-flex align-items-center gap-2 mb-4" style="display:none!important">
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
<div>
<strong>系統初始化提示:</strong>
請依序完成以下步驟以啟動 SSO 認證:<br>
<span class="badge bg-secondary me-1">1</span>設定 Keycloak 連線資訊並測試連線
<span class="badge bg-secondary mx-1">2</span>初始化 SSO Realm
<span class="badge bg-secondary mx-1">3</span>建立管理租戶 (is_manager=true) 及帳號
<span class="badge bg-secondary mx-1">4</span>啟用 SSO
</div>
</div>
<div class="row g-4">
<!-- 基本設定 -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white fw-semibold">
<i class="bi bi-sliders me-1 text-primary"></i>基本設定
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">系統標題</label>
<input type="text" class="form-control" id="s-title" placeholder="VMIS Admin Portal">
<div class="form-text">瀏覽器 tab 顯示的標題文字</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">版本宣告</label>
<input type="text" class="form-control" id="s-version" placeholder="2.0.0">
<div class="form-text">顯示於頁面底部的系統版本號</div>
</div>
<div class="mb-0">
<label class="form-label fw-semibold">資料時區</label>
<select class="form-select" id="s-timezone">
<option value="Asia/Taipei">Asia/Taipei (UTC+8)</option>
<option value="Asia/Tokyo">Asia/Tokyo (UTC+9)</option>
<option value="Asia/Singapore">Asia/Singapore (UTC+8)</option>
<option value="Asia/Shanghai">Asia/Shanghai (UTC+8)</option>
<option value="UTC">UTC (UTC+0)</option>
<option value="America/New_York">America/New_York (UTC-5)</option>
<option value="Europe/London">Europe/London (UTC+0)</option>
</select>
<div class="form-text">資料庫紀錄儲存使用的時區,變更後需重啟後端才完全生效</div>
</div>
</div>
</div>
</div>
<!-- SSO 設定 -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white fw-semibold">
<i class="bi bi-shield-lock me-1 text-warning"></i>SSO 驗證設定
</div>
<div class="card-body">
<!-- SSO 啟用 -->
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="s-sso" role="switch">
<label class="form-check-label fw-semibold" for="s-sso">啟用 SSO 登入</label>
</div>
<div class="form-text mt-1">
啟用後,進入系統必須通過 Keycloak 登入驗證。<br>
<strong class="text-danger">前提:需有管理租戶 (is_manager=true) 及其帳號。</strong>
</div>
</div>
<hr>
<!-- Master Realm 管理帳號 -->
<p class="text-muted small mb-2"><i class="bi bi-database me-1"></i>Master Realm 管理帳號(後端操作租戶 realm 用)</p>
<div class="mb-3">
<label class="form-label fw-semibold">Keycloak URL</label>
<input type="text" class="form-control" id="s-kc-url" placeholder="https://auth.lab.taipei">
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label fw-semibold">管理帳號</label>
<input type="text" class="form-control" id="s-kc-admin-user" placeholder="admin">
</div>
<div class="col-6">
<label class="form-label fw-semibold">管理密碼</label>
<div class="input-group">
<input type="password" class="form-control" id="s-kc-admin-pass" placeholder="••••••••">
<button class="btn btn-outline-secondary" type="button" onclick="togglePass('s-kc-admin-pass',this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<!-- 測試連線 -->
<div class="d-flex align-items-center gap-2 mb-3">
<button class="btn btn-outline-primary btn-sm" onclick="testKeycloak()">
<i class="bi bi-plug me-1"></i>測試連線
</button>
<span id="kc-test-result" class="small"></span>
</div>
<hr>
<!-- Admin Portal SSO -->
<p class="text-muted small mb-2"><i class="bi bi-browser-chrome me-1"></i>Admin Portal 前端登入設定</p>
<div class="mb-3">
<label class="form-label fw-semibold">前端 Client ID</label>
<input type="text" class="form-control" id="s-kc-client" placeholder="vmis-portal">
<div class="form-text">前端登入用的 Keycloak Public Client ID。SSO Realm 由<strong>管理租戶</strong>的 Keycloak Realm 決定,請先至<a href="tenants.html">租戶管理</a>設定 is_manager=true 的租戶。</div>
</div>
<!-- 初始化 SSO Realm -->
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-warning btn-sm" onclick="initSsoRealm()">
<i class="bi bi-shield-plus me-1"></i>初始化 SSO Realm
</button>
<span id="kc-init-result" class="small"></span>
</div>
<div class="form-text mt-1">
在管理租戶的 Realm 中建立前端 Client ID依序儲存設定 → 測試連線 → 建立管理租戶 → 再執行)
</div>
</div>
</div>
</div>
<!-- 目前版本資訊(唯讀) -->
<div class="col-12">
<div class="card shadow-sm border-0 bg-light">
<div class="card-body d-flex align-items-center gap-4 py-3">
<div>
<small class="text-muted d-block">系統名稱</small>
<span class="fw-semibold" id="info-title"></span>
</div>
<div>
<small class="text-muted d-block">版本</small>
<span class="badge bg-primary" id="info-version"></span>
</div>
<div>
<small class="text-muted d-block">時區</small>
<span id="info-tz"></span>
</div>
<div>
<small class="text-muted d-block">SSO</small>
<span id="info-sso"></span>
</div>
<div>
<small class="text-muted d-block">Keycloak Realm</small>
<span id="info-realm" class="font-monospace small"></span>
</div>
<div>
<small class="text-muted d-block">管理租戶</small>
<span id="info-manager"></span>
</div>
<div>
<small class="text-muted d-block">最後更新</small>
<span id="info-updated"></span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary px-4" onclick="saveSettings()">
<i class="bi bi-check-lg me-1"></i>儲存設定
</button>
</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>
function togglePass(id, btn) {
const el = document.getElementById(id);
const isPass = el.type === 'password';
el.type = isPass ? 'text' : 'password';
btn.innerHTML = isPass ? '<i class="bi bi-eye-slash"></i>' : '<i class="bi bi-eye"></i>';
}
async function loadSettings() {
const s = await apiFetch('/settings');
document.getElementById('s-title').value = s.site_title;
document.getElementById('s-version').value = s.version;
document.getElementById('s-timezone').value = s.timezone;
document.getElementById('s-sso').checked = s.sso_enabled;
document.getElementById('s-kc-url').value = s.keycloak_url;
document.getElementById('s-kc-admin-user').value = s.keycloak_admin_user;
document.getElementById('s-kc-admin-pass').value = s.keycloak_admin_pass;
document.getElementById('s-kc-client').value = s.keycloak_client;
// Info bar
document.getElementById('info-title').textContent = s.site_title;
document.getElementById('info-version').textContent = s.version;
document.getElementById('info-tz').textContent = s.timezone;
document.getElementById('info-sso').innerHTML = s.sso_enabled
? '<span class="badge bg-success">啟用</span>'
: '<span class="badge bg-secondary">停用</span>';
document.getElementById('info-realm').textContent = s.keycloak_realm || '—';
document.getElementById('info-updated').textContent = fmtDt(s.updated_at);
document.getElementById('last-update').textContent = '最後更新:' + fmtDt(s.updated_at);
if (s.site_title) document.title = `系統設定 — ${s.site_title}`;
// 載入管理租戶狀態
await loadManagerStatus();
}
async function loadManagerStatus() {
try {
const tenants = await apiFetch('/tenants');
const manager = tenants.find(t => t.is_manager && t.is_active);
const el = document.getElementById('info-manager');
if (manager) {
el.innerHTML = `<span class="badge bg-success">${manager.name}</span>`;
// 若沒有帳號也要提示
const accounts = await apiFetch(`/accounts?tenant_id=${manager.id}&is_active=true`);
if (accounts.length === 0) {
el.innerHTML += ' <span class="badge bg-warning text-dark">尚無帳號</span>';
} else {
el.innerHTML += ` <small class="text-muted">(${accounts.length} 帳號)</small>`;
}
} else {
el.innerHTML = '<span class="badge bg-danger">尚未建立</span>';
document.getElementById('init-alert').style.removeProperty('display');
}
} catch (e) {
document.getElementById('info-manager').textContent = '—';
}
}
async function saveSettings() {
const payload = {
site_title: document.getElementById('s-title').value.trim(),
version: document.getElementById('s-version').value.trim(),
timezone: document.getElementById('s-timezone').value,
sso_enabled: document.getElementById('s-sso').checked,
keycloak_url: document.getElementById('s-kc-url').value.trim(),
keycloak_admin_user: document.getElementById('s-kc-admin-user').value.trim(),
keycloak_admin_pass: document.getElementById('s-kc-admin-pass').value,
keycloak_client: document.getElementById('s-kc-client').value.trim(),
};
try {
await apiFetch('/settings', { method: 'PUT', body: JSON.stringify(payload) });
toast('設定已儲存');
loadSettings();
} catch (e) {
toast(e.message, 'error');
}
}
async function testKeycloak() {
const el = document.getElementById('kc-test-result');
el.innerHTML = '<span class="text-muted">測試中...</span>';
try {
const r = await apiFetch('/settings/test-keycloak', { method: 'POST' });
el.innerHTML = `<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>${r.message}</span>`;
} catch (e) {
el.innerHTML = `<span class="text-danger"><i class="bi bi-x-circle-fill me-1"></i>${e.message}</span>`;
}
}
async function initSsoRealm() {
const el = document.getElementById('kc-init-result');
el.innerHTML = '<span class="text-muted">初始化中...</span>';
try {
const r = await apiFetch('/settings/init-sso-realm', { method: 'POST' });
el.innerHTML = `<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>完成Realm: ${r.realm}</span>`;
toast('SSO Realm 初始化完成:' + r.details.join('、'));
loadSettings();
} catch (e) {
el.innerHTML = `<span class="text-danger"><i class="bi bi-x-circle-fill me-1"></i>${e.message}</span>`;
toast(e.message, 'error');
}
}
loadSettings();
</script>
</body>
</html>