diff --git a/README.md b/README.md index 47b1afd..6d21e26 100644 --- a/README.md +++ b/README.md @@ -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**) diff --git a/admin/assets/app.css b/admin/assets/app.css index 55df046..8f1eefd 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -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 --- */ diff --git a/admin/assets/js/01-core.js b/admin/assets/js/01-core.js index 1f1a822..a0f172b 100644 --- a/admin/assets/js/01-core.js +++ b/admin/assets/js/01-core.js @@ -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"); diff --git a/admin/assets/js/02-shell.js b/admin/assets/js/02-shell.js index bb80a65..63a3051 100644 --- a/admin/assets/js/02-shell.js +++ b/admin/assets/js/02-shell.js @@ -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"); diff --git a/admin/assets/js/03-ssh-users.js b/admin/assets/js/03-ssh-users.js index 6a6c883..2c46719 100644 --- a/admin/assets/js/03-ssh-users.js +++ b/admin/assets/js/03-ssh-users.js @@ -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}); diff --git a/admin/assets/js/04-xray.js b/admin/assets/js/04-xray.js index 91bc889..80c4262 100644 --- a/admin/assets/js/04-xray.js +++ b/admin/assets/js/04-xray.js @@ -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 = ` -
-
- -
- - -
-
-
-
-
-
-
-
- - -
`; - addForm.querySelector(`#genUUID-${formKey}`).addEventListener("click", () => { document.getElementById(`newUUID-${formKey}`).value = genUUID(); }); - addForm.querySelector(`#addClient-${formKey}`).addEventListener("click", () => addClient(ib.tag, formKey)); - addForm.querySelector(`#cancelAddClient-${formKey}`).addEventListener("click", () => addForm.classList.add("hidden")); - section.appendChild(addForm); - // Clients table const tblWrap = document.createElement("div"); tblWrap.className = "tbl-wrap"; @@ -286,36 +266,122 @@ function renderInbounds(inbounds, options = {}) { }); } -function openAddClient(tag, formKey = tag) { - const form = document.getElementById(`add-form-${formKey}`); - if (form) { form.classList.remove("hidden"); } - const uuidField = document.getElementById(`newUUID-${formKey}`); - if (uuidField && !uuidField.value) uuidField.value = genUUID(); +let xrayCreatorInbounds = []; +let xrayCreatorInboundSignature = ""; + +function syncXrayCreatorInbounds(inbounds = []) { + const select = document.getElementById("xCreateInbound"); + if (!select) return; + const previous = select.value; + const nextInbounds = (inbounds || []).filter(ib => ib?.tag).map(ib => ({ + tag: String(ib.tag), + protocol: String(ib.protocol || "xray").toUpperCase(), + port: ib.port ?? "?", + })); + const nextSignature = JSON.stringify(nextInbounds); + xrayCreatorInbounds = nextInbounds; + if (nextSignature === xrayCreatorInboundSignature) { + updateXrayCreatorInboundLabel(); + return; + } + xrayCreatorInboundSignature = nextSignature; + select.replaceChildren(); + if (!xrayCreatorInbounds.length) { + const option = document.createElement("option"); + option.value = ""; + option.textContent = t("No compatible inbound found"); + select.appendChild(option); + select.disabled = true; + } else { + xrayCreatorInbounds.forEach(inbound => { + const option = document.createElement("option"); + option.value = inbound.tag; + option.textContent = `${inbound.protocol} · ${inbound.tag} · :${inbound.port}`; + select.appendChild(option); + }); + select.disabled = false; + select.value = xrayCreatorInbounds.some(inbound => inbound.tag === previous) ? previous : xrayCreatorInbounds[0].tag; + } + updateXrayCreatorInboundLabel(); } -async function addClient(tag, formKey = tag) { - const uuidEl = document.getElementById(`newUUID-${formKey}`); - const emailEl = document.getElementById(`newEmail-${formKey}`); - const nameEl = document.getElementById(`newName-${formKey}`); - const expiryEl = document.getElementById(`newExpiry-${formKey}`); - const maxConnsEl = document.getElementById(`newMaxConns-${formKey}`); - const uuid = (uuidEl?.value || "").trim(); - const email = (emailEl?.value || "").trim(); - const name = (nameEl?.value || "").trim(); - const expiresAt = isoFromLocal(expiryEl?.value || ""); - const maxConns = parseInt(maxConnsEl?.value || "0", 10) || 0; - if (!uuid) { xStatus.textContent = t("UUID required."); return; } +function updateXrayCreatorInboundLabel() { + const selected = document.getElementById("xCreateInbound")?.value || ""; + const inbound = xrayCreatorInbounds.find(item => item.tag === selected); + const chip = document.getElementById("xCreateProtocolChip"); + const hint = document.getElementById("xCreateInboundHint"); + if (chip) chip.textContent = inbound ? inbound.protocol : t("No inbound"); + if (hint) hint.textContent = inbound + ? t("The client will be added to {tag} on port {port}.", {tag:inbound.tag, port:inbound.port}) + : t("Create or enable a compatible inbound before adding a client."); +} + +function prepareXrayClientCreator(preferredTag = "") { + const form = document.getElementById("xCreateClientForm"); + form?.reset(); + const inbound = document.getElementById("xCreateInbound"); + if (inbound && preferredTag && xrayCreatorInbounds.some(item => item.tag === preferredTag)) inbound.value = preferredTag; + const uuid = document.getElementById("xCreateUUID"); + if (uuid) uuid.value = genUUID(); + const maxConns = document.getElementById("xCreateMaxConns"); + if (maxConns) maxConns.value = "0"; + const status = document.getElementById("xCreateClientStatus"); + if (status) status.textContent = xrayCreatorInbounds.length ? t("Ready to create a new Xray client.") : t("Waiting for a compatible inbound."); + updateXrayCreatorInboundLabel(); + requestAnimationFrame(() => document.getElementById("xCreateName")?.focus()); +} + +function openAddClient(tag) { + setWorkspaceSection("xray", "create"); + prepareXrayClientCreator(tag); +} + +async function submitXrayClientCreator(event) { + event?.preventDefault?.(); + const tag = document.getElementById("xCreateInbound")?.value || ""; + const uuid = (document.getElementById("xCreateUUID")?.value || "").trim(); + const status = document.getElementById("xCreateClientStatus"); + const button = document.getElementById("xCreateClientBtn"); + if (!tag) { if (status) status.textContent = t("Select a compatible inbound."); return false; } + if (!uuid) { if (status) status.textContent = t("UUID required."); return false; } + const payload = { + inbound_tag: tag, + uuid, + email: (document.getElementById("xCreateEmail")?.value || "").trim(), + name: (document.getElementById("xCreateName")?.value || "").trim(), + expires_at: isoFromLocal(document.getElementById("xCreateExpiry")?.value || ""), + max_connections: parseInt(document.getElementById("xCreateMaxConns")?.value || "0", 10) || 0, + server_id: selectedXrayServer(), + }; + if (button) button.disabled = true; + if (status) status.textContent = t("Creating Xray client…"); try { - const res = await api("/api/xray/clients/add", { - method: "POST", - body: JSON.stringify({ inbound_tag: tag, uuid, email, name, expires_at: expiresAt, max_connections: maxConns, server_id: selectedXrayServer() }), - }); + const res = await api("/api/xray/clients/add", { method:"POST", body:JSON.stringify(payload) }); if (!res.ok) throw new Error(await res.text()); - xStatus.textContent = t("Client {id}… added. Native mode hot-reloads without restart.", {id: uuid.slice(0,8)}); - setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500); + const success = t("Client {id}… added. Native mode hot-reloads without restart.", {id:uuid.slice(0,8)}); + if (status) status.textContent = success; + xStatus.textContent = success; + showPanelToast(t("Xray user created successfully."), "success", t("Xray user")); + setTimeout(() => { loadInbounds({ force:true }); if (currentRole === "reseller") loadMe(); }, 700); + const name = document.getElementById("xCreateName"); + const email = document.getElementById("xCreateEmail"); + const expiry = document.getElementById("xCreateExpiry"); + if (name) name.value = ""; + if (email) email.value = ""; + if (expiry) expiry.value = ""; + const nextUUID = document.getElementById("xCreateUUID"); + if (nextUUID) nextUUID.value = genUUID(); + return true; } catch (e) { - if (e.message==="auth") doAuthError(); - else xStatus.textContent = t("Error: {error}", {error: e.message}); + if (e.message === "auth") doAuthError(); + else { + if (status) status.textContent = t("Error: {error}", {error:e.message}); + xStatus.textContent = t("Error: {error}", {error:e.message}); + showPanelToast(t("Could not create the Xray user: {error}", {error:e.message}), "error", t("Xray user")); + } + return false; + } finally { + if (button) button.disabled = false; } } diff --git a/admin/assets/js/05-resellers.js b/admin/assets/js/05-resellers.js index b0ea67a..d3d601b 100644 --- a/admin/assets/js/05-resellers.js +++ b/admin/assets/js/05-resellers.js @@ -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; diff --git a/admin/index.html b/admin/index.html index 1063f62..91b0ffa 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -85,8 +85,7 @@
-
Command center

