bot TG
This commit is contained in:
@@ -9,6 +9,7 @@ const tabTitles = {
|
||||
stats: ["Server", "Monitoring"],
|
||||
vnstat: ["Traffic", "VnStat"],
|
||||
logs: ["System", "Logs"],
|
||||
bot: ["Vendas", "Bot / Telegram"],
|
||||
server: ["System", "Settings"],
|
||||
};
|
||||
function updatePageHeading() {
|
||||
@@ -39,6 +40,7 @@ function selectTab(tab) {
|
||||
if (tab === "servers-status" && currentRole === "superadmin") loadServersStatus();
|
||||
if (tab === "resellers" && currentRole === "superadmin") loadResellers();
|
||||
if (tab === "servers" && currentRole === "superadmin") loadServers();
|
||||
if (tab === "bot" && currentRole === "superadmin" && typeof loadBotTab === "function") loadBotTab();
|
||||
}
|
||||
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => btn.addEventListener("click", () => selectTab(btn.dataset.tab)));
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
// ─── Bot / Vendas (Telegram + Mercado Pago) ─────────────────────────────────
|
||||
// Superadmin-only tab. Uses the shared api() helper from 01-core.js.
|
||||
|
||||
function botEsc(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
.replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
function botStatus(id, msg, ok) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) { el.textContent = msg; el.style.color = ok === false ? "var(--danger,#e5484d)" : ""; }
|
||||
}
|
||||
function brl(cents) {
|
||||
return "R$ " + (Number(cents || 0) / 100).toFixed(2).replace(".", ",");
|
||||
}
|
||||
|
||||
async function loadBotTab() {
|
||||
loadBotConfig();
|
||||
loadBotInbounds();
|
||||
loadBotPlans();
|
||||
loadBotPkgs();
|
||||
loadBotUsers();
|
||||
loadBotTxns();
|
||||
loadBotSettings();
|
||||
}
|
||||
|
||||
// ─── Config ───
|
||||
async function loadBotConfig() {
|
||||
try {
|
||||
const res = await api("/api/bot/config");
|
||||
const c = await res.json();
|
||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.value = v ?? ""; };
|
||||
const chk = (id, v) => { const e = document.getElementById(id); if (e) e.checked = !!v; };
|
||||
chk("botEnabled", c.enabled);
|
||||
set("botTelegramMode", c.telegram_mode);
|
||||
set("botTelegramWebhookURL", c.telegram_webhook_url);
|
||||
set("botMPConfirmMode", c.mp_confirm_mode);
|
||||
set("botMPPollInterval", c.mp_poll_interval);
|
||||
set("botPixExp", c.pix_expiration_minutes);
|
||||
chk("botTrialEnabled", c.trial_enabled);
|
||||
set("botTrialHours", c.trial_hours);
|
||||
set("botTrialMaxConns", c.trial_max_connections);
|
||||
set("botTrialKind", c.trial_kind);
|
||||
set("botTrialInbound", c.trial_inbound_tag);
|
||||
set("botAdminIDs", (c.admin_telegram_ids || []).join(","));
|
||||
set("botPublicHost", c.public_host);
|
||||
set("botXrayPublicHost", c.xray_public_host);
|
||||
document.getElementById("botHasTgToken").textContent = c.has_telegram_token ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasTgSecret").textContent = c.has_telegram_webhook_secret ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasMpToken").textContent = c.has_mp_access_token ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasMpSecret").textContent = c.has_mp_webhook_secret ? "✓ configurado" : "não definido";
|
||||
botStatus("botConfigStatus", "Carregado.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
|
||||
async function saveBotConfig() {
|
||||
const val = id => (document.getElementById(id)?.value || "").trim();
|
||||
const num = id => parseInt(document.getElementById(id)?.value || "0", 10) || 0;
|
||||
const chk = id => !!document.getElementById(id)?.checked;
|
||||
const ids = val("botAdminIDs").split(",").map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
|
||||
const body = {
|
||||
enabled: chk("botEnabled"),
|
||||
telegram_token: val("botTelegramToken"),
|
||||
telegram_mode: val("botTelegramMode"),
|
||||
telegram_webhook_url: val("botTelegramWebhookURL"),
|
||||
telegram_webhook_secret: val("botTelegramWebhookSecret"),
|
||||
mp_access_token: val("botMPToken"),
|
||||
mp_confirm_mode: val("botMPConfirmMode"),
|
||||
mp_webhook_secret: val("botMPWebhookSecret"),
|
||||
mp_poll_interval: val("botMPPollInterval"),
|
||||
pix_expiration_minutes: num("botPixExp"),
|
||||
trial_enabled: chk("botTrialEnabled"),
|
||||
trial_hours: num("botTrialHours"),
|
||||
trial_max_connections: num("botTrialMaxConns"),
|
||||
trial_kind: val("botTrialKind"),
|
||||
trial_inbound_tag: val("botTrialInbound"),
|
||||
admin_telegram_ids: ids,
|
||||
public_host: val("botPublicHost"),
|
||||
xray_public_host: val("botXrayPublicHost"),
|
||||
};
|
||||
try {
|
||||
await api("/api/bot/config", { method: "POST", body: JSON.stringify(body) });
|
||||
["botTelegramToken", "botTelegramWebhookSecret", "botMPToken", "botMPWebhookSecret"].forEach(id => { const e = document.getElementById(id); if (e) e.value = ""; });
|
||||
botStatus("botConfigStatus", "Configuração salva e bot reiniciado.");
|
||||
loadBotConfig();
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao salvar.", false); }
|
||||
}
|
||||
|
||||
async function testBot() {
|
||||
botStatus("botConfigStatus", "Testando...");
|
||||
const body = {
|
||||
telegram_token: (document.getElementById("botTelegramToken")?.value || "").trim(),
|
||||
mp_access_token: (document.getElementById("botMPToken")?.value || "").trim(),
|
||||
};
|
||||
try {
|
||||
const res = await api("/api/bot/test", { method: "POST", body: JSON.stringify(body) });
|
||||
const r = await res.json();
|
||||
const tg = r.telegram_ok ? `Telegram OK (${r.telegram_bot || ""})` : `Telegram: ${r.telegram_error || "falha"}`;
|
||||
const mp = r.mp_ok ? "Mercado Pago OK" : `Mercado Pago: ${r.mp_error || "falha"}`;
|
||||
botStatus("botConfigStatus", tg + " · " + mp, r.telegram_ok && r.mp_ok);
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro no teste.", false); }
|
||||
}
|
||||
|
||||
// ─── Inbounds datalist ───
|
||||
async function loadBotInbounds() {
|
||||
try {
|
||||
const res = await api("/api/xray/inbounds");
|
||||
const list = await res.json();
|
||||
const dl = document.getElementById("botInboundList");
|
||||
if (dl) dl.innerHTML = (list || []).map(ib => `<option value="${botEsc(ib.tag)}">${botEsc(ib.protocol)}</option>`).join("");
|
||||
} catch (e) { /* xray may be off; ignore */ }
|
||||
}
|
||||
|
||||
// ─── Plans ───
|
||||
async function loadBotPlans() {
|
||||
try {
|
||||
const res = await api("/api/bot/plans");
|
||||
const plans = await res.json() || [];
|
||||
document.getElementById("botPlanCount").textContent = plans.length;
|
||||
document.getElementById("botPlansBody").innerHTML = plans.map(p => `
|
||||
<tr>
|
||||
<td>${botEsc(p.Name)}</td><td>${botEsc(p.Kind)}</td><td>${p.Days}</td>
|
||||
<td>${brl(p.PriceCents)}</td><td>${p.CreditCost}</td>
|
||||
<td>${p.IsActive ? "✅" : "—"}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botEditPlan(${JSON.stringify(p)})'>Editar</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botDeletePlan(${p.ID})'>Excluir</button>
|
||||
</td>
|
||||
</tr>`).join("");
|
||||
botStatus("botPlansStatus", "Ready.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botPlansStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
function botEditPlan(p) {
|
||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.value = v ?? ""; };
|
||||
set("planId", p.ID); set("planName", p.Name); set("planKind", p.Kind); set("planDays", p.Days);
|
||||
set("planMaxConns", p.MaxConnections); set("planUpMbps", p.LimitMbpsUp); set("planDownMbps", p.LimitMbpsDown);
|
||||
set("planInbound", p.XrayInboundTag); set("planProtocol", p.XrayProtocol);
|
||||
set("planPrice", (p.PriceCents / 100).toFixed(2)); set("planCreditCost", p.CreditCost);
|
||||
set("planServerId", p.ServerID); set("planSort", p.SortOrder);
|
||||
document.getElementById("planActive").checked = !!p.IsActive;
|
||||
document.getElementById("botPlanFormTitle").textContent = "Editar Plano #" + p.ID;
|
||||
}
|
||||
function botClearPlanForm() {
|
||||
document.getElementById("botPlanForm").reset();
|
||||
document.getElementById("planId").value = "";
|
||||
document.getElementById("botPlanFormTitle").textContent = "Novo Plano";
|
||||
}
|
||||
async function botSavePlan(ev) {
|
||||
ev.preventDefault();
|
||||
const val = id => document.getElementById(id).value;
|
||||
const num = id => parseInt(document.getElementById(id).value || "0", 10) || 0;
|
||||
const body = {
|
||||
id: num("planId"), name: val("planName"), kind: val("planKind"), days: num("planDays"),
|
||||
max_connections: num("planMaxConns"), limit_mbps_up: num("planUpMbps"), limit_mbps_down: num("planDownMbps"),
|
||||
xray_inbound_tag: val("planInbound"), xray_protocol: val("planProtocol"),
|
||||
price_cents: Math.round(parseFloat(val("planPrice") || "0") * 100),
|
||||
credit_cost: num("planCreditCost"), server_id: val("planServerId"), sort_order: num("planSort"),
|
||||
is_active: document.getElementById("planActive").checked,
|
||||
};
|
||||
// map to Go struct JSON tags (exported field names)
|
||||
const payload = {
|
||||
ID: body.id, Name: body.name, Kind: body.kind, Days: body.days, MaxConnections: body.max_connections,
|
||||
LimitMbpsUp: body.limit_mbps_up, LimitMbpsDown: body.limit_mbps_down, XrayInboundTag: body.xray_inbound_tag,
|
||||
XrayProtocol: body.xray_protocol, PriceCents: body.price_cents, CreditCost: body.credit_cost,
|
||||
ServerID: body.server_id, IsActive: body.is_active, SortOrder: body.sort_order,
|
||||
};
|
||||
try {
|
||||
await api("/api/bot/plans", { method: "POST", body: JSON.stringify(payload) });
|
||||
botClearPlanForm(); loadBotPlans(); botStatus("botPlansStatus", "Plano salvo.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botPlansStatus", "Erro ao salvar.", false); }
|
||||
}
|
||||
async function botDeletePlan(id) {
|
||||
if (!confirm("Excluir este plano?")) return;
|
||||
try { await api("/api/bot/plans?id=" + id, { method: "DELETE" }); loadBotPlans(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botPlansStatus", "Erro ao excluir.", false); }
|
||||
}
|
||||
|
||||
// ─── Credit packages ───
|
||||
async function loadBotPkgs() {
|
||||
try {
|
||||
const res = await api("/api/bot/credit-packages");
|
||||
const pkgs = await res.json() || [];
|
||||
document.getElementById("botPkgCount").textContent = pkgs.length;
|
||||
document.getElementById("botPkgsBody").innerHTML = pkgs.map(p => `
|
||||
<tr>
|
||||
<td>${botEsc(p.Name)}</td><td>${p.Credits}</td><td>${brl(p.PriceCents)}</td>
|
||||
<td>${p.IsActive ? "✅" : "—"}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botEditPkg(${JSON.stringify(p)})'>Editar</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botDeletePkg(${p.ID})'>Excluir</button>
|
||||
</td>
|
||||
</tr>`).join("");
|
||||
botStatus("botPkgStatus", "Ready.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botPkgStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
function botEditPkg(p) {
|
||||
const set = (id, v) => { document.getElementById(id).value = v ?? ""; };
|
||||
set("pkgId", p.ID); set("pkgName", p.Name); set("pkgCredits", p.Credits);
|
||||
set("pkgPrice", (p.PriceCents / 100).toFixed(2)); set("pkgSort", p.SortOrder);
|
||||
document.getElementById("pkgActive").checked = !!p.IsActive;
|
||||
}
|
||||
function botClearPkgForm() { document.getElementById("botPkgForm").reset(); document.getElementById("pkgId").value = ""; }
|
||||
async function botSavePkg(ev) {
|
||||
ev.preventDefault();
|
||||
const val = id => document.getElementById(id).value;
|
||||
const num = id => parseInt(document.getElementById(id).value || "0", 10) || 0;
|
||||
const payload = {
|
||||
ID: num("pkgId"), Name: val("pkgName"), Credits: num("pkgCredits"),
|
||||
PriceCents: Math.round(parseFloat(val("pkgPrice") || "0") * 100),
|
||||
SortOrder: num("pkgSort"), IsActive: document.getElementById("pkgActive").checked,
|
||||
};
|
||||
try { await api("/api/bot/credit-packages", { method: "POST", body: JSON.stringify(payload) }); botClearPkgForm(); loadBotPkgs(); botStatus("botPkgStatus", "Pacote salvo."); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botPkgStatus", "Erro ao salvar.", false); }
|
||||
}
|
||||
async function botDeletePkg(id) {
|
||||
if (!confirm("Excluir este pacote?")) return;
|
||||
try { await api("/api/bot/credit-packages?id=" + id, { method: "DELETE" }); loadBotPkgs(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botPkgStatus", "Erro ao excluir.", false); }
|
||||
}
|
||||
|
||||
// ─── Bot users ───
|
||||
async function loadBotUsers() {
|
||||
try {
|
||||
const res = await api("/api/bot/users");
|
||||
const users = await res.json() || [];
|
||||
document.getElementById("botUserCount").textContent = users.length;
|
||||
document.getElementById("botUsersBody").innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.TelegramID}</td><td>${botEsc(u.FirstName)}</td><td>${botEsc(u.Username)}</td>
|
||||
<td>${botEsc(u.Role)}</td><td>${botEsc(u.LinkedAdminUsername)}</td><td>${u.CreditBalance}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botPromote(${u.TelegramID})'>Papel</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botCredit(${u.TelegramID})'>Créditos</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick='botBlock(${u.TelegramID}, ${u.Role === "blocked"})'>${u.Role === "blocked" ? "Desbloq." : "Bloquear"}</button>
|
||||
</td>
|
||||
</tr>`).join("");
|
||||
botStatus("botUsersStatus", "Ready.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botUsersStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
async function botPromote(tid) {
|
||||
const role = prompt("Papel (customer / reseller / blocked):", "reseller");
|
||||
if (!role) return;
|
||||
let linked = "";
|
||||
if (role === "reseller") { linked = prompt("Username do revendedor (admin_users) vinculado:", "") || ""; }
|
||||
try { await api("/api/bot/users", { method: "POST", body: JSON.stringify({ telegram_id: tid, action: "set_role", role, linked_admin_username: linked }) }); loadBotUsers(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botUsersStatus", "Erro.", false); }
|
||||
}
|
||||
async function botCredit(tid) {
|
||||
const v = prompt("Ajuste de créditos (use negativo para remover):", "10");
|
||||
if (v === null) return;
|
||||
const n = parseInt(v, 10); if (isNaN(n)) return;
|
||||
try { await api("/api/bot/users", { method: "POST", body: JSON.stringify({ telegram_id: tid, action: "adjust_credits", credits: n }) }); loadBotUsers(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botUsersStatus", "Erro (saldo insuficiente?).", false); }
|
||||
}
|
||||
async function botBlock(tid, isBlocked) {
|
||||
try { await api("/api/bot/users", { method: "POST", body: JSON.stringify({ telegram_id: tid, action: isBlocked ? "unblock" : "block" }) }); loadBotUsers(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botUsersStatus", "Erro.", false); }
|
||||
}
|
||||
|
||||
// ─── Transactions ───
|
||||
async function loadBotTxns() {
|
||||
const filter = document.getElementById("botTxnFilter")?.value || "";
|
||||
try {
|
||||
const res = await api("/api/bot/transactions?limit=200&status=" + encodeURIComponent(filter));
|
||||
const txns = await res.json() || [];
|
||||
document.getElementById("botTxnCount").textContent = txns.length;
|
||||
document.getElementById("botTxnsBody").innerHTML = txns.map(t => `
|
||||
<tr>
|
||||
<td>${t.ID}</td><td>${t.TelegramID}</td><td>${botEsc(t.Type)}</td><td>${brl(t.AmountCents)}</td>
|
||||
<td>${botEsc(t.Status)}</td><td>${botEsc(t.TargetUsername)}</td>
|
||||
<td>${botEsc((t.CreatedAt || "").slice(0, 16).replace("T", " "))}</td>
|
||||
<td>
|
||||
${t.Status === "pending" || t.Status === "approved" ? `<button class="btn btn-ghost btn-sm" onclick='botReprocess(${t.ID})'>Reprocessar</button>` : ""}
|
||||
${t.Status !== "refunded" ? `<button class="btn btn-ghost btn-sm" onclick='botRefund(${t.ID})'>Estornar</button>` : ""}
|
||||
</td>
|
||||
</tr>`).join("");
|
||||
botStatus("botTxnStatus", "Ready.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botTxnStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
async function botReprocess(id) {
|
||||
try { await api("/api/bot/transactions", { method: "POST", body: JSON.stringify({ id, action: "reprocess" }) }); botStatus("botTxnStatus", "Reprocessando #" + id + "..."); setTimeout(loadBotTxns, 1500); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botTxnStatus", "Erro (bot ativo?).", false); }
|
||||
}
|
||||
async function botRefund(id) {
|
||||
if (!confirm("Marcar pagamento #" + id + " como estornado?")) return;
|
||||
try { await api("/api/bot/transactions", { method: "POST", body: JSON.stringify({ id, action: "refund" }) }); loadBotTxns(); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botTxnStatus", "Erro.", false); }
|
||||
}
|
||||
|
||||
// ─── Settings (messages) ───
|
||||
async function loadBotSettings() {
|
||||
try {
|
||||
const res = await api("/api/bot/settings");
|
||||
const s = await res.json() || {};
|
||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.value = v || ""; };
|
||||
set("setWelcome", s.welcome_text); set("setContact", s.contact_text);
|
||||
set("setAppText", s.app_text); set("setAppUrl", s.app_url);
|
||||
botStatus("botSettingsStatus", "Ready.");
|
||||
} catch (e) { if (e.message !== "auth") botStatus("botSettingsStatus", "Erro ao carregar.", false); }
|
||||
}
|
||||
async function saveBotSettings() {
|
||||
const val = id => document.getElementById(id)?.value || "";
|
||||
const payload = { welcome_text: val("setWelcome"), contact_text: val("setContact"), app_text: val("setAppText"), app_url: val("setAppUrl") };
|
||||
try { await api("/api/bot/settings", { method: "POST", body: JSON.stringify(payload) }); botStatus("botSettingsStatus", "Mensagens salvas."); }
|
||||
catch (e) { if (e.message !== "auth") botStatus("botSettingsStatus", "Erro ao salvar.", false); }
|
||||
}
|
||||
|
||||
// ─── Wiring ───
|
||||
document.getElementById("botConfigSaveBtn")?.addEventListener("click", saveBotConfig);
|
||||
document.getElementById("botConfigReloadBtn")?.addEventListener("click", loadBotConfig);
|
||||
document.getElementById("botTestBtn")?.addEventListener("click", testBot);
|
||||
document.getElementById("botReloadPlansBtn")?.addEventListener("click", loadBotPlans);
|
||||
document.getElementById("botNewPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||
document.getElementById("botCancelPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||
document.getElementById("botPlanForm")?.addEventListener("submit", botSavePlan);
|
||||
document.getElementById("botReloadPkgsBtn")?.addEventListener("click", loadBotPkgs);
|
||||
document.getElementById("botNewPkgBtn")?.addEventListener("click", botClearPkgForm);
|
||||
document.getElementById("botPkgForm")?.addEventListener("submit", botSavePkg);
|
||||
document.getElementById("botReloadUsersBtn")?.addEventListener("click", loadBotUsers);
|
||||
document.getElementById("botReloadTxnsBtn")?.addEventListener("click", loadBotTxns);
|
||||
document.getElementById("botTxnFilter")?.addEventListener("change", loadBotTxns);
|
||||
document.getElementById("botSaveSettingsBtn")?.addEventListener("click", saveBotSettings);
|
||||
document.getElementById("botReloadSettingsBtn")?.addEventListener("click", loadBotSettings);
|
||||
@@ -57,6 +57,7 @@
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="stats"><span class="nav-icon">📊</span><span>Servidor</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="vnstat"><span class="nav-icon">⇅</span><span>Tráfego</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="logs"><span class="nav-icon">☰</span><span>Logs</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="bot"><span class="nav-icon">🤖</span><span>Bot / Vendas</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="server"><span class="nav-icon">⚙</span><span>Configurações</span></button>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -948,6 +949,177 @@
|
||||
</div>
|
||||
</div><!-- /tab-logs -->
|
||||
|
||||
<!-- ═══════════ Bot / Vendas Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-bot">
|
||||
|
||||
<!-- Config -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">🤖 Configuração do Bot</div>
|
||||
<div style="display:flex;gap:5px;">
|
||||
<button class="btn btn-ghost btn-sm" id="botTestBtn">Testar conexão</button>
|
||||
<button class="btn btn-ghost btn-sm" id="botConfigReloadBtn">Recarregar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Bot ativo</label><input id="botEnabled" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||
<div class="field"><label>Token do Telegram <span class="hint" id="botHasTgToken"></span></label><input id="botTelegramToken" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
||||
<div class="field"><label>Modo do Telegram</label><select id="botTelegramMode"><option value="polling">polling (recomendado)</option><option value="webhook">webhook</option></select></div>
|
||||
<div class="field"><label>Telegram Webhook URL</label><input id="botTelegramWebhookURL" placeholder="https://seu.dominio/api/telegram/webhook"/></div>
|
||||
<div class="field"><label>Telegram Webhook Secret <span class="hint" id="botHasTgSecret"></span></label><input id="botTelegramWebhookSecret" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
||||
<div class="field"><label>Mercado Pago Access Token <span class="hint" id="botHasMpToken"></span></label><input id="botMPToken" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
||||
<div class="field"><label>Confirmação do pagamento</label><select id="botMPConfirmMode"><option value="polling">polling (recomendado)</option><option value="webhook">webhook</option></select></div>
|
||||
<div class="field"><label>MP Webhook Secret <span class="hint" id="botHasMpSecret"></span></label><input id="botMPWebhookSecret" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
||||
<div class="field"><label>Intervalo de verificação (polling)</label><input id="botMPPollInterval" placeholder="20s"/></div>
|
||||
<div class="field"><label>Expiração do PIX (min)</label><input id="botPixExp" type="number" min="1" placeholder="30"/></div>
|
||||
<div class="field"><label>Teste grátis ativo</label><input id="botTrialEnabled" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||
<div class="field"><label>Teste: horas</label><input id="botTrialHours" type="number" min="1" placeholder="1"/></div>
|
||||
<div class="field"><label>Teste: conexões</label><input id="botTrialMaxConns" type="number" min="1" placeholder="1"/></div>
|
||||
<div class="field"><label>Teste: tipo</label><select id="botTrialKind"><option value="ssh">SSH</option><option value="xray">Xray</option></select></div>
|
||||
<div class="field"><label>Teste: inbound Xray</label><input id="botTrialInbound" placeholder="tag do inbound (se Xray)"/></div>
|
||||
<div class="field"><label>IDs admin no Telegram <span class="hint">(vírgula)</span></label><input id="botAdminIDs" placeholder="111111111,222222222"/></div>
|
||||
<div class="field"><label>Host público SSH</label><input id="botPublicHost" placeholder="seu.dominio ou IP"/></div>
|
||||
<div class="field"><label>Host público Xray</label><input id="botXrayPublicHost" placeholder="seu.dominio (para links vless/vmess)"/></div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn" id="botConfigSaveBtn">Salvar configuração</button>
|
||||
</div>
|
||||
<div class="statusbar"><span id="botConfigStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<!-- Plans -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">💎 Planos <span class="chip" id="botPlanCount">0</span></div>
|
||||
<div style="display:flex;gap:5px;">
|
||||
<button class="btn btn-ghost btn-sm" id="botNewPlanBtn">+ Novo</button>
|
||||
<button class="btn btn-ghost btn-sm" id="botReloadPlansBtn">Recarregar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Nome</th><th>Tipo</th><th>Dias</th><th>Preço</th><th>Créd.</th><th>Ativo</th><th>Ações</th></tr></thead>
|
||||
<tbody id="botPlansBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="statusbar"><span id="botPlansStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Plan form -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title" id="botPlanFormTitle">Novo Plano</div>
|
||||
<button class="btn btn-ghost btn-sm" id="botCancelPlanBtn">Limpar</button>
|
||||
</div>
|
||||
<form id="botPlanForm">
|
||||
<input type="hidden" id="planId"/>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Nome</label><input id="planName" required/></div>
|
||||
<div class="field"><label>Tipo</label><select id="planKind"><option value="ssh">SSH</option><option value="xray">Xray</option></select></div>
|
||||
<div class="field"><label>Dias de validade</label><input id="planDays" type="number" min="1" value="30"/></div>
|
||||
<div class="field"><label>Máx. conexões</label><input id="planMaxConns" type="number" min="0" value="1"/></div>
|
||||
<div class="field"><label>Limite Mbps ↑ (SSH)</label><input id="planUpMbps" type="number" min="0" value="0"/></div>
|
||||
<div class="field"><label>Limite Mbps ↓ (SSH)</label><input id="planDownMbps" type="number" min="0" value="0"/></div>
|
||||
<div class="field"><label>Inbound Xray</label><input id="planInbound" list="botInboundList" placeholder="tag (se Xray)"/><datalist id="botInboundList"></datalist></div>
|
||||
<div class="field"><label>Protocolo Xray</label><select id="planProtocol"><option value="">-</option><option value="vless">vless</option><option value="vmess">vmess</option><option value="trojan">trojan</option></select></div>
|
||||
<div class="field"><label>Preço (R$)</label><input id="planPrice" type="number" step="0.01" min="0" value="0"/></div>
|
||||
<div class="field"><label>Custo em créditos</label><input id="planCreditCost" type="number" min="0" value="1"/></div>
|
||||
<div class="field"><label>Server ID <span class="hint">(vazio=local)</span></label><input id="planServerId"/></div>
|
||||
<div class="field"><label>Ordem</label><input id="planSort" type="number" value="0"/></div>
|
||||
<div class="field"><label>Ativo</label><input id="planActive" type="checkbox" checked style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||
</div>
|
||||
<div class="form-actions"><button class="btn" type="submit">Salvar plano</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<!-- Credit packages -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">💳 Pacotes de Crédito <span class="chip" id="botPkgCount">0</span></div>
|
||||
<div style="display:flex;gap:5px;">
|
||||
<button class="btn btn-ghost btn-sm" id="botNewPkgBtn">+ Novo</button>
|
||||
<button class="btn btn-ghost btn-sm" id="botReloadPkgsBtn">Recarregar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Nome</th><th>Créditos</th><th>Preço</th><th>Ativo</th><th>Ações</th></tr></thead>
|
||||
<tbody id="botPkgsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form id="botPkgForm" style="margin-top:10px;">
|
||||
<input type="hidden" id="pkgId"/>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Nome</label><input id="pkgName" required/></div>
|
||||
<div class="field"><label>Créditos</label><input id="pkgCredits" type="number" min="1" value="10"/></div>
|
||||
<div class="field"><label>Preço (R$)</label><input id="pkgPrice" type="number" step="0.01" min="0" value="0"/></div>
|
||||
<div class="field"><label>Ordem</label><input id="pkgSort" type="number" value="0"/></div>
|
||||
<div class="field"><label>Ativo</label><input id="pkgActive" type="checkbox" checked style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||
</div>
|
||||
<div class="form-actions"><button class="btn" type="submit">Salvar pacote</button></div>
|
||||
</form>
|
||||
<div class="statusbar"><span id="botPkgStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">✉️ Mensagens do Bot</div>
|
||||
<button class="btn btn-ghost btn-sm" id="botReloadSettingsBtn">Recarregar</button>
|
||||
</div>
|
||||
<div class="field"><label>Boas-vindas <span class="hint">({name} = nome)</span></label><textarea id="setWelcome" rows="4"></textarea></div>
|
||||
<div class="field"><label>Texto de Contato</label><textarea id="setContact" rows="3"></textarea></div>
|
||||
<div class="field"><label>Texto Baixar APP</label><textarea id="setAppText" rows="2"></textarea></div>
|
||||
<div class="field"><label>URL do APP <span class="hint">(botão vira link)</span></label><input id="setAppUrl" placeholder="https://..."/></div>
|
||||
<div class="form-actions"><button class="btn" id="botSaveSettingsBtn">Salvar mensagens</button></div>
|
||||
<div class="statusbar"><span id="botSettingsStatus">Ready.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot users -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">👥 Clientes do Bot <span class="chip" id="botUserCount">0</span></div>
|
||||
<button class="btn btn-ghost btn-sm" id="botReloadUsersBtn">Recarregar</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Telegram ID</th><th>Nome</th><th>@user</th><th>Papel</th><th>Revenda</th><th>Créditos</th><th>Ações</th></tr></thead>
|
||||
<tbody id="botUsersBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="statusbar"><span id="botUsersStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">💰 Pagamentos <span class="chip" id="botTxnCount">0</span></div>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<select id="botTxnFilter" class="btn-sm">
|
||||
<option value="">Todos</option>
|
||||
<option value="pending">Pendentes</option>
|
||||
<option value="approved">Aprovados</option>
|
||||
<option value="expired">Expirados</option>
|
||||
<option value="refunded">Estornados</option>
|
||||
</select>
|
||||
<button class="btn btn-ghost btn-sm" id="botReloadTxnsBtn">Recarregar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Telegram</th><th>Tipo</th><th>Valor</th><th>Status</th><th>Conta</th><th>Data</th><th>Ações</th></tr></thead>
|
||||
<tbody id="botTxnsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="statusbar"><span id="botTxnStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-bot -->
|
||||
|
||||
<!-- ═══════════ Server Config Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-server">
|
||||
|
||||
@@ -1271,6 +1443,7 @@
|
||||
<script defer src="assets/js/08-server-config.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/11-update-status.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/12-bot.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260711updatecheck1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user