fix: resolve stuck detecting state for panel stats on subpath deploy

Always inject panel_base from PANEL_PATH for static/API URLs and set
SCRIPT_NAME in middleware so /api/stats works behind nginx subpaths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 10:31:58 +08:00
parent ba361eb5b8
commit d75193d527
5 changed files with 58 additions and 13 deletions
+16 -6
View File
@@ -52,18 +52,20 @@ if _panel_path:
app.config["SESSION_COOKIE_PATH"] = f"/{_panel_path}/"
app.config["PREFERRED_URL_SCHEME"] = "http"
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=0, x_prefix=1
app.wsgi_app, x_for=1, x_proto=1, x_host=0, x_prefix=0
)
class _PanelHostMiddleware:
"""CDN/反代未传 Host 时,避免重定向到 http://[No Host]/..."""
class _PanelPrefixMiddleware:
"""始终用 PANEL_PATH 设置 SCRIPT_NAME,不依赖反代头是否完整"""
def __init__(self, app, domain: str):
def __init__(self, app, prefix: str, domain: str = ""):
self.app = app
self.script_name = f"/{prefix.strip('/')}"
self.domain = domain
def __call__(self, environ, start_response):
environ["SCRIPT_NAME"] = self.script_name
if self.domain and not environ.get("HTTP_HOST"):
environ["HTTP_HOST"] = self.domain
if not environ.get("HTTP_X_FORWARDED_PROTO"):
@@ -71,8 +73,16 @@ class _PanelHostMiddleware:
return self.app(environ, start_response)
if _panel_domain:
app.wsgi_app = _PanelHostMiddleware(app.wsgi_app, _panel_domain)
if _panel_path:
app.wsgi_app = _PanelPrefixMiddleware(
app.wsgi_app, _panel_path, _panel_domain
)
@app.context_processor
def inject_panel_base() -> dict[str, str]:
base = f"/{_panel_path}" if _panel_path else ""
return {"panel_base": base}
def login_required(view):
+18 -4
View File
@@ -5,9 +5,18 @@ function toast(msg) {
setTimeout(() => el.classList.add("hidden"), 2200);
}
function panelBase() {
const fromBody = (document.body.dataset.base || "").replace(/\/$/, "");
if (fromBody) return fromBody;
const parts = location.pathname.split("/").filter(Boolean);
if (parts.length && parts[0].startsWith("jiedian-")) {
return `/${parts[0]}`;
}
return "";
}
function apiUrl(path) {
const base = (document.body.dataset.base || "").replace(/\/$/, "");
return `${base}${path}`;
return `${panelBase()}${path}`;
}
function copyText(text) {
@@ -169,18 +178,23 @@ function updateStats(data) {
async function refreshStats() {
try {
const res = await fetch(apiUrl("/api/stats"));
const res = await fetch(apiUrl("/api/stats"), { credentials: "same-origin" });
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
updateStats(data);
} catch {
} catch (err) {
const statusEl = document.getElementById("summaryStatus");
if (statusEl) {
statusEl.textContent = "不可用";
statusEl.className = "status-text err";
}
document.querySelectorAll('[data-role="status"]').forEach((el) => {
el.textContent = "未知";
el.classList.add("offline");
});
console.error("stats refresh failed:", err);
}
}
+2 -2
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}jiedian 面板{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ panel_base }}/static/style.css">
</head>
<body{% if request.script_root %} data-base="{{ request.script_root }}"{% endif %}>
<body data-base="{{ panel_base }}">
{% block body %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
+1 -1
View File
@@ -107,5 +107,5 @@
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='app.js') }}"></script>
<script src="{{ panel_base }}/static/app.js"></script>
{% endblock %}