Visão geral

Contas, conexões e infraestrutura em uma leitura rápida.

- +

Visão geral

Contas, conexões e infraestrutura em uma leitura rápida.

@@ -221,6 +220,16 @@
+
+ + +
+ -
+
+
01 · Gestão

Usuários SSH e SlowDNS

Consulte conexões, limites, validade e ações de cada conta.

@@ -273,16 +283,17 @@
-
Ready.
+
+
+
02 · Cadastro

Criar usuário

Defina autenticação, validade, conexões e velocidade em uma tela dedicada.

-
+
-
Create / update user
- +
Criar usuário SSH
- +
+
Ready.
@@ -337,6 +349,21 @@
+ +
+ + +
+ +
+
+
+
01 · Gestão

Usuários Xray

Consulte clientes, conexões, validade e tráfego separados por inbound.

-
+
Inbounds & Clients
@@ -387,9 +417,31 @@
Loading inbounds…
+
+
+
02 · Cadastro

Criar usuário Xray

Escolha o inbound e cadastre o cliente sem abrir a tabela de usuários.

+
+
Novo cliente Xray
inbound
+
+
+
Selecione onde o cliente será adicionado.
+
+
+
+
+
+
+
+
Preencha os dados do novo cliente.
+
+
+
+ +
+
03 · Serviço

