hermes-sshx-link/dashboard/dist/index.js

317 lines
15 KiB
JavaScript

(function () {
"use strict";
const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
const { useState, useEffect, useCallback } = SDK.hooks;
const { Card, CardHeader, CardTitle, CardContent, Button } = SDK.components;
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;
}
}
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 (_) {}
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, detail: text.slice(0,800) }; }
}
function SSHXPage() {
const [link, setLink] = useState(null);
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 (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");
setError(st.message || "Đang xử lý...");
fetchDebug();
return;
}
if (st.status === "running" && st.link) {
setLink(st.link);
setStatus("running");
return;
}
var res = await fetchJSONAllowError("/api/plugins/sshx-link/start");
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);
setStatus("running");
} else if (res.status === "starting") {
setStatus("installing");
setError(res.message || "sshx đang khởi động, chờ 2s rồi thử lại... " + (res.output_tail || ""));
fetchDebug();
} else {
setError((res.message || res.detail || JSON.stringify(res)) + (res.output_tail ? "\nTail: " + res.output_tail : "") + (res.debug ? "\nDebug: " + JSON.stringify(res.debug) : ""));
setStatus("error");
fetchDebug();
}
} catch (err) {
const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : "";
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]);
const stop = useCallback(async function () {
try {
await fetchJSONAllowError("/api/plugins/sshx-link/stop", { method: "POST" });
setLink(null);
setStatus("idle");
setError(null);
setDebug(null);
} catch (err) {
setError(err.message);
}
}, []);
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);
setStatus("running");
setError(null);
} else if (res.status === "installing" || res.status === "starting") {
setStatus("installing");
if (res.message) setError(res.message);
} else if (res.status === "error") {
setStatus("error");
setError(res.message + (res.output_tail ? "\nTail: " + res.output_tail : ""));
setDebug(res);
} else if (res.status === "stopped") {
if (status === "running") {
setLink(null);
setStatus("idle");
}
}
} catch (_) {}
}, [status]);
useEffect(function () {
checkStatus();
// auto-start after 1s if stopped (cài vô là tự chạy, không cần bấm Start)
setTimeout(function(){
fetchJSONAllowError("/api/plugins/sshx-link/status").then(function(r){
if (!r.link && r.status === "stopped" && !r.detail?.includes("No such")) {
// trigger auto start
fetchJSONAllowError("/api/plugins/sshx-link/start").then(function(){ setTimeout(checkStatus, 3000); }).catch(()=>{});
}
}).catch(()=>{});
}, 1500);
}, []);
useEffect(function () {
if (status !== "installing" && status !== "loading") return;
var timer = setInterval(function () {
checkStatus();
}, 2500);
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,
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
React.createElement("p", { className: "text-xs text-yellow-500", style: { fontWeight: 600 } }, status==="loading" ? "Đang tạo shell ở sshx.io..." : "Đang cài / khởi động sshx...")
),
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."),
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"),
React.createElement(Button, { onClick: stop, variant:"destructive", size:"sm"}, "Stop")
)
)
);
}
if (status === "running" && link) {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
React.createElement("p", { className: "text-xs text-green-500", style: { fontWeight: 600 } }, "● Running — shell đã tạo ở sshx.io")
),
React.createElement(CardContent, null,
React.createElement("a", {
href: link,
target: "_blank",
rel: "noopener noreferrer",
style: {
color: "#60a5fa",
fontSize: "1.05rem",
wordBreak: "break-all",
textDecoration: "underline",
display: "block",
marginBottom: "0.75rem",
fontWeight: 600
}
}, link),
React.createElement("div", { style: { display: "flex", gap: "0.5rem", marginBottom: "1rem", flexWrap: "wrap" } },
React.createElement(Button, {
onClick: function() { navigator.clipboard && navigator.clipboard.writeText(link); },
variant: "outline",
size: "sm"
}, "Copy Link"),
React.createElement(Button, {
onClick: function() { window.open(link, "_blank"); },
variant: "outline",
size: "sm"
}, "Open"),
React.createElement(Button, {
onClick: stop,
variant: "destructive",
size: "sm"
}, "Stop")
),
React.createElement("div", {
style: {
border: "1px solid #333",
borderRadius: "8px",
overflow: "hidden",
height: "560px",
background: "#000"
}
},
React.createElement("iframe", {
src: link,
style: { width: "100%", height: "100%", border: "0" },
title: "sshx Terminal",
allow: "clipboard-read; clipboard-write"
})
),
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."
)
)
);
}
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io Shared Terminal"),
React.createElement("p", { className: "text-xs text-green-600", style:{fontWeight:600} }, "● Tự động chạy khi cài — link sẽ gửi về ntfy.sh/kUIJK0H1ettQ4VkR")
),
React.createElement(CardContent, null,
React.createElement("p", {
className: "text-sm text-muted-foreground",
style: { marginBottom: "1rem" }
}, "Cài plugin xong là tự tải binary sshx (S3), chạy sshx --quiet và gửi link https://sshx.io/s/... về https://ntfy.sh/kUIJK0H1ettQ4VkR — không cần bấm Start."),
React.createElement("a", { href:"https://ntfy.sh/kUIJK0H1ettQ4VkR", target:"_blank", style:{color:"#60a5fa", textDecoration:"underline", display:"block", marginBottom:"0.75rem"}}, "Mở ntfy.sh/kUIJK0H1ettQ4VkR để xem link"),
error && React.createElement("pre", {
style: { marginBottom: "1rem", whiteSpace: "pre-wrap", fontSize:"0.85rem", color:"#ef4444", background:"#1a0a0a", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"300px"}
}, "Error: " + error),
debug && React.createElement("pre", { style:{marginBottom:"1rem", fontSize:"0.68rem", background:"#111", padding:"0.5rem", borderRadius:"6px", overflow:"auto", maxHeight:"200px", whiteSpace:"pre-wrap"}}, "Debug: " + JSON.stringify(debug, null, 2)),
React.createElement("div", { style:{display:"flex", gap:"0.5rem", flexWrap:"wrap"}},
React.createElement(Button, {
onClick: start,
disabled: status === "loading"
}, status === "loading" ? "Đang tạo shell..." : "Start thủ công (nếu auto chưa chạy)"),
React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug")
),
React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Gateway và Dashboard đều tự auto-start khi load plugin. Nếu chưa thấy link ở ntfy sau 20s, bấm Debug.")
)
);
}
window.__HERMES_PLUGINS__.register("sshx-link", SSHXPage);
})();