perf: 外部化共用底板資源
Some checks failed
CD Pipeline / deploy (push) Failing after 27s

This commit is contained in:
OoO
2026-05-18 00:31:21 +08:00
parent cf52b07809
commit ebc6f2cfe5
4 changed files with 395 additions and 397 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.164"
SYSTEM_VERSION = "V10.165"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -36,301 +36,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-v3-page-guard.css') }}">
{% block extra_head %}{% endblock %}
<style>
/* ═══════════════════════════════════════════════════
* Bootstrap override — 限定在 .momo-app 內
* ═══════════════════════════════════════════════════ */
/* 主 accent 統一替換為群組色 */
.momo-app .btn-primary {
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
color: var(--momo-page-inverse);
box-shadow: none;
}
.momo-app .btn-primary:hover,
.momo-app .btn-primary:focus {
background: var(--momo-page-accent-dark);
border-color: var(--momo-page-accent-dark);
color: var(--momo-page-inverse);
}
.momo-app .btn-outline-primary {
color: var(--momo-page-accent-dark);
border-color: var(--momo-page-accent-line);
background: transparent;
}
.momo-app .btn-outline-primary:hover {
color: var(--momo-page-inverse);
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
}
.momo-app .text-primary {
color: var(--momo-page-accent-dark) !important;
}
.momo-app .bg-primary {
background: var(--momo-page-accent) !important;
color: var(--momo-page-inverse) !important;
}
.momo-app .border-primary {
border-color: var(--momo-page-accent-line) !important;
}
/* Card 統一風格 — 平面化 */
.momo-app .card {
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: none;
}
.momo-app .card-header {
background: transparent;
border-bottom: 1px solid var(--momo-border-light);
padding: var(--momo-space-3) var(--momo-space-4);
font-family: var(--momo-font-display);
font-weight: 700;
color: var(--momo-text-primary);
}
.momo-app .card-body {
padding: var(--momo-space-4);
}
/* Page header — 移除原本黑色/橘色漸層 hero */
.momo-app .page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--momo-space-4);
padding: var(--momo-space-4) 0 var(--momo-space-5);
border-bottom: 1px solid var(--momo-border-light);
margin-bottom: var(--momo-space-5);
background: transparent;
}
.momo-app .page-header h1,
.momo-app .page-header h2,
.momo-app .page-header h3 {
margin: 0;
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-headline);
font-weight: 700;
letter-spacing: 0;
line-height: 1.2;
}
.momo-app .page-header p,
.momo-app .page-header small,
.momo-app .page-header .text-muted {
color: var(--momo-text-secondary);
}
/* Bootstrap badges → 暖色 tag */
.momo-app .badge.bg-primary {
background: var(--momo-tag-caramel-bg) !important;
color: var(--momo-tag-caramel-text) !important;
border: 1px solid var(--momo-tag-caramel-border);
font-weight: 600;
}
.momo-app .badge.bg-warning,
.momo-app .text-bg-warning {
background: var(--momo-tag-honey-bg) !important;
color: var(--momo-tag-honey-text) !important;
border: 1px solid var(--momo-tag-honey-border);
}
.momo-app .badge.bg-danger,
.momo-app .text-bg-danger {
background: var(--momo-tag-rust-bg) !important;
color: var(--momo-tag-rust-text) !important;
border: 1px solid var(--momo-tag-rust-border);
}
.momo-app .badge.bg-success,
.momo-app .text-bg-success {
background: var(--momo-tag-success-bg) !important;
color: var(--momo-tag-success-text) !important;
border: 1px solid var(--momo-tag-success-border);
}
.momo-app .badge.bg-info,
.momo-app .text-bg-info {
background: var(--momo-tag-olive-bg) !important;
color: var(--momo-tag-olive-text) !important;
border: 1px solid var(--momo-tag-olive-border);
}
.momo-app .badge.bg-dark,
.momo-app .badge.bg-secondary {
background: var(--momo-tag-ink-bg) !important;
color: var(--momo-tag-ink-text) !important;
border: 1px solid var(--momo-tag-ink-border);
}
.momo-app .badge {
font-family: var(--momo-font-mono);
font-weight: 600;
font-size: var(--momo-text-label);
letter-spacing: 0.02em;
padding: 3px 8px;
border-radius: var(--momo-radius-sm);
}
/* Table — 平面化 */
.momo-app .table {
color: var(--momo-text-primary);
margin-bottom: 0;
}
.momo-app .table > :not(caption) > * > * {
background: transparent;
border-bottom-color: var(--momo-border-light);
padding: var(--momo-space-3) var(--momo-space-3);
}
.momo-app .table thead th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
border-bottom: 1px solid var(--momo-border-light);
font-family: var(--momo-font-display);
font-size: var(--momo-text-label);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
}
.momo-app .table tbody tr:hover {
background: var(--momo-page-accent-soft);
}
/* Form */
.momo-app .form-control,
.momo-app .form-select {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
color: var(--momo-text-primary);
font-size: var(--momo-text-body-sm);
box-shadow: none;
}
.momo-app .form-control:focus,
.momo-app .form-select:focus {
border-color: var(--momo-page-accent-line);
box-shadow: var(--momo-shadow-focus);
}
/* Progress bar */
.momo-app .progress {
background: var(--momo-bg-subtle);
border-radius: var(--momo-radius-pill);
height: 6px;
box-shadow: none;
}
.momo-app .progress-bar {
background: var(--momo-page-accent);
}
/* Filter section — 移除原本 gradient header */
.momo-app .filter-section {
padding: var(--momo-space-4);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
color: var(--momo-text-primary);
}
.momo-app .filter-section h3,
.momo-app .filter-section h4,
.momo-app .filter-section h5 {
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-weight: 700;
}
.momo-app .filter-section .form-label {
color: var(--momo-text-secondary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-label);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* Nav pills / tabs */
.momo-app .nav-pills .nav-link.active,
.momo-app .nav-tabs .nav-link.active {
background: var(--momo-page-accent);
color: var(--momo-page-inverse);
border-color: var(--momo-page-accent);
}
.momo-app .nav-pills .nav-link,
.momo-app .nav-tabs .nav-link {
color: var(--momo-text-secondary);
font-weight: 500;
}
/* Pagination */
.momo-app .page-item.active .page-link {
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
color: var(--momo-page-inverse);
}
.momo-app .page-link {
color: var(--momo-text-secondary);
background: var(--momo-bg-elevated);
border-color: var(--momo-border-light);
}
/* Dropdown */
.momo-app .dropdown-menu {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: var(--momo-shadow-modal);
}
.momo-app .dropdown-item.active,
.momo-app .dropdown-item:active {
background: var(--momo-page-accent);
color: var(--momo-page-inverse);
}
.momo-app .dropdown-item:hover {
background: var(--momo-page-accent-soft);
color: var(--momo-page-accent-dark);
}
/* Modal */
.momo-app .modal-content {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-xl);
box-shadow: var(--momo-shadow-modal);
}
.momo-app .modal-header {
border-bottom-color: var(--momo-border-light);
}
.momo-app .modal-footer {
border-top-color: var(--momo-border-light);
}
/* Alert — 去飽和 */
.momo-app .alert {
border-radius: var(--momo-radius-md);
border-width: 1px;
font-size: var(--momo-text-body-sm);
}
.momo-app .alert-warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.momo-app .alert-danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.momo-app .alert-success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.momo-app .alert-info {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-bootstrap-overrides.css') }}">
</head>
<body class="momo-v2-body {% if active_page|default('') in [
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',
@@ -429,107 +135,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{# MOMO 404 防呆攔截:避免無效商品連結導向 EC404 #}
<script>
(function () {
if (window.__momoLinkGuardInstalled) return;
window.__momoLinkGuardInstalled = true;
const MOMO_CODE_RE = /^[A-Za-z0-9_-]{4,}$/;
function toText(value) {
return value == null ? '' : String(value);
}
function isLikelyMomoCode(value) {
const cleaned = toText(value).trim();
if (!cleaned) return false;
const lowered = cleaned.toLowerCase();
if (['nan', 'none', 'null', 'undefined'].includes(lowered)) return false;
if (lowered.startsWith('momo_') || lowered.startsWith('manual_') || lowered.startsWith('pchome_')) return false;
return MOMO_CODE_RE.test(cleaned);
}
function extractIcode(url) {
const target = toText(url).trim();
if (!target) return '';
try {
const parsed = new URL(target, location.origin);
return toText(parsed.searchParams.get('i_code')).trim();
} catch (error) {
const match = /[?&]i_code=([^&#]+)/i.exec(target);
return match ? decodeURIComponent(match[1] || '').trim() : '';
}
}
function buildSafeMomoUrl(iCode) {
const cleaned = toText(iCode).trim();
if (!isLikelyMomoCode(cleaned)) return '';
return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(cleaned)}`;
}
document.addEventListener('click', function (event) {
const link = event.target.closest ? event.target.closest('a.momo-tracked-link') : null;
if (!link) return;
const href = toText(link.getAttribute('href')).trim();
const original = toText(link.dataset && link.dataset.momoOriginalUrl).trim();
const iCode = toText(link.dataset && (link.dataset.trackIcode || link.dataset.trackProductId)).trim()
|| extractIcode(original)
|| extractIcode(href);
const safeUrl = buildSafeMomoUrl(iCode);
if (safeUrl && (!href || href === '#' || /ec404/i.test(href) || /ec404/i.test(original))) {
event.preventDefault();
link.setAttribute('href', safeUrl);
window.open(safeUrl, link.getAttribute('target') || '_self', 'noopener,noreferrer');
}
}, true);
})();
</script>
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const fetchWithCSRF = (url, options = {}) => {
options.headers = { ...options.headers, 'X-CSRFToken': csrfToken };
return fetch(url, options);
};
(function () {
const shell = document.getElementById('momo-shell');
const toggle = document.querySelector('[data-momo-sidebar-toggle]');
const close = document.querySelector('[data-momo-sidebar-close]');
if (!shell || !toggle) return;
toggle.addEventListener('click', () => shell.classList.toggle('is-sidebar-open'));
if (close) close.addEventListener('click', () => shell.classList.remove('is-sidebar-open'));
})();
</script>
<script>
(function() {
const link = document.getElementById('momo-obs-link');
const badge = document.getElementById('momo-obs-badge');
if (!link || !badge) return;
async function refresh() {
try {
const r = await fetch('/observability/api/health_indicator', { credentials: 'same-origin' });
if (!r.ok) return;
const d = await r.json();
if (!d.ok) return;
link.title = d.tooltip || 'AI 觀測台';
if (d.alert_count > 0) {
badge.textContent = d.alert_count;
badge.hidden = false;
link.classList.add('is-alert');
} else {
badge.hidden = true;
link.classList.remove('is-alert');
}
} catch (e) {}
}
refresh();
setInterval(refresh, 60000);
})();
</script>
<script src="{{ url_for('static', filename='js/ewoooc-base.js') }}"></script>
{% if active_page|default('') in [
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',

View File

@@ -0,0 +1,294 @@
/* EwoooC base Bootstrap overrides extracted from templates/ewoooc_base.html. */
/* ═══════════════════════════════════════════════════
* Bootstrap override — 限定在 .momo-app 內
* ═══════════════════════════════════════════════════ */
/* 主 accent 統一替換為群組色 */
.momo-app .btn-primary {
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
color: var(--momo-page-inverse);
box-shadow: none;
}
.momo-app .btn-primary:hover,
.momo-app .btn-primary:focus {
background: var(--momo-page-accent-dark);
border-color: var(--momo-page-accent-dark);
color: var(--momo-page-inverse);
}
.momo-app .btn-outline-primary {
color: var(--momo-page-accent-dark);
border-color: var(--momo-page-accent-line);
background: transparent;
}
.momo-app .btn-outline-primary:hover {
color: var(--momo-page-inverse);
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
}
.momo-app .text-primary {
color: var(--momo-page-accent-dark) !important;
}
.momo-app .bg-primary {
background: var(--momo-page-accent) !important;
color: var(--momo-page-inverse) !important;
}
.momo-app .border-primary {
border-color: var(--momo-page-accent-line) !important;
}
/* Card 統一風格 — 平面化 */
.momo-app .card {
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: none;
}
.momo-app .card-header {
background: transparent;
border-bottom: 1px solid var(--momo-border-light);
padding: var(--momo-space-3) var(--momo-space-4);
font-family: var(--momo-font-display);
font-weight: 700;
color: var(--momo-text-primary);
}
.momo-app .card-body {
padding: var(--momo-space-4);
}
/* Page header — 移除原本黑色/橘色漸層 hero */
.momo-app .page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--momo-space-4);
padding: var(--momo-space-4) 0 var(--momo-space-5);
border-bottom: 1px solid var(--momo-border-light);
margin-bottom: var(--momo-space-5);
background: transparent;
}
.momo-app .page-header h1,
.momo-app .page-header h2,
.momo-app .page-header h3 {
margin: 0;
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-headline);
font-weight: 700;
letter-spacing: 0;
line-height: 1.2;
}
.momo-app .page-header p,
.momo-app .page-header small,
.momo-app .page-header .text-muted {
color: var(--momo-text-secondary);
}
/* Bootstrap badges → 暖色 tag */
.momo-app .badge.bg-primary {
background: var(--momo-tag-caramel-bg) !important;
color: var(--momo-tag-caramel-text) !important;
border: 1px solid var(--momo-tag-caramel-border);
font-weight: 600;
}
.momo-app .badge.bg-warning,
.momo-app .text-bg-warning {
background: var(--momo-tag-honey-bg) !important;
color: var(--momo-tag-honey-text) !important;
border: 1px solid var(--momo-tag-honey-border);
}
.momo-app .badge.bg-danger,
.momo-app .text-bg-danger {
background: var(--momo-tag-rust-bg) !important;
color: var(--momo-tag-rust-text) !important;
border: 1px solid var(--momo-tag-rust-border);
}
.momo-app .badge.bg-success,
.momo-app .text-bg-success {
background: var(--momo-tag-success-bg) !important;
color: var(--momo-tag-success-text) !important;
border: 1px solid var(--momo-tag-success-border);
}
.momo-app .badge.bg-info,
.momo-app .text-bg-info {
background: var(--momo-tag-olive-bg) !important;
color: var(--momo-tag-olive-text) !important;
border: 1px solid var(--momo-tag-olive-border);
}
.momo-app .badge.bg-dark,
.momo-app .badge.bg-secondary {
background: var(--momo-tag-ink-bg) !important;
color: var(--momo-tag-ink-text) !important;
border: 1px solid var(--momo-tag-ink-border);
}
.momo-app .badge {
font-family: var(--momo-font-mono);
font-weight: 600;
font-size: var(--momo-text-label);
letter-spacing: 0.02em;
padding: 3px 8px;
border-radius: var(--momo-radius-sm);
}
/* Table — 平面化 */
.momo-app .table {
color: var(--momo-text-primary);
margin-bottom: 0;
}
.momo-app .table > :not(caption) > * > * {
background: transparent;
border-bottom-color: var(--momo-border-light);
padding: var(--momo-space-3) var(--momo-space-3);
}
.momo-app .table thead th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
border-bottom: 1px solid var(--momo-border-light);
font-family: var(--momo-font-display);
font-size: var(--momo-text-label);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
}
.momo-app .table tbody tr:hover {
background: var(--momo-page-accent-soft);
}
/* Form */
.momo-app .form-control,
.momo-app .form-select {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
color: var(--momo-text-primary);
font-size: var(--momo-text-body-sm);
box-shadow: none;
}
.momo-app .form-control:focus,
.momo-app .form-select:focus {
border-color: var(--momo-page-accent-line);
box-shadow: var(--momo-shadow-focus);
}
/* Progress bar */
.momo-app .progress {
background: var(--momo-bg-subtle);
border-radius: var(--momo-radius-pill);
height: 6px;
box-shadow: none;
}
.momo-app .progress-bar {
background: var(--momo-page-accent);
}
/* Filter section — 移除原本 gradient header */
.momo-app .filter-section {
padding: var(--momo-space-4);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
color: var(--momo-text-primary);
}
.momo-app .filter-section h3,
.momo-app .filter-section h4,
.momo-app .filter-section h5 {
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-weight: 700;
}
.momo-app .filter-section .form-label {
color: var(--momo-text-secondary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-label);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* Nav pills / tabs */
.momo-app .nav-pills .nav-link.active,
.momo-app .nav-tabs .nav-link.active {
background: var(--momo-page-accent);
color: var(--momo-page-inverse);
border-color: var(--momo-page-accent);
}
.momo-app .nav-pills .nav-link,
.momo-app .nav-tabs .nav-link {
color: var(--momo-text-secondary);
font-weight: 500;
}
/* Pagination */
.momo-app .page-item.active .page-link {
background: var(--momo-page-accent);
border-color: var(--momo-page-accent);
color: var(--momo-page-inverse);
}
.momo-app .page-link {
color: var(--momo-text-secondary);
background: var(--momo-bg-elevated);
border-color: var(--momo-border-light);
}
/* Dropdown */
.momo-app .dropdown-menu {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
box-shadow: var(--momo-shadow-modal);
}
.momo-app .dropdown-item.active,
.momo-app .dropdown-item:active {
background: var(--momo-page-accent);
color: var(--momo-page-inverse);
}
.momo-app .dropdown-item:hover {
background: var(--momo-page-accent-soft);
color: var(--momo-page-accent-dark);
}
/* Modal */
.momo-app .modal-content {
background: var(--momo-bg-elevated);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-xl);
box-shadow: var(--momo-shadow-modal);
}
.momo-app .modal-header {
border-bottom-color: var(--momo-border-light);
}
.momo-app .modal-footer {
border-top-color: var(--momo-border-light);
}
/* Alert — 去飽和 */
.momo-app .alert {
border-radius: var(--momo-radius-md);
border-width: 1px;
font-size: var(--momo-text-body-sm);
}
.momo-app .alert-warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.momo-app .alert-danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.momo-app .alert-success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.momo-app .alert-info {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}

View File

@@ -0,0 +1,98 @@
/* EwoooC base interactions extracted from templates/ewoooc_base.html. */
(function () {
if (window.__momoLinkGuardInstalled) return;
window.__momoLinkGuardInstalled = true;
const MOMO_CODE_RE = /^[A-Za-z0-9_-]{4,}$/;
function toText(value) {
return value == null ? '' : String(value);
}
function isLikelyMomoCode(value) {
const cleaned = toText(value).trim();
if (!cleaned) return false;
const lowered = cleaned.toLowerCase();
if (['nan', 'none', 'null', 'undefined'].includes(lowered)) return false;
if (lowered.startsWith('momo_') || lowered.startsWith('manual_') || lowered.startsWith('pchome_')) return false;
return MOMO_CODE_RE.test(cleaned);
}
function extractIcode(url) {
const target = toText(url).trim();
if (!target) return '';
try {
const parsed = new URL(target, location.origin);
return toText(parsed.searchParams.get('i_code')).trim();
} catch (error) {
const match = /[?&]i_code=([^&#]+)/i.exec(target);
return match ? decodeURIComponent(match[1] || '').trim() : '';
}
}
function buildSafeMomoUrl(iCode) {
const cleaned = toText(iCode).trim();
if (!isLikelyMomoCode(cleaned)) return '';
return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(cleaned)}`;
}
document.addEventListener('click', function (event) {
const link = event.target.closest ? event.target.closest('a.momo-tracked-link') : null;
if (!link) return;
const href = toText(link.getAttribute('href')).trim();
const original = toText(link.dataset && link.dataset.momoOriginalUrl).trim();
const iCode = toText(link.dataset && (link.dataset.trackIcode || link.dataset.trackProductId)).trim()
|| extractIcode(original)
|| extractIcode(href);
const safeUrl = buildSafeMomoUrl(iCode);
if (safeUrl && (!href || href === '#' || /ec404/i.test(href) || /ec404/i.test(original))) {
event.preventDefault();
link.setAttribute('href', safeUrl);
window.open(safeUrl, link.getAttribute('target') || '_self', 'noopener,noreferrer');
}
}, true);
})();
(function () {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
window.fetchWithCSRF = (url, options = {}) => {
options.headers = { ...options.headers, 'X-CSRFToken': csrfToken };
return fetch(url, options);
};
})();
(function () {
const shell = document.getElementById('momo-shell');
const toggle = document.querySelector('[data-momo-sidebar-toggle]');
const close = document.querySelector('[data-momo-sidebar-close]');
if (!shell || !toggle) return;
toggle.addEventListener('click', () => shell.classList.toggle('is-sidebar-open'));
if (close) close.addEventListener('click', () => shell.classList.remove('is-sidebar-open'));
})();
(function() {
const link = document.getElementById('momo-obs-link');
const badge = document.getElementById('momo-obs-badge');
if (!link || !badge) return;
async function refresh() {
try {
const r = await fetch('/observability/api/health_indicator', { credentials: 'same-origin' });
if (!r.ok) return;
const d = await r.json();
if (!d.ok) return;
link.title = d.tooltip || 'AI 觀測台';
if (d.alert_count > 0) {
badge.textContent = d.alert_count;
badge.hidden = false;
link.classList.add('is-alert');
} else {
badge.hidden = true;
link.classList.remove('is-alert');
}
} catch (e) {}
}
refresh();
setInterval(refresh, 60000);
})();