Files
SingBox-Gopanel/frontend/theme/Nebula/assets/enhancer.js
CN-JS-HuiBai 1ed31b9292
All checks were successful
build / build (api, amd64, linux) (push) Successful in -47s
build / build (api, arm64, linux) (push) Successful in -48s
build / build (api.exe, amd64, windows) (push) Successful in -47s
first commit
2026-04-17 09:49:16 +08:00

430 lines
15 KiB
JavaScript

(function () {
"use strict";
var theme = window.NEBULA_THEME || {};
var loader = document.getElementById("nebula-loader");
var appRoot = document.getElementById("app");
applyCustomBackground();
mountShell();
mountMonitor();
hideLoaderWhenReady();
function mountShell() {
if (!appRoot || document.getElementById("nebula-app-shell")) {
return;
}
var shell = document.createElement("div");
shell.className = "app-shell nebula-app-shell";
shell.id = "nebula-app-shell";
shell.innerHTML = renderShellChrome();
var parent = appRoot.parentNode;
parent.insertBefore(shell, appRoot);
var appStage = shell.querySelector(".nebula-app-stage");
if (appStage) {
appStage.appendChild(appRoot);
}
}
function mountMonitor() {
var rail = document.getElementById("nebula-side-rail");
var panel = document.createElement("aside");
panel.className = "nebula-monitor";
panel.id = "nebula-monitor";
panel.innerHTML = renderLoadingPanel();
if (rail) {
rail.appendChild(panel);
} else {
document.body.appendChild(panel);
}
panel.addEventListener("click", function (event) {
var actionEl = event.target.closest("[data-nebula-action]");
if (!actionEl) {
return;
}
var action = actionEl.getAttribute("data-nebula-action");
if (action === "refresh") {
loadDeviceOverview(panel);
}
if (action === "toggle") {
panel.classList.toggle("is-collapsed");
}
});
loadDeviceOverview(panel);
window.setInterval(function () {
loadDeviceOverview(panel, true);
}, 30000);
}
async function loadDeviceOverview(panel, silent) {
if (!silent) {
panel.innerHTML = renderLoadingPanel();
}
try {
var response = await fetch("/api/v1/user/user-online-devices/get-ip", {
method: "GET",
headers: getRequestHeaders(),
credentials: "same-origin"
});
if (response.status === 401 || response.status === 403) {
panel.innerHTML = renderGuestPanel();
return;
}
if (!response.ok) {
throw new Error("Unable to load device overview");
}
var resJson = await response.json();
var data = resJson && resJson.data ? resJson.data : {};
console.log("[Nebula] Device overview data:", data); // Diagnostic log
var overview = data.session_overview || data;
panel.innerHTML = renderPanel(overview);
} catch (error) {
panel.innerHTML = renderErrorPanel(error.message || "Unable to load device overview");
}
}
function renderShellChrome() {
return [
'<header class="topbar nebula-topbar glass-card">',
'<div class="brand">',
'<div class="brand-mark"></div>',
'<div>',
'<h1>' + escapeHtml(theme.title || "Nebula") + '</h1>',
'<p>' + escapeHtml(theme.description || "Refined user workspace") + '</p>',
'</div>',
'</div>',
'<div class="topbar-actions">',
'<span class="tiny-pill">Nebula Theme</span>',
'<span class="tiny-pill">Platform Control Surface</span>',
'</div>',
'</header>',
'<section class="dashboard-hero nebula-hero-layout">',
'<article class="hero glass-card">',
'<span class="nebula-pill">User Console</span>',
'<h2>Subscriptions, sessions, and live access in one view.</h2>',
'<p>' + escapeHtml((theme.config && theme.config.slogan) || "Current IP visibility, online devices, and the original workflow remain available.") + '</p>',
'<div class="metric-strip">',
'<div class="metric-box"><span class="value">Live</span><span class="label">Current ingress overview</span></div>',
'<div class="metric-box"><span class="value">Native</span><span class="label">Original features retained</span></div>',
'<div class="metric-box"><span class="value">Refresh</span><span class="label">Auto-updated every 30 seconds</span></div>',
'</div>',
'</article>',
'<aside class="section-card glass-card nebula-side-summary">',
'<div class="section-head"><div><span class="tiny-pill">Workspace</span><h3>Account operations hub</h3></div></div>',
'<div class="stack">',
'<div class="notice-item"><div class="notice-copy"><strong>Current IP visibility</strong><div class="notice-meta">Track how many IPs and devices are online for the signed-in user.</div></div></div>',
'<div class="notice-item"><div class="notice-copy"><strong>Original dashboard compatibility</strong><div class="notice-meta">The core application bundle is still powering forms, plans, notices, and navigation.</div></div></div>',
'<div class="notice-item"><div class="notice-copy"><strong>Immersive layout</strong><div class="notice-meta">A new command-center shell wraps the app with clearer spacing, hierarchy, and status context.</div></div></div>',
'</div>',
'</aside>',
'</section>',
'<section class="nebula-main-grid">',
'<div class="nebula-app-stage glass-card"></div>',
'<aside class="nebula-side-rail" id="nebula-side-rail"></aside>',
'</section>'
].join("");
}
function renderLoadingPanel() {
return [
'<div class="nebula-monitor__head">',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">Checking online IPs, devices, and active sessions...</div></div>',
'<div class="nebula-monitor__actions"><button data-nebula-action="toggle">Collapse</button></div>',
'</div>',
'<div class="nebula-monitor__body">',
'<div class="nebula-monitor__section"><div class="nebula-monitor__empty">Loading...</div></div>',
'</div>'
].join("");
}
function renderGuestPanel() {
return [
'<div class="nebula-monitor__head">',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">The panel is ready, but this browser session is not authenticated for the API yet.</div></div>',
'<div class="nebula-monitor__actions"><button data-nebula-action="refresh">Retry</button><button data-nebula-action="toggle">Collapse</button></div>',
'</div>',
'<div class="nebula-monitor__body">',
'<div class="nebula-monitor__section"><div class="nebula-monitor__empty">Sign in first, then refresh this panel to view current IP and device data.</div></div>',
'</div>'
].join("");
}
function renderErrorPanel(message) {
return [
'<div class="nebula-monitor__head">',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">The panel loaded, but the device overview endpoint did not respond as expected.</div></div>',
'<div class="nebula-monitor__actions"><button data-nebula-action="refresh">Retry</button><button data-nebula-action="toggle">Collapse</button></div>',
'</div>',
'<div class="nebula-monitor__body">',
'<div class="nebula-monitor__section"><div class="nebula-monitor__empty">' + escapeHtml(message) + '</div></div>',
'</div>'
].join("");
}
function renderPanel(data) {
var ips = Array.isArray(data.online_ips) ? data.online_ips : [];
var sessions = Array.isArray(data.sessions) ? data.sessions.slice(0, 4) : [];
return [
'<div class="nebula-monitor__head">',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">' + escapeHtml((theme.config && theme.config.slogan) || "Current IP and session visibility") + '</div></div>',
'<div class="nebula-monitor__actions"><button data-nebula-action="refresh">Refresh</button><button data-nebula-action="toggle">Collapse</button></div>',
'</div>',
'<div class="nebula-monitor__body">',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__grid">',
metric(String(data.online_ip_count || 0), "Current IPs"),
metric(String(data.online_device_count || 0), "Online devices"),
metric(String(data.active_session_count || 0), "Stored sessions"),
metric(formatLimit(data.device_limit), "Device limit"),
'</div>',
'<div class="nebula-monitor__meta">Last online: ' + formatDate(data.last_online_at) + '</div>',
'</div>',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__meta">Online IP addresses</div>',
ips.length ? '<div class="nebula-monitor__chips">' + ips.map(function (ip) {
return '<div class="nebula-monitor__chip"><code>' + escapeHtml(ip) + '</code></div>';
}).join("") + '</div>' : '<div class="nebula-monitor__empty">No IP data reported yet.</div>',
'</div>',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__meta">Recent sessions</div>',
sessions.length ? '<div class="nebula-monitor__sessions">' + sessions.map(function (session) {
return [
'<div class="nebula-monitor__session">',
'<strong>' + escapeHtml(session.name || ("Session #" + session.id)) + (session.is_current ? " · current" : "") + '</strong>',
'<div class="nebula-monitor__meta">Last used: ' + formatDate(session.last_used_at) + '</div>',
'</div>'
].join("");
}).join("") + '</div>' : '<div class="nebula-monitor__empty">No session records available.</div>',
'</div>',
'</div>'
].join("");
}
function metric(value, label) {
return '<div class="nebula-monitor__metric"><strong>' + escapeHtml(value) + '</strong><span>' + escapeHtml(label) + '</span></div>';
}
function getRequestHeaders() {
var headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
};
var token = getStoredToken();
if (token) {
headers.Authorization = token;
}
return headers;
}
function hideLoaderWhenReady() {
if (!loader) {
return;
}
var hide = function () {
loader.classList.add("is-hidden");
window.setTimeout(function () {
if (loader && loader.parentNode) {
loader.parentNode.removeChild(loader);
}
}, 350);
};
if (appRoot && appRoot.children.length > 0) {
hide();
return;
}
var observer = new MutationObserver(function () {
if (appRoot && appRoot.children.length > 0) {
observer.disconnect();
hide();
}
});
if (appRoot) {
observer.observe(appRoot, { childList: true });
}
window.setTimeout(hide, 6000);
}
function applyCustomBackground() {
if (!theme.config || !theme.config.backgroundUrl) {
return;
}
document.body.style.backgroundImage =
'linear-gradient(180deg, rgba(5, 13, 22, 0.76), rgba(5, 13, 22, 0.9)), url("' + String(theme.config.backgroundUrl).replace(/"/g, '\\"') + '")';
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center";
document.body.style.backgroundAttachment = "fixed";
}
function getStoredToken() {
var candidates = [];
var directKeys = ["access_token", "auth_data", "__nebula_auth_data__", "token", "auth_token"];
collectStorageValues(window.localStorage, directKeys, candidates);
collectStorageValues(window.sessionStorage, directKeys, candidates);
collectCookieValues(candidates);
collectGlobalValues(candidates);
for (var i = 0; i < candidates.length; i += 1) {
var token = extractToken(candidates[i]);
if (token) {
return token;
}
}
return "";
}
function collectStorageValues(storage, keys, target) {
if (!storage) {
return;
}
for (var i = 0; i < keys.length; i += 1) {
pushCandidate(storage.getItem(keys[i]), target);
}
}
function collectCookieValues(target) {
if (!document.cookie) {
return;
}
var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i += 1) {
var part = cookies[i].split("=");
if (part.length < 2) {
continue;
}
var key = String(part[0] || "").trim();
if (key.indexOf("token") !== -1 || key.indexOf("auth") !== -1) {
pushCandidate(decodeURIComponent(part.slice(1).join("=")), target);
}
}
}
function collectGlobalValues(target) {
pushCandidate(window.__INITIAL_STATE__, target);
pushCandidate(window.g_initialProps, target);
pushCandidate(window.g_app, target);
}
function pushCandidate(value, target) {
if (value === null || typeof value === "undefined" || value === "") {
return;
}
target.push(value);
}
function extractToken(value, depth) {
if (depth > 4 || value === null || typeof value === "undefined") {
return "";
}
if (typeof value === "string") {
var trimmed = value.trim();
if (!trimmed) {
return "";
}
if (trimmed.indexOf("Bearer ") === 0) {
return trimmed;
}
if (/^[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+(\.[A-Za-z0-9\-_.+/=]+)?$/.test(trimmed)) {
return "Bearer " + trimmed;
}
if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf("{") === -1) {
return "Bearer " + trimmed;
}
if ((trimmed.charAt(0) === "{" && trimmed.charAt(trimmed.length - 1) === "}") ||
(trimmed.charAt(0) === "[" && trimmed.charAt(trimmed.length - 1) === "]")) {
try {
return extractToken(JSON.parse(trimmed), (depth || 0) + 1);
} catch (error) {
return "";
}
}
return "";
}
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i += 1) {
var nestedToken = extractToken(value[i], (depth || 0) + 1);
if (nestedToken) {
return nestedToken;
}
}
return "";
}
if (typeof value === "object") {
var keys = ["access_token", "auth_data", "token", "Authorization", "authorization"];
for (var j = 0; j < keys.length; j += 1) {
if (Object.prototype.hasOwnProperty.call(value, keys[j])) {
var directToken = extractToken(value[keys[j]], (depth || 0) + 1);
if (directToken) {
return directToken;
}
}
}
for (var key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
var token = extractToken(value[key], (depth || 0) + 1);
if (token) {
return token;
}
}
}
return "";
}
function formatDate(value) {
if (!value) {
return "-";
}
if (typeof value === "number") {
return new Date(value * 1000).toLocaleString();
}
var parsed = Date.parse(value);
if (Number.isNaN(parsed)) {
return "-";
}
return new Date(parsed).toLocaleString();
}
function formatLimit(value) {
if (value === null || typeof value === "undefined" || value === "") {
return "Unlimited";
}
return String(value);
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
})();