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:
@@ -74,6 +74,27 @@ systemctl restart jiedian-panel
|
|||||||
|
|
||||||
若域名在阿里云/Cloudflare 开了 **CDN 代理**,建议对管理域名 **关闭 CDN**(仅 DNS 解析到 VPS),否则 80 端口回源也可能异常。
|
若域名在阿里云/Cloudflare 开了 **CDN 代理**,建议对管理域名 **关闭 CDN**(仅 DNS 解析到 VPS),否则 80 端口回源也可能异常。
|
||||||
|
|
||||||
|
### 在线节点 / 统计一直显示「检测中」
|
||||||
|
|
||||||
|
页面初始状态是「检测中」。若长期不变且数字一直是 `-`,说明 **前端 JS 或 `/api/stats` 请求失败**(常见:静态资源路径缺少 `PANEL_PATH` 前缀)。
|
||||||
|
|
||||||
|
**处理**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/jiedian
|
||||||
|
git pull
|
||||||
|
grep PANEL_PATH /opt/jiedian/.env
|
||||||
|
grep PANEL /etc/systemd/system/jiedian-panel.service
|
||||||
|
systemctl restart jiedian-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器 **Ctrl+F5** 强制刷新。按 F12 → Network,确认:
|
||||||
|
|
||||||
|
- `.../jiedian-xxxx/static/app.js` 返回 200(不是 `/static/app.js`)
|
||||||
|
- `.../jiedian-xxxx/api/stats` 返回 200 JSON
|
||||||
|
|
||||||
|
若 `app.js` 404 或 `api/stats` 返回纯文本 `ok`,说明路径前缀仍未生效,需确认 `.env` 中 `PANEL_PATH` 与 systemd 里 `Environment=PANEL_PATH=...` 一致。
|
||||||
|
|
||||||
### sing-box 报错 v2ray api is not included in this build
|
### sing-box 报错 v2ray api is not included in this build
|
||||||
|
|
||||||
GitHub 下载的官方 sing-box **默认不带** `v2ray_api` 模块。若配置里写了 `experimental.v2ray_api`,启动时会直接失败:
|
GitHub 下载的官方 sing-box **默认不带** `v2ray_api` 模块。若配置里写了 `experimental.v2ray_api`,启动时会直接失败:
|
||||||
|
|||||||
+16
-6
@@ -52,18 +52,20 @@ if _panel_path:
|
|||||||
app.config["SESSION_COOKIE_PATH"] = f"/{_panel_path}/"
|
app.config["SESSION_COOKIE_PATH"] = f"/{_panel_path}/"
|
||||||
app.config["PREFERRED_URL_SCHEME"] = "http"
|
app.config["PREFERRED_URL_SCHEME"] = "http"
|
||||||
app.wsgi_app = ProxyFix(
|
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:
|
class _PanelPrefixMiddleware:
|
||||||
"""CDN/反代未传 Host 时,避免重定向到 http://[No Host]/...。"""
|
"""始终用 PANEL_PATH 设置 SCRIPT_NAME,不依赖反代头是否完整。"""
|
||||||
|
|
||||||
def __init__(self, app, domain: str):
|
def __init__(self, app, prefix: str, domain: str = ""):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.script_name = f"/{prefix.strip('/')}"
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
environ["SCRIPT_NAME"] = self.script_name
|
||||||
if self.domain and not environ.get("HTTP_HOST"):
|
if self.domain and not environ.get("HTTP_HOST"):
|
||||||
environ["HTTP_HOST"] = self.domain
|
environ["HTTP_HOST"] = self.domain
|
||||||
if not environ.get("HTTP_X_FORWARDED_PROTO"):
|
if not environ.get("HTTP_X_FORWARDED_PROTO"):
|
||||||
@@ -71,8 +73,16 @@ class _PanelHostMiddleware:
|
|||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
|
|
||||||
|
|
||||||
if _panel_domain:
|
if _panel_path:
|
||||||
app.wsgi_app = _PanelHostMiddleware(app.wsgi_app, _panel_domain)
|
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):
|
def login_required(view):
|
||||||
|
|||||||
+18
-4
@@ -5,9 +5,18 @@ function toast(msg) {
|
|||||||
setTimeout(() => el.classList.add("hidden"), 2200);
|
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) {
|
function apiUrl(path) {
|
||||||
const base = (document.body.dataset.base || "").replace(/\/$/, "");
|
return `${panelBase()}${path}`;
|
||||||
return `${base}${path}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyText(text) {
|
function copyText(text) {
|
||||||
@@ -169,18 +178,23 @@ function updateStats(data) {
|
|||||||
|
|
||||||
async function refreshStats() {
|
async function refreshStats() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl("/api/stats"));
|
const res = await fetch(apiUrl("/api/stats"), { credentials: "same-origin" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP ${res.status}`);
|
throw new Error(`HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
updateStats(data);
|
updateStats(data);
|
||||||
} catch {
|
} catch (err) {
|
||||||
const statusEl = document.getElementById("summaryStatus");
|
const statusEl = document.getElementById("summaryStatus");
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = "不可用";
|
statusEl.textContent = "不可用";
|
||||||
statusEl.className = "status-text err";
|
statusEl.className = "status-text err";
|
||||||
}
|
}
|
||||||
|
document.querySelectorAll('[data-role="status"]').forEach((el) => {
|
||||||
|
el.textContent = "未知";
|
||||||
|
el.classList.add("offline");
|
||||||
|
});
|
||||||
|
console.error("stats refresh failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}jiedian 面板{% endblock %}</title>
|
<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>
|
</head>
|
||||||
<body{% if request.script_root %} data-base="{{ request.script_root }}"{% endif %}>
|
<body data-base="{{ panel_base }}">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -107,5 +107,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
<script src="{{ panel_base }}/static/app.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user