/* =============================================================
ServerClass Admin — Dashboard Client
Socket.IO driven; no client-side polling.
============================================================= */
(() => {
const socket = io({ transports: ["websocket", "polling"] });
// State
let allUsers = [];
let lastStatus = null;
let lastTotal = 0;
// DOM refs
const $ = (id) => document.getElementById(id);
const connDot = $("connDot");
const connLabel = $("connLabel");
const lastSync = $("lastSync");
const errorBox = $("errorBox");
const statsGrid = $("statsGrid");
const serversC = $("serversContent");
const usersC = $("usersContent");
const userCount = $("userCount");
// -----------------------------------------------------------
// Helpers
// -----------------------------------------------------------
const escapeHtml = (s) => {
if (s === null || s === undefined) return "";
return String(s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
const fmtDate = (iso) => {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d)) return "—";
return d.toLocaleString(undefined, {
year: "numeric", month: "short", day: "2-digit",
hour: "2-digit", minute: "2-digit"
});
} catch { return "—"; }
};
const fmtTime = (ts) => {
const d = ts ? new Date(ts * 1000) : new Date();
return d.toLocaleTimeString(undefined, { hour12: false });
};
const setConn = (state, label) => {
connDot.dataset.state = state;
connLabel.textContent = label;
};
const pulse = (el) => {
if (!el) return;
el.classList.remove("pulse");
// force reflow to restart animation
void el.offsetWidth;
el.classList.add("pulse");
};
// -----------------------------------------------------------
// Render: errors
// -----------------------------------------------------------
const renderErrors = (errors) => {
if (!errors || Object.keys(errors).length === 0) {
errorBox.innerHTML = "";
return;
}
errorBox.innerHTML = Object.entries(errors).map(([k, v]) => `
${escapeHtml(k)}
${escapeHtml(v)}
`).join("");
};
// -----------------------------------------------------------
// Render: stats
// -----------------------------------------------------------
const renderStats = (status, total) => {
const cards = [];
cards.push(`
Total Users
${total ?? "—"}
Registered accounts
`);
if (status) {
const cap = status.total_servers * status.max_per_server;
const pct = cap > 0 ? Math.round((total / cap) * 100) : 0;
cards.push(`
Servers
${status.total_servers}
Max ${status.max_per_server} ea.
Reservations
${status.total_reservations}
Pending registrations
Capacity
${pct}%
${total} / ${cap}
`);
} else {
cards.push(`
Servers
—
Awaiting API key
Reservations
—
Awaiting API key
Capacity
—
Awaiting API key
`);
}
statsGrid.innerHTML = cards.join("");
pulse(statsGrid);
};
// -----------------------------------------------------------
// Render: servers
// -----------------------------------------------------------
const renderServers = (servers) => {
if (!servers || !servers.length) {
serversC.innerHTML = `No server data available.
`;
return;
}
const rows = servers.map((s) => {
const pct = s.max > 0 ? (s.effective / s.max) * 100 : 0;
const state = pct >= 100 ? "full" : pct >= 75 ? "warn" : "ok";
const statusBadge = s.available
? `Open`
: `Full`;
return `
| № ${escapeHtml(String(s.server_num).padStart(2, "0"))} |
${s.users} |
${s.reserved} |
${s.effective} / ${s.max}
|
${statusBadge} |
${escapeHtml(s.url || "—")} |
`;
}).join("");
serversC.innerHTML = `
| № |
Users |
Reserved |
Capacity |
Status |
Endpoint |
${rows}
`;
pulse(serversC);
};
// -----------------------------------------------------------
// Render: users
// -----------------------------------------------------------
const renderUsers = (users) => {
userCount.textContent = `${users.length} record${users.length === 1 ? "" : "s"}`;
if (!users.length) {
usersC.innerHTML = `No users match the current filter.
`;
return;
}
const rows = users.map((u, i) => {
const tokens = u.tokens_count || 0;
const tokensBadge = tokens > 0
? `${tokens} token${tokens > 1 ? "s" : ""}`
: `none`;
return `
| ${String(i + 1).padStart(3, "0")} |
${escapeHtml(u.username || "—")} |
${escapeHtml(u.telegram_id || "—")} |
Server ${escapeHtml(String(u.server_num ?? "?"))} |
${tokensBadge} |
${escapeHtml(fmtDate(u.created_at))} |
${escapeHtml(fmtDate(u.last_login))} |
`;
}).join("");
usersC.innerHTML = `
| # |
Username |
Telegram ID |
Server |
Tokens |
Created |
Last login |
${rows}
`;
pulse(usersC);
};
// -----------------------------------------------------------
// Filter
// -----------------------------------------------------------
window.filterUsers = () => {
const q = ($("userSearch").value || "").trim().toLowerCase();
if (!q) return renderUsers(allUsers);
const filtered = allUsers.filter((u) =>
(u.username || "").toLowerCase().includes(q) ||
(u.telegram_id || "").toLowerCase().includes(q) ||
String(u.server_num || "").includes(q)
);
renderUsers(filtered);
};
// -----------------------------------------------------------
// Form events
// -----------------------------------------------------------
$("authForm").addEventListener("submit", (e) => {
e.preventDefault();
const admin_secret = $("adminSecret").value.trim();
const api_key = $("apiKey").value.trim();
if (!admin_secret && !api_key) {
renderErrors({ auth: "Provide at least an Admin Secret or API Key." });
return;
}
setConn("warn", "AUTHENTICATING…");
socket.emit("authenticate", { admin_secret, api_key });
});
$("refreshBtn").addEventListener("click", () => {
setConn("warn", "REFRESHING…");
socket.emit("refresh");
});
// -----------------------------------------------------------
// Socket.IO lifecycle
// -----------------------------------------------------------
socket.on("connect", () => {
setConn("on", "LIVE");
});
socket.on("connected", (data) => {
if (data && data.poll_interval) {
const el = $("pollInterval");
if (el) el.textContent = data.poll_interval;
}
});
socket.on("disconnect", () => {
setConn("off", "DISCONNECTED");
});
socket.on("connect_error", () => {
setConn("off", "CONNECTION ERROR");
});
socket.on("error", (data) => {
renderErrors({ socket: (data && data.message) || "Unknown error" });
});
// -----------------------------------------------------------
// Main payload handler
// -----------------------------------------------------------
socket.on("data_update", (payload) => {
if (!payload) return;
if (payload.errors) renderErrors(payload.errors);
allUsers = payload.users || [];
lastTotal = payload.total ?? allUsers.length;
lastStatus = payload.status || null;
renderStats(lastStatus, lastTotal);
renderServers(lastStatus ? lastStatus.servers : null);
renderUsers(allUsers);
lastSync.textContent = fmtTime(payload.timestamp);
setConn("on", payload.source === "auto" ? "LIVE · AUTO" : "LIVE");
});
})();