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