794 lines
27 KiB
PHP
794 lines
27 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Online IP Monitor</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f3f4f6;
|
|
--panel: #ffffff;
|
|
--line: #e5e7eb;
|
|
--text: #111827;
|
|
--muted: #6b7280;
|
|
--accent: #0f766e;
|
|
--accent-soft: rgba(15, 118, 110, 0.1);
|
|
--shadow: 0 18px 45px rgba(17, 24, 39, 0.08);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
color: var(--text);
|
|
background:
|
|
radial-gradient(circle at top right, rgba(15, 118, 110, 0.08), transparent 30%),
|
|
linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
|
}
|
|
|
|
.wrap {
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 28px 20px 40px;
|
|
}
|
|
|
|
.hero {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.eyebrow {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
h1 {
|
|
margin: 8px 0 10px;
|
|
font-size: clamp(30px, 4vw, 52px);
|
|
line-height: 1.05;
|
|
}
|
|
|
|
.subtitle {
|
|
max-width: 820px;
|
|
font-size: 15px;
|
|
line-height: 1.8;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.input,
|
|
.select,
|
|
.btn {
|
|
border-radius: 14px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.input,
|
|
.select {
|
|
padding: 12px 14px;
|
|
min-height: 46px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.input {
|
|
min-width: 260px;
|
|
flex: 1 1 280px;
|
|
}
|
|
|
|
.select {
|
|
min-width: 120px;
|
|
}
|
|
|
|
.btn {
|
|
min-height: 46px;
|
|
padding: 0 16px;
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.btn.primary {
|
|
color: #fff;
|
|
border-color: var(--accent);
|
|
background: linear-gradient(135deg, #0f766e, #115e59);
|
|
}
|
|
|
|
.btn.secondary {
|
|
color: var(--text);
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
.summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 14px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.card {
|
|
background: var(--panel);
|
|
border: 1px solid rgba(229, 231, 235, 0.92);
|
|
border-radius: 24px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stat {
|
|
padding: 22px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.stat-value {
|
|
margin-top: 10px;
|
|
font-size: 34px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.stat-note {
|
|
margin-top: 8px;
|
|
font-size: 14px;
|
|
color: var(--muted);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.panel {
|
|
padding: 12px;
|
|
}
|
|
|
|
.table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
th,
|
|
td {
|
|
padding: 14px 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--line);
|
|
vertical-align: top;
|
|
font-size: 14px;
|
|
}
|
|
|
|
th {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
tr:last-child td {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.count {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 42px;
|
|
padding: 6px 10px;
|
|
border-radius: 999px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
font-weight: 800;
|
|
}
|
|
|
|
.ips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.ip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 6px 10px;
|
|
border-radius: 999px;
|
|
background: #f3f4f6;
|
|
color: #374151;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.muted {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.error,
|
|
.empty {
|
|
padding: 16px 18px;
|
|
border-radius: 18px;
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.error {
|
|
margin-bottom: 14px;
|
|
background: rgba(185, 28, 28, 0.08);
|
|
border: 1px solid rgba(185, 28, 28, 0.14);
|
|
color: #991b1b;
|
|
}
|
|
|
|
.empty {
|
|
background: #f9fafb;
|
|
border: 1px dashed var(--line);
|
|
color: var(--muted);
|
|
margin: 10px;
|
|
}
|
|
|
|
.footer {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 14px 4px 2px;
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.summary {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrap">
|
|
<section class="hero">
|
|
<div class="eyebrow">Xboard Admin Monitor</div>
|
|
<h1>All Users Online IP Count</h1>
|
|
<div class="subtitle">
|
|
This page is intended for administrators. It shows the current online IP quantity of all users reported by Xboard nodes, and supports keyword search by user ID or email.
|
|
</div>
|
|
</section>
|
|
|
|
<div id="error-box" class="error" style="display:none;"></div>
|
|
|
|
<section class="summary">
|
|
<article class="card stat">
|
|
<div class="stat-label">Users On Current Page</div>
|
|
<div class="stat-value" id="page-users">0</div>
|
|
<div class="stat-note">How many user rows are currently loaded</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-label">Users With Online IP</div>
|
|
<div class="stat-value" id="online-users">0</div>
|
|
<div class="stat-note">Users on this page whose online IP count is greater than zero</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-label">Total Online IP Count</div>
|
|
<div class="stat-value" id="total-ips">0</div>
|
|
<div class="stat-note" id="refresh-note">Waiting for data</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="toolbar">
|
|
<input class="input" id="keyword-input" type="text" placeholder="Search by user ID or email">
|
|
<select class="select" id="page-size">
|
|
<option value="20">20 / page</option>
|
|
<option value="50">50 / page</option>
|
|
<option value="100">100 / page</option>
|
|
</select>
|
|
<button class="btn primary" id="search-btn" type="button">Search</button>
|
|
<button class="btn secondary" id="reload-btn" type="button">Reload</button>
|
|
<button class="btn secondary" id="home-btn" type="button">Back Admin</button>
|
|
</section>
|
|
|
|
<section class="card panel">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Email</th>
|
|
<th>Online IP Count</th>
|
|
<th>Online IP List</th>
|
|
<th>Last Online</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<div id="pagination-text">Page 1</div>
|
|
<div>
|
|
<button class="btn secondary" id="prev-btn" type="button">Prev</button>
|
|
<button class="btn secondary" id="next-btn" type="button">Next</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const API_ENDPOINT = @json($apiEndpoint);
|
|
const ADMIN_HOME_URL = @json($adminHomeUrl);
|
|
const REFRESH_MS = 30000;
|
|
const ADMIN_TOKEN_KEYS = [
|
|
'XBOARD_ACCESS_TOKEN',
|
|
'Xboard_access_token',
|
|
'Xboard_ACCESS_TOKEN',
|
|
'xboard_access_token'
|
|
];
|
|
const TOKEN_KEYS = [
|
|
'XBOARD_ACCESS_TOKEN',
|
|
'Xboard_access_token',
|
|
'Xboard_ACCESS_TOKEN',
|
|
'xboard_access_token',
|
|
'auth_data',
|
|
'AUTH_DATA',
|
|
'admin_auth_data',
|
|
'ADMIN_AUTH_DATA',
|
|
'VUE_NAIVE_ACCESS_TOKEN',
|
|
'Vue_Naive_access_token',
|
|
'access_token',
|
|
'AccessToken',
|
|
'ACCESS_TOKEN'
|
|
];
|
|
const state = {
|
|
current: 1,
|
|
pageSize: 20,
|
|
total: 0,
|
|
lastPage: 1,
|
|
keyword: ''
|
|
};
|
|
|
|
function isObject(value) {
|
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function normalizeToken(candidate) {
|
|
if (typeof candidate !== 'string') return '';
|
|
|
|
const trimmed = candidate.trim().replace(/^"|"$/g, '');
|
|
if (!trimmed) return '';
|
|
|
|
const raw = trimmed.replace(/^Bearer\s+/i, '').trim();
|
|
if (raw.length < 20 || /\s/.test(raw)) return '';
|
|
|
|
return 'Bearer ' + raw;
|
|
}
|
|
|
|
function normalizeExpiryTimestamp(value) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return 0;
|
|
}
|
|
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (numeric > 9999999999) {
|
|
return Math.floor(numeric / 1000);
|
|
}
|
|
|
|
return Math.floor(numeric);
|
|
}
|
|
|
|
function addCandidateToken(bucket, candidate, priority) {
|
|
const normalized = normalizeToken(candidate);
|
|
if (!normalized) return;
|
|
|
|
const existing = bucket.get(normalized);
|
|
if (!existing || priority > existing.priority) {
|
|
bucket.set(normalized, {
|
|
token: normalized,
|
|
priority: priority
|
|
});
|
|
}
|
|
}
|
|
|
|
function extractTokenFromParsedValue(input) {
|
|
if (!input) return '';
|
|
|
|
if (typeof input === 'string') {
|
|
return normalizeToken(input);
|
|
}
|
|
|
|
if (Array.isArray(input)) {
|
|
for (const item of input) {
|
|
const token = extractTokenFromParsedValue(item);
|
|
if (token) return token;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
if (!isObject(input)) {
|
|
return '';
|
|
}
|
|
|
|
if (typeof input.expired === 'boolean' && input.expired) {
|
|
return '';
|
|
}
|
|
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
const timeStoredAt = normalizeExpiryTimestamp(input.time || input.created_at || input.createdAt || input.stored_at || input.storedAt);
|
|
const absoluteExpiresAt = normalizeExpiryTimestamp(input.expires_at || input.expiresAt || input.expire_at || input.expireAt);
|
|
const relativeExpireSeconds = normalizeExpiryTimestamp(input.expire);
|
|
|
|
if (absoluteExpiresAt && absoluteExpiresAt < nowSeconds) {
|
|
return '';
|
|
}
|
|
|
|
if (timeStoredAt && relativeExpireSeconds && (timeStoredAt + relativeExpireSeconds) < nowSeconds) {
|
|
return '';
|
|
}
|
|
|
|
const directKeys = ['value', 'token', 'access_token', 'accessToken', 'data'];
|
|
for (const key of directKeys) {
|
|
const token = extractTokenFromParsedValue(input[key]);
|
|
if (token) return token;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function collectTokensFromUnknown(input, bucket, priority) {
|
|
if (!input) return;
|
|
|
|
if (typeof input === 'string') {
|
|
addCandidateToken(bucket, input, priority);
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(input)) {
|
|
input.forEach(function (item) {
|
|
collectTokensFromUnknown(item, bucket, priority - 1);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!isObject(input)) {
|
|
return;
|
|
}
|
|
|
|
if (input.is_admin === true || input.isAdmin === true) {
|
|
addCandidateToken(bucket, input.auth_data || input.authData || input.authorization || input.token, priority + 20);
|
|
}
|
|
|
|
const directKeys = ['auth_data', 'authData', 'authorization', 'token', 'access_token', 'accessToken', 'value', 'data'];
|
|
directKeys.forEach(function (key) {
|
|
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
|
collectTokensFromUnknown(input[key], bucket, priority - (key === 'data' ? 2 : 0));
|
|
}
|
|
});
|
|
|
|
Object.keys(input).forEach(function (key) {
|
|
if (directKeys.indexOf(key) !== -1) return;
|
|
collectTokensFromUnknown(input[key], bucket, priority - 3);
|
|
});
|
|
}
|
|
|
|
function parseStoredToken(rawValue) {
|
|
if (!rawValue || typeof rawValue !== 'string') return '';
|
|
|
|
const trimmed = rawValue.trim();
|
|
const direct = normalizeToken(trimmed);
|
|
if (direct) return direct;
|
|
|
|
if (trimmed[0] === '{' || trimmed[0] === '[') {
|
|
try {
|
|
return extractTokenFromParsedValue(JSON.parse(trimmed));
|
|
} catch (error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function collectStorageCandidates(storage, bucket) {
|
|
if (!storage) return;
|
|
|
|
for (const key of TOKEN_KEYS) {
|
|
addCandidateToken(bucket, storage.getItem(key), 100);
|
|
}
|
|
|
|
for (let index = 0; index < storage.length; index += 1) {
|
|
const key = storage.key(index);
|
|
if (!key) continue;
|
|
|
|
const rawValue = storage.getItem(key);
|
|
if (!rawValue) continue;
|
|
|
|
const normalizedKey = key.toLowerCase();
|
|
const priority = normalizedKey.indexOf('admin') !== -1
|
|
? 90
|
|
: normalizedKey.indexOf('auth') !== -1
|
|
? 80
|
|
: normalizedKey.indexOf('token') !== -1
|
|
? 70
|
|
: 20;
|
|
|
|
addCandidateToken(bucket, rawValue, priority);
|
|
|
|
const trimmed = rawValue.trim();
|
|
if (trimmed[0] === '{' || trimmed[0] === '[') {
|
|
try {
|
|
collectTokensFromUnknown(JSON.parse(trimmed), bucket, priority);
|
|
} catch (error) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAdminAuthorizationFromXboardStorage() {
|
|
const storages = [window.localStorage, window.sessionStorage];
|
|
|
|
for (const storage of storages) {
|
|
if (!storage) continue;
|
|
|
|
for (const key of ADMIN_TOKEN_KEYS) {
|
|
const token = parseStoredToken(storage.getItem(key));
|
|
if (token) {
|
|
return token;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function buildAuthorizationCandidates() {
|
|
const bucket = new Map();
|
|
collectStorageCandidates(window.localStorage, bucket);
|
|
collectStorageCandidates(window.sessionStorage, bucket);
|
|
|
|
return Array.from(bucket.values())
|
|
.sort(function (left, right) {
|
|
return right.priority - left.priority;
|
|
})
|
|
.map(function (item) {
|
|
return item.token;
|
|
});
|
|
}
|
|
|
|
async function pickAuthorization() {
|
|
const candidates = [];
|
|
const seen = new Set();
|
|
const adminAuthorization = getAdminAuthorizationFromXboardStorage();
|
|
|
|
if (adminAuthorization) {
|
|
candidates.push(adminAuthorization);
|
|
seen.add(adminAuthorization);
|
|
}
|
|
|
|
buildAuthorizationCandidates().forEach(function (authorization) {
|
|
if (!seen.has(authorization)) {
|
|
seen.add(authorization);
|
|
candidates.push(authorization);
|
|
}
|
|
});
|
|
|
|
for (const authorization of candidates) {
|
|
try {
|
|
const url = new URL(API_ENDPOINT, window.location.origin);
|
|
url.searchParams.set('current', '1');
|
|
url.searchParams.set('pageSize', '1');
|
|
|
|
const response = await fetch(url.toString(), {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Authorization': authorization
|
|
},
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
if (response.ok) {
|
|
return authorization;
|
|
}
|
|
} catch (error) {
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function formatTime(timestamp) {
|
|
if (!timestamp) return '-';
|
|
|
|
let date = null;
|
|
if (typeof timestamp === 'number') {
|
|
date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000);
|
|
} else if (typeof timestamp === 'string') {
|
|
const trimmed = timestamp.trim();
|
|
if (!trimmed) return '-';
|
|
if (/^\d+$/.test(trimmed)) {
|
|
const numeric = Number(trimmed);
|
|
date = new Date(numeric > 9999999999 ? numeric : numeric * 1000);
|
|
} else {
|
|
date = new Date(trimmed);
|
|
}
|
|
} else {
|
|
date = new Date(timestamp);
|
|
}
|
|
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
return '-';
|
|
}
|
|
|
|
const pad = function (value) { return String(value).padStart(2, '0'); };
|
|
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
|
}
|
|
|
|
function setError(message) {
|
|
const box = document.getElementById('error-box');
|
|
box.style.display = 'block';
|
|
box.textContent = message;
|
|
}
|
|
|
|
function clearError() {
|
|
const box = document.getElementById('error-box');
|
|
box.style.display = 'none';
|
|
box.textContent = '';
|
|
}
|
|
|
|
function resetTable() {
|
|
document.getElementById('table-body').innerHTML = '<tr><td colspan="6"><div class="empty">No data available.</div></td></tr>';
|
|
document.getElementById('page-users').textContent = '0';
|
|
document.getElementById('online-users').textContent = '0';
|
|
document.getElementById('total-ips').textContent = '0';
|
|
document.getElementById('refresh-note').textContent = 'Waiting for data';
|
|
document.getElementById('pagination-text').textContent = 'Page 1';
|
|
}
|
|
|
|
async function getJson(url, authorization) {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Authorization': authorization
|
|
},
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (!response.ok || !result || typeof result !== 'object') {
|
|
throw new Error('Request failed');
|
|
}
|
|
|
|
return result.data || {};
|
|
}
|
|
|
|
function renderData(payload) {
|
|
const list = Array.isArray(payload.list) ? payload.list : [];
|
|
const onlineUsers = list.filter(function (item) { return Number(item.online_count || 0) > 0; }).length;
|
|
const totalIps = list.reduce(function (sum, item) {
|
|
return sum + Number(item.online_count || 0);
|
|
}, 0);
|
|
|
|
document.getElementById('page-users').textContent = String(list.length);
|
|
document.getElementById('online-users').textContent = String(onlineUsers);
|
|
document.getElementById('total-ips').textContent = String(totalIps);
|
|
document.getElementById('refresh-note').textContent = 'Refresh every ' + ((payload.meta && payload.meta.refresh_interval_seconds) || 30) + 's';
|
|
|
|
const pagination = payload.pagination || {};
|
|
state.total = Number(pagination.total || 0);
|
|
state.lastPage = Number(pagination.lastPage || 1);
|
|
document.getElementById('pagination-text').textContent = 'Page ' + state.current + ' / ' + state.lastPage + ' , Total ' + state.total;
|
|
|
|
const body = document.getElementById('table-body');
|
|
body.innerHTML = '';
|
|
|
|
if (!list.length) {
|
|
body.innerHTML = '<tr><td colspan="6"><div class="empty">No users found for the current filter.</div></td></tr>';
|
|
return;
|
|
}
|
|
|
|
list.forEach(function (item) {
|
|
const row = document.createElement('tr');
|
|
const devices = Array.isArray(item.online_devices) ? item.online_devices : [];
|
|
const ipHtml = devices.length
|
|
? '<div class="ips">' + devices.map(function (device) {
|
|
return '<span class="ip">' + device.ip + '</span>';
|
|
}).join('') + '</div>'
|
|
: '<span class="muted">No online IP</span>';
|
|
|
|
row.innerHTML =
|
|
'<td>' + item.id + '</td>' +
|
|
'<td>' + item.email + '</td>' +
|
|
'<td><span class="count">' + item.online_count + '</span></td>' +
|
|
'<td>' + ipHtml + '</td>' +
|
|
'<td>' + formatTime(item.last_online_at) + '</td>' +
|
|
'<td>' + formatTime(item.created_at) + '</td>';
|
|
body.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function loadData() {
|
|
const authorization = await pickAuthorization();
|
|
if (!authorization) {
|
|
resetTable();
|
|
setError('No usable admin login token was found in browser storage. Please log in to the Xboard admin panel first, then refresh this page.');
|
|
return;
|
|
}
|
|
|
|
clearError();
|
|
|
|
const url = new URL(API_ENDPOINT, window.location.origin);
|
|
url.searchParams.set('current', String(state.current));
|
|
url.searchParams.set('pageSize', String(state.pageSize));
|
|
if (state.keyword) {
|
|
url.searchParams.set('keyword', state.keyword);
|
|
}
|
|
|
|
try {
|
|
const payload = await getJson(url.toString(), authorization);
|
|
renderData(payload);
|
|
} catch (error) {
|
|
resetTable();
|
|
setError('Failed to load admin user online IP data. Please confirm you are logged in as an administrator and the plugin is enabled.');
|
|
}
|
|
}
|
|
|
|
document.getElementById('search-btn').addEventListener('click', function () {
|
|
state.current = 1;
|
|
state.keyword = document.getElementById('keyword-input').value.trim();
|
|
state.pageSize = Number(document.getElementById('page-size').value || 20);
|
|
loadData();
|
|
});
|
|
|
|
document.getElementById('reload-btn').addEventListener('click', function () {
|
|
state.pageSize = Number(document.getElementById('page-size').value || 20);
|
|
loadData();
|
|
});
|
|
|
|
document.getElementById('home-btn').addEventListener('click', function () {
|
|
window.location.href = ADMIN_HOME_URL;
|
|
});
|
|
|
|
document.getElementById('prev-btn').addEventListener('click', function () {
|
|
if (state.current <= 1) return;
|
|
state.current -= 1;
|
|
loadData();
|
|
});
|
|
|
|
document.getElementById('next-btn').addEventListener('click', function () {
|
|
if (state.current >= state.lastPage) return;
|
|
state.current += 1;
|
|
loadData();
|
|
});
|
|
|
|
document.getElementById('keyword-input').addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter') {
|
|
document.getElementById('search-btn').click();
|
|
}
|
|
});
|
|
|
|
resetTable();
|
|
loadData();
|
|
window.setInterval(loadData, REFRESH_MS);
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|