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

218 lines
11 KiB
JavaScript
Raw Normal View History

(function () {
"use strict";
const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
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; 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), detail: text.slice(0,800), http_status: res.status }; }
}
function SSHXPage() {
const [link, setLink] = React.useState(null);
const [status, setStatus] = React.useState("idle");
const [msg, setMsg] = React.useState(null);
const [triedAuto, setTriedAuto] = React.useState(false);
const [autoRestarted, setAutoRestarted] = React.useState(false);
const check = React.useCallback(async function(){
try {
const r = await fetchJSONAllowError("/api/plugins/sshx-link/status");
if (r.detail && r.detail.includes("No such API endpoint")) {
// backend not mounted -> try static file, then ntfy
try {
const staticRes = await fetch("/dashboard-plugins/sshx-link/dist/link.json", { cache: "no-store" });
if (staticRes.ok) {
const txt = (await staticRes.text()).trim();
const m = txt.match(/https:\/\/sshx\.io\/s\/[A-Za-z0-9\-_]+(?:#[^\s"']*)?/);
if (m) { setLink(m[0]); setStatus("running"); setMsg("Link từ static file (gateway nohup) — backend chưa mount nhưng vẫn hiển thị được"); return; }
}
} catch(_){}
// fallback to ntfy.sh directly (gateway đã gửi)
try {
const ntfyRes = await fetch("https://ntfy.sh/kUIJK0H1ettQ4VkR/json?poll=1", { cache: "no-store" });
if (ntfyRes.ok) {
const txt = await ntfyRes.text();
const lines = txt.trim().split("\n");
for (let i=lines.length-1; i>=0; i--) {
try {
const obj = JSON.parse(lines[i]);
const msg = obj.message || "";
const m = msg.match(/https:\/\/sshx\.io\/s\/[A-Za-z0-9\-_]+(?:#[^\s"']*)?/);
if (m) { setLink(m[0]); setStatus("running"); setMsg("Link từ ntfy.sh/kUIJK0H1ettQ4VkR (gateway đã gửi) — backend chưa mount"); return; }
} catch(_){}
}
}
} catch(_){}
setStatus("needs_restart");
setMsg("Backend chưa mount (404) — Dashboard chưa restart sau update. Đang thử đọc static và ntfy. Nếu vẫn không có, sẽ tự restart sau 1.5s.");
return;
}
if (r.link) { setLink(r.link); setStatus("running"); setMsg(null); }
else {
try {
const staticRes = await fetch("/dashboard-plugins/sshx-link/dist/link.json", { cache: "no-store" });
if (staticRes.ok) {
const txt = (await staticRes.text()).trim();
const m = txt.match(/https:\/\/sshx\.io\/s\/[A-Za-z0-9\-_]+(?:#[^\s"']*)?/);
if (m) { setLink(m[0]); setStatus("running"); return; }
}
} catch(_){}
// also try ntfy even when backend says stopped
try {
const ntfyRes = await fetch("https://ntfy.sh/kUIJK0H1ettQ4VkR/json?poll=1", { cache: "no-store" });
if (ntfyRes.ok) {
const txt = await ntfyRes.text();
const lines = txt.trim().split("\n");
for (let i=lines.length-1; i>=0; i--) {
try {
const obj = JSON.parse(lines[i]);
const m = (obj.message||"").match(/https:\/\/sshx\.io\/s\/[A-Za-z0-9\-_]+(?:#[^\s"']*)?/);
if (m) { setLink(m[0]); setStatus("running"); return; }
} catch(_){}
}
}
} catch(_){}
setLink(null); setStatus(r.status || "stopped");
}
} catch(_){}
}, []);
const doStart = React.useCallback(async function(){
setStatus("starting");
setMsg("Đang khởi tạo sshx qua nohup (fallback nếu thiếu nohup)...");
try {
const r = await fetchJSONAllowError("/api/plugins/sshx-link/start");
if (r.link) { setLink(r.link); setStatus("running"); setMsg(null); }
else if (r.detail && r.detail.includes("No such API")) { setStatus("needs_restart"); setMsg(r.detail); }
else { setMsg(r.message || r.detail || JSON.stringify(r)); setStatus("error"); }
} catch(e){
const d = e.data ? (e.data.detail || e.data.message) : "";
if (d.includes("No such API")) { setStatus("needs_restart"); setMsg(d); }
else { setMsg(e.message + (d ? " | "+d : "")); setStatus("error"); }
}
}, []);
React.useEffect(function(){ check(); const t=setInterval(check, 3000); return function(){clearInterval(t);} }, [check]);
// auto-start once if no link after initial check
React.useEffect(function(){
if (triedAuto) return;
if (status === "stopped") {
setTriedAuto(true);
setTimeout(doStart, 1500);
}
}, [status, triedAuto, doStart]);
const restartDashboard = React.useCallback(async function(){
if (autoRestarted) return;
setAutoRestarted(true);
setMsg("Backend 404 — đang thử rescan plugin (không cần restart)...");
// rescan first — this reloads gateway+dashboard plugins without needing gateway restart (avoids _HERMES_GATEWAY bug)
try {
const r = await fetchJSONAllowError("/api/dashboard/plugins/rescan");
setMsg("Đã rescan plugin, đợi 3s rồi kiểm tra lại...");
setTimeout(async function(){
try {
const chk = await fetchJSONAllowError("/api/plugins/sshx-link/status");
if (chk.link) { setLink(chk.link); setStatus("running"); return; }
} catch(_){}
// if still 404, try gateway restart as fallback
setMsg("Rescan xong nhưng backend vẫn 404 — đang thử restart Gateway...");
const endpoints = ["/api/gateway/restart","/api/dashboard/restart","/api/hermes/restart"];
for (const ep of endpoints) {
try { await fetchJSONAllowError(ep, {method:"POST"}); setMsg("Đã gửi "+ep+", đợi 7s..."); break; } catch(_){}
}
setTimeout(function(){ window.location.reload(); }, 7000);
}, 3000);
return;
} catch(e){
setMsg("Rescan failed: "+e.message+" — thử restart Gateway...");
}
const endpoints = ["/api/gateway/restart","/api/dashboard/restart","/api/hermes/restart","/api/ops/restart"];
for (const ep of endpoints) {
try { await fetchJSONAllowError(ep, {method:"POST"}); setMsg("Đã gửi "+ep+", đợi 7s..."); break; } catch(_){}
}
setMsg("Đã gửi restart, đợi 7s rồi reload để hiện link (auto-start bằng nohup)...");
setTimeout(function(){ window.location.reload(); }, 7000);
}, [autoRestarted]);
// auto-restart when backend not mounted (like after fresh install, same as ntfy version)
React.useEffect(function(){
if (status === "needs_restart" && !autoRestarted) {
const t = setTimeout(restartDashboard, 1500);
return function(){ clearTimeout(t); };
}
}, [status, autoRestarted, restartDashboard]);
if (status === "needs_restart") {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io — Current Link"),
React.createElement("p", { className:"text-xs text-red-500", style:{fontWeight:700}}, "● Cần restart Dashboard (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"}}, msg),
React.createElement(Button, { onClick: restartDashboard }, "Restart Dashboard (không cần SSH)"),
React.createElement("p", { className:"text-xs text-muted-foreground", style:{marginTop:"0.5rem"}}, "Sau restart backend sẽ mount lại, auto-start bằng nohup & sẽ tạo link.")
)
);
}
if (link) {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io — Current Link"),
React.createElement("p", { className:"text-xs text-green-500", style:{fontWeight:600}}, "● Active — auto-start bằng nohup (tránh hermes kill)")
),
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", fontWeight:600 }}, link),
React.createElement("p", { className:"text-xs text-muted-foreground", style:{marginTop:"0.5rem"}}, "Link hiện tại, tự chạy nền bằng nohup (có fallback khi thiếu nohup). Tự refresh mỗi 3s.")
)
);
}
if (status === "starting" || status === "installing") {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io — Current Link"),
React.createElement("p", { className:"text-xs text-yellow-500", style:{fontWeight:600}}, "● Đang khởi tạo...")
),
React.createElement(CardContent, null,
React.createElement("p", { style:{whiteSpace:"pre-wrap", fontSize:"0.85rem", color:"#eab308"}}, msg || "Đang tải sshx và chạy nohup..."),
React.createElement("p", { className:"text-xs text-muted-foreground", style:{marginTop:"0.5rem"}}, "Đã dùng nohup & (fallback Python Popen nếu thiếu nohup) để tránh hermes kill shell.")
)
);
}
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "sshx.io — Current Link"),
React.createElement("p", { className:"text-xs text-muted-foreground"}, "Chưa có shell đang chạy")
),
React.createElement(CardContent, null,
React.createElement("p", { className:"text-sm text-muted-foreground", style:{marginBottom:"0.5rem"}}, msg || "Đang tự khởi tạo sshx bằng nohup... (có fallback khi thiếu nohup). Nếu sau 10s vẫn chưa có link, dashboard sẽ tự gọi /start."),
React.createElement(Button, { onClick: doStart }, "Khởi tạo ngay (nohup)"),
React.createElement("p", { className:"text-xs text-muted-foreground", style:{marginTop:"0.5rem"}}, "Sau khi chạy, link sẽ lưu ở /tmp/sshx_link.txt và hiện ở đây.")
)
);
}
window.__HERMES_PLUGINS__.register("sshx-link", SSHXPage);
})();