Configuração Xray

Edite endpoint, inbounds, TLS e opções avançadas visualmente ou em JSON.

-
+
Xray Config
@@ -592,21 +644,35 @@
+
+
+
04 · Diagnóstico

Logs do Xray

Acompanhe as últimas mensagens do serviço em uma tela focada.

-
+
Logs last 200 lines

     
+
-
Partner operations

Revendedores

Controle cotas, validade e acesso dos parceiros em um só lugar.

CotasExpiraçãoAuditoria
-
+
Partner operations

Revendedores

Controle cotas, validade e acesso dos parceiros em um só lugar.

+
+ + +
+
+
01 · Gestão

Revendedores cadastrados

Consulte cotas, consumo compartilhado, validade e situação de cada parceiro.

@@ -624,11 +690,13 @@
-
Ready.
+
+
+
02 · Cadastro

Criar revendedor

Defina login, limite compartilhado, validade e acesso em uma tela dedicada.

-
+
Create Reseller
@@ -646,7 +714,8 @@
-
+
+
Ready.
@@ -1123,11 +1192,25 @@
-
System studio

Configurações

Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.

NetworkTunnelsTLS
+
System studio

Configurações

Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.

-
- -
+
+ + +
+ +
+
+
01 · Base

Rede e SSH

Configure listeners, limites padrão, tempo ocioso e o banner de conexão.

+
@@ -1192,7 +1275,7 @@
-
+
SSH Banner
live @@ -1204,11 +1287,11 @@
Banner file: /opt/sshpanel/banner.txt
-
- - -
+
+
+
+
02 · Túnel DNS

SlowDNS / DNSTT

Gerencie domínios, DNS local, capacidade, filas e reinício controlado.

@@ -1290,6 +1373,10 @@
+
+ +
+
03 · UDP

UDP Gateway

Defina listener, capacidade, expiração de mapa e reinício do serviço.

@@ -1335,6 +1422,10 @@
+
+ +
+
04 · Segurança

Encaminhadores TLS

Crie listeners TLS com certificado automático, colado ou armazenado em arquivo.

@@ -1385,6 +1476,10 @@
+
+ +
+
05 · Core

Xray

Ative o core, escolha o runtime e aplique os ajustes nativos seguros.

@@ -1411,8 +1506,8 @@
-
-
+ +
@@ -1446,17 +1541,17 @@ - - - - - - - - - - - - + + + + + + + + + + + +