-
-
-
DEPLOYMENT / READINESS
-
推版準備檢查
-
-
-
-
- loading
-
-
+
+
{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py
index bd77891..e8ed652 100644
--- a/tests/test_frontend_v2_assets.py
+++ b/tests/test_frontend_v2_assets.py
@@ -47,6 +47,36 @@ def test_frontend_v2_shell_uses_real_runtime_context():
assert all(marker not in combined for marker in forbidden_markers)
+def test_topbar_observability_indicator_is_cached_and_timeout_bounded():
+ base_js = (ROOT / "web/static/js/ewoooc-base.js").read_text(encoding="utf-8")
+ observability_route = (ROOT / "routes/admin_observability_routes.py").read_text(encoding="utf-8")
+
+ assert "momoObsHealthIndicator:v1" in base_js
+ assert "sessionStorage.getItem(cacheKey)" in base_js
+ assert "sessionStorage.setItem(cacheKey" in base_js
+ assert "const cacheTtlMs = 60000" in base_js
+ assert "new AbortController()" in base_js
+ assert "setTimeout(() => controller.abort(), 2500)" in base_js
+ assert "setInterval(() => refresh(false), 60000)" in base_js
+ assert "_HEALTH_INDICATOR_CACHE_LOCK" in observability_route
+ assert "_HEALTH_INDICATOR_CACHE_TTL_SECONDS = 30" in observability_route
+ assert "return jsonify(dict(cached_payload))" in observability_route
+
+
+def test_market_intel_disabled_page_stays_lightweight_and_action_oriented():
+ template_path = ROOT / "templates/market_intel/disabled.html"
+ template = template_path.read_text(encoding="utf-8")
+
+ assert template_path.stat().st_size < 40000
+ assert "市場情報模組待啟用" in template
+ assert "比價覆核" in template
+ assert "PChome 爬蟲" in template
+ assert "AI 觀測台" in template
+ assert "data-market-intel-preview" not in template
+ assert "/api/market_intel/" not in template
+ assert "讀取候選預覽中" not in template
+
+
def test_frontend_v2_syncs_latest_momo_pro_prototype_tokens_and_shell():
tokens = (ROOT / "web/static/css/ewoooc-tokens.css").read_text(encoding="utf-8")
shell = (ROOT / "web/static/css/ewoooc-shell.css").read_text(encoding="utf-8")
@@ -325,6 +355,7 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data():
assert ".dashboard-review-envelope" in dashboard_css
assert ".dashboard-review-actions" in dashboard_css
assert ".dashboard-review-action.is-research" in dashboard_css
+ assert "grid-template-columns: repeat(auto-fit, minmax(128px, 1fr))" in dashboard_css
def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis():
diff --git a/web/static/css/page-dashboard-v2.css b/web/static/css/page-dashboard-v2.css
index 01c86e0..af83c1a 100644
--- a/web/static/css/page-dashboard-v2.css
+++ b/web/static/css/page-dashboard-v2.css
@@ -632,21 +632,23 @@
}
.dashboard-review-segments {
- display: flex;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
gap: 8px;
padding: 12px 20px;
- overflow-x: auto;
+ overflow: visible;
border-bottom: 1px solid var(--momo-border-light);
- -webkit-overflow-scrolling: touch;
}
.dashboard-review-segments a {
display: inline-flex;
align-items: center;
+ justify-content: space-between;
gap: 8px;
- flex: 0 0 auto;
+ min-width: 0;
min-height: 30px;
padding: 6px 10px;
+ overflow: hidden;
color: var(--momo-text-secondary);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
@@ -656,6 +658,13 @@
text-decoration: none;
}
+ .dashboard-review-segments a span:first-child {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
.dashboard-review-segments a.is-active {
color: var(--momo-text-inverse);
background: var(--momo-ink);
diff --git a/web/static/js/ewoooc-base.js b/web/static/js/ewoooc-base.js
index a56f102..dbf69fe 100644
--- a/web/static/js/ewoooc-base.js
+++ b/web/static/js/ewoooc-base.js
@@ -76,23 +76,63 @@
const link = document.getElementById('momo-obs-link');
const badge = document.getElementById('momo-obs-badge');
if (!link || !badge) return;
- async function refresh() {
+
+ const cacheKey = 'momoObsHealthIndicator:v1';
+ const cacheTtlMs = 60000;
+
+ function applyIndicator(d) {
+ if (!d || !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');
+ }
+ }
+
+ function readCachedIndicator() {
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');
- }
+ const cached = JSON.parse(sessionStorage.getItem(cacheKey) || 'null');
+ if (!cached || !cached.data || Date.now() - cached.ts > cacheTtlMs) return null;
+ return cached.data;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ function writeCachedIndicator(data) {
+ try {
+ sessionStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data }));
} catch (e) {}
}
+
+ async function refresh(useCache = true) {
+ if (useCache) {
+ const cached = readCachedIndicator();
+ if (cached) {
+ applyIndicator(cached);
+ return;
+ }
+ }
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), 2500);
+ try {
+ const r = await fetch('/observability/api/health_indicator', {
+ credentials: 'same-origin',
+ signal: controller.signal,
+ });
+ if (!r.ok) return;
+ const d = await r.json();
+ writeCachedIndicator(d);
+ applyIndicator(d);
+ } catch (e) {
+ } finally {
+ clearTimeout(timer);
+ }
+ }
refresh();
- setInterval(refresh, 60000);
+ setInterval(() => refresh(false), 60000);
})();