fix: 404 No such API endpoint - add restart via dashboard (no SSH needed)
- frontend detects 'No such API endpoint' and shows needs_restart card - Restart Dashboard button calls /api/gateway/restart via SDK.fetchJSON (no SSH), waits 6s + rescan + reload - handles 404 for /status and /start uniformly - bump v2.0.3
This commit is contained in:
parent
44744cf279
commit
572792241b
|
|
@ -6,60 +6,14 @@
|
||||||
const { useState, useEffect, useCallback } = SDK.hooks;
|
const { useState, useEffect, useCallback } = SDK.hooks;
|
||||||
const { Card, CardHeader, CardTitle, CardContent, Button } = SDK.components;
|
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) {
|
async function fetchJSONAllowError(path, opts) {
|
||||||
if (SDK && SDK.fetchJSON) {
|
if (SDK && SDK.fetchJSON) {
|
||||||
try { return await SDK.fetchJSON(path, opts || {}); } catch (e) {
|
try { return await SDK.fetchJSON(path, opts || {}); } catch (e) {
|
||||||
if (e && e.data) return e.data;
|
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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +29,7 @@
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
const res = await fetch(url, opts || {});
|
const res = await fetch(url, opts || {});
|
||||||
const text = await res.text();
|
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() {
|
function SSHXPage() {
|
||||||
|
|
@ -83,19 +37,51 @@
|
||||||
const [status, setStatus] = useState("idle");
|
const [status, setStatus] = useState("idle");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [debug, setDebug] = useState(null);
|
const [debug, setDebug] = useState(null);
|
||||||
|
const [restarting, setRestarting] = useState(false);
|
||||||
|
|
||||||
const fetchDebug = useCallback(async function(){
|
const fetchDebug = useCallback(async function(){
|
||||||
try { const d = await fetchJSONAllowError("/api/plugins/sshx-link/debug"); setDebug(d); } catch(e){ setDebug({error: e.message}); }
|
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 () {
|
const start = useCallback(async function () {
|
||||||
setStatus("loading");
|
setStatus("loading");
|
||||||
setError(null);
|
setError(null);
|
||||||
setDebug(null);
|
setDebug(null);
|
||||||
try {
|
try {
|
||||||
var st = await fetchJSONAllowError("/api/plugins/sshx-link/status");
|
var st = await fetchJSONAllowError("/api/plugins/sshx-link/status");
|
||||||
if (st && st.detail === "Unauthorized") {
|
if (isMissingAPI(st)) {
|
||||||
throw new Error("Unauthorized — thử dùng SDK.fetchJSON (đã fix). Hãy hard reload Ctrl+Shift+R sau khi update plugin v2.0.2");
|
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") {
|
if (st.status === "installing" || st.status === "starting") {
|
||||||
setStatus("installing");
|
setStatus("installing");
|
||||||
|
|
@ -109,8 +95,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var res = await fetchJSONAllowError("/api/plugins/sshx-link/start");
|
var res = await fetchJSONAllowError("/api/plugins/sshx-link/start");
|
||||||
if (res.detail === "Unauthorized") {
|
if (isMissingAPI(res)) {
|
||||||
throw new Error("Unauthorized khi gọi /start — plugin frontend chưa dùng SDK auth. Đã fix ở v2.0.2, hãy hard reload.");
|
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) {
|
if (res.link) {
|
||||||
setLink(res.link);
|
setLink(res.link);
|
||||||
|
|
@ -126,8 +114,13 @@
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : "";
|
const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : "";
|
||||||
setError((err.message || "Failed to start sshx") + (detail ? " | detail: " + detail : ""));
|
if (detail.includes("No such API endpoint")) {
|
||||||
setStatus("error");
|
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();
|
||||||
}
|
}
|
||||||
}, [fetchDebug]);
|
}, [fetchDebug]);
|
||||||
|
|
@ -147,6 +140,11 @@
|
||||||
const checkStatus = useCallback(async function () {
|
const checkStatus = useCallback(async function () {
|
||||||
try {
|
try {
|
||||||
const res = await fetchJSONAllowError("/api/plugins/sshx-link/status");
|
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.detail === "Unauthorized") return;
|
||||||
if (res.status === "running" && res.link) {
|
if (res.status === "running" && res.link) {
|
||||||
setLink(res.link);
|
setLink(res.link);
|
||||||
|
|
@ -180,6 +178,25 @@
|
||||||
return function () { clearInterval(timer); };
|
return function () { clearInterval(timer); };
|
||||||
}, [status, checkStatus]);
|
}, [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") {
|
if (status === "installing" || status === "loading") {
|
||||||
return React.createElement(Card, null,
|
return React.createElement(Card, null,
|
||||||
React.createElement(CardHeader, null,
|
React.createElement(CardHeader, null,
|
||||||
|
|
@ -188,7 +205,7 @@
|
||||||
),
|
),
|
||||||
React.createElement(CardContent, null,
|
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-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)),
|
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("div", { style:{display:"flex", gap:"0.5rem", marginTop:"0.75rem"}},
|
||||||
React.createElement(Button, { onClick: checkStatus, variant:"outline", size:"sm"}, "Refresh"),
|
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" } },
|
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/<id>#<key> — 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"),
|
}, status === "loading" ? "Đang tạo shell..." : "Start sshx → Lấy link"),
|
||||||
React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug")
|
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.")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"label": "sshx.io Terminal",
|
"label": "sshx.io Terminal",
|
||||||
"description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart",
|
"description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart",
|
||||||
"icon": "Terminal",
|
"icon": "Terminal",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"tab": {
|
"tab": {
|
||||||
"path": "/sshx",
|
"path": "/sshx",
|
||||||
"position": "end"
|
"position": "end"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: sshx-link
|
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
|
description: Tạo shell ở sshx.io và trả link chia sẻ — auto arch detection, không cần restart dashboard
|
||||||
author: sisyphus
|
author: sisyphus
|
||||||
provides_tools:
|
provides_tools:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue