diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8ec0843..5f2f138 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -74,6 +74,27 @@ systemctl restart jiedian-panel 若域名在阿里云/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 GitHub 下载的官方 sing-box **默认不带** `v2ray_api` 模块。若配置里写了 `experimental.v2ray_api`,启动时会直接失败: diff --git a/panel/app.py b/panel/app.py index c973b8f..42fb9c4 100644 --- a/panel/app.py +++ b/panel/app.py @@ -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): diff --git a/panel/static/app.js b/panel/static/app.js index 8c4080b..5bdbc47 100644 --- a/panel/static/app.js +++ b/panel/static/app.js @@ -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); } } diff --git a/panel/templates/base.html b/panel/templates/base.html index 758c8b5..7e96bc2 100644 --- a/panel/templates/base.html +++ b/panel/templates/base.html @@ -4,9 +4,9 @@