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
+2
View File
@@ -15,6 +15,7 @@ DragonCoreSSH V40 é um painel/servidor em Go para SSH com HTTP Injection, paine
- Endpoint XHTTP compartilhado no modo nativo: VLESS **ou** VMess em `/` e SSH em `/ssh`, usando o mesmo domínio/porta/TLS
- Área compacta de infraestrutura com Servidores, Status, Monitoramento e Tráfego no mesmo seletor visual
- Cartões de status ao vivo nos espaços SSH, Xray e Infraestrutura, com confirmações integradas ao painel
- Navegação interna consistente com o Bot: SSH/SlowDNS e Revendedores separam consulta de cadastro; Xray separa Usuários, Criar usuário, Configuração e Logs; Configurações separa Rede/SSH, SlowDNS, UDP, TLS e Xray
- Contas de revendedor (reseller) com cota de usuários e escopo próprio
- Gerenciamento multi-servidor (master/slave) direto pelo painel
- API HTTP completa para bots/automações (ver **HTTP API Reference**)
@@ -543,6 +544,7 @@ DragonCoreSSH V40 is a Go-based SSH HTTP Injection server with a web panel, Post
- Native shared XHTTP endpoint: VLESS **or** VMess on `/` and SSH on `/ssh`, using the same domain/port/TLS
- Compact infrastructure workspace with Servers, Status, Monitoring, and Traffic in one visual switcher
- Live status cards across SSH, Xray, and Infrastructure, with panel-native confirmations
- Bot-style section navigation throughout the panel: SSH/SlowDNS and Resellers separate lists from creation; Xray separates Users, Create User, Configuration, and Logs; Settings separates Network/SSH, SlowDNS, UDP, TLS, and Xray
- Reseller accounts with a user quota and self-scoped access
- Multi-server (master/slave) management directly from the panel
- Full HTTP API for bots/automations (see **HTTP API Reference**)
+8 -3
View File
@@ -674,6 +674,11 @@ select:disabled {
/* Four infrastructure screens share one compact section switcher. */
.infra-nav-shell{position:sticky;top:92px;z-index:12;margin-bottom:20px;padding:6px;border:1px solid rgba(160,174,192,.14);border-radius:19px;background:rgba(7,10,16,.88);box-shadow:0 14px 40px rgba(0,0,0,.24);backdrop-filter:blur(16px)}.infra-section-nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:5px}.infra-section-nav button{min-height:42px;border:1px solid transparent;border-radius:14px;background:transparent;color:var(--muted);font-size:.76rem;font-weight:850;cursor:pointer;transition:.15s ease}.infra-section-nav button span{margin-right:6px;color:#80abff}.infra-section-nav button:hover{color:var(--text);background:rgba(255,255,255,.04)}.infra-section-nav button.active{color:#fff;border-color:rgba(80,145,255,.3);background:linear-gradient(135deg,rgba(80,145,255,.2),rgba(34,211,238,.08));box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}.infra-section-select{display:none;width:100%;padding:10px 12px;border:1px solid rgba(80,145,255,.3);border-radius:13px;background:#090d15;color:var(--text);font-weight:850}
/* Bot-style sub-navigation shared by SSH, Xray, resellers, and settings. */
#tab-ssh{--section-accent:49,214,123}#tab-xray{--section-accent:139,92,246}#tab-resellers{--section-accent:255,200,87}#tab-server{--section-accent:80,145,255}
.workspace-nav-shell{--section-accent:80,145,255;position:sticky;top:92px;z-index:12;margin-bottom:20px;padding:6px;border:1px solid rgba(160,174,192,.14);border-radius:19px;background:rgba(7,10,16,.9);box-shadow:0 14px 40px rgba(0,0,0,.24);backdrop-filter:blur(16px)}.workspace-nav-shell[data-tone="green"]{--section-accent:49,214,123}.workspace-nav-shell[data-tone="purple"]{--section-accent:139,92,246}.workspace-nav-shell[data-tone="amber"]{--section-accent:255,200,87}.workspace-nav-shell[data-tone="blue"]{--section-accent:80,145,255}.workspace-section-nav{display:grid;grid-template-columns:repeat(var(--workspace-nav-columns,2),minmax(0,1fr));gap:5px}.workspace-section-nav button{min-height:42px;border:1px solid transparent;border-radius:14px;background:transparent;color:var(--muted);font-size:.76rem;font-weight:850;cursor:pointer;transition:.15s ease}.workspace-section-nav button span{margin-right:6px;color:rgb(var(--section-accent));font-size:.72rem}.workspace-section-nav button:hover{color:var(--text);background:rgba(255,255,255,.04)}.workspace-section-nav button.active{color:#fff;border-color:rgba(var(--section-accent),.3);background:linear-gradient(135deg,rgba(var(--section-accent),.19),rgba(34,211,238,.07));box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}.workspace-section-select{display:none;width:100%;padding:10px 12px;border:1px solid rgba(var(--section-accent),.3);border-radius:13px;background:#090d15;color:var(--text);font-weight:850}.workspace-section{display:none;min-width:0}.workspace-section.active{display:block}.workspace-section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin:0 2px 16px}.workspace-section-heading>div>span{display:block;color:rgb(var(--section-accent,80,145,255));font-size:.69rem;font-weight:900;letter-spacing:.17em;text-transform:uppercase}.workspace-section-heading h3{margin:5px 0 4px;font-size:1.34rem;letter-spacing:-.025em}.workspace-section-heading p{color:var(--muted);font-size:.79rem;line-height:1.5}.workspace-form-card{width:min(100%,920px)}.workspace-section-status{margin-top:16px}.settings-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.settings-panel-grid>.card{margin-top:0!important}.settings-panel-grid>.settings-span-all{grid-column:1/-1}.settings-workspace>.workspace-section>.card{width:min(100%,1040px);margin-top:0!important}
.role-reseller #xraySectionNav{grid-template-columns:repeat(2,minmax(0,1fr))}
/* Shared panel-native confirmations and non-blocking notifications. */
.panel-dialog{position:fixed;inset:0;z-index:120;display:grid;place-items:center;padding:20px}.panel-dialog.hidden{display:none!important}.panel-dialog-backdrop{position:absolute;inset:0;background:rgba(1,3,6,.78);backdrop-filter:blur(8px)}.panel-dialog-card{--hero-accent:80,145,255;position:relative;width:min(100%,510px);padding:22px;border:1px solid rgba(var(--hero-accent),.28);border-radius:25px;background:radial-gradient(circle at 100% 0,rgba(var(--hero-accent),.13),transparent 34%),linear-gradient(180deg,#121925,#080c13);box-shadow:0 36px 110px rgba(0,0,0,.66);animation:fadeIn .16s ease both}.panel-dialog-card.is-danger{--hero-accent:255,91,105}.panel-dialog-card.is-success{--hero-accent:49,214,123}.panel-dialog-head{display:flex;align-items:center;gap:13px}.panel-dialog-head h3{margin-top:5px;font-size:1.15rem;letter-spacing:-.025em}.panel-dialog-icon{display:grid;place-items:center;flex:0 0 auto;width:45px;height:45px;border:1px solid rgba(var(--hero-accent),.28);border-radius:15px;background:rgba(var(--hero-accent),.11);color:rgb(var(--hero-accent));font-size:.76rem;font-weight:950}.panel-dialog-card>p{margin-top:16px;color:var(--text-2);font-size:.79rem;line-height:1.58}.panel-dialog-detail{margin-top:12px;padding:12px 13px;border:1px solid rgba(148,163,184,.11);border-radius:14px;background:rgba(255,255,255,.035);color:var(--muted);font-size:.7rem;line-height:1.5;white-space:pre-line}.panel-dialog-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:19px;padding-top:15px;border-top:1px solid rgba(148,163,184,.1)}.panel-dialog-open{overflow:hidden}.panel-toast-stack{position:fixed;right:20px;bottom:20px;z-index:130;display:flex;flex-direction:column-reverse;gap:9px;width:min(390px,calc(100vw - 40px));pointer-events:none}.panel-toast{--toast-accent:80,145,255;display:flex;align-items:flex-start;gap:10px;padding:12px 13px;border:1px solid rgba(var(--toast-accent),.28);border-radius:17px;background:rgba(10,15,24,.96);box-shadow:0 18px 55px rgba(0,0,0,.48);backdrop-filter:blur(15px);animation:toastIn .18s ease both;pointer-events:auto}.panel-toast.success{--toast-accent:49,214,123}.panel-toast.warning{--toast-accent:255,200,87}.panel-toast.error{--toast-accent:255,91,105}.panel-toast-icon{display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border-radius:10px;background:rgba(var(--toast-accent),.12);color:rgb(var(--toast-accent));font-size:.68rem;font-weight:950}.panel-toast-copy{min-width:0;flex:1}.panel-toast-copy strong{display:block;color:var(--text);font-size:.75rem}.panel-toast-copy p{margin-top:3px;color:var(--muted);font-size:.69rem;line-height:1.42}.panel-toast-close{padding:2px;border:0;background:transparent;color:var(--muted);font-size:.85rem;cursor:pointer}@keyframes toastIn{from{opacity:0;transform:translateY(9px) scale(.98)}to{opacity:1;transform:none}}
@@ -691,9 +696,9 @@ select:disabled {
.legacy-ssh-btn{margin-right:auto;border-color:rgba(49,214,123,.3)!important;background:linear-gradient(135deg,rgba(49,214,123,.18),rgba(34,211,238,.09))!important;color:#8af0b5!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}.legacy-ssh-btn:hover:not(:disabled){border-color:rgba(49,214,123,.52)!important;transform:translateY(-1px)}.legacy-ssh-btn.is-enabled:disabled{opacity:1;border-color:rgba(49,214,123,.16)!important;background:rgba(49,214,123,.07)!important;color:#72b98e!important;cursor:default}
.visual-inbound-editor{margin:14px 0;padding:18px;border:1px solid rgba(34,211,238,.2);border-radius:22px;background:radial-gradient(circle at 100% 0,rgba(34,211,238,.09),transparent 28%),rgba(6,10,16,.86)}.visual-editor-heading{--hero-accent:34,211,238;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:15px;padding-bottom:13px;border-bottom:1px solid rgba(148,163,184,.1)}.visual-save-bar{position:sticky;bottom:14px;z-index:8;display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:16px;padding:13px 15px;border:1px solid rgba(139,92,246,.2);border-radius:19px;background:rgba(8,12,20,.9);box-shadow:0 18px 48px rgba(0,0,0,.35);backdrop-filter:blur(16px)}
@media(max-width:1180px){.workspace-overview-grid.five{grid-template-columns:repeat(3,minmax(0,1fr))}.infra-nav-shell{top:78px}}
@media(max-width:1100px){.workspace-overview-grid,.workspace-overview-grid.five{grid-template-columns:repeat(2,minmax(0,1fr))}.shared-endpoint-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.visual-inbound-list{grid-template-columns:1fr}}
@media(max-width:760px){.page-hero{min-height:0;padding:20px;border-radius:22px;align-items:flex-start;flex-direction:column}.page-hero.status-hero{display:grid;grid-template-columns:1fr}.page-hero h2{font-size:1.55rem}.page-hero-pills{max-width:none;justify-content:flex-start}.page-hero-mark{width:50px;height:50px;border-radius:17px}.workspace-hero-actions{justify-content:flex-start;max-width:none;margin-top:14px}.workspace-live-status{max-width:100%}.workspace-overview-grid,.workspace-overview-grid.five{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:18px}.workspace-hero-toolbar{align-items:stretch;flex-direction:column}.workspace-toolbar-actions{justify-content:flex-start}.workspace-toolbar-actions .input-sm{width:100%;max-width:none}.infra-section-nav{display:none}.infra-section-select{display:block}.infra-nav-shell{top:76px}.shared-endpoint-card{padding:15px}.shared-endpoint-head,.shared-endpoint-actions,.visual-save-bar{align-items:flex-start;flex-direction:column}.shared-endpoint-actions .btn,.visual-save-bar .btn{width:100%}.shared-route-preview{grid-template-columns:1fr}.shared-route-preview i{width:1px;height:22px;justify-self:center}.shared-route-preview i::after{right:-3px;top:auto;bottom:0}.shared-endpoint-grid{grid-template-columns:1fr!important}.legacy-xhttp-migration{align-items:flex-start}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}.visual-inbound-actions{align-items:stretch;flex-wrap:wrap}.legacy-ssh-btn{flex:1 0 100%;margin-right:0}.panel-dialog{padding:14px}.panel-dialog-card{padding:19px}.panel-dialog-actions .btn{flex:1}.panel-toast-stack{right:14px;bottom:14px;width:calc(100vw - 28px)}}
@media(max-width:1180px){.workspace-overview-grid.five{grid-template-columns:repeat(3,minmax(0,1fr))}.infra-nav-shell,.workspace-nav-shell{top:78px}}
@media(max-width:1100px){.workspace-overview-grid,.workspace-overview-grid.five{grid-template-columns:repeat(2,minmax(0,1fr))}#configSectionNav{grid-template-columns:repeat(3,minmax(0,1fr))}#xraySectionNav{grid-template-columns:repeat(2,minmax(0,1fr))}.shared-endpoint-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.visual-inbound-list{grid-template-columns:1fr}}
@media(max-width:760px){.page-hero{min-height:0;padding:20px;border-radius:22px;align-items:flex-start;flex-direction:column}.page-hero.status-hero{display:grid;grid-template-columns:1fr}.page-hero h2{font-size:1.55rem}.page-hero-pills{max-width:none;justify-content:flex-start}.page-hero-mark{width:50px;height:50px;border-radius:17px}.workspace-hero-actions{justify-content:flex-start;max-width:none;margin-top:14px}.workspace-live-status{max-width:100%}.workspace-overview-grid,.workspace-overview-grid.five{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:18px}.workspace-hero-toolbar{align-items:stretch;flex-direction:column}.workspace-toolbar-actions{justify-content:flex-start}.workspace-toolbar-actions .input-sm{width:100%;max-width:none}.infra-section-nav,.workspace-section-nav{display:none}.infra-section-select,.workspace-section-select{display:block}.infra-nav-shell,.workspace-nav-shell{top:76px}.workspace-section-heading{align-items:flex-start;flex-direction:column}.workspace-section-heading>.btn,.workspace-section-heading>.card-actions{width:100%}.workspace-section-heading>.btn{justify-content:center}.settings-panel-grid{grid-template-columns:1fr}.settings-panel-grid>.settings-span-all{grid-column:auto}.shared-endpoint-card{padding:15px}.shared-endpoint-head,.shared-endpoint-actions,.visual-save-bar{align-items:flex-start;flex-direction:column}.shared-endpoint-actions .btn,.visual-save-bar .btn{width:100%}.shared-route-preview{grid-template-columns:1fr}.shared-route-preview i{width:1px;height:22px;justify-self:center}.shared-route-preview i::after{right:-3px;top:auto;bottom:0}.shared-endpoint-grid{grid-template-columns:1fr!important}.legacy-xhttp-migration{align-items:flex-start}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}.visual-inbound-actions{align-items:stretch;flex-wrap:wrap}.legacy-ssh-btn{flex:1 0 100%;margin-right:0}.panel-dialog{padding:14px}.panel-dialog-card{padding:19px}.panel-dialog-actions .btn{flex:1}.panel-toast-stack{right:14px;bottom:14px;width:calc(100vw - 28px)}}
@media(max-width:460px){.workspace-overview-grid,.workspace-overview-grid.five{grid-template-columns:1fr}.workspace-hero-actions .btn{flex:1}.workspace-toolbar-actions .btn{flex:1}.workspace-overview-card{padding:11px 12px}}
/* --- Bot sales workspace --- */
+18 -3
View File
@@ -6,7 +6,6 @@ localStorage.removeItem("SESSION_TOKEN");
let currentRole = "";
let currentUser = "";
let statsTimer = null, usersTimer = null, xrayTimer = null;
let formCollapsed = true;
let tlsForwardersState = [];
let managedTlsForwardersState = [];
let editingXrayClientId = null;
@@ -155,6 +154,24 @@ Object.assign(I18N_TEXT["pt-BR"], {
"Delete reseller":"Excluir revendedor","Delete reseller \"{name}\"?":"Excluir o revendedor \"{name}\"?","Their owned access will be removed and active SSH sessions will be disconnected.":"Os acessos pertencentes a ele serão removidos e as sessões SSH ativas serão desconectadas.","Remove inbound":"Remover inbound","Remove inbound {name}?":"Remover o inbound {name}?","Clients attached only to this inbound will stop connecting after the configuration is saved.":"Clientes vinculados somente a este inbound deixarão de conectar após salvar a configuração.",
"This endpoint already has an SSH /ssh route.":"Este endpoint já possui uma rota SSH /ssh.","Add SSH /ssh without rebuilding this inbound.":"Adicione SSH /ssh sem recriar este inbound.","SSH migration attention":"Atenção na migração SSH","Could not enable SSH /ssh":"Não foi possível ativar SSH /ssh","Select a VLESS/VMess inbound using XHTTP.":"Selecione um inbound VLESS/VMess usando XHTTP.","Load the selected server configuration before enabling SSH.":"Carregue a configuração do servidor selecionado antes de ativar SSH.","Shared SSH requires native Xray mode. Select Internal native emulator and save the mode first.":"O SSH compartilhado exige o modo Xray nativo. Selecione Internal native emulator e salve o modo primeiro.","Inbound {name} uses {security}, which native SSH XHTTP does not support. Use TLS or no TLS.":"O inbound {name} usa {security}, que o SSH XHTTP nativo não suporta. Use TLS ou sem TLS.","SSH is already enabled on /ssh by inbound {name}.":"SSH já está ativado em /ssh pelo inbound {name}.","Path /ssh is already used by inbound {name}. Edit that path first.":"O caminho /ssh já é usado pelo inbound {name}. Edite esse caminho primeiro.","This inbound uses TLS but has no reusable certificate and key file paths.":"Este inbound usa TLS, mas não possui caminhos reutilizáveis para certificado e chave.","Safe XHTTP migration":"Migração XHTTP segura","Enable SSH on /ssh":"Ativar SSH em /ssh","Add SSH to the same endpoint without rebuilding {name}?":"Adicionar SSH ao mesmo endpoint sem recriar {name}?","Listener":"Listener","Existing path preserved":"Path existente preservado","New SSH path":"Novo path SSH","Clients preserved":"Clientes preservados","Enabling SSH…":"Ativando SSH…","Could not enable SSH: {error}":"Não foi possível ativar SSH: {error}","Migration was cancelled because it would alter the old inbound.":"A migração foi cancelada porque alteraria o inbound antigo.","SSH /ssh added to the draft without changing {name}.":"SSH /ssh foi adicionado ao rascunho sem alterar {name}.","Enabling SSH /ssh without changing {name}…":"Ativando SSH /ssh sem alterar {name}…","The SSH route could not be saved. The old inbound was not changed.":"A rota SSH não pôde ser salva. O inbound antigo não foi alterado.","SSH /ssh was saved, but Xray could not restart. Use the Restart button after checking the Xray log.":"SSH /ssh foi salvo, mas o Xray não conseguiu reiniciar. Verifique o log e use o botão Reiniciar.","SSH /ssh is active. The old inbound and all clients were preserved.":"SSH /ssh está ativo. O inbound antigo e todos os clientes foram preservados.","Configuration for this server was not loaded.":"A configuração deste servidor não foi carregada.","Invalid visual config: {error}":"Configuração visual inválida: {error}","Xray could not restart.":"O Xray não conseguiu reiniciar.","Could not save configuration: {error}":"Não foi possível salvar a configuração: {error}","selected":"selecionado","this inbound":"este inbound","the old inbound":"o inbound antigo","untagged":"sem tag"
});
Object.assign(I18N_TEXT["en-US"], {
"Create user":"Create user","Edit user":"Edit user","Create SSH user":"Create SSH user","SSH user saved successfully.":"SSH user saved successfully.","SSH and SlowDNS areas":"SSH and SlowDNS areas","SSH and SlowDNS area":"SSH and SlowDNS area",
"01 · Management":"01 · Management","02 · Registration":"02 · Registration","SSH and SlowDNS users":"SSH and SlowDNS users","SSH users section copy":"Review connections, limits, expiration, and actions for every account.","SSH create section copy":"Set authentication, expiration, connections, and speed in a dedicated screen.",
"Configuration":"Configuration","Xray areas":"Xray areas","Xray area":"Xray area","Xray users section copy":"Review clients, connections, expiration, and traffic by inbound.","Create Xray user":"Create Xray user","Xray create section copy":"Choose the inbound and register the client without opening the users table.","New Xray client":"New Xray client","Loading inbounds…":"Loading inbounds…","Select where the client will be added.":"Select where the client will be added.","Generated automatically":"Generated automatically","Generate":"Generate","Display name":"Display name","Email / identifier":"Email / identifier","Expires on":"Expires on","Maximum connections":"Maximum connections","0 = unlimited":"0 = unlimited","Back to users":"Back to users","Fill in the new client details.":"Fill in the new client details.",
"No compatible inbound found":"No compatible inbound found","No inbound":"No inbound","The client will be added to {tag} on port {port}.":"The client will be added to {tag} on port {port}.","Create or enable a compatible inbound before adding a client.":"Create or enable a compatible inbound before adding a client.","Ready to create a new Xray client.":"Ready to create a new Xray client.","Waiting for a compatible inbound.":"Waiting for a compatible inbound.","Select a compatible inbound.":"Select a compatible inbound.","Creating Xray client…":"Creating Xray client…","Xray user created successfully.":"Xray user created successfully.","Xray user":"Xray user","Could not create the Xray user: {error}":"Could not create the Xray user: {error}",
"03 · Service":"03 · Service","04 · Diagnostics":"04 · Diagnostics","Xray configuration section copy":"Edit the endpoint, inbounds, TLS, and advanced options visually or as JSON.","Xray logs section copy":"Review the latest service messages in a focused screen.",
"Reseller areas":"Reseller areas","Reseller area":"Reseller area","Create reseller":"Create reseller","Edit reseller":"Edit reseller","Registered resellers":"Registered resellers","Reseller list section copy":"Review quotas, shared usage, expiration, and status for every partner.","Reseller create section copy":"Set login, shared limit, expiration, and access in a dedicated screen.","Reseller saved successfully.":"Reseller saved successfully.",
"Configuration areas":"Configuration areas","Configuration area":"Configuration area","Network and SSH":"Network and SSH","SlowDNS / DNSTT":"SlowDNS / DNSTT","TLS forwarders":"TLS forwarders","01 · Base":"01 · Base","02 · DNS tunnel":"02 · DNS tunnel","03 · UDP":"03 · UDP","04 · Security":"04 · Security","05 · Core":"05 · Core","Network and SSH section copy":"Configure listeners, default limits, idle timeout, and the connection banner.","SlowDNS section copy":"Manage domains, local DNS, capacity, queues, and controlled restarts.","UDP section copy":"Set the listener, capacity, map expiration, and service restart.","TLS section copy":"Create TLS listeners with automatic, pasted, or file-based certificates.","Xray core section copy":"Enable the core, choose the runtime, and apply safe native tuning."
});
Object.assign(I18N_TEXT["pt-BR"], {
"Create user":"Criar usuário","Edit user":"Editar usuário","Create SSH user":"Criar usuário SSH","SSH user saved successfully.":"Usuário SSH salvo com sucesso.","SSH and SlowDNS areas":"Áreas SSH e SlowDNS","SSH and SlowDNS area":"Área SSH e SlowDNS",
"01 · Management":"01 · Gestão","02 · Registration":"02 · Cadastro","SSH and SlowDNS users":"Usuários SSH e SlowDNS","SSH users section copy":"Consulte conexões, limites, validade e ações de cada conta.","SSH create section copy":"Defina autenticação, validade, conexões e velocidade em uma tela dedicada.",
"Configuration":"Configuração","Xray areas":"Áreas do Xray","Xray area":"Área do Xray","Xray users section copy":"Consulte clientes, conexões, validade e tráfego separados por inbound.","Create Xray user":"Criar usuário Xray","Xray create section copy":"Escolha o inbound e cadastre o cliente sem abrir a tabela de usuários.","New Xray client":"Novo cliente Xray","Loading inbounds…":"Carregando inbounds…","Select where the client will be added.":"Selecione onde o cliente será adicionado.","Generated automatically":"Gerado automaticamente","Generate":"Gerar","Display name":"Nome de exibição","Email / identifier":"Email / identificação","Expires on":"Expira em","Maximum connections":"Máximo de conexões","0 = unlimited":"0 = ilimitado","Back to users":"Voltar aos usuários","Fill in the new client details.":"Preencha os dados do novo cliente.",
"No compatible inbound found":"Nenhum inbound compatível encontrado","No inbound":"Sem inbound","The client will be added to {tag} on port {port}.":"O cliente será adicionado em {tag} na porta {port}.","Create or enable a compatible inbound before adding a client.":"Crie ou ative um inbound compatível antes de adicionar um cliente.","Ready to create a new Xray client.":"Pronto para criar um novo cliente Xray.","Waiting for a compatible inbound.":"Aguardando um inbound compatível.","Select a compatible inbound.":"Selecione um inbound compatível.","Creating Xray client…":"Criando cliente Xray…","Xray user created successfully.":"Usuário Xray criado com sucesso.","Xray user":"Usuário Xray","Could not create the Xray user: {error}":"Não foi possível criar o usuário Xray: {error}",
"03 · Service":"03 · Serviço","04 · Diagnostics":"04 · Diagnóstico","Xray configuration section copy":"Edite endpoint, inbounds, TLS e opções avançadas visualmente ou em JSON.","Xray logs section copy":"Acompanhe as últimas mensagens do serviço em uma tela focada.",
"Reseller areas":"Áreas de revendedores","Reseller area":"Área de revendedores","Create reseller":"Criar revendedor","Edit reseller":"Editar revendedor","Registered resellers":"Revendedores cadastrados","Reseller list section copy":"Consulte cotas, consumo compartilhado, validade e situação de cada parceiro.","Reseller create section copy":"Defina login, limite compartilhado, validade e acesso em uma tela dedicada.","Reseller saved successfully.":"Revendedor salvo com sucesso.",
"Configuration areas":"Áreas de configuração","Configuration area":"Área de configuração","Network and SSH":"Rede e SSH","SlowDNS / DNSTT":"SlowDNS / DNSTT","TLS forwarders":"Encaminhadores TLS","01 · Base":"01 · Base","02 · DNS tunnel":"02 · Túnel DNS","03 · UDP":"03 · UDP","04 · Security":"04 · Segurança","05 · Core":"05 · Core","Network and SSH section copy":"Configure listeners, limites padrão, tempo ocioso e o banner de conexão.","SlowDNS section copy":"Gerencie domínios, DNS local, capacidade, filas e reinício controlado.","UDP section copy":"Defina listener, capacidade, expiração de mapa e reinício do serviço.","TLS section copy":"Crie listeners TLS com certificado automático, colado ou armazenado em arquivo.","Xray core section copy":"Ative o core, escolha o runtime e aplique os ajustes nativos seguros."
});
const I18N_REVERSE = Object.fromEntries(SUPPORTED_LANGS.map(lang => [lang, Object.fromEntries(Object.entries(I18N_TEXT[lang] || {}).map(([k, v]) => [v, k]))]));
let currentLang = detectInitialLanguage();
let i18nTranslating = false;
@@ -304,8 +321,6 @@ const rStatus = document.getElementById("rStatus");
// User form
const userForm = document.getElementById("userForm");
const userFormWrap = document.getElementById("userFormWrap");
const toggleFormBtn = document.getElementById("toggleFormBtn");
const cancelUserBtn = document.getElementById("cancelUserBtn");
const newUserBtn = document.getElementById("newUserBtn");
const saveUserBtn = document.getElementById("saveUserBtn");
+76 -1
View File
@@ -57,6 +57,73 @@ function mountInfrastructureNavigation() {
syncInfrastructureNavigation();
}
const workspaceSectionDefaults = {
ssh: "users",
xray: "users",
resellers: "users",
config: "general",
};
function workspaceSectionRoot(workspace) {
const tab = workspace === "config" ? "server" : workspace;
return document.getElementById(`tab-${tab}`);
}
function activeWorkspaceSection(workspace) {
const root = workspaceSectionRoot(workspace);
return root?.querySelector(`[data-workspace-panel="${workspace}"].active`)?.dataset.workspaceSectionPanel
|| workspaceSectionDefaults[workspace]
|| "";
}
function setWorkspaceSection(workspace, section, options = {}) {
const root = workspaceSectionRoot(workspace);
if (!root) return false;
const panels = Array.from(root.querySelectorAll(`[data-workspace-panel="${workspace}"]`));
const targets = panels.filter(panel => panel.dataset.workspaceSectionPanel === section && !panel.classList.contains("hidden"));
if (!targets.length) {
section = workspaceSectionDefaults[workspace] || panels.find(panel => !panel.classList.contains("hidden"))?.dataset.workspaceSectionPanel || "";
}
panels.forEach(panel => panel.classList.toggle("active", panel.dataset.workspaceSectionPanel === section));
root.querySelectorAll(`[data-workspace="${workspace}"][data-workspace-section]`).forEach(button => {
const active = button.dataset.workspaceSection === section;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
button.tabIndex = active ? 0 : -1;
});
const select = root.querySelector(`[data-workspace-select="${workspace}"]`);
if (select) select.value = section;
if (!options.silent) {
if (workspace === "xray" && section === "config" && currentRole === "superadmin" && typeof loadWizardFromConfig === "function") loadWizardFromConfig();
if (workspace === "xray" && section === "logs" && currentRole === "superadmin" && typeof loadXrayLogs === "function") loadXrayLogs();
}
return true;
}
function prepareWorkspaceSection(workspace, section) {
if (section !== "create") return;
if (workspace === "ssh" && typeof prepareNewSSHUser === "function") prepareNewSSHUser();
if (workspace === "xray" && typeof prepareXrayClientCreator === "function") prepareXrayClientCreator();
if (workspace === "resellers" && typeof prepareNewReseller === "function") prepareNewReseller();
}
function navigateWorkspaceSection(workspace, section) {
prepareWorkspaceSection(workspace, section);
return setWorkspaceSection(workspace, section);
}
function mountWorkspaceSectionNavigation() {
document.querySelectorAll("[data-workspace][data-workspace-section]").forEach(button => {
button.setAttribute("role", "tab");
button.addEventListener("click", () => navigateWorkspaceSection(button.dataset.workspace, button.dataset.workspaceSection));
});
document.querySelectorAll("[data-workspace-select]").forEach(select => {
select.addEventListener("change", () => navigateWorkspaceSection(select.dataset.workspaceSelect, select.value));
});
Object.entries(workspaceSectionDefaults).forEach(([workspace, section]) => setWorkspaceSection(workspace, section, { silent:true }));
}
function updatePageHeading() {
const [eyebrow, title] = tabTitles[currentTab] || ["Dashboard", currentTab];
if (pageEyebrow) pageEyebrow.textContent = t(eyebrow);
@@ -81,7 +148,7 @@ function selectTab(tab) {
if (tab === "xray") {
loadXrayStatus();
loadInbounds({ silent: true });
if (currentRole === "superadmin") loadWizardFromConfig();
if (currentRole === "superadmin" && activeWorkspaceSection("xray") === "config") loadWizardFromConfig();
}
if (tab === "stats" && currentRole === "superadmin") loadStats();
if (tab === "vnstat" && currentRole === "superadmin") loadVnstat();
@@ -92,6 +159,7 @@ function selectTab(tab) {
}
mountInfrastructureNavigation();
mountWorkspaceSectionNavigation();
document.querySelectorAll(".tab-btn").forEach(btn => btn.addEventListener("click", () => selectTab(btn.dataset.tab)));
menuToggle?.addEventListener("click", () => document.body.classList.add("sidebar-open"));
drawerBackdrop?.addEventListener("click", () => document.body.classList.remove("sidebar-open"));
@@ -166,6 +234,13 @@ function initAfterLogin() {
document.querySelectorAll(".xray-admin-only").forEach(el => {
el.classList.toggle("hidden", currentRole !== "superadmin");
});
document.querySelectorAll("option.xray-admin-only").forEach(option => {
option.hidden = currentRole !== "superadmin";
option.disabled = currentRole !== "superadmin";
});
if (currentRole !== "superadmin" && ["config", "logs"].includes(activeWorkspaceSection("xray"))) {
setWorkspaceSection("xray", "users", { silent:true });
}
resellerInfoCard.classList.toggle("hidden", currentRole !== "reseller");
dashboardQuotaCard?.classList.toggle("hidden", currentRole !== "reseller");
+19 -13
View File
@@ -1,15 +1,21 @@
// ─── SSH Users ────────────────────────────────────────────────────────────────
document.getElementById("reloadUsersBtn").addEventListener("click", loadUsers);
document.getElementById("sshHeroRefreshBtn")?.addEventListener("click", loadUsers);
newUserBtn.addEventListener("click", () => {
setFormCollapsed(false);
newUserBtn.addEventListener("click", () => navigateWorkspaceSection("ssh", "create"));
cancelUserBtn.addEventListener("click", () => {
prepareNewSSHUser();
setWorkspaceSection("ssh", "users");
});
function prepareNewSSHUser() {
userForm.reset();
fTotpPeriod.value = 60; fTotpWindow.value = 1; fTotpDigits.value = 6;
const heading = document.getElementById("userFormHeading");
const title = document.getElementById("userFormTitle");
if (heading) heading.textContent = t("Create user");
if (title) title.textContent = t("Create SSH user");
userStatus.textContent = t("New user.");
fUsername.focus();
});
cancelUserBtn.addEventListener("click", () => setFormCollapsed(true));
toggleFormBtn.addEventListener("click", () => setFormCollapsed(!formCollapsed));
requestAnimationFrame(() => fUsername.focus());
}
document.getElementById("genTotpBtn").addEventListener("click", () => {
fTotpSecret.value = genBase32();
if (!fTotpPeriod.value) fTotpPeriod.value = 60;
@@ -19,12 +25,6 @@ document.getElementById("genTotpBtn").addEventListener("click", () => {
});
document.getElementById("clearTotpBtn").addEventListener("click", () => { fTotpSecret.value = ""; });
function setFormCollapsed(v) {
formCollapsed = v;
userFormWrap.classList.toggle("collapsed", v);
toggleFormBtn.textContent = v ? t("Show form") : t("Hide form");
}
async function loadUsers() {
userStatus.textContent = t("Loading…");
if (sshLiveStatus) {
@@ -113,7 +113,7 @@ function renderUsers(users) {
}
function fillUserForm(u) {
setFormCollapsed(false);
setWorkspaceSection("ssh", "create");
fUsername.value = u.username || "";
fPassword.value = "";
fTotpSecret.value = u.totp_secret || "";
@@ -125,6 +125,10 @@ function fillUserForm(u) {
fUp.value = u.limit_mbps_up || "";
fDown.value = u.limit_mbps_down || "";
fExpires.value = u.expires_at ? localFromISO(u.expires_at) : "";
const heading = document.getElementById("userFormHeading");
const title = document.getElementById("userFormTitle");
if (heading) heading.textContent = t("Edit user");
if (title) title.textContent = t("Editing {name}", {name:u.username});
userStatus.textContent = t("Editing {name}", {name: u.username});
}
@@ -153,6 +157,8 @@ userForm.addEventListener("submit", async e => {
fPassword.value = "";
loadUsers();
if (currentRole === "reseller") loadMe();
showPanelToast(t("SSH user saved successfully."), "success", t("SSH / SlowDNS"));
setWorkspaceSection("ssh", "users");
} catch (e) {
if (e.message==="auth") doAuthError();
else userStatus.textContent = t("Error: {error}", {error: e.message});
+122 -56
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});
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;
}
}
+16 -7
View File
@@ -1,16 +1,20 @@
// ─── Resellers ────────────────────────────────────────────────────────────────
document.getElementById("reloadResellersBtn").addEventListener("click", loadResellers);
document.getElementById("newResellerBtn").addEventListener("click", () => {
document.getElementById("newResellerBtn").addEventListener("click", () => navigateWorkspaceSection("resellers", "create"));
document.getElementById("cancelResellerBtn").addEventListener("click", () => {
prepareNewReseller();
setWorkspaceSection("resellers", "users");
});
function prepareNewReseller() {
resellerFormTitle.textContent = "Create Reseller";
const heading = document.getElementById("resellerFormHeading");
if (heading) heading.textContent = t("Create reseller");
resellerForm.reset();
rActive.checked = true;
resellerStatus.textContent = "New reseller.";
});
document.getElementById("cancelResellerBtn").addEventListener("click", () => {
resellerForm.reset();
rActive.checked = true;
resellerFormTitle.textContent = "Create Reseller";
});
requestAnimationFrame(() => rUsername.focus());
}
document.querySelector("[data-tab='resellers']")?.addEventListener("click", loadResellers);
@@ -63,7 +67,10 @@ function renderResellers(list) {
}
function fillResellerForm(r) {
setWorkspaceSection("resellers", "create");
resellerFormTitle.textContent = `Edit: ${r.username}`;
const heading = document.getElementById("resellerFormHeading");
if (heading) heading.textContent = t("Edit reseller");
rUsername.value = r.username;
rPassword.value = "";
rMaxUsers.value = r.max_users || 0;
@@ -91,6 +98,8 @@ resellerForm.addEventListener("submit", async e => {
resellerForm.reset(); rActive.checked = true;
resellerFormTitle.textContent = "Create Reseller";
loadResellers();
showPanelToast(t("Reseller saved successfully."), "success", t("Resellers"));
setWorkspaceSection("resellers", "users");
} catch (e) {
if (e.message==="auth") doAuthError();
else resellerStatus.textContent = "Error: "+e.message;
+136 -41
View File
@@ -16,7 +16,7 @@
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
})();
</script>
<link rel="stylesheet" href="assets/app.css?v=20260713infra4"/>
<link rel="stylesheet" href="assets/app.css?v=20260713sections5"/>
</head>
<body>
<div class="app">
@@ -85,8 +85,7 @@
<!-- ═══════════ Dashboard Tab ═══════════ -->
<div class="tab-pane active" id="tab-dashboard">
<section class="page-hero" data-tone="cyan">
<div class="page-hero-copy"><span class="page-kicker">Command center</span><h2>Visão geral</h2><p>Contas, conexões e infraestrutura em uma leitura rápida.</p></div>
<div class="page-hero-mark" aria-hidden="true">DC</div>
<div class="page-hero-copy"><h2>Visão geral</h2><p>Contas, conexões e infraestrutura em uma leitura rápida.</p></div>
</section>
<div class="dash-grid">
<div class="dash-card accent-blue">
@@ -221,6 +220,16 @@
</div>
</section>
<div class="workspace-nav-shell" data-tone="green">
<nav class="workspace-section-nav" id="sshSectionNav" aria-label="Áreas SSH e SlowDNS" style="--workspace-nav-columns:2">
<button class="active" type="button" data-workspace="ssh" data-workspace-section="users"><span></span> Usuários</button>
<button type="button" data-workspace="ssh" data-workspace-section="create"><span></span> Criar usuário</button>
</nav>
<select id="sshSection" class="workspace-section-select" data-workspace-select="ssh" aria-label="Área SSH e SlowDNS">
<option value="users">Usuários</option><option value="create">Criar usuário</option>
</select>
</div>
<!-- Reseller info card (visible to resellers only) -->
<div id="resellerInfoCard" class="card hidden" style="margin-bottom:12px;">
<div class="card-hdr"><div class="card-title">My Account</div></div>
@@ -252,7 +261,8 @@
<div class="hint" id="sshServerHint">Servers with SSH enabled are available here.</div>
</div>
<div class="grid2">
<section class="workspace-section active" data-workspace-panel="ssh" data-workspace-section-panel="users">
<div class="workspace-section-heading"><div><span>01 · Gestão</span><h3>Usuários SSH e SlowDNS</h3><p>Consulte conexões, limites, validade e ações de cada conta.</p></div></div>
<!-- Users list -->
<div class="card">
<div class="card-hdr">
@@ -273,16 +283,17 @@
<tbody id="usersBody"></tbody>
</table>
</div>
<div class="statusbar"><span id="userStatus">Ready.</span><span id="lastReload"></span></div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="ssh" data-workspace-section-panel="create">
<div class="workspace-section-heading"><div><span>02 · Cadastro</span><h3 id="userFormHeading">Criar usuário</h3><p>Defina autenticação, validade, conexões e velocidade em uma tela dedicada.</p></div></div>
<!-- Create / edit user form -->
<div class="card">
<div class="card workspace-form-card">
<div class="card-hdr">
<div class="card-title">Create / update user</div>
<button class="btn btn-ghost btn-sm" id="toggleFormBtn">Show form</button>
<div class="card-title" id="userFormTitle">Criar usuário SSH</div>
</div>
<div id="userFormWrap" class="collapsible collapsed">
<div id="userFormWrap">
<form id="userForm">
<div class="form-grid">
<div class="field"><label>Username</label><input id="fUsername" required autocomplete="off"/></div>
@@ -310,7 +321,8 @@
</form>
</div>
</div>
</div>
</section>
<div class="statusbar workspace-section-status"><span id="userStatus">Ready.</span><span id="lastReload"></span></div>
</div><!-- /tab-ssh -->
<!-- ═══════════ Xray Tab (superadmin only) ═══════════ -->
@@ -337,6 +349,21 @@
</div>
</div>
</section>
<div class="workspace-nav-shell" data-tone="purple">
<nav class="workspace-section-nav" id="xraySectionNav" aria-label="Áreas do Xray" style="--workspace-nav-columns:4">
<button class="active" type="button" data-workspace="xray" data-workspace-section="users"><span></span> Usuários</button>
<button type="button" data-workspace="xray" data-workspace-section="create"><span></span> Criar usuário</button>
<button class="xray-admin-only" type="button" data-workspace="xray" data-workspace-section="config"><span></span> Configuração</button>
<button class="xray-admin-only" type="button" data-workspace="xray" data-workspace-section="logs"><span></span> Logs</button>
</nav>
<select id="xraySection" class="workspace-section-select" data-workspace-select="xray" aria-label="Área do Xray">
<option value="users">Usuários</option><option value="create">Criar usuário</option>
<option class="xray-admin-only" value="config">Configuração</option><option class="xray-admin-only" value="logs">Logs</option>
</select>
</div>
<section class="workspace-section active" data-workspace-panel="xray" data-workspace-section-panel="users">
<!-- Edit Client Panel (hidden by default) -->
<div id="editXrayClientPanel" class="card hidden" style="margin-bottom:12px;border-color:rgba(245,158,11,.4);">
<div class="card-hdr">
@@ -355,6 +382,7 @@
</div>
<div id="editXrayClientStatus" class="hint" style="margin-top:4px;"></div>
</div>
</section>
<div class="card reseller-only hidden reseller-helper-card">
<div class="card-hdr"><div class="card-title">Área do revendedor</div><span class="chip green">cota única</span></div>
<p class="hint">Crie clientes Xray com a mesma experiência do painel principal. Cada cliente Xray desconta do mesmo limite usado pelas contas SSH.</p>
@@ -377,8 +405,10 @@
<div class="hint hidden" id="xrayServerHint"></div>
</div>
<section class="workspace-section active" data-workspace-panel="xray" data-workspace-section-panel="users">
<div class="workspace-section-heading"><div><span>01 · Gestão</span><h3>Usuários Xray</h3><p>Consulte clientes, conexões, validade e tráfego separados por inbound.</p></div><button class="btn btn-sm" id="xrayOpenCreateBtn" type="button">Criar usuário</button></div>
<!-- Inbounds & clients -->
<div class="card" style="margin-top:12px;">
<div class="card">
<div class="card-hdr">
<div class="card-title">Inbounds &amp; Clients</div>
<div class="card-actions"><button class="btn btn-ghost btn-sm" id="xLoadInboundsBtn">Reload</button></div>
@@ -387,9 +417,31 @@
<div class="hint" style="padding:8px 0;">Loading inbounds…</div>
</div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="xray" data-workspace-section-panel="create">
<div class="workspace-section-heading"><div><span>02 · Cadastro</span><h3>Criar usuário Xray</h3><p>Escolha o inbound e cadastre o cliente sem abrir a tabela de usuários.</p></div></div>
<div class="card workspace-form-card">
<div class="card-hdr"><div class="card-title">Novo cliente Xray</div><span class="chip" id="xCreateProtocolChip">inbound</span></div>
<form id="xCreateClientForm">
<div class="form-grid">
<div class="field" style="grid-column:1/-1"><label>Inbound</label><select id="xCreateInbound" required><option value="">Carregando inbounds…</option></select><span class="hint" id="xCreateInboundHint">Selecione onde o cliente será adicionado.</span></div>
<div class="field"><label>UUID</label><div class="field-row"><input id="xCreateUUID" required autocomplete="off" placeholder="Gerado automaticamente"/><button class="btn btn-ghost btn-sm" id="xCreateUUIDBtn" type="button">Gerar</button></div></div>
<div class="field"><label>Nome de exibição</label><input id="xCreateName" autocomplete="off" placeholder="Ex.: Cliente 01"/></div>
<div class="field"><label>Email / identificação</label><input id="xCreateEmail" autocomplete="off" placeholder="cliente@example"/></div>
<div class="field"><label>Expira em</label><input id="xCreateExpiry" type="datetime-local"/></div>
<div class="field"><label>Máximo de conexões <span class="hint">0 = ilimitado</span></label><input id="xCreateMaxConns" type="number" min="0" value="0"/></div>
</div>
<div class="form-actions"><button class="btn" id="xCreateClientBtn" type="submit">Criar usuário</button><button class="btn btn-ghost" id="xCreateCancelBtn" type="button">Voltar aos usuários</button></div>
<div class="statusbar"><span id="xCreateClientStatus">Preencha os dados do novo cliente.</span></div>
</form>
</div>
</section>
<section class="workspace-section xray-admin-only" data-workspace-panel="xray" data-workspace-section-panel="config">
<div class="workspace-section-heading"><div><span>03 · Serviço</span><h3>Configuração Xray</h3><p>Edite endpoint, inbounds, TLS e opções avançadas visualmente ou em JSON.</p></div></div>
<!-- Config editor -->
<div class="card xray-admin-only" style="margin-top:12px;">
<div class="card">
<div class="card-hdr">
<div class="card-title">Xray Config</div>
<div class="card-actions">
@@ -592,21 +644,35 @@
</div>
</div>
</div>
</section>
<section class="workspace-section xray-admin-only" data-workspace-panel="xray" data-workspace-section-panel="logs">
<div class="workspace-section-heading"><div><span>04 · Diagnóstico</span><h3>Logs do Xray</h3><p>Acompanhe as últimas mensagens do serviço em uma tela focada.</p></div></div>
<!-- Logs -->
<div class="card xray-admin-only" style="margin-top:12px;">
<div class="card">
<div class="card-hdr">
<div class="card-title">Logs <span class="chip">last 200 lines</span></div>
<div class="card-actions"><button class="btn btn-ghost btn-sm" id="xLoadLogsBtn">Refresh</button></div>
</div>
<pre class="log-box" id="xLogsBox"></pre>
</div>
</section>
</div><!-- /tab-xray -->
<!-- ═══════════ Resellers Tab (superadmin only) ═══════════ -->
<div class="tab-pane" id="tab-resellers">
<section class="page-hero" data-tone="amber"><div class="page-hero-copy"><span class="page-kicker">Partner operations</span><h2>Revendedores</h2><p>Controle cotas, validade e acesso dos parceiros em um só lugar.</p></div><div class="page-hero-pills"><span>Cotas</span><span>Expiração</span><span>Auditoria</span></div></section>
<div class="grid2">
<section class="page-hero" data-tone="amber"><div class="page-hero-copy"><span class="page-kicker">Partner operations</span><h2>Revendedores</h2><p>Controle cotas, validade e acesso dos parceiros em um só lugar.</p></div></section>
<div class="workspace-nav-shell" data-tone="amber">
<nav class="workspace-section-nav" id="resellerSectionNav" aria-label="Áreas de revendedores" style="--workspace-nav-columns:2">
<button class="active" type="button" data-workspace="resellers" data-workspace-section="users"><span></span> Revendedores</button>
<button type="button" data-workspace="resellers" data-workspace-section="create"><span></span> Criar revendedor</button>
</nav>
<select id="resellerSection" class="workspace-section-select" data-workspace-select="resellers" aria-label="Área de revendedores">
<option value="users">Revendedores</option><option value="create">Criar revendedor</option>
</select>
</div>
<section class="workspace-section active" data-workspace-panel="resellers" data-workspace-section-panel="users">
<div class="workspace-section-heading"><div><span>01 · Gestão</span><h3>Revendedores cadastrados</h3><p>Consulte cotas, consumo compartilhado, validade e situação de cada parceiro.</p></div></div>
<!-- Resellers list -->
<div class="card">
<div class="card-hdr">
@@ -624,11 +690,13 @@
<tbody id="resellersBody"></tbody>
</table>
</div>
<div class="statusbar"><span id="resellerStatus">Ready.</span></div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="resellers" data-workspace-section-panel="create">
<div class="workspace-section-heading"><div><span>02 · Cadastro</span><h3 id="resellerFormHeading">Criar revendedor</h3><p>Defina login, limite compartilhado, validade e acesso em uma tela dedicada.</p></div></div>
<!-- Create / edit reseller form -->
<div class="card">
<div class="card workspace-form-card">
<div class="card-hdr">
<div class="card-title" id="resellerFormTitle">Create Reseller</div>
<button class="btn btn-ghost btn-sm" id="cancelResellerBtn">Cancel</button>
@@ -646,7 +714,8 @@
</div>
</form>
</div>
</div>
</section>
<div class="statusbar workspace-section-status"><span id="resellerStatus">Ready.</span></div>
</div><!-- /tab-resellers -->
<!-- ═══════════ Servers Tab (superadmin only) ═══════════ -->
@@ -1123,11 +1192,25 @@
<!-- ═══════════ Server Config Tab (superadmin only) ═══════════ -->
<div class="tab-pane" id="tab-server">
<section class="page-hero" data-tone="blue"><div class="page-hero-copy"><span class="page-kicker">System studio</span><h2>Configurações</h2><p>Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.</p></div><div class="page-hero-pills"><span>Network</span><span>Tunnels</span><span>TLS</span></div></section>
<section class="page-hero" data-tone="blue"><div class="page-hero-copy"><span class="page-kicker">System studio</span><h2>Configurações</h2><p>Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.</p></div></section>
<div class="grid2">
<!-- ── Left column ── -->
<div>
<div class="workspace-nav-shell" data-tone="blue">
<nav class="workspace-section-nav" id="configSectionNav" aria-label="Áreas de configuração" style="--workspace-nav-columns:5">
<button class="active" type="button" data-workspace="config" data-workspace-section="general"><span></span> Rede e SSH</button>
<button type="button" data-workspace="config" data-workspace-section="slowdns"><span>DNS</span> SlowDNS</button>
<button type="button" data-workspace="config" data-workspace-section="udp"><span></span> UDP Gateway</button>
<button type="button" data-workspace="config" data-workspace-section="tls"><span></span> TLS</button>
<button type="button" data-workspace="config" data-workspace-section="xray"><span></span> Xray</button>
</nav>
<select id="configSection" class="workspace-section-select" data-workspace-select="config" aria-label="Área de configuração">
<option value="general">Rede e SSH</option><option value="slowdns">SlowDNS</option><option value="udp">UDP Gateway</option><option value="tls">TLS</option><option value="xray">Xray</option>
</select>
</div>
<div class="settings-workspace">
<section class="workspace-section active" data-workspace-panel="config" data-workspace-section-panel="general">
<div class="workspace-section-heading"><div><span>01 · Base</span><h3>Rede e SSH</h3><p>Configure listeners, limites padrão, tempo ocioso e o banner de conexão.</p></div></div>
<div class="settings-panel-grid">
<!-- Network -->
<div class="card">
@@ -1192,7 +1275,7 @@
</div>
<!-- Banner -->
<div class="card" style="margin-top:12px">
<div class="card settings-span-all" style="margin-top:12px">
<div class="card-hdr">
<div class="card-title">SSH Banner</div>
<span class="chip green">live</span>
@@ -1204,11 +1287,11 @@
<div class="hint" style="margin-top:6px;">Banner file: /opt/sshpanel/banner.txt</div>
</div>
</div><!-- /left -->
<!-- ── Right column ── -->
<div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="config" data-workspace-section-panel="slowdns">
<div class="workspace-section-heading"><div><span>02 · Túnel DNS</span><h3>SlowDNS / DNSTT</h3><p>Gerencie domínios, DNS local, capacidade, filas e reinício controlado.</p></div></div>
<!-- DNSTT -->
<div class="card">
<div class="card-hdr">
@@ -1290,6 +1373,10 @@
</div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="config" data-workspace-section-panel="udp">
<div class="workspace-section-heading"><div><span>03 · UDP</span><h3>UDP Gateway</h3><p>Defina listener, capacidade, expiração de mapa e reinício do serviço.</p></div></div>
<!-- UDPGW -->
<div class="card" style="margin-top:12px">
<div class="card-hdr">
@@ -1335,6 +1422,10 @@
</div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="config" data-workspace-section-panel="tls">
<div class="workspace-section-heading"><div><span>04 · Segurança</span><h3>Encaminhadores TLS</h3><p>Crie listeners TLS com certificado automático, colado ou armazenado em arquivo.</p></div></div>
<!-- TLS Forwarders -->
<div class="card" style="margin-top:12px">
<div class="card-hdr">
@@ -1385,6 +1476,10 @@
</div>
</div>
</section>
<section class="workspace-section" data-workspace-panel="config" data-workspace-section-panel="xray">
<div class="workspace-section-heading"><div><span>05 · Core</span><h3>Xray</h3><p>Ative o core, escolha o runtime e aplique os ajustes nativos seguros.</p></div></div>
<!-- Xray -->
<div class="card" style="margin-top:12px">
<div class="card-hdr">
@@ -1411,8 +1506,8 @@
</details>
</div>
</div><!-- /right -->
</div><!-- /grid2 -->
</section>
</div><!-- /settings-workspace -->
<!-- Save bar -->
<div class="save-bar">
@@ -1446,17 +1541,17 @@
<!-- app.js was split into ordered modules for maintainability. They are plain
classic scripts sharing one global scope; `defer` preserves execution order,
so behavior is identical to the old single file. Keep this load order. -->
<script defer src="assets/js/01-core.js?v=20260713infra4"></script>
<script defer src="assets/js/02-shell.js?v=20260713infra4"></script>
<script defer src="assets/js/03-ssh-users.js?v=20260713infra4"></script>
<script defer src="assets/js/04-xray.js?v=20260713infra4"></script>
<script defer src="assets/js/05-resellers.js?v=20260713infra4"></script>
<script defer src="assets/js/06-servers.js?v=20260713infra4"></script>
<script defer src="assets/js/07-stats-logs.js?v=20260713infra4"></script>
<script defer src="assets/js/08-server-config.js?v=20260713infra4"></script>
<script defer src="assets/js/09-xray-wizard.js?v=20260713infra4"></script>
<script defer src="assets/js/11-update-status.js?v=20260713infra4"></script>
<script defer src="assets/js/12-bot.js?v=20260713infra4"></script>
<script defer src="assets/js/10-boot.js?v=20260713infra4"></script>
<script defer src="assets/js/01-core.js?v=20260713sections5"></script>
<script defer src="assets/js/02-shell.js?v=20260713sections5"></script>
<script defer src="assets/js/03-ssh-users.js?v=20260713sections5"></script>
<script defer src="assets/js/04-xray.js?v=20260713sections5"></script>
<script defer src="assets/js/05-resellers.js?v=20260713sections5"></script>
<script defer src="assets/js/06-servers.js?v=20260713sections5"></script>
<script defer src="assets/js/07-stats-logs.js?v=20260713sections5"></script>
<script defer src="assets/js/08-server-config.js?v=20260713sections5"></script>
<script defer src="assets/js/09-xray-wizard.js?v=20260713sections5"></script>
<script defer src="assets/js/11-update-status.js?v=20260713sections5"></script>
<script defer src="assets/js/12-bot.js?v=20260713sections5"></script>
<script defer src="assets/js/10-boot.js?v=20260713sections5"></script>
</body>
</html>