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