695 lines
24 KiB
JavaScript
695 lines
24 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var cfg = window.ADMIN_APP_CONFIG || {};
|
|
var api = cfg.api || {};
|
|
var root = document.getElementById("admin-app");
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var state = {
|
|
token: readToken(),
|
|
user: null,
|
|
route: normalizeRoute(readRoute()),
|
|
message: "",
|
|
messageType: "",
|
|
config: null,
|
|
system: null,
|
|
plugins: [],
|
|
integration: {},
|
|
realname: null,
|
|
devices: null,
|
|
busy: false
|
|
};
|
|
|
|
boot();
|
|
|
|
async function boot() {
|
|
window.addEventListener("hashchange", function () {
|
|
state.route = normalizeRoute(readRoute());
|
|
render();
|
|
hydrateRoute();
|
|
});
|
|
|
|
root.addEventListener("click", onClick);
|
|
root.addEventListener("submit", onSubmit);
|
|
|
|
if (state.token) {
|
|
await loadBootstrap();
|
|
}
|
|
|
|
render();
|
|
hydrateRoute();
|
|
}
|
|
|
|
async function loadBootstrap() {
|
|
try {
|
|
state.busy = true;
|
|
var loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" }));
|
|
if (!loginCheck || !loginCheck.is_login || !loginCheck.is_admin) {
|
|
clearSession();
|
|
return;
|
|
}
|
|
|
|
state.user = loginCheck;
|
|
|
|
var results = await Promise.all([
|
|
request(api.adminConfig, { method: "GET" }),
|
|
request(api.systemStatus, { method: "GET" }),
|
|
request(api.plugins, { method: "GET" }),
|
|
request(api.integration, { method: "GET" })
|
|
]);
|
|
|
|
state.config = unwrap(results[0]) || {};
|
|
state.system = unwrap(results[1]) || {};
|
|
state.plugins = toArray(unwrap(results[2]));
|
|
state.integration = unwrap(results[3]) || {};
|
|
} catch (error) {
|
|
clearSession();
|
|
show(error.message || "Failed to load admin data.", "error");
|
|
} finally {
|
|
state.busy = false;
|
|
}
|
|
}
|
|
|
|
async function hydrateRoute() {
|
|
if (!state.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (state.route === "realname") {
|
|
state.realname = unwrap(await request(api.realnameBase + "/records?per_page=50", { method: "GET" })) || {};
|
|
render();
|
|
return;
|
|
}
|
|
|
|
if (state.route === "user-online-devices") {
|
|
state.devices = unwrap(await request(api.onlineDevices + "?per_page=50", { method: "GET" })) || {};
|
|
render();
|
|
}
|
|
} catch (error) {
|
|
show(error.message || "Failed to load page data.", "error");
|
|
render();
|
|
}
|
|
}
|
|
|
|
function onClick(event) {
|
|
var actionEl = event.target.closest("[data-action]");
|
|
if (!actionEl) {
|
|
return;
|
|
}
|
|
|
|
var action = actionEl.getAttribute("data-action");
|
|
|
|
if (action === "logout") {
|
|
clearSession();
|
|
render();
|
|
return;
|
|
}
|
|
|
|
if (action === "nav") {
|
|
window.location.hash = actionEl.getAttribute("data-route") || "overview";
|
|
return;
|
|
}
|
|
|
|
if (action === "refresh") {
|
|
refreshAll();
|
|
return;
|
|
}
|
|
|
|
if (action === "approve-all") {
|
|
adminPost(api.realnameBase + "/approve-all", {}, "Approved all pending records.").then(hydrateRoute);
|
|
return;
|
|
}
|
|
|
|
if (action === "sync-all") {
|
|
adminPost(api.realnameBase + "/sync-all", {}, "Triggered real-name sync.").then(hydrateRoute);
|
|
return;
|
|
}
|
|
|
|
if (action === "clear-cache") {
|
|
adminPost(api.realnameBase + "/clear-cache", {}, "Cleared plugin cache.");
|
|
return;
|
|
}
|
|
|
|
if (action === "review") {
|
|
var userId = actionEl.getAttribute("data-user-id");
|
|
var status = actionEl.getAttribute("data-status") || "approved";
|
|
var reason = "";
|
|
if (status === "rejected") {
|
|
reason = window.prompt("Reject reason", "") || "";
|
|
}
|
|
|
|
adminPost(
|
|
api.realnameBase + "/review/" + encodeURIComponent(userId),
|
|
{ status: status, reason: reason },
|
|
"Updated review result."
|
|
).then(hydrateRoute);
|
|
return;
|
|
}
|
|
|
|
if (action === "reset-record") {
|
|
var resetUserId = actionEl.getAttribute("data-user-id");
|
|
adminPost(
|
|
api.realnameBase + "/reset/" + encodeURIComponent(resetUserId),
|
|
{},
|
|
"Reset the verification record."
|
|
).then(hydrateRoute);
|
|
}
|
|
}
|
|
|
|
function onSubmit(event) {
|
|
var form = event.target;
|
|
if (form.getAttribute("data-form") !== "login") {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
var formData = serializeForm(form);
|
|
|
|
request("/api/v1/passport/auth/login", {
|
|
method: "POST",
|
|
auth: false,
|
|
body: formData
|
|
}).then(function (response) {
|
|
var payload = unwrap(response);
|
|
if (!payload || !payload.auth_data || !payload.is_admin) {
|
|
throw new Error("This account does not have admin access.");
|
|
}
|
|
|
|
saveToken(payload.auth_data);
|
|
state.token = readToken();
|
|
return loadBootstrap();
|
|
}).then(function () {
|
|
show("Admin login successful.", "success");
|
|
render();
|
|
hydrateRoute();
|
|
}).catch(function (error) {
|
|
show(error.message || "Login failed.", "error");
|
|
render();
|
|
});
|
|
}
|
|
|
|
function refreshAll() {
|
|
state.realname = null;
|
|
state.devices = null;
|
|
state.busy = true;
|
|
render();
|
|
loadBootstrap().then(function () {
|
|
render();
|
|
return hydrateRoute();
|
|
});
|
|
}
|
|
|
|
function clearSession() {
|
|
clearToken();
|
|
state.user = null;
|
|
state.config = null;
|
|
state.system = null;
|
|
state.plugins = [];
|
|
state.integration = {};
|
|
state.realname = null;
|
|
state.devices = null;
|
|
state.route = "overview";
|
|
state.busy = false;
|
|
}
|
|
|
|
function render() {
|
|
root.innerHTML = state.user ? renderDashboard() : renderLogin();
|
|
}
|
|
|
|
function renderLogin() {
|
|
return [
|
|
'<div class="login-shell">',
|
|
'<section class="login-card stack">',
|
|
'<div><h1 style="margin:0 0 8px;">' + escapeHtml(cfg.title || "Admin") + '</h1><p class="hint">Use an administrator account to sign in and open the rebuilt backend workspace.</p></div>',
|
|
state.message ? renderNotice() : "",
|
|
'<form data-form="login">',
|
|
'<div class="field"><label>Email</label><input type="email" name="email" autocomplete="username" required /></div>',
|
|
'<div class="field"><label>Password</label><input type="password" name="password" autocomplete="current-password" required /></div>',
|
|
'<button class="btn btn-primary" type="submit">Sign In</button>',
|
|
'</form>',
|
|
'<div class="hint">The backend now uses a single Go-rendered admin shell with integrated plugin pages.</div>',
|
|
'</section>',
|
|
'</div>'
|
|
].join("");
|
|
}
|
|
|
|
function renderDashboard() {
|
|
return [
|
|
'<div class="admin-shell">',
|
|
renderSidebar(),
|
|
'<main class="admin-main">',
|
|
renderTopbar(),
|
|
'<div class="page-shell">',
|
|
state.message ? renderNotice() : "",
|
|
renderMainContent(),
|
|
'</div>',
|
|
'</main>',
|
|
'</div>'
|
|
].join("");
|
|
}
|
|
|
|
function renderSidebar() {
|
|
var items = [
|
|
navItem("overview", "Overview", "System status, plugin visibility, and entry summary"),
|
|
navItem("realname", "Real Name", "Review and operate the verification workflow"),
|
|
navItem("user-online-devices", "Online Devices", "Inspect user sessions and online device data"),
|
|
navItem("ipv6-subscription", "IPv6 Subscription", "Check the IPv6 shadow-account integration state"),
|
|
navItem("plugin-status", "Plugin Status", "Compare current backend integrations by module")
|
|
];
|
|
|
|
return [
|
|
'<aside class="admin-sidebar">',
|
|
'<div class="sidebar-brand"><span class="sidebar-brand-mark">Console</span><strong>' + escapeHtml(cfg.title || "Admin") + '</strong><span>Secure Path: /' + escapeHtml(getSecurePath()) + '</span></div>',
|
|
'<div class="sidebar-nav-label">Workspace</div>',
|
|
'<nav class="sidebar-nav">' + items.join("") + '</nav>',
|
|
'<div class="sidebar-meta"><span class="sidebar-meta-label">Current View</span><strong>' + escapeHtml(getRouteTitle(state.route)) + '</strong></div>',
|
|
'<div class="sidebar-footer">The layout keeps the classic backend pattern: dark sidebar, top workspace bar, and a card-based content area.</div>',
|
|
'</aside>'
|
|
].join("");
|
|
}
|
|
|
|
function renderTopbar() {
|
|
return [
|
|
'<header class="topbar">',
|
|
'<div class="topbar-copy">',
|
|
'<span class="topbar-eyebrow">Admin Workspace</span>',
|
|
'<h1>' + escapeHtml(getRouteTitle(state.route)) + '</h1>',
|
|
'<p>' + escapeHtml(getRouteDescription(state.route)) + '</p>',
|
|
'<div class="topbar-meta">',
|
|
'<span class="topbar-chip">Secure Path /' + escapeHtml(getSecurePath()) + '</span>',
|
|
'<span class="topbar-chip">Server Time ' + escapeHtml(formatDate(state.system && state.system.server_time)) + '</span>',
|
|
state.busy ? '<span class="topbar-chip">Refreshing</span>' : "",
|
|
'</div>',
|
|
'</div>',
|
|
'<div class="toolbar"><button class="btn btn-secondary" data-action="refresh">Refresh</button><button class="btn btn-ghost" data-action="logout">Logout</button></div>',
|
|
'</header>'
|
|
].join("");
|
|
}
|
|
|
|
function renderMainContent() {
|
|
if (state.route === "realname") {
|
|
return renderRealName();
|
|
}
|
|
if (state.route === "user-online-devices") {
|
|
return renderOnlineDevices();
|
|
}
|
|
if (state.route === "ipv6-subscription") {
|
|
return renderIPv6Integration();
|
|
}
|
|
if (state.route === "plugin-status") {
|
|
return renderPluginStatus();
|
|
}
|
|
return renderOverview();
|
|
}
|
|
|
|
function renderOverview() {
|
|
var enabledCount = countEnabledPlugins();
|
|
return [
|
|
'<section class="page-hero card">',
|
|
'<div class="page-hero-copy">',
|
|
'<span class="page-hero-label">Overview</span>',
|
|
'<h2>Classic backend structure, rebuilt with the current Go APIs.</h2>',
|
|
'<p>The page keeps the familiar admin navigation pattern while exposing plugin data, system state, and backend integration results in one place.</p>',
|
|
'</div>',
|
|
'<div class="page-hero-side">',
|
|
'<div class="hero-metric"><span>Enabled Plugins</span><strong>' + escapeHtml(String(enabledCount)) + '</strong></div>',
|
|
'<div class="hero-metric"><span>Secure Path</span><strong>/' + escapeHtml(getSecurePath()) + '</strong></div>',
|
|
'</div>',
|
|
'</section>',
|
|
'<section class="grid grid-3">',
|
|
statCard("Server Time", formatDate(state.system && state.system.server_time), "Source: /system/getSystemStatus"),
|
|
statCard("Admin Path", "/" + getSecurePath(), "Synced from current backend settings"),
|
|
statCard("Plugin Count", String((state.plugins || []).length), "Read from the integrated plugin list"),
|
|
'</section>',
|
|
'<section class="card page-section"><div class="section-headline"><div><span class="section-kicker">Plugins</span><h2>Integrated plugin list</h2></div><p class="section-copy">Each entry reflects the current Go backend response and the copied integration status.</p></div><div class="table-wrap">' + renderPluginsTable() + '</div></section>'
|
|
].join("");
|
|
}
|
|
|
|
function renderRealName() {
|
|
var rows = toArray(state.realname, "data");
|
|
var loading = state.realname === null;
|
|
return [
|
|
'<section class="card stack page-section">',
|
|
'<div class="section-headline"><div><span class="section-kicker">Workflow</span><h2>Real-name verification</h2></div><p class="section-copy">Batch actions stay in the toolbar while the review table keeps the classic admin operating rhythm.</p></div>',
|
|
'<div class="toolbar"><button class="btn btn-primary" data-action="approve-all">Approve All</button><button class="btn btn-secondary" data-action="sync-all">Sync All</button><button class="btn btn-ghost" data-action="clear-cache">Clear Cache</button></div>',
|
|
'<div class="table-wrap"><table><thead><tr><th>ID</th><th>Email</th><th>Status</th><th>Real Name</th><th>ID Number</th><th>Submitted At</th><th>Actions</th></tr></thead><tbody>',
|
|
loading ? '<tr><td colspan="7">Loading verification records...</td></tr>' : rows.length ? rows.map(function (row) {
|
|
return [
|
|
'<tr>',
|
|
'<td>' + escapeHtml(String(row.id || "")) + '</td>',
|
|
'<td>' + escapeHtml(row.email || "-") + '</td>',
|
|
'<td>' + renderStatus(row.status) + '</td>',
|
|
'<td>' + escapeHtml(row.real_name || "-") + '</td>',
|
|
'<td>' + escapeHtml(row.identity_no_masked || "-") + '</td>',
|
|
'<td>' + escapeHtml(formatDate(row.submitted_at)) + '</td>',
|
|
'<td><div class="row-actions"><button class="btn btn-secondary" data-action="review" data-user-id="' + escapeHtml(String(row.id || "")) + '" data-status="approved">Approve</button><button class="btn btn-ghost" data-action="review" data-user-id="' + escapeHtml(String(row.id || "")) + '" data-status="rejected">Reject</button><button class="btn btn-ghost" data-action="reset-record" data-user-id="' + escapeHtml(String(row.id || "")) + '">Reset</button></div></td>',
|
|
'</tr>'
|
|
].join("");
|
|
}).join("") : '<tr><td colspan="7">No verification records available.</td></tr>',
|
|
'</tbody></table></div>',
|
|
'</section>'
|
|
].join("");
|
|
}
|
|
|
|
function renderOnlineDevices() {
|
|
var rows = toArray(state.devices, "list");
|
|
var loading = state.devices === null;
|
|
return [
|
|
'<section class="card stack page-section">',
|
|
'<div class="section-headline"><div><span class="section-kicker">Monitoring</span><h2>Online devices</h2></div><p class="section-copy">This table keeps the admin-friendly scan pattern for sessions, subscription names, IPs, and recent activity.</p></div>',
|
|
'<div class="table-wrap"><table><thead><tr><th>User</th><th>Subscription</th><th>Online Count</th><th>Online IPs</th><th>Last Seen</th><th>Created At</th></tr></thead><tbody>',
|
|
loading ? '<tr><td colspan="6">Loading device records...</td></tr>' : rows.length ? rows.map(function (row) {
|
|
return [
|
|
'<tr>',
|
|
'<td>' + escapeHtml(row.email || "-") + '</td>',
|
|
'<td>' + escapeHtml(row.subscription_name || "-") + '</td>',
|
|
'<td>' + escapeHtml(String(row.online_count || 0)) + '</td>',
|
|
'<td>' + escapeHtml(formatDeviceList(row.online_devices)) + '</td>',
|
|
'<td>' + escapeHtml(row.last_online_text || "-") + '</td>',
|
|
'<td>' + escapeHtml(row.created_text || "-") + '</td>',
|
|
'</tr>'
|
|
].join("");
|
|
}).join("") : '<tr><td colspan="6">No online device records available.</td></tr>',
|
|
'</tbody></table></div>',
|
|
'</section>'
|
|
].join("");
|
|
}
|
|
|
|
function renderIPv6Integration() {
|
|
var integration = (state.integration && state.integration.user_add_ipv6_subscription) || {};
|
|
return [
|
|
'<section class="card stack page-section">',
|
|
'<div class="section-headline"><div><span class="section-kicker">Integration</span><h2>IPv6 shadow subscription</h2></div><p class="section-copy">' + escapeHtml(buildSummary(integration, "This panel shows the current runtime status of the IPv6 shadow-account integration.")) + '</p></div>',
|
|
'<div class="status-strip">' + renderStatus(integration.status || "unknown") + '</div>',
|
|
'<pre class="code-panel">' + escapeHtml(stringifyJSON(integration)) + '</pre>',
|
|
'</section>'
|
|
].join("");
|
|
}
|
|
|
|
function renderPluginStatus() {
|
|
var cards = [
|
|
sectionCard("Real-name verification", state.integration && state.integration.real_name_verification),
|
|
sectionCard("Online devices", state.integration && state.integration.user_online_devices),
|
|
sectionCard("IPv6 shadow subscription", state.integration && state.integration.user_add_ipv6_subscription)
|
|
];
|
|
|
|
return '<section class="grid plugin-grid">' + cards.join("") + '</section>';
|
|
}
|
|
|
|
function renderPluginsTable() {
|
|
var rows = toArray(state.plugins);
|
|
return '<table><thead><tr><th>ID</th><th>Code</th><th>Status</th><th>Config</th></tr></thead><tbody>' + (rows.length ? rows.map(function (row) {
|
|
return '<tr><td>' + escapeHtml(String(row.id || "")) + '</td><td>' + escapeHtml(row.code || row.name || "-") + '</td><td>' + renderStatus(row.is_enabled ? "enabled" : "disabled") + '</td><td><pre class="table-code">' + escapeHtml(formatPluginConfig(row.config)) + '</pre></td></tr>';
|
|
}).join("") : '<tr><td colspan="4">No plugin data available.</td></tr>') + '</tbody></table>';
|
|
}
|
|
|
|
function sectionCard(title, data) {
|
|
data = data || {};
|
|
return [
|
|
'<article class="card stack plugin-card">',
|
|
'<div class="section-headline"><div><span class="section-kicker">Module</span><h2>' + escapeHtml(title) + '</h2></div></div>',
|
|
'<p class="section-copy">' + escapeHtml(buildSummary(data, "No summary has been returned by the backend for this module.")) + '</p>',
|
|
renderStatus(data.status || "unknown"),
|
|
'<pre class="code-panel">' + escapeHtml(stringifyJSON(data)) + '</pre>',
|
|
'</article>'
|
|
].join("");
|
|
}
|
|
|
|
function navItem(route, title, desc) {
|
|
return '<a class="sidebar-item ' + (state.route === route ? "active" : "") + '" data-action="nav" data-route="' + escapeHtml(route) + '" href="#' + escapeHtml(route) + '"><strong>' + escapeHtml(title) + '</strong><span>' + escapeHtml(desc) + '</span></a>';
|
|
}
|
|
|
|
function statCard(title, value, hint) {
|
|
return '<article class="card stat stat-card"><span class="hint">' + escapeHtml(title) + '</span><strong>' + escapeHtml(value || "-") + '</strong><p>' + escapeHtml(hint || "") + '</p></article>';
|
|
}
|
|
|
|
function renderNotice() {
|
|
return '<div class="notice ' + escapeHtml(state.messageType || "") + '">' + escapeHtml(state.message || "") + '</div>';
|
|
}
|
|
|
|
function countEnabledPlugins() {
|
|
return toArray(state.plugins).filter(function (item) {
|
|
return !!item.is_enabled;
|
|
}).length;
|
|
}
|
|
|
|
function buildSummary(data, fallback) {
|
|
if (!data) {
|
|
return fallback;
|
|
}
|
|
if (typeof data.summary === "string" && data.summary.trim()) {
|
|
return data.summary.trim();
|
|
}
|
|
if (typeof data.message === "string" && data.message.trim()) {
|
|
return data.message.trim();
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function formatPluginConfig(value) {
|
|
if (typeof value === "string" && value.trim()) {
|
|
return value;
|
|
}
|
|
return stringifyJSON(value || {});
|
|
}
|
|
|
|
function stringifyJSON(value) {
|
|
try {
|
|
return JSON.stringify(value == null ? {} : value, null, 2);
|
|
} catch (error) {
|
|
return String(value == null ? "" : value);
|
|
}
|
|
}
|
|
|
|
function formatDeviceList(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.join(", ") || "-";
|
|
}
|
|
if (typeof value === "string" && value.trim()) {
|
|
return value;
|
|
}
|
|
return "-";
|
|
}
|
|
|
|
function toArray(value, preferredKey) {
|
|
if (Array.isArray(value)) {
|
|
return value;
|
|
}
|
|
if (!value || typeof value !== "object") {
|
|
return [];
|
|
}
|
|
if (preferredKey && Array.isArray(value[preferredKey])) {
|
|
return value[preferredKey];
|
|
}
|
|
if (Array.isArray(value.data)) {
|
|
return value.data;
|
|
}
|
|
if (Array.isArray(value.list)) {
|
|
return value.list;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function request(url, options) {
|
|
options = options || {};
|
|
|
|
var headers = {
|
|
"Content-Type": "application/json"
|
|
};
|
|
|
|
if (options.auth !== false && state.token) {
|
|
headers.Authorization = state.token;
|
|
}
|
|
|
|
return fetch(url, {
|
|
method: options.method || "GET",
|
|
headers: headers,
|
|
credentials: "same-origin",
|
|
body: options.body ? JSON.stringify(options.body) : undefined
|
|
}).then(async function (response) {
|
|
var payload = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch (error) {
|
|
payload = null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
clearSession();
|
|
render();
|
|
}
|
|
throw new Error(getErrorMessage(payload) || "Request failed.");
|
|
}
|
|
|
|
return payload;
|
|
});
|
|
}
|
|
|
|
function adminPost(url, body, successMessage) {
|
|
return request(url, {
|
|
method: "POST",
|
|
body: body || {}
|
|
}).then(function (payload) {
|
|
show(successMessage || "Operation completed.", "success");
|
|
render();
|
|
return payload;
|
|
}).catch(function (error) {
|
|
show(error.message || "Operation failed.", "error");
|
|
render();
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
function unwrap(payload) {
|
|
if (!payload) {
|
|
return null;
|
|
}
|
|
return typeof payload.data !== "undefined" ? payload.data : payload;
|
|
}
|
|
|
|
function getErrorMessage(payload) {
|
|
if (!payload) {
|
|
return "";
|
|
}
|
|
return payload.message || payload.msg || payload.error || "";
|
|
}
|
|
|
|
function saveToken(token) {
|
|
var normalized = /^Bearer /.test(token) ? token : "Bearer " + token;
|
|
window.localStorage.setItem("__gopanel_admin_auth__", normalized);
|
|
}
|
|
|
|
function serializeForm(form) {
|
|
var result = {};
|
|
var formData = new FormData(form);
|
|
formData.forEach(function (value, key) {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function readToken() {
|
|
var token = window.localStorage.getItem("__gopanel_admin_auth__") ||
|
|
window.localStorage.getItem("__nebula_auth_data__") ||
|
|
window.localStorage.getItem("auth_data") ||
|
|
"";
|
|
|
|
if (!token) {
|
|
return "";
|
|
}
|
|
|
|
return /^Bearer /.test(token) ? token : "Bearer " + token;
|
|
}
|
|
|
|
function clearToken() {
|
|
window.localStorage.removeItem("__gopanel_admin_auth__");
|
|
state.token = "";
|
|
}
|
|
|
|
function readRoute() {
|
|
return (window.location.hash || "#overview").slice(1) || "overview";
|
|
}
|
|
|
|
function normalizeRoute(route) {
|
|
var allowed = {
|
|
overview: true,
|
|
realname: true,
|
|
"user-online-devices": true,
|
|
"ipv6-subscription": true,
|
|
"plugin-status": true
|
|
};
|
|
|
|
return allowed[route] ? route : "overview";
|
|
}
|
|
|
|
function getSecurePath() {
|
|
return (state.config && state.config.secure_path) || cfg.securePath || "admin";
|
|
}
|
|
|
|
function getRouteTitle(route) {
|
|
if (route === "realname") {
|
|
return "Real-name Verification";
|
|
}
|
|
if (route === "user-online-devices") {
|
|
return "Online Devices";
|
|
}
|
|
if (route === "ipv6-subscription") {
|
|
return "IPv6 Subscription";
|
|
}
|
|
if (route === "plugin-status") {
|
|
return "Plugin Status";
|
|
}
|
|
return "Overview";
|
|
}
|
|
|
|
function getRouteDescription(route) {
|
|
if (route === "realname") {
|
|
return "Review records, run batch actions, and keep the original backend workflow readable.";
|
|
}
|
|
if (route === "user-online-devices") {
|
|
return "Inspect active users, current devices, and recent online activity in one table.";
|
|
}
|
|
if (route === "ipv6-subscription") {
|
|
return "Check the replicated backend integration output for the IPv6 shadow-account module.";
|
|
}
|
|
if (route === "plugin-status") {
|
|
return "Compare plugin integration payloads and runtime summaries side by side.";
|
|
}
|
|
return "A familiar backend workspace with sidebar navigation, top control bar, and focused content cards.";
|
|
}
|
|
|
|
function show(message, type) {
|
|
state.message = message || "";
|
|
state.messageType = type || "";
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) {
|
|
return "-";
|
|
}
|
|
|
|
var numeric = Number(value);
|
|
if (!Number.isNaN(numeric) && numeric > 0) {
|
|
return new Date(numeric * 1000).toLocaleString();
|
|
}
|
|
|
|
var parsed = Date.parse(value);
|
|
if (!Number.isNaN(parsed)) {
|
|
return new Date(parsed).toLocaleString();
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
function renderStatus(status) {
|
|
var raw = String(status || "unknown");
|
|
var normalized = raw.toLowerCase();
|
|
var klass = "status-pill status-ok";
|
|
|
|
if (normalized.indexOf("warn") !== -1 || normalized.indexOf("pending") !== -1 || normalized.indexOf("runtime") !== -1) {
|
|
klass = "status-pill status-warn";
|
|
}
|
|
|
|
if (normalized.indexOf("disabled") !== -1 || normalized.indexOf("fail") !== -1 || normalized.indexOf("error") !== -1 || normalized.indexOf("reject") !== -1 || normalized.indexOf("unknown") !== -1) {
|
|
klass = "status-pill status-danger";
|
|
}
|
|
|
|
return '<span class="' + klass + '">' + escapeHtml(raw) + '</span>';
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value == null ? "" : value)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
})();
|