diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index 0c8fc41..1f26f52 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -6,60 +6,14 @@ const { useState, useEffect, useCallback } = SDK.hooks; const { Card, CardHeader, CardTitle, CardContent, Button } = SDK.components; - // Auth-aware fetch: use SDK.fetchJSON if available (handles profile token/headers), - // fallback to raw fetch with ?profile= forwarding - async function safeFetchJSON(path, opts) { - // Prefer SDK's auth-aware helper - if (SDK && SDK.fetchJSON) { - try { - const data = await SDK.fetchJSON(path, opts || {}); - // SDK.fetchJSON returns parsed JSON directly - return data; - } catch (e) { - // SDK.fetchJSON may throw with body, try to extract - if (e && e.data) return e.data; - // fallback to raw fetch below - if (e && e.message && e.message.includes("Unauthorized")) { - // will try raw with profile below - } else { - throw e; - } - } - } - // Fallback: raw fetch but forward ?profile= from current URL (hermes needs it behind nginx) - let url = path; - try { - const cur = new URL(window.location.href); - const profile = cur.searchParams.get("profile"); - if (profile) { - const u = new URL(path, window.location.origin); - if (!u.searchParams.get("profile")) u.searchParams.set("profile", profile); - url = u.pathname + u.search; - } - } catch (_) { url = path; } - const res = await fetch(url, opts || {}); - const text = await res.text(); - let json; - try { json = JSON.parse(text); } catch (_) { - throw new Error("Server returned non-JSON (status " + res.status + "): " + text.slice(0,800)); - } - if (!res.ok) { - // attach body for error display but also return json so UI can show debug - const err = new Error(json.message || json.detail || ("HTTP " + res.status + ": " + text.slice(0,300))); - err.data = json; - err.status = res.status; - // if 401/403, include profile hint - if (res.status === 401) err.message += " (Unauthorized — thiếu profile/token, đang thử lại với ?profile=)"; - throw err; - } - return json; - } - - // Wrapper that returns JSON even on error status so UI can show debug async function fetchJSONAllowError(path, opts) { if (SDK && SDK.fetchJSON) { try { return await SDK.fetchJSON(path, opts || {}); } catch (e) { if (e && e.data) return e.data; + // SDK may throw Error with message containing JSON + const msg = e && e.message || ""; + // try to parse detail + try { const j = JSON.parse(msg.slice(msg.indexOf("{"))); return j; } catch(_){} throw e; } } @@ -75,7 +29,7 @@ } catch (_) {} const res = await fetch(url, opts || {}); const text = await res.text(); - try { return JSON.parse(text); } catch (_) { return { status: "error", message: text.slice(0,800), http_status: res.status }; } + try { return JSON.parse(text); } catch (_) { return { status: "error", message: text.slice(0,800), http_status: res.status, detail: text.slice(0,800) }; } } function SSHXPage() { @@ -83,19 +37,51 @@ const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); const [debug, setDebug] = useState(null); + const [restarting, setRestarting] = useState(false); const fetchDebug = useCallback(async function(){ try { const d = await fetchJSONAllowError("/api/plugins/sshx-link/debug"); setDebug(d); } catch(e){ setDebug({error: e.message}); } }, []); + const isMissingAPI = function(res){ + const s = JSON.stringify(res||{}); + return s.includes("No such API endpoint") || (res && res.detail && res.detail.includes("No such API endpoint")); + }; + + const restartDashboard = useCallback(async function(){ + setRestarting(true); + setError("Đang khởi động lại Gateway/Dashboard (không cần SSH)..."); + try { + // Try core Hermes restart endpoints (these are always mounted, even when plugin_api 404) + let lastErr = ""; + const tryPost = async (p) => { + try { await fetchJSONAllowError(p, { method:"POST"}); return true; } catch(e){ lastErr = e.message; return false; } + }; + // SDK path handles auth + await tryPost("/api/gateway/restart") || await tryPost("/api/dashboard/restart") || await tryPost("/api/hermes/restart"); + if (lastErr && lastErr.includes("No such")) { + // fallback: try rescan then wait + await fetchJSONAllowError("/api/dashboard/plugins/rescan"); + } + } catch(e){} + setError("Đã gửi lệnh restart. Đợi 6s rồi tự reload..."); + setTimeout(async function(){ + try { await fetchJSONAllowError("/api/dashboard/plugins/rescan"); } catch(_){} + setTimeout(function(){ window.location.reload(); }, 2500); + }, 6000); + }, []); + const start = useCallback(async function () { setStatus("loading"); setError(null); setDebug(null); try { var st = await fetchJSONAllowError("/api/plugins/sshx-link/status"); - if (st && st.detail === "Unauthorized") { - throw new Error("Unauthorized — thử dùng SDK.fetchJSON (đã fix). Hãy hard reload Ctrl+Shift+R sau khi update plugin v2.0.2"); + if (isMissingAPI(st)) { + setStatus("needs_restart"); + setError('Backend chưa mount: No such API endpoint: /api/plugins/sshx-link/status — Plugin vừa update lên v2.0.2/2.0.3 nhưng Dashboard chưa restart (plugin_api chỉ load lúc start). Cần restart Gateway/Dashboard — có thể làm ngay trên dashboard, không cần SSH.'); + fetchDebug(); + return; } if (st.status === "installing" || st.status === "starting") { setStatus("installing"); @@ -109,8 +95,10 @@ return; } var res = await fetchJSONAllowError("/api/plugins/sshx-link/start"); - if (res.detail === "Unauthorized") { - throw new Error("Unauthorized khi gọi /start — plugin frontend chưa dùng SDK auth. Đã fix ở v2.0.2, hãy hard reload."); + if (isMissingAPI(res)) { + setStatus("needs_restart"); + setError('Backend chưa mount: No such API endpoint: /api/plugins/sshx-link/start — Cần restart Dashboard (nút bên dưới, không cần SSH).'); + return; } if (res.link) { setLink(res.link); @@ -126,8 +114,13 @@ } } catch (err) { const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : ""; - setError((err.message || "Failed to start sshx") + (detail ? " | detail: " + detail : "")); - setStatus("error"); + if (detail.includes("No such API endpoint")) { + setStatus("needs_restart"); + setError("Backend chưa mount (404 No such API endpoint) — Dashboard chưa restart sau khi update plugin. Dùng nút Restart bên dưới, không cần SSH."); + } else { + setError((err.message || "Failed to start sshx") + (detail ? " | detail: " + detail : "")); + setStatus("error"); + } fetchDebug(); } }, [fetchDebug]); @@ -147,6 +140,11 @@ const checkStatus = useCallback(async function () { try { const res = await fetchJSONAllowError("/api/plugins/sshx-link/status"); + if (isMissingAPI(res)) { + setStatus("needs_restart"); + setError("Backend 404 — cần restart Dashboard (không cần SSH)."); + return; + } if (res.detail === "Unauthorized") return; if (res.status === "running" && res.link) { setLink(res.link); @@ -180,6 +178,25 @@ return function () { clearInterval(timer); }; }, [status, checkStatus]); + if (status === "needs_restart") { + return React.createElement(Card, null, + React.createElement(CardHeader, null, + React.createElement(CardTitle, null, "sshx.io Shared Terminal"), + React.createElement("p", { className: "text-xs text-red-500", style: { fontWeight: 700 } }, "● Cần restart Dashboard — Backend chưa mount (404)") + ), + React.createElement(CardContent, null, + React.createElement("pre", { style:{whiteSpace:"pre-wrap", fontSize:"0.85rem", color:"#fbbf24", background:"#1a1a0a", padding:"0.75rem", borderRadius:"6px", marginBottom:"0.75rem"}}, error || "No such API endpoint: /api/plugins/sshx-link/status — plugin vừa update nhưng Dashboard chưa restart."), + React.createElement("p", { className:"text-sm text-muted-foreground", style:{marginBottom:"0.75rem"}}, "Không cần SSH. Bấm nút dưới để restart Gateway/Dashboard ngay trên dashboard (dùng /api/gateway/restart). Sau ~8s trang sẽ tự reload."), + React.createElement("div", { style:{display:"flex", gap:"0.5rem", flexWrap:"wrap"}}, + React.createElement(Button, { onClick: restartDashboard, disabled: restarting }, restarting ? "Đang restart..." : "Restart Dashboard ngay (không cần SSH)"), + React.createElement(Button, { onClick: function(){ fetchJSONAllowError("/api/dashboard/plugins/rescan").then(()=>location.reload()); }, variant:"outline", size:"sm"}, "Rescan plugins") + ), + React.createElement("p", { className:"text-xs text-muted-foreground", style:{marginTop:"0.75rem"}}, "Sau restart, /api/plugins/sshx-link/status sẽ trả 200 và nút Start sẽ tạo link https://sshx.io/s/..."), + debug && React.createElement("pre", { style:{marginTop:"0.75rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, JSON.stringify(debug, null, 2)) + ) + ); + } + if (status === "installing" || status === "loading") { return React.createElement(Card, null, React.createElement(CardHeader, null, @@ -188,7 +205,7 @@ ), React.createElement(CardContent, null, React.createElement("p", { className: "text-sm", style: { whiteSpace:"pre-wrap", fontWeight:600, color:"#eab308"}}, error || "Đang tải binary từ s3.amazonaws.com/sshx..."), - React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, "Tự động thử lại mỗi 2.5s. Nếu kẹt quá 20s, kiểm tra outbound tới s3.amazonaws.com và sshx.io trên server 13.140.132.105."), + React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, "Tự động thử lại mỗi 2.5s."), debug && React.createElement("pre", { style:{marginTop:"0.75rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, JSON.stringify(debug, null, 2)), React.createElement("div", { style:{display:"flex", gap:"0.5rem", marginTop:"0.75rem"}}, React.createElement(Button, { onClick: checkStatus, variant:"outline", size:"sm"}, "Refresh"), @@ -253,7 +270,7 @@ }) ), React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.5rem" } }, - "Terminal nhúng ở trên. Nếu iframe bị chặn, bấm Open. Link dạng https://sshx.io/s/# — chia sẻ cho user là xong." + "Terminal nhúng ở trên. Nếu iframe bị chặn, bấm Open." ) ) ); @@ -280,7 +297,7 @@ }, status === "loading" ? "Đang tạo shell..." : "Start sshx → Lấy link"), React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug") ), - React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu lỗi Unauthorized, hãy hard reload (Ctrl+Shift+R) để load v2.0.2 — bản này dùng SDK.fetchJSON có auth và tự forward ?profile=.") + React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu báo 404 No such API endpoint, bấm Start sẽ tự hiện nút Restart Dashboard — không cần SSH.") ) ); } diff --git a/dashboard/manifest.json b/dashboard/manifest.json index 0d4c50e..6867de0 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "label": "sshx.io Terminal", "description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart", "icon": "Terminal", - "version": "2.0.2", + "version": "2.0.3", "tab": { "path": "/sshx", "position": "end" diff --git a/plugin.yaml b/plugin.yaml index f66d65f..43f5b5f 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: sshx-link -version: 2.0.2 +version: 2.0.3 description: Tạo shell ở sshx.io và trả link chia sẻ — auto arch detection, không cần restart dashboard author: sisyphus provides_tools: