Files
SingBox-Gopanel/frontend/theme/Nebula/assets/enhancer.js
CN-JS-HuiBai 8cca428d89
Some checks failed
build / build (api, amd64, linux) (push) Failing after -51s
build / build (api, arm64, linux) (push) Failing after -52s
build / build (api.exe, amd64, windows) (push) Failing after -51s
规范化UI JS CONFIG
2026-04-18 21:55:54 +08:00

430 lines
15 KiB
JavaScript

(function () {
"use strict";
const theme = window.NEBULA_THEME || {};
const loader = document.getElementById('nebula-loader');
const appRoot = document.getElementById('app');
applyCustomBackground();
mountShell();
mountMonitor();
hideLoaderWhenReady();
function mountShell() {
if (!appRoot || document.getElementById("nebula-app-shell")) {
return;
}
const shell = document.createElement('div');
shell.className = 'app-shell nebula-app-shell';
shell.id = 'nebula-app-shell';
shell.innerHTML = renderShellChrome();
const parent = appRoot.parentNode;
parent.insertBefore(shell, appRoot);
var appStage = shell.querySelector(".nebula-app-stage");
if (appStage) {
appStage.appendChild(appRoot);
}
}
function mountMonitor() {
const rail = document.getElementById('nebula-side-rail');
const 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', (event) => {
const actionEl = event.target.closest('[data-nebula-action]');
if (!actionEl) {
return;
}
const 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 {
const 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');
}
const resJson = await response.json();
const data = resJson && resJson.data ? resJson.data : {};
console.log('[Nebula] Device overview data:', data); // Diagnostic log
const 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) {
const ips = Array.isArray(data.online_ips) ? data.online_ips : [];
const 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((ip) => {
return '<div class="nebula-monitor__chip"><code>' + escapeHtml(ip) + '</code></div>';
}).join('') : '<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((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 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() {
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
const 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() {
const candidates = [];
const 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 (let i = 0; i < candidates.length; i += 1) {
const 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;
}
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i += 1) {
const part = cookies[i].split('=');
if (part.length < 2) {
continue;
}
const 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') {
const 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') {
const keys = ['access_token', 'auth_data', 'token', 'Authorization', 'authorization'];
for (let j = 0; j < keys.length; j += 1) {
if (Object.prototype.hasOwnProperty.call(value, keys[j])) {
const directToken = extractToken(value[keys[j]], (depth || 0) + 1);
if (directToken) {
return directToken;
}
}
}
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const 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;");
}
})();