430 lines
15 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
})();
|