(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 (needs restart) -> fallback to static file written by gateway nohup 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(_){} setStatus("needs_restart"); setMsg("Backend chưa mount (404) — Dashboard chưa restart sau update. Đang thử đọc /dashboard-plugins/sshx-link/dist/link.json (gateway đã tạo). 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 { // also try static fallback even when backend says stopped (gateway may have created file) 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(_){} 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("Đang gửi lệnh restart Gateway/Dashboard (tự động khi cài)..."); const endpoints = ["/api/gateway/restart","/api/dashboard/restart","/api/hermes/restart","/api/ops/restart","/api/gateway/stop","/api/gateway/start"]; for (const ep of endpoints) { try { await fetchJSONAllowError(ep, {method:"POST"}); setMsg("Đã gửi "+ep+", đợi 7s..."); break; } catch(_){} } try { await fetchJSONAllowError("/api/dashboard/plugins/rescan"); } 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); })();