fix: add webcrumbs loader fallback
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.652"
|
||||
SYSTEM_VERSION = "V10.653"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -68,5 +68,6 @@ style.css
|
||||
- Host data metadata 需同時輸出 `matched_count/coverage_rate` 與 `fresh_match_count/fresh_match_rate/stale_match_count/pending_match_count`,讓共用 UI 分清「身份覆蓋」與「價格新鮮度」。
|
||||
- `/webcrumbs` 未取得登入 session 或 `X-Internal-Key` 時只能注入 `auth_required` 空狀態,不得把真實 SKU、價格或價差寫進 inline seed data。
|
||||
- `/webcrumbs-assets/loader/webcrumbs-compatible-loader.js` 回 200 且 content type 是 JavaScript。
|
||||
- 若 Shared UI Hub 或 `WEBCRUMBS_ASSET_UPSTREAM_URL` 暫時不可用,`/webcrumbs-assets/loader/webcrumbs-compatible-loader.js` 必須回 200 的本地 fallback loader,避免正式頁面出現 502;fallback 只負責安全降級與標示 runtime 離線,不取代真實 plugin bundle。
|
||||
- `ewoooc_base.html` 在 `WEBCRUMBS_ENABLED=true` 且 runtime URL 有效時輸出 `<script data-webcrumbs-runtime=...>`。
|
||||
- 任一試點頁嵌入 plugin 後,瀏覽器 console 不應有 `MISSING_URI`、`STYLE_LOAD_ERROR`、`SCRIPT_LOAD_ERROR`。
|
||||
|
||||
@@ -44,6 +44,32 @@ LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = os.getenv('PUBLIC_URL', '服務啟動中...')
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'web/static')
|
||||
WEBCRUMBS_ASSET_ALLOWED_PREFIXES = ('loader/', 'plugins/', 'demo/')
|
||||
WEBCRUMBS_COMPATIBLE_LOADER_PATH = 'loader/webcrumbs-compatible-loader.js'
|
||||
WEBCRUMBS_FALLBACK_LOADER = """
|
||||
(() => {
|
||||
const state = { status: 'fallback', reason: 'upstream_unavailable' };
|
||||
window.WebcrumbsRuntime = Object.assign(window.WebcrumbsRuntime || {}, state);
|
||||
if (!customElements.get('stock-platform-plugin')) {
|
||||
customElements.define('stock-platform-plugin', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if (this.dataset.webcrumbsFallbackRendered === '1') return;
|
||||
this.dataset.webcrumbsFallbackRendered = '1';
|
||||
const uri = this.getAttribute('uri') || '';
|
||||
const box = document.createElement('div');
|
||||
box.setAttribute('role', 'status');
|
||||
box.style.cssText = 'border:1px solid rgba(42,37,32,.14);border-radius:8px;padding:12px;background:#fffaf0;color:#3b332b;font:600 13px/1.5 system-ui,sans-serif;';
|
||||
const title = document.createElement('strong');
|
||||
title.textContent = 'Webcrumbs runtime temporarily offline';
|
||||
const detail = document.createElement('div');
|
||||
detail.textContent = uri ? `Plugin waiting for runtime: ${uri}` : 'Plugin waiting for runtime.';
|
||||
box.append(title, detail);
|
||||
this.replaceChildren(box);
|
||||
}
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('webcrumbs:runtime-fallback', { detail: state }));
|
||||
})();
|
||||
""".strip()
|
||||
|
||||
|
||||
def _has_sensitive_webcrumbs_access():
|
||||
@@ -167,11 +193,24 @@ def _normalize_webcrumbs_asset_path(asset_path):
|
||||
return normalized
|
||||
|
||||
|
||||
def _webcrumbs_fallback_loader_response(reason):
|
||||
response = Response(WEBCRUMBS_FALLBACK_LOADER, status=200, mimetype='application/javascript')
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['Referrer-Policy'] = 'no-referrer'
|
||||
response.headers['X-Webcrumbs-Fallback'] = reason
|
||||
return response
|
||||
|
||||
|
||||
@system_public_bp.route('/webcrumbs-assets/<path:asset_path>')
|
||||
def webcrumbs_asset_proxy(asset_path):
|
||||
"""Serve allowlisted shared-ui assets through momo-pro's own origin."""
|
||||
normalized_path = _normalize_webcrumbs_asset_path(asset_path)
|
||||
if not WEBCRUMBS_ENABLED or not WEBCRUMBS_ASSET_UPSTREAM_URL or not normalized_path:
|
||||
if not normalized_path or not WEBCRUMBS_ENABLED:
|
||||
return Response('Webcrumbs asset not available', status=404, mimetype='text/plain')
|
||||
if not WEBCRUMBS_ASSET_UPSTREAM_URL:
|
||||
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
|
||||
return _webcrumbs_fallback_loader_response('missing-upstream')
|
||||
return Response('Webcrumbs asset not available', status=404, mimetype='text/plain')
|
||||
|
||||
upstream_url = f"{WEBCRUMBS_ASSET_UPSTREAM_URL}/{quote(normalized_path, safe='/@._-')}"
|
||||
@@ -179,10 +218,14 @@ def webcrumbs_asset_proxy(asset_path):
|
||||
upstream_response = requests.get(upstream_url, timeout=(2, 8))
|
||||
except requests.RequestException as exc:
|
||||
sys_log.warning(f"[Webcrumbs] Asset proxy failed: {normalized_path} -> {exc}")
|
||||
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
|
||||
return _webcrumbs_fallback_loader_response('upstream-unavailable')
|
||||
return Response('Webcrumbs asset upstream unavailable', status=502, mimetype='text/plain')
|
||||
|
||||
if upstream_response.status_code >= 400:
|
||||
sys_log.warning(f"[Webcrumbs] Asset proxy returned {upstream_response.status_code}: {normalized_path}")
|
||||
if normalized_path == WEBCRUMBS_COMPATIBLE_LOADER_PATH:
|
||||
return _webcrumbs_fallback_loader_response(f'upstream-{upstream_response.status_code}')
|
||||
return Response('Webcrumbs asset upstream returned error', status=upstream_response.status_code, mimetype='text/plain')
|
||||
|
||||
content_type = upstream_response.headers.get('Content-Type')
|
||||
|
||||
11
tests/test_webcrumbs_asset_proxy.py
Normal file
11
tests/test_webcrumbs_asset_proxy.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_webcrumbs_loader_has_safe_fallback_response():
|
||||
source = Path("routes/system_public_routes.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "WEBCRUMBS_COMPATIBLE_LOADER_PATH = 'loader/webcrumbs-compatible-loader.js'" in source
|
||||
assert "WEBCRUMBS_FALLBACK_LOADER" in source
|
||||
assert "status=200, mimetype='application/javascript'" in source
|
||||
assert "X-Webcrumbs-Fallback" in source
|
||||
assert "upstream-unavailable" in source
|
||||
Reference in New Issue
Block a user