Panel update

This commit is contained in:
2026-07-13 02:21:56 -03:00
parent 64b1fc5cb3
commit 635f190630
8 changed files with 398 additions and 125 deletions
+123 -57
View File
@@ -9,6 +9,14 @@ document.getElementById("xLoadInboundsBtn").addEventListener("click", () => load
document.getElementById("xLoadCfgBtn").addEventListener("click", loadXrayCfg);
document.getElementById("xSaveCfgBtn").addEventListener("click", saveXrayCfg);
document.getElementById("xLoadLogsBtn").addEventListener("click", loadXrayLogs);
document.getElementById("xrayOpenCreateBtn")?.addEventListener("click", () => navigateWorkspaceSection("xray", "create"));
document.getElementById("xCreateCancelBtn")?.addEventListener("click", () => setWorkspaceSection("xray", "users"));
document.getElementById("xCreateUUIDBtn")?.addEventListener("click", () => {
const field = document.getElementById("xCreateUUID");
if (field) field.value = genUUID();
});
document.getElementById("xCreateInbound")?.addEventListener("change", updateXrayCreatorInboundLabel);
document.getElementById("xCreateClientForm")?.addEventListener("submit", submitXrayClientCreator);
async function loadXrayStatus() {
@@ -160,6 +168,7 @@ async function copyText(text) {
function renderInbounds(inbounds, options = {}) {
const { silent = false, force = false } = options || {};
updateDashboardXray(inbounds);
syncXrayCreatorInbounds(inbounds);
const nextStructure = inboundStructure(inbounds);
if (silent && !force && nextStructure === lastInboundsStructure && patchRenderedInbounds(inbounds)) return;
@@ -176,8 +185,7 @@ function renderInbounds(inbounds, options = {}) {
}
inboundsContainer.innerHTML = "";
lastInboundsStructure = nextStructure;
inbounds.forEach((ib, inboundIndex) => {
const formKey = String(inboundIndex);
inbounds.forEach(ib => {
const section = document.createElement("div");
section.dataset.inboundTag = String(ib.tag || "");
section.dataset.inboundProtocol = String(ib.protocol || "");
@@ -199,39 +207,11 @@ function renderInbounds(inbounds, options = {}) {
const openButton = document.createElement("button");
openButton.className = "btn btn-sm";
openButton.type = "button";
openButton.textContent = t("+ Add Client");
openButton.addEventListener("click", () => openAddClient(ib.tag, formKey));
openButton.textContent = t("Create user");
openButton.addEventListener("click", () => openAddClient(ib.tag));
hdr.appendChild(openButton);
section.appendChild(hdr);
// Add client mini-form (hidden by default)
const addForm = document.createElement("div");
addForm.id = `add-form-${formKey}`;
addForm.className = "hidden";
addForm.style = "background:rgba(15,23,42,.9);border:1px solid var(--border);border-radius:8px;padding:10px;margin-bottom:8px;";
addForm.innerHTML = `
<div class="form-grid" style="grid-template-columns:1fr 1fr;">
<div class="field">
<label>UUID</label>
<div class="field-row">
<input id="newUUID-${formKey}" placeholder="auto-generate" style="border-radius:6px;"/>
<button class="btn btn-ghost btn-sm" id="genUUID-${formKey}" type="button">Gen</button>
</div>
</div>
<div class="field"><label>${t("Email / label")}</label><input id="newEmail-${formKey}" placeholder="user@example" style="border-radius:6px;"/></div>
<div class="field"><label>${t("Display Name")}</label><input id="newName-${formKey}" placeholder="e.g. Maykinho01" style="border-radius:6px;"/></div>
<div class="field"><label>${t("Expiry Date")}</label><input type="datetime-local" id="newExpiry-${formKey}" style="border-radius:6px;color-scheme:dark;"/></div>
<div class="field"><label>${t("Max Connections")} <span class="hint">${t("(0 = unlimited)")}</span></label><input type="number" min="0" id="newMaxConns-${formKey}" placeholder="0" style="border-radius:6px;"/></div>
</div>
<div class="form-actions" style="margin-top:6px;">
<button class="btn btn-sm" id="addClient-${formKey}" type="button">${t("Add")}</button>
<button class="btn btn-ghost btn-sm" id="cancelAddClient-${formKey}" type="button">${t("Cancel")}</button>
</div>`;
addForm.querySelector(`#genUUID-${formKey}`).addEventListener("click", () => { document.getElementById(`newUUID-${formKey}`).value = genUUID(); });
addForm.querySelector(`#addClient-${formKey}`).addEventListener("click", () => addClient(ib.tag, formKey));
addForm.querySelector(`#cancelAddClient-${formKey}`).addEventListener("click", () => addForm.classList.add("hidden"));
section.appendChild(addForm);
// Clients table
const tblWrap = document.createElement("div");
tblWrap.className = "tbl-wrap";
@@ -286,36 +266,122 @@ function renderInbounds(inbounds, options = {}) {
});
}
function openAddClient(tag, formKey = tag) {
const form = document.getElementById(`add-form-${formKey}`);
if (form) { form.classList.remove("hidden"); }
const uuidField = document.getElementById(`newUUID-${formKey}`);
if (uuidField && !uuidField.value) uuidField.value = genUUID();
let xrayCreatorInbounds = [];
let xrayCreatorInboundSignature = "";
function syncXrayCreatorInbounds(inbounds = []) {
const select = document.getElementById("xCreateInbound");
if (!select) return;
const previous = select.value;
const nextInbounds = (inbounds || []).filter(ib => ib?.tag).map(ib => ({
tag: String(ib.tag),
protocol: String(ib.protocol || "xray").toUpperCase(),
port: ib.port ?? "?",
}));
const nextSignature = JSON.stringify(nextInbounds);
xrayCreatorInbounds = nextInbounds;
if (nextSignature === xrayCreatorInboundSignature) {
updateXrayCreatorInboundLabel();
return;
}
xrayCreatorInboundSignature = nextSignature;
select.replaceChildren();
if (!xrayCreatorInbounds.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = t("No compatible inbound found");
select.appendChild(option);
select.disabled = true;
} else {
xrayCreatorInbounds.forEach(inbound => {
const option = document.createElement("option");
option.value = inbound.tag;
option.textContent = `${inbound.protocol} · ${inbound.tag} · :${inbound.port}`;
select.appendChild(option);
});
select.disabled = false;
select.value = xrayCreatorInbounds.some(inbound => inbound.tag === previous) ? previous : xrayCreatorInbounds[0].tag;
}
updateXrayCreatorInboundLabel();
}
async function addClient(tag, formKey = tag) {
const uuidEl = document.getElementById(`newUUID-${formKey}`);
const emailEl = document.getElementById(`newEmail-${formKey}`);
const nameEl = document.getElementById(`newName-${formKey}`);
const expiryEl = document.getElementById(`newExpiry-${formKey}`);
const maxConnsEl = document.getElementById(`newMaxConns-${formKey}`);
const uuid = (uuidEl?.value || "").trim();
const email = (emailEl?.value || "").trim();
const name = (nameEl?.value || "").trim();
const expiresAt = isoFromLocal(expiryEl?.value || "");
const maxConns = parseInt(maxConnsEl?.value || "0", 10) || 0;
if (!uuid) { xStatus.textContent = t("UUID required."); return; }
function updateXrayCreatorInboundLabel() {
const selected = document.getElementById("xCreateInbound")?.value || "";
const inbound = xrayCreatorInbounds.find(item => item.tag === selected);
const chip = document.getElementById("xCreateProtocolChip");
const hint = document.getElementById("xCreateInboundHint");
if (chip) chip.textContent = inbound ? inbound.protocol : t("No inbound");
if (hint) hint.textContent = inbound
? t("The client will be added to {tag} on port {port}.", {tag:inbound.tag, port:inbound.port})
: t("Create or enable a compatible inbound before adding a client.");
}
function prepareXrayClientCreator(preferredTag = "") {
const form = document.getElementById("xCreateClientForm");
form?.reset();
const inbound = document.getElementById("xCreateInbound");
if (inbound && preferredTag && xrayCreatorInbounds.some(item => item.tag === preferredTag)) inbound.value = preferredTag;
const uuid = document.getElementById("xCreateUUID");
if (uuid) uuid.value = genUUID();
const maxConns = document.getElementById("xCreateMaxConns");
if (maxConns) maxConns.value = "0";
const status = document.getElementById("xCreateClientStatus");
if (status) status.textContent = xrayCreatorInbounds.length ? t("Ready to create a new Xray client.") : t("Waiting for a compatible inbound.");
updateXrayCreatorInboundLabel();
requestAnimationFrame(() => document.getElementById("xCreateName")?.focus());
}
function openAddClient(tag) {
setWorkspaceSection("xray", "create");
prepareXrayClientCreator(tag);
}
async function submitXrayClientCreator(event) {
event?.preventDefault?.();
const tag = document.getElementById("xCreateInbound")?.value || "";
const uuid = (document.getElementById("xCreateUUID")?.value || "").trim();
const status = document.getElementById("xCreateClientStatus");
const button = document.getElementById("xCreateClientBtn");
if (!tag) { if (status) status.textContent = t("Select a compatible inbound."); return false; }
if (!uuid) { if (status) status.textContent = t("UUID required."); return false; }
const payload = {
inbound_tag: tag,
uuid,
email: (document.getElementById("xCreateEmail")?.value || "").trim(),
name: (document.getElementById("xCreateName")?.value || "").trim(),
expires_at: isoFromLocal(document.getElementById("xCreateExpiry")?.value || ""),
max_connections: parseInt(document.getElementById("xCreateMaxConns")?.value || "0", 10) || 0,
server_id: selectedXrayServer(),
};
if (button) button.disabled = true;
if (status) status.textContent = t("Creating Xray client…");
try {
const res = await api("/api/xray/clients/add", {
method: "POST",
body: JSON.stringify({ inbound_tag: tag, uuid, email, name, expires_at: expiresAt, max_connections: maxConns, server_id: selectedXrayServer() }),
});
const res = await api("/api/xray/clients/add", { method:"POST", body:JSON.stringify(payload) });
if (!res.ok) throw new Error(await res.text());
xStatus.textContent = t("Client {id}… added. Native mode hot-reloads without restart.", {id: uuid.slice(0,8)});
setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500);
const success = t("Client {id}… added. Native mode hot-reloads without restart.", {id:uuid.slice(0,8)});
if (status) status.textContent = success;
xStatus.textContent = success;
showPanelToast(t("Xray user created successfully."), "success", t("Xray user"));
setTimeout(() => { loadInbounds({ force:true }); if (currentRole === "reseller") loadMe(); }, 700);
const name = document.getElementById("xCreateName");
const email = document.getElementById("xCreateEmail");
const expiry = document.getElementById("xCreateExpiry");
if (name) name.value = "";
if (email) email.value = "";
if (expiry) expiry.value = "";
const nextUUID = document.getElementById("xCreateUUID");
if (nextUUID) nextUUID.value = genUUID();
return true;
} catch (e) {
if (e.message==="auth") doAuthError();
else xStatus.textContent = t("Error: {error}", {error: e.message});
if (e.message === "auth") doAuthError();
else {
if (status) status.textContent = t("Error: {error}", {error:e.message});
xStatus.textContent = t("Error: {error}", {error:e.message});
showPanelToast(t("Could not create the Xray user: {error}", {error:e.message}), "error", t("Xray user"));
}
return false;
} finally {
if (button) button.disabled = false;
}
}