Fix admin panel
This commit is contained in:
@@ -13,6 +13,8 @@ DragonCoreSSH V40 é um painel/servidor em Go para SSH com HTTP Injection, paine
|
||||
- Configurador visual para VLESS e VMess
|
||||
- Editor visual completo de inbounds (criar, editar, duplicar e remover)
|
||||
- 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
|
||||
- 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**)
|
||||
@@ -61,6 +63,8 @@ O listener HTTP interno escolhe o caminho mais específico, portanto `/ssh` é e
|
||||
|
||||
Para configurações XHTTP antigas, carregue a configuração visual e clique em **Ativar SSH /ssh** no cartão VLESS/VMess existente. O painel preserva o inbound original, seus clientes, path, host, modo, opções avançadas e TLS; cria apenas o inbound SSH correspondente na mesma porta e salva/reinicia o Xray. Não é necessário recriar clientes ou o endpoint.
|
||||
|
||||
A confirmação dessa migração é exibida dentro do próprio painel. Se a gravação falhar, o inbound SSH temporário é removido do rascunho e o inbound antigo permanece intacto, permitindo tentar novamente após corrigir o erro exibido.
|
||||
|
||||
### Requisitos
|
||||
|
||||
- Servidor Linux com `systemd`
|
||||
@@ -537,6 +541,8 @@ DragonCoreSSH V40 is a Go-based SSH HTTP Injection server with a web panel, Post
|
||||
- Visual configurator for VLESS and VMess
|
||||
- Full visual inbound editor (create, edit, duplicate, and remove)
|
||||
- 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
|
||||
- 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**)
|
||||
@@ -585,6 +591,8 @@ The internal HTTP listener selects the most specific path, so `/ssh` reaches the
|
||||
|
||||
For older XHTTP configurations, load the visual configuration and click **Enable SSH /ssh** on the existing VLESS/VMess card. The panel preserves the original inbound, clients, path, host, mode, advanced options, and TLS; it only creates the matching SSH inbound on the same port and then saves/restarts Xray. There is no need to recreate clients or the endpoint.
|
||||
|
||||
The migration confirmation is rendered inside the panel. If saving fails, the temporary SSH inbound is removed from the draft and the old inbound remains intact, so the operation can be retried after fixing the displayed error.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Linux server with `systemd`
|
||||
|
||||
+15
-2
@@ -666,6 +666,17 @@ select:disabled {
|
||||
.page-hero[data-tone="green"]{--hero-accent:49,214,123}.page-hero[data-tone="purple"]{--hero-accent:139,92,246}.page-hero[data-tone="amber"]{--hero-accent:255,200,87}.page-hero[data-tone="blue"]{--hero-accent:80,145,255}
|
||||
.page-hero-copy,.page-hero-pills,.page-hero-mark{position:relative;z-index:1}.page-hero-copy{max-width:700px}.page-kicker{display:block;color:rgb(var(--hero-accent));font-size:.69rem;font-weight:900;letter-spacing:.17em;text-transform:uppercase}.page-hero h2{margin-top:7px;font-size:2rem;line-height:1.05;letter-spacing:-.045em}.page-hero p{max-width:650px;margin-top:9px;color:var(--muted);font-size:.82rem;line-height:1.55}.page-hero-pills{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap;max-width:42%}.page-hero-pills span{padding:7px 10px;border:1px solid rgba(var(--hero-accent),.18);border-radius:999px;background:rgba(var(--hero-accent),.075);color:var(--text-2);font-size:.69rem;font-weight:850;letter-spacing:.04em}.page-hero-mark{display:grid;place-items:center;width:64px;height:64px;border:1px solid rgba(var(--hero-accent),.25);border-radius:22px;background:rgba(var(--hero-accent),.10);color:rgb(var(--hero-accent));font-size:1.05rem;font-weight:950;box-shadow:0 18px 44px rgba(0,0,0,.24)}
|
||||
|
||||
/* Bot-style live workspace heroes shared by SSH, Xray, and infrastructure. */
|
||||
.page-hero.status-hero{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;min-height:0}.status-hero .page-hero-copy,.workspace-hero-actions,.workspace-overview-grid,.workspace-hero-toolbar{position:relative;z-index:1}.workspace-hero-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;max-width:620px}.workspace-live-status{display:inline-flex;align-items:center;gap:7px;max-width:360px;min-height:34px;padding:7px 11px;overflow:hidden;border:1px solid rgba(160,174,192,.14);border-radius:999px;background:rgba(255,255,255,.04);color:var(--muted);font-size:.7rem;font-weight:850;text-overflow:ellipsis;white-space:nowrap}.workspace-live-status::before{content:"";width:7px;height:7px;flex:0 0 auto;border-radius:50%;background:currentColor;box-shadow:0 0 12px currentColor}.workspace-live-status.is-ok{color:#72e6a4;border-color:rgba(49,214,123,.25);background:rgba(49,214,123,.08)}.workspace-live-status.is-warn{color:#ffd36d;border-color:rgba(255,200,87,.25);background:rgba(255,200,87,.08)}.workspace-live-status.is-error{color:#ff8f99;border-color:rgba(255,91,105,.28);background:rgba(255,91,105,.08)}.workspace-live-status.is-loading{color:#71dff0;border-color:rgba(34,211,238,.23);background:rgba(34,211,238,.075)}
|
||||
.workspace-overview-grid{grid-column:1/-1;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:11px;width:100%;margin-top:23px}.workspace-overview-grid.five{grid-template-columns:repeat(5,minmax(0,1fr))}.workspace-overview-card{display:flex;align-items:center;gap:11px;min-width:0;padding:13px 14px;border:1px solid rgba(160,174,192,.14);border-radius:18px;background:rgba(255,255,255,.04);backdrop-filter:blur(8px)}.workspace-overview-card>div{display:flex;flex:1;flex-direction:column;gap:4px;min-width:0}.workspace-overview-card small{color:var(--muted);font-size:.64rem;font-weight:800;letter-spacing:.095em;text-transform:uppercase}.workspace-overview-card strong{overflow:hidden;color:var(--text);font-size:1rem;text-overflow:ellipsis;white-space:nowrap}.workspace-card-note{overflow:hidden;color:var(--muted);font-size:.61rem;text-overflow:ellipsis;white-space:nowrap}.workspace-overview-icon{display:grid;place-items:center;flex:0 0 auto;width:34px;height:34px;border:1px solid rgba(34,211,238,.17);border-radius:12px;background:rgba(34,211,238,.12);color:#4de0ef;font-size:.68rem;font-weight:950}.workspace-overview-icon.green{color:#72e6a4;border-color:rgba(49,214,123,.18);background:rgba(49,214,123,.11)}.workspace-overview-icon.purple{color:#b19cff;border-color:rgba(139,92,246,.2);background:rgba(139,92,246,.13)}.workspace-overview-icon.amber{color:#ffd36d;border-color:rgba(255,200,87,.18);background:rgba(255,200,87,.11)}.workspace-overview-icon.blue{color:#84b3ff;border-color:rgba(80,145,255,.2);background:rgba(80,145,255,.12)}.workspace-overview-icon.red{color:#ff8f99;border-color:rgba(255,91,105,.2);background:rgba(255,91,105,.11)}.workspace-mini-meter{height:3px;margin-top:2px;overflow:hidden;border-radius:999px;background:rgba(148,163,184,.1)}.workspace-mini-meter span{display:block;width:0;height:100%;border-radius:inherit;background:linear-gradient(90deg,rgb(var(--hero-accent)),#74edb0);transition:width .25s ease}
|
||||
.workspace-hero-toolbar{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:13px;padding-top:13px;border-top:1px solid rgba(148,163,184,.1)}.workspace-toolbar-status{min-width:0;color:var(--muted);font-size:.7rem;line-height:1.45}.workspace-toolbar-actions{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}.workspace-toolbar-actions .input-sm{max-width:220px}
|
||||
|
||||
/* 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}
|
||||
|
||||
/* 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}}
|
||||
|
||||
/* Bring legacy screens up to the Bot workspace's information density. */
|
||||
.tab-pane:not(#tab-bot)>.grid2,.tab-pane:not(#tab-bot)>#serversListView>.grid2{gap:16px}.tab-pane:not(#tab-bot) .card-hdr{padding-bottom:12px;border-bottom:1px solid rgba(148,163,184,.09)}.tab-pane:not(#tab-bot) .card-title{font-size:.96rem}.tab-pane:not(#tab-bot) .statusbar{margin-top:13px;padding-top:11px;border-top:1px solid rgba(148,163,184,.08)}
|
||||
|
||||
@@ -680,8 +691,10 @@ 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:1100px){.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 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}.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}}
|
||||
@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: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 --- */
|
||||
#tab-bot{--bot-accent:#7c5cff;--bot-line:rgba(160,174,192,.14);}
|
||||
|
||||
@@ -133,6 +133,28 @@ Object.assign(I18N_TEXT["pt-BR"], {
|
||||
"Configured inbounds":"Inbounds configurados","Visual inbound help":"Edite qualquer cartão visualmente ou use JSON para campos avançados.","New inbound":"Novo inbound","Visual editor":"Editor visual","Add inbound":"Adicionar inbound","Save changes":"Salvar alterações","Duplicate":"Duplicar","Remove":"Remover","Save config and restart":"Salvar configuração e reiniciar",
|
||||
"Old XHTTP configuration?":"Configuração XHTTP antiga?","Legacy XHTTP migration copy":"Use “Ativar SSH /ssh” no cartão do inbound existente. O painel mantém o inbound, os clientes e todas as opções atuais; adiciona somente a rota SSH e reinicia o Xray.","Enable SSH /ssh":"Ativar SSH /ssh","SSH /ssh enabled":"SSH /ssh ativado","Legacy XHTTP migration":"Migração de configuração XHTTP antiga"
|
||||
});
|
||||
Object.assign(I18N_TEXT["en-US"], {
|
||||
"Infrastructure":"Infrastructure","Infrastructure areas":"Infrastructure areas","Infrastructure area":"Infrastructure area","Waiting for data":"Waiting for data","Operation":"Operation","Accounts":"Accounts","Active accounts":"Active","Online now":"Online now","Counters":"Counters","Save mode":"Save mode",
|
||||
"Nodes":"Nodes","Active nodes":"Active","SSH enabled":"SSH enabled","Xray enabled":"Xray enabled","Sessions":"Sessions","Processor":"Processor","Memory":"Memory","Network now":"Network now","Interfaces":"Interfaces","Today":"Today","This month":"This month","Recent period":"Recent period","Update":"Refresh","Clear history":"Clear history",
|
||||
"Infrastructure workspace copy":"Manage nodes, monitor system health, and inspect traffic from one workspace.","Confirmation":"Confirmation","Confirm action":"Confirm action","Confirm":"Confirm","Completed":"Completed","Action failed":"Action failed","Attention":"Attention","Information":"Information","Close":"Close",
|
||||
"Loading SSH status…":"Loading SSH status…","Loading":"Loading","Could not load SSH status":"Could not load SSH status","Error":"Error","Online":"Online","SSH data updated at {time}":"SSH data updated at {time}","Delete SSH account":"Delete SSH account","The active SSH sessions for this account will be disconnected.":"The active SSH sessions for this account will be disconnected.","Delete account":"Delete account",
|
||||
"Loading Xray status…":"Loading Xray status…","Could not load Xray status":"Could not load Xray status","Remove Xray client":"Remove Xray client","The client will lose access immediately after the configuration reload.":"The client will lose access immediately after the configuration reload.","Remove client":"Remove client","Client removed successfully.":"Client removed successfully.","Xray client":"Xray client",
|
||||
"Loading infrastructure…":"Loading infrastructure…","Infrastructure loaded with fallback data":"Infrastructure loaded with fallback data","{count} active nodes · updated {time}":"{count} active nodes · updated {time}","Delete managed server":"Delete managed server","Delete server \"{name}\"?":"Delete server \"{name}\"?","The remote node is not erased, but it will be removed from this panel and can no longer receive managed actions.":"The remote node is not erased, but it will be removed from this panel and can no longer receive managed actions.","Delete server":"Delete server",
|
||||
"Updating live status…":"Updating live status…","Live · updated {time}":"Live · updated {time}","Error loading server status":"Error loading server status","Clean live interface totals":"Clean live interface totals","Clean the live Interface totals now?":"Clean the live Interface totals now?","VnStat daily and monthly history will be preserved.":"VnStat daily and monthly history will be preserved.","Clean totals":"Clean totals","Live interface totals were cleaned.":"Live interface totals were cleaned.","Traffic counters":"Traffic counters","Clean VnStat history":"Clean VnStat history","Clean all daily and monthly traffic history?":"Clean all daily and monthly traffic history?","Live interface totals are separate and will not be reset.":"Live interface totals are separate and will not be reset.","Clean history":"Clean history","VnStat history was cleaned.":"VnStat history was cleaned.","Traffic history":"Traffic history","Clean panel log":"Clean panel log","Clean the current panel log now?":"Clean the current panel log now?","This only clears the panel log file. Automatic size-based cleanup remains enabled.":"This only clears the panel log file. Automatic size-based cleanup remains enabled.","Clean log":"Clean log",
|
||||
"Delete reseller":"Delete reseller","Delete reseller \"{name}\"?":"Delete reseller \"{name}\"?","Their owned access will be removed and active SSH sessions will be disconnected.":"Their owned access will be removed and active SSH sessions will be disconnected.","Remove inbound":"Remove inbound","Remove inbound {name}?":"Remove inbound {name}?","Clients attached only to this inbound will stop connecting after the configuration is saved.":"Clients attached only to this inbound will stop connecting after the configuration is saved.",
|
||||
"This endpoint already has an SSH /ssh route.":"This endpoint already has an SSH /ssh route.","Add SSH /ssh without rebuilding this inbound.":"Add SSH /ssh without rebuilding this inbound.","SSH migration attention":"SSH migration attention","Could not enable SSH /ssh":"Could not enable SSH /ssh","Select a VLESS/VMess inbound using XHTTP.":"Select a VLESS/VMess inbound using XHTTP.","Load the selected server configuration before enabling SSH.":"Load the selected server configuration before enabling SSH.","Shared SSH requires native Xray mode. Select Internal native emulator and save the mode first.":"Shared SSH requires native Xray mode. Select Internal native emulator and save the mode first.","Inbound {name} uses {security}, which native SSH XHTTP does not support. Use TLS or no TLS.":"Inbound {name} uses {security}, which native SSH XHTTP does not support. Use TLS or no TLS.","SSH is already enabled on /ssh by inbound {name}.":"SSH is already enabled on /ssh by inbound {name}.","Path /ssh is already used by inbound {name}. Edit that path first.":"Path /ssh is already used by inbound {name}. Edit that path first.","This inbound uses TLS but has no reusable certificate and key file paths.":"This inbound uses TLS but has no reusable certificate and key file paths.","Safe XHTTP migration":"Safe XHTTP migration","Enable SSH on /ssh":"Enable SSH on /ssh","Add SSH to the same endpoint without rebuilding {name}?":"Add SSH to the same endpoint without rebuilding {name}?","Listener":"Listener","Existing path preserved":"Existing path preserved","New SSH path":"New SSH path","Clients preserved":"Clients preserved","Enabling SSH…":"Enabling SSH…","Could not enable SSH: {error}":"Could not enable SSH: {error}","Migration was cancelled because it would alter the old inbound.":"Migration was cancelled because it would alter the old inbound.","SSH /ssh added to the draft without changing {name}.":"SSH /ssh added to the draft without changing {name}.","Enabling SSH /ssh without changing {name}…":"Enabling SSH /ssh without changing {name}…","The SSH route could not be saved. The old inbound was not changed.":"The SSH route could not be saved. The old inbound was not changed.","SSH /ssh was saved, but Xray could not restart. Use the Restart button after checking the Xray log.":"SSH /ssh was saved, but Xray could not restart. Use the Restart button after checking the Xray log.","SSH /ssh is active. The old inbound and all clients were preserved.":"SSH /ssh is active. The old inbound and all clients were preserved.","Configuration for this server was not loaded.":"Configuration for this server was not loaded.","Invalid visual config: {error}":"Invalid visual config: {error}","Xray could not restart.":"Xray could not restart.","Could not save configuration: {error}":"Could not save configuration: {error}","selected":"selected","this inbound":"this inbound","the old inbound":"the old inbound","untagged":"untagged"
|
||||
});
|
||||
Object.assign(I18N_TEXT["pt-BR"], {
|
||||
"Infrastructure":"Infraestrutura","Infrastructure areas":"Áreas da infraestrutura","Infrastructure area":"Área da infraestrutura","Waiting for data":"Aguardando dados","Operation":"Operação","Accounts":"Contas","Active accounts":"Ativas","Online now":"Online agora","Counters":"Contadores","Save mode":"Salvar modo",
|
||||
"Nodes":"Nós","Active nodes":"Ativos","SSH enabled":"SSH habilitado","Xray enabled":"Xray habilitado","Sessions":"Sessões","Processor":"Processador","Memory":"Memória","Network now":"Rede agora","Interfaces":"Interfaces","Today":"Hoje","This month":"Este mês","Recent period":"Período recente","Update":"Atualizar","Clear history":"Limpar histórico",
|
||||
"Infrastructure workspace copy":"Gerencie nós, acompanhe a saúde do sistema e consulte tráfego em um único espaço.","Confirmation":"Confirmação","Confirm action":"Confirmar ação","Confirm":"Confirmar","Completed":"Concluído","Action failed":"Ação não concluída","Attention":"Atenção","Information":"Informação","Close":"Fechar",
|
||||
"Loading SSH status…":"Carregando status SSH…","Loading":"Carregando","Could not load SSH status":"Não foi possível carregar o status SSH","Error":"Erro","Online":"Online","SSH data updated at {time}":"Dados SSH atualizados às {time}","Delete SSH account":"Excluir conta SSH","The active SSH sessions for this account will be disconnected.":"As sessões SSH ativas desta conta serão desconectadas.","Delete account":"Excluir conta",
|
||||
"Loading Xray status…":"Carregando status do Xray…","Could not load Xray status":"Não foi possível carregar o status do Xray","Remove Xray client":"Remover cliente Xray","The client will lose access immediately after the configuration reload.":"O cliente perderá o acesso imediatamente após recarregar a configuração.","Remove client":"Remover cliente","Client removed successfully.":"Cliente removido com sucesso.","Xray client":"Cliente Xray",
|
||||
"Loading infrastructure…":"Carregando infraestrutura…","Infrastructure loaded with fallback data":"Infraestrutura carregada com dados locais de segurança","{count} active nodes · updated {time}":"{count} nós ativos · atualizado às {time}","Delete managed server":"Excluir servidor gerenciado","Delete server \"{name}\"?":"Excluir o servidor \"{name}\"?","The remote node is not erased, but it will be removed from this panel and can no longer receive managed actions.":"O nó remoto não será apagado, mas será removido deste painel e deixará de receber ações gerenciadas.","Delete server":"Excluir servidor",
|
||||
"Updating live status…":"Atualizando status ao vivo…","Live · updated {time}":"Ao vivo · atualizado às {time}","Error loading server status":"Erro ao carregar status do servidor","Clean live interface totals":"Limpar totais ao vivo das interfaces","Clean the live Interface totals now?":"Limpar agora os totais ao vivo das interfaces?","VnStat daily and monthly history will be preserved.":"O histórico diário e mensal do VnStat será preservado.","Clean totals":"Limpar totais","Live interface totals were cleaned.":"Os totais ao vivo das interfaces foram limpos.","Traffic counters":"Contadores de tráfego","Clean VnStat history":"Limpar histórico VnStat","Clean all daily and monthly traffic history?":"Limpar todo o histórico diário e mensal de tráfego?","Live interface totals are separate and will not be reset.":"Os totais ao vivo das interfaces são separados e não serão zerados.","Clean history":"Limpar histórico","VnStat history was cleaned.":"O histórico VnStat foi limpo.","Traffic history":"Histórico de tráfego","Clean panel log":"Limpar log do painel","Clean the current panel log now?":"Limpar agora o log atual do painel?","This only clears the panel log file. Automatic size-based cleanup remains enabled.":"Isso limpa somente o arquivo de log do painel. A limpeza automática por tamanho continuará ativa.","Clean log":"Limpar log",
|
||||
"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"
|
||||
});
|
||||
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;
|
||||
@@ -298,6 +320,11 @@ const fMaxConn = document.getElementById("fMaxConn");
|
||||
const fExpires = document.getElementById("fExpires");
|
||||
const fUp = document.getElementById("fUp");
|
||||
const fDown = document.getElementById("fDown");
|
||||
const sshLiveStatus = document.getElementById("sshLiveStatus");
|
||||
const sshMetricState = document.getElementById("sshMetricState");
|
||||
const sshMetricTotal = document.getElementById("sshMetricTotal");
|
||||
const sshMetricActive = document.getElementById("sshMetricActive");
|
||||
const sshMetricOnline = document.getElementById("sshMetricOnline");
|
||||
|
||||
// Xray
|
||||
const xrayChip = document.getElementById("xrayChip");
|
||||
@@ -354,6 +381,14 @@ const managedConfigStatus = document.getElementById("managedConfigStatus");
|
||||
const serversStatusGrid = document.getElementById("serversStatusGrid");
|
||||
const serversStatusPageStatus = document.getElementById("serversStatusPageStatus");
|
||||
const serversStatusCountChip = document.getElementById("serversStatusCountChip");
|
||||
const fleetLiveStatus = document.getElementById("fleetLiveStatus");
|
||||
const fleetMetricNodes = document.getElementById("fleetMetricNodes");
|
||||
const fleetMetricActive = document.getElementById("fleetMetricActive");
|
||||
const fleetMetricSSH = document.getElementById("fleetMetricSSH");
|
||||
const fleetMetricXray = document.getElementById("fleetMetricXray");
|
||||
const fleetStatusOnline = document.getElementById("fleetStatusOnline");
|
||||
const fleetStatusOffline = document.getElementById("fleetStatusOffline");
|
||||
const fleetStatusSessions = document.getElementById("fleetStatusSessions");
|
||||
|
||||
// Stats
|
||||
const cpuVal = document.getElementById("cpuVal");
|
||||
@@ -364,6 +399,8 @@ const memDetail = document.getElementById("memDetail");
|
||||
const ifaceBody = document.getElementById("ifaceBody");
|
||||
const ifaceSummary = document.getElementById("ifaceSummary");
|
||||
const statsUpdated = document.getElementById("statsUpdated");
|
||||
const statsNetVal = document.getElementById("statsNetVal");
|
||||
const statsIfaceVal = document.getElementById("statsIfaceVal");
|
||||
const resetIfaceStatsBtn = document.getElementById("resetIfaceStatsBtn");
|
||||
const dnsttDashboardCard = document.getElementById("dnsttDashboardCard");
|
||||
const dnsttHealthUpdated = document.getElementById("dnsttHealthUpdated");
|
||||
@@ -381,9 +418,99 @@ const vnstatStatus = document.getElementById("vnstatStatus");
|
||||
const vnTodayTotal = document.getElementById("vnTodayTotal");
|
||||
const vnMonthTotal = document.getElementById("vnMonthTotal");
|
||||
const vnIfaceCount = document.getElementById("vnIfaceCount");
|
||||
const vnLatestPeriod = document.getElementById("vnLatestPeriod");
|
||||
const reloadVnstatBtn = document.getElementById("reloadVnstatBtn");
|
||||
const resetVnstatBtn = document.getElementById("resetVnstatBtn");
|
||||
|
||||
// Shared panel-native dialog and toast surfaces. Destructive and migration
|
||||
// actions use these instead of browser confirm/alert prompts.
|
||||
const panelConfirmDialog = document.getElementById("panelConfirmDialog");
|
||||
const panelDialogCard = panelConfirmDialog?.querySelector(".panel-dialog-card");
|
||||
const panelDialogIcon = document.getElementById("panelDialogIcon");
|
||||
const panelDialogEyebrow = document.getElementById("panelDialogEyebrow");
|
||||
const panelDialogTitle = document.getElementById("panelDialogTitle");
|
||||
const panelDialogMessage = document.getElementById("panelDialogMessage");
|
||||
const panelDialogDetail = document.getElementById("panelDialogDetail");
|
||||
const panelDialogCancelBtn = document.getElementById("panelDialogCancelBtn");
|
||||
const panelDialogConfirmBtn = document.getElementById("panelDialogConfirmBtn");
|
||||
const panelToastStack = document.getElementById("panelToastStack");
|
||||
let panelDialogResolver = null;
|
||||
let panelDialogLastFocus = null;
|
||||
|
||||
function closePanelConfirm(accepted = false) {
|
||||
if (!panelConfirmDialog || panelConfirmDialog.classList.contains("hidden")) return;
|
||||
panelConfirmDialog.classList.add("hidden");
|
||||
panelConfirmDialog.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("panel-dialog-open");
|
||||
const resolver = panelDialogResolver;
|
||||
panelDialogResolver = null;
|
||||
resolver?.(!!accepted);
|
||||
panelDialogLastFocus?.focus?.();
|
||||
panelDialogLastFocus = null;
|
||||
}
|
||||
|
||||
function panelConfirm(options = {}) {
|
||||
const opts = typeof options === "string" ? { message: options } : options;
|
||||
if (!panelConfirmDialog || !panelDialogConfirmBtn) return Promise.resolve(false);
|
||||
if (panelDialogResolver) closePanelConfirm(false);
|
||||
panelDialogLastFocus = document.activeElement;
|
||||
const tone = opts.tone || (opts.danger ? "danger" : "default");
|
||||
panelDialogCard?.classList.toggle("is-danger", tone === "danger");
|
||||
panelDialogCard?.classList.toggle("is-success", tone === "success");
|
||||
if (panelDialogIcon) panelDialogIcon.textContent = opts.icon || (tone === "danger" ? "!" : tone === "success" ? "✓" : "?");
|
||||
if (panelDialogEyebrow) panelDialogEyebrow.textContent = opts.eyebrow || t("Confirmation");
|
||||
if (panelDialogTitle) panelDialogTitle.textContent = opts.title || t("Confirm action");
|
||||
if (panelDialogMessage) panelDialogMessage.textContent = opts.message || "";
|
||||
if (panelDialogDetail) {
|
||||
panelDialogDetail.textContent = opts.detail || "";
|
||||
panelDialogDetail.classList.toggle("hidden", !opts.detail);
|
||||
}
|
||||
if (panelDialogCancelBtn) panelDialogCancelBtn.textContent = opts.cancelLabel || t("Cancel");
|
||||
panelDialogConfirmBtn.textContent = opts.confirmLabel || t("Confirm");
|
||||
panelDialogConfirmBtn.className = tone === "danger" ? "btn btn-danger" : "btn";
|
||||
panelConfirmDialog.classList.remove("hidden");
|
||||
panelConfirmDialog.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("panel-dialog-open");
|
||||
setTimeout(() => panelDialogConfirmBtn.focus(), 0);
|
||||
return new Promise(resolve => { panelDialogResolver = resolve; });
|
||||
}
|
||||
|
||||
function showPanelToast(message, tone = "info", title = "", duration = 4800) {
|
||||
if (!panelToastStack || !message) return null;
|
||||
const toast = document.createElement("article");
|
||||
toast.className = `panel-toast ${tone}`;
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "panel-toast-icon";
|
||||
icon.textContent = tone === "success" ? "✓" : tone === "error" ? "!" : tone === "warning" ? "i" : "•";
|
||||
const copy = document.createElement("div");
|
||||
copy.className = "panel-toast-copy";
|
||||
const heading = document.createElement("strong");
|
||||
heading.textContent = title || (tone === "success" ? t("Completed") : tone === "error" ? t("Action failed") : tone === "warning" ? t("Attention") : t("Information"));
|
||||
const body = document.createElement("p");
|
||||
body.textContent = message;
|
||||
copy.append(heading, body);
|
||||
const close = document.createElement("button");
|
||||
close.className = "panel-toast-close";
|
||||
close.type = "button";
|
||||
close.setAttribute("aria-label", t("Close"));
|
||||
close.textContent = "×";
|
||||
const remove = () => toast.remove();
|
||||
close.addEventListener("click", remove);
|
||||
toast.append(icon, copy, close);
|
||||
panelToastStack.appendChild(toast);
|
||||
if (duration > 0) setTimeout(remove, duration);
|
||||
return toast;
|
||||
}
|
||||
|
||||
panelDialogCancelBtn?.addEventListener("click", () => closePanelConfirm(false));
|
||||
panelDialogConfirmBtn?.addEventListener("click", () => closePanelConfirm(true));
|
||||
panelConfirmDialog?.querySelector("[data-panel-dialog-close]")?.addEventListener("click", () => closePanelConfirm(false));
|
||||
document.addEventListener("keydown", event => {
|
||||
if (!panelConfirmDialog || panelConfirmDialog.classList.contains("hidden")) return;
|
||||
if (event.key === "Escape") closePanelConfirm(false);
|
||||
if (event.key === "Enter" && document.activeElement !== panelDialogCancelBtn) closePanelConfirm(true);
|
||||
});
|
||||
|
||||
// ─── API helper ───────────────────────────────────────────────────────────────
|
||||
async function api(path, opts = {}) {
|
||||
const o = Object.assign({ headers: {} }, opts);
|
||||
|
||||
@@ -4,14 +4,59 @@ const tabTitles = {
|
||||
ssh: ["Accounts", "SSH / SlowDNS"],
|
||||
xray: ["Accounts", "Xray Users"],
|
||||
resellers: ["Administration", "Resellers"],
|
||||
servers: ["Administration", "Servers"],
|
||||
"servers-status": ["Administration", "Servers Status"],
|
||||
stats: ["Server", "Monitoring"],
|
||||
vnstat: ["Traffic", "VnStat"],
|
||||
servers: ["Infrastructure", "Servers"],
|
||||
"servers-status": ["Infrastructure", "Servers Status"],
|
||||
stats: ["Infrastructure", "Monitoring"],
|
||||
vnstat: ["Infrastructure", "Traffic"],
|
||||
logs: ["System", "Logs"],
|
||||
bot: ["Vendas", "Bot / Telegram"],
|
||||
server: ["System", "Settings"],
|
||||
};
|
||||
const infrastructureTabs = ["servers", "servers-status", "stats", "vnstat"];
|
||||
const infrastructureNavItems = [
|
||||
{ tab:"servers", icon:"▣", label:"Servers" },
|
||||
{ tab:"servers-status", icon:"●", label:"Status" },
|
||||
{ tab:"stats", icon:"◴", label:"Server" },
|
||||
{ tab:"vnstat", icon:"⇅", label:"Traffic" },
|
||||
];
|
||||
|
||||
function syncInfrastructureNavigation(tab = currentTab) {
|
||||
if (!infrastructureTabs.includes(tab)) return;
|
||||
document.querySelectorAll("[data-infra-tab]").forEach(button => button.classList.toggle("active", button.dataset.infraTab === tab));
|
||||
document.querySelectorAll(".infra-section-select").forEach(select => { select.value = tab; });
|
||||
}
|
||||
|
||||
function mountInfrastructureNavigation() {
|
||||
document.querySelectorAll(".infra-nav-mount").forEach(mount => {
|
||||
const shell = document.createElement("div");
|
||||
shell.className = "infra-nav-shell";
|
||||
const nav = document.createElement("nav");
|
||||
nav.className = "infra-section-nav";
|
||||
nav.setAttribute("aria-label", t("Infrastructure areas"));
|
||||
const select = document.createElement("select");
|
||||
select.className = "infra-section-select";
|
||||
select.setAttribute("aria-label", t("Infrastructure area"));
|
||||
infrastructureNavItems.forEach(item => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.infraTab = item.tab;
|
||||
const icon = document.createElement("span");
|
||||
icon.textContent = item.icon;
|
||||
button.append(icon, document.createTextNode(" " + t(item.label)));
|
||||
button.addEventListener("click", () => selectTab(item.tab));
|
||||
nav.appendChild(button);
|
||||
const option = document.createElement("option");
|
||||
option.value = item.tab;
|
||||
option.textContent = t(item.label);
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener("change", () => selectTab(select.value));
|
||||
shell.append(nav, select);
|
||||
mount.replaceChildren(shell);
|
||||
});
|
||||
syncInfrastructureNavigation();
|
||||
}
|
||||
|
||||
function updatePageHeading() {
|
||||
const [eyebrow, title] = tabTitles[currentTab] || ["Dashboard", currentTab];
|
||||
if (pageEyebrow) pageEyebrow.textContent = t(eyebrow);
|
||||
@@ -21,12 +66,14 @@ function updatePageHeading() {
|
||||
function selectTab(tab) {
|
||||
currentTab = tab;
|
||||
const pane = document.getElementById("tab-" + tab);
|
||||
const btn = document.querySelector(`.tab-btn[data-tab="${tab}"]`);
|
||||
if (!pane || !btn) return;
|
||||
const navTab = infrastructureTabs.includes(tab) ? "servers" : tab;
|
||||
const btn = document.querySelector(`.tab-btn[data-tab="${navTab}"]`);
|
||||
if (!pane) return;
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".tab-pane").forEach(p => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
btn?.classList.add("active");
|
||||
pane.classList.add("active");
|
||||
syncInfrastructureNavigation(tab);
|
||||
updatePageHeading();
|
||||
document.body.classList.remove("sidebar-open");
|
||||
|
||||
@@ -37,12 +84,14 @@ function selectTab(tab) {
|
||||
if (currentRole === "superadmin") loadWizardFromConfig();
|
||||
}
|
||||
if (tab === "stats" && currentRole === "superadmin") loadStats();
|
||||
if (tab === "vnstat" && currentRole === "superadmin") loadVnstat();
|
||||
if (tab === "servers-status" && currentRole === "superadmin") loadServersStatus();
|
||||
if (tab === "resellers" && currentRole === "superadmin") loadResellers();
|
||||
if (tab === "servers" && currentRole === "superadmin") loadServers();
|
||||
if (tab === "bot" && currentRole === "superadmin" && typeof loadBotTab === "function") loadBotTab();
|
||||
}
|
||||
|
||||
mountInfrastructureNavigation();
|
||||
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"));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ─── SSH Users ────────────────────────────────────────────────────────────────
|
||||
document.getElementById("reloadUsersBtn").addEventListener("click", loadUsers);
|
||||
document.getElementById("sshHeroRefreshBtn")?.addEventListener("click", loadUsers);
|
||||
newUserBtn.addEventListener("click", () => {
|
||||
setFormCollapsed(false);
|
||||
userForm.reset();
|
||||
@@ -26,6 +27,11 @@ function setFormCollapsed(v) {
|
||||
|
||||
async function loadUsers() {
|
||||
userStatus.textContent = t("Loading…");
|
||||
if (sshLiveStatus) {
|
||||
sshLiveStatus.textContent = t("Loading SSH status…");
|
||||
sshLiveStatus.className = "workspace-live-status is-loading";
|
||||
}
|
||||
if (sshMetricState) sshMetricState.textContent = t("Loading");
|
||||
try {
|
||||
const res = await api(withServerParam("/api/users", selectedSSHServer()));
|
||||
const data = await res.json();
|
||||
@@ -33,6 +39,11 @@ async function loadUsers() {
|
||||
userStatus.textContent = t("Loaded.");
|
||||
lastReload.textContent = t("Last reload: {time}", {time: new Date().toLocaleTimeString()});
|
||||
} catch (e) {
|
||||
if (sshLiveStatus) {
|
||||
sshLiveStatus.textContent = t("Could not load SSH status");
|
||||
sshLiveStatus.className = "workspace-live-status is-error";
|
||||
}
|
||||
if (sshMetricState) sshMetricState.textContent = t("Error");
|
||||
if (e.message==="auth") { doAuthError(); } else { userStatus.textContent = t("Error loading users."); }
|
||||
}
|
||||
}
|
||||
@@ -91,6 +102,14 @@ function renderUsers(users) {
|
||||
});
|
||||
const activeCount = Math.max(0, users.length - expiredCount);
|
||||
userCountChip.textContent = t("{count} total · {active} active · {online} online", {count: users.length, active: activeCount, online});
|
||||
if (sshMetricTotal) sshMetricTotal.textContent = String(users.length);
|
||||
if (sshMetricActive) sshMetricActive.textContent = String(activeCount);
|
||||
if (sshMetricOnline) sshMetricOnline.textContent = String(online);
|
||||
if (sshMetricState) sshMetricState.textContent = t("Online");
|
||||
if (sshLiveStatus) {
|
||||
sshLiveStatus.textContent = t("SSH data updated at {time}", {time:new Date().toLocaleTimeString()});
|
||||
sshLiveStatus.className = "workspace-live-status is-ok";
|
||||
}
|
||||
}
|
||||
|
||||
function fillUserForm(u) {
|
||||
@@ -143,7 +162,13 @@ userForm.addEventListener("submit", async e => {
|
||||
});
|
||||
|
||||
async function deleteUser(username) {
|
||||
if (!confirm(t("Delete user \"{name}\"?", {name: username}))) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Delete SSH account"),
|
||||
message:t("Delete user \"{name}\"?", {name: username}),
|
||||
detail:t("The active SSH sessions for this account will be disconnected."),
|
||||
confirmLabel:t("Delete account"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
userStatus.textContent = t("Deleting {name}…", {name: username});
|
||||
try {
|
||||
const res = await api(withServerParam(`/api/users/delete?username=${encodeURIComponent(username)}`, selectedSSHServer()), { method:"DELETE" });
|
||||
@@ -156,4 +181,3 @@ async function deleteUser(username) {
|
||||
else userStatus.textContent = t("Error deleting.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,17 @@ document.getElementById("xLoadLogsBtn").addEventListener("click", loadXrayLogs);
|
||||
|
||||
|
||||
async function loadXrayStatus() {
|
||||
if (xrayChip) {
|
||||
xrayChip.textContent = t("Loading Xray status…");
|
||||
xrayChip.className = "workspace-live-status is-loading";
|
||||
}
|
||||
try {
|
||||
const res = await api(withServerParam("/api/xray/status", selectedXrayServer()));
|
||||
if (!res.ok) throw new Error((await res.text()).trim() || `HTTP ${res.status}`);
|
||||
const s = await res.json();
|
||||
const run = !!s.running;
|
||||
xrayChip.textContent = run ? t("running") : (s.enabled ? t("stopped") : t("disabled"));
|
||||
xrayChip.className = "chip " + (run ? "green" : "red");
|
||||
xrayChip.className = "workspace-live-status " + (run ? "is-ok" : (s.enabled ? "is-warn" : "is-error"));
|
||||
xRunning.textContent = run ? t("Running") : t("Stopped");
|
||||
xRunning.style.color = run ? "var(--success)" : "var(--danger)";
|
||||
xPID.textContent = s.pid || (s.native ? "internal" : "--");
|
||||
@@ -43,7 +48,15 @@ async function loadXrayStatus() {
|
||||
if (dashServerStatus) dashServerStatus.textContent = (serversCache || []).length > 1 ? `${(serversCache || []).filter(n => n.is_active !== false).length} nodes configured` : (run ? t("{count} online", {count: 1}) : (s.enabled ? t("stopped") : t("disabled")));
|
||||
renderDashboardCounters();
|
||||
if (s.error) xStatus.textContent = t("Error: {error}", {error: s.error});
|
||||
} catch (e) { if (e.message==="auth") doAuthError(); }
|
||||
} catch (e) {
|
||||
if (xrayChip) {
|
||||
xrayChip.textContent = t("Could not load Xray status");
|
||||
xrayChip.className = "workspace-live-status is-error";
|
||||
}
|
||||
if (xRunning) { xRunning.textContent = t("Error"); xRunning.style.color = "var(--danger)"; }
|
||||
if (xStatus && e.message !== "auth") xStatus.textContent = t("Error: {error}", {error:e.message});
|
||||
if (e.message==="auth") doAuthError();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveXrayCoreMode() {
|
||||
@@ -307,11 +320,18 @@ async function addClient(tag, formKey = tag) {
|
||||
}
|
||||
|
||||
async function removeClient(tag, uuid) {
|
||||
if (!confirm(t("Remove client {id}… from {tag}?", {id: uuid.slice(0,8), tag}))) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Remove Xray client"),
|
||||
message:t("Remove client {id}… from {tag}?", {id: uuid.slice(0,8), tag}),
|
||||
detail:t("The client will lose access immediately after the configuration reload."),
|
||||
confirmLabel:t("Remove client"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
try {
|
||||
const res = await api(withServerParam(`/api/xray/clients/remove?inbound_tag=${encodeURIComponent(tag)}&uuid=${encodeURIComponent(uuid)}`, selectedXrayServer()), { method:"DELETE" });
|
||||
if (!res.ok && res.status !== 204) throw new Error(await res.text());
|
||||
xStatus.textContent = t("Client removed. Native mode hot-reloads without restart.");
|
||||
showPanelToast(t("Client removed successfully."), "success", t("Xray client"));
|
||||
setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500);
|
||||
} catch (e) {
|
||||
if (e.message==="auth") doAuthError();
|
||||
|
||||
@@ -98,7 +98,13 @@ resellerForm.addEventListener("submit", async e => {
|
||||
});
|
||||
|
||||
async function deleteReseller(username) {
|
||||
if (!confirm(`Delete reseller "${username}"? All their SSH sessions will be disconnected.`)) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Delete reseller"),
|
||||
message:t("Delete reseller \"{name}\"?", {name:username}),
|
||||
detail:t("Their owned access will be removed and active SSH sessions will be disconnected."),
|
||||
confirmLabel:t("Delete reseller"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
resellerStatus.textContent = `Deleting ${username}…`;
|
||||
try {
|
||||
const res = await api(`/api/resellers/delete?username=${encodeURIComponent(username)}`, { method:"DELETE" });
|
||||
|
||||
@@ -19,8 +19,6 @@ document.getElementById("reloadServersBtn")?.addEventListener("click", loadServe
|
||||
document.getElementById("reloadServersBtn2")?.addEventListener("click", loadServers);
|
||||
document.getElementById("refreshServersBtn")?.addEventListener("click", loadServers);
|
||||
document.getElementById("refreshServersStatusBtn")?.addEventListener("click", () => loadServersStatus());
|
||||
document.querySelector("[data-tab='servers']")?.addEventListener("click", loadServers);
|
||||
document.querySelector("[data-tab='servers-status']")?.addEventListener("click", () => loadServersStatus());
|
||||
document.getElementById("clearServerFormBtn")?.addEventListener("click", clearServerForm);
|
||||
document.getElementById("testServerBtn")?.addEventListener("click", testServerForm);
|
||||
document.getElementById("backToServersBtn")?.addEventListener("click", () => showServerListView());
|
||||
@@ -34,17 +32,39 @@ serverForm?.addEventListener("submit", async e => {
|
||||
});
|
||||
|
||||
async function loadServers() {
|
||||
let loadError = null;
|
||||
if (fleetLiveStatus) {
|
||||
fleetLiveStatus.textContent = t("Loading infrastructure…");
|
||||
fleetLiveStatus.className = "workspace-live-status is-loading";
|
||||
}
|
||||
try {
|
||||
const res = await api("/api/servers");
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
serversCache = await res.json() || [];
|
||||
} catch (e) {
|
||||
loadError = e;
|
||||
serversCache = [{ id:"local", name:"Master node", base_url:"local", enable_ssh:true, enable_xray:true, is_active:true, is_local:true }];
|
||||
if (serversStatus) serversStatus.textContent = "Error loading servers: " + e.message;
|
||||
if (e.message === "auth") doAuthError();
|
||||
}
|
||||
renderServerSelectors();
|
||||
renderServersTable();
|
||||
updateFleetOverview(loadError);
|
||||
}
|
||||
|
||||
function updateFleetOverview(error = null) {
|
||||
const rows = Array.isArray(serversCache) ? serversCache.filter(Boolean) : [];
|
||||
const active = rows.filter(server => server.is_active !== false);
|
||||
if (fleetMetricNodes) fleetMetricNodes.textContent = String(rows.length);
|
||||
if (fleetMetricActive) fleetMetricActive.textContent = String(active.length);
|
||||
if (fleetMetricSSH) fleetMetricSSH.textContent = String(active.filter(server => server.enable_ssh || server.is_local).length);
|
||||
if (fleetMetricXray) fleetMetricXray.textContent = String(active.filter(server => server.enable_xray || server.is_local).length);
|
||||
if (fleetLiveStatus) {
|
||||
fleetLiveStatus.textContent = error
|
||||
? t("Infrastructure loaded with fallback data")
|
||||
: t("{count} active nodes · updated {time}", {count:active.length, time:new Date().toLocaleTimeString()});
|
||||
fleetLiveStatus.className = `workspace-live-status ${error ? "is-warn" : "is-ok"}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +77,7 @@ async function loadServersStatus(options = {}) {
|
||||
if (serversStatusCountChip) serversStatusCountChip.textContent = String(nodes.length);
|
||||
if (!silent) {
|
||||
serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading servers...");
|
||||
if (serversStatusPageStatus) serversStatusPageStatus.className = "workspace-live-status is-loading";
|
||||
serversStatusGrid.innerHTML = `<div class="hint">Loading servers...</div>`;
|
||||
}
|
||||
const rows = await Promise.all(nodes.map(loadSingleServerStatus));
|
||||
@@ -64,11 +85,23 @@ async function loadServersStatus(options = {}) {
|
||||
if (serversStatusPageStatus) {
|
||||
const online = rows.filter(r => r.ok).length;
|
||||
serversStatusPageStatus.textContent = `${online}/${rows.length} servers online - Updated ${new Date().toLocaleTimeString()}`;
|
||||
serversStatusPageStatus.className = `workspace-live-status ${online === rows.length ? "is-ok" : online > 0 ? "is-warn" : "is-error"}`;
|
||||
}
|
||||
const online = rows.filter(row => row.ok).length;
|
||||
const sessions = rows.reduce((total, row) => {
|
||||
const ssh = (Array.isArray(row.users) ? row.users : []).reduce((sum, user) => sum + Number(user.active_conns || 0), 0);
|
||||
let xray = 0;
|
||||
(Array.isArray(row.inbounds) ? row.inbounds : []).forEach(inbound => { xray += (inbound.clients || []).filter(client => !!client.online).length; });
|
||||
return total + ssh + xray;
|
||||
}, 0);
|
||||
if (fleetStatusOnline) fleetStatusOnline.textContent = String(online);
|
||||
if (fleetStatusOffline) fleetStatusOffline.textContent = String(Math.max(0, rows.length - online));
|
||||
if (fleetStatusSessions) fleetStatusSessions.textContent = String(sessions);
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else {
|
||||
serversStatusPageStatus && (serversStatusPageStatus.textContent = "Error loading server status: " + e.message);
|
||||
if (serversStatusPageStatus) serversStatusPageStatus.className = "workspace-live-status is-error";
|
||||
if (!silent) serversStatusGrid.innerHTML = `<div class="hint">Error loading server status.</div>`;
|
||||
}
|
||||
}
|
||||
@@ -321,7 +354,13 @@ async function testServerForm() {
|
||||
}
|
||||
|
||||
async function deleteServer(s) {
|
||||
if (!confirm(`Delete server "${s.name || s.base_url}"?`)) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Delete managed server"),
|
||||
message:t("Delete server \"{name}\"?", {name:s.name || s.base_url}),
|
||||
detail:t("The remote node is not erased, but it will be removed from this panel and can no longer receive managed actions."),
|
||||
confirmLabel:t("Delete server"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
try {
|
||||
const res = await api(`/api/servers?id=${encodeURIComponent(s.id)}`, { method:"DELETE" });
|
||||
if (!res.ok && res.status !== 204) throw new Error(await res.text());
|
||||
@@ -665,5 +704,3 @@ async function loadManagedDnsttPubkey() {
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ─── Stats ────────────────────────────────────────────────────────────────────
|
||||
document.querySelector("[data-tab='stats']")?.addEventListener("click", loadStats);
|
||||
document.getElementById("refreshStatsBtn")?.addEventListener("click", loadStats);
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
@@ -115,6 +115,10 @@ async function loadDnsttHealth() {
|
||||
|
||||
|
||||
async function loadStats() {
|
||||
if (statsUpdated) {
|
||||
statsUpdated.textContent = t("Updating live status…");
|
||||
statsUpdated.className = "workspace-live-status is-loading";
|
||||
}
|
||||
try {
|
||||
const res = await api("/api/stats");
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
@@ -139,24 +143,40 @@ async function loadStats() {
|
||||
ifaceBody.appendChild(tr);
|
||||
});
|
||||
if (ifaceSummary) ifaceSummary.textContent = `Total: ${fmtBytes(totRx)} rx / ${fmtBytes(totTx)} tx`;
|
||||
if (statsUpdated) statsUpdated.textContent = "Updated: " + new Date().toLocaleTimeString();
|
||||
const currentNetwork = ifaces.reduce((sum, item) => sum + Number(item.rx_mbps || 0) + Number(item.tx_mbps || 0), 0);
|
||||
if (statsNetVal) statsNetVal.textContent = `${fmtMbps(currentNetwork)} Mb/s`;
|
||||
if (statsIfaceVal) statsIfaceVal.textContent = String(ifaces.length);
|
||||
if (statsUpdated) {
|
||||
statsUpdated.textContent = t("Live · updated {time}", {time:new Date().toLocaleTimeString()});
|
||||
statsUpdated.className = "workspace-live-status is-ok";
|
||||
}
|
||||
await loadDnsttHealth();
|
||||
} catch (e) {
|
||||
if (e.message==="auth") doAuthError();
|
||||
else if (statsUpdated) statsUpdated.textContent = "Erro ao carregar stats.";
|
||||
else if (statsUpdated) {
|
||||
statsUpdated.textContent = t("Error loading server status");
|
||||
statsUpdated.className = "workspace-live-status is-error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetIfaceStatsBtn?.addEventListener("click", resetInterfaceStats);
|
||||
|
||||
async function resetInterfaceStats() {
|
||||
if (!confirm("Clean the live Interface totals now? This does not delete VnStat daily/monthly history.")) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"⇅", title:t("Clean live interface totals"),
|
||||
message:t("Clean the live Interface totals now?"),
|
||||
detail:t("VnStat daily and monthly history will be preserved."),
|
||||
confirmLabel:t("Clean totals"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
resetIfaceStatsBtn.disabled = true;
|
||||
ifaceSummary.textContent = "Cleaning interface totals…";
|
||||
try {
|
||||
const res = await api("/api/stats/interfaces/reset", { method:"POST" });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
ifaceSummary.textContent = "Interface totals cleaned. Auto-clean remains every 30 days.";
|
||||
showPanelToast(t("Live interface totals were cleaned."), "success", t("Traffic counters"));
|
||||
loadStats();
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
@@ -167,7 +187,6 @@ async function resetInterfaceStats() {
|
||||
}
|
||||
|
||||
// ─── VnStat ───────────────────────────────────────────────────────────────────
|
||||
document.querySelector("[data-tab='vnstat']")?.addEventListener("click", loadVnstat);
|
||||
reloadVnstatBtn?.addEventListener("click", loadVnstat);
|
||||
resetVnstatBtn?.addEventListener("click", resetVnstatHistory);
|
||||
|
||||
@@ -188,6 +207,7 @@ function renderVnstatRows(body, rows, emptyLabel) {
|
||||
|
||||
async function loadVnstat() {
|
||||
vnstatStatus.textContent = "Loading VnStat usage…";
|
||||
vnstatStatus.className = "workspace-live-status is-loading";
|
||||
try {
|
||||
const res = await api("/api/vnstat?days=31&months=12");
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
@@ -208,21 +228,33 @@ async function loadVnstat() {
|
||||
vnTodayTotal.textContent = fmtBytes(todayTotal);
|
||||
vnMonthTotal.textContent = fmtBytes(monthTotal);
|
||||
vnIfaceCount.textContent = String(data.interface_count ?? ifaces.size ?? 0);
|
||||
if (vnLatestPeriod) vnLatestPeriod.textContent = daily[0]?.period || monthly[0]?.period || "--";
|
||||
vnstatStatus.textContent = "Updated: " + new Date().toLocaleTimeString() + " · history is kept until manually cleaned.";
|
||||
vnstatStatus.className = "workspace-live-status is-ok";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else vnstatStatus.textContent = "Error loading VnStat usage: " + e.message;
|
||||
else {
|
||||
vnstatStatus.textContent = "Error loading VnStat usage: " + e.message;
|
||||
vnstatStatus.className = "workspace-live-status is-error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resetVnstatHistory() {
|
||||
if (!confirm("Clean all VnStat daily/monthly usage history? This does not reset the live Interface totals.")) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Clean VnStat history"),
|
||||
message:t("Clean all daily and monthly traffic history?"),
|
||||
detail:t("Live interface totals are separate and will not be reset."),
|
||||
confirmLabel:t("Clean history"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
resetVnstatBtn.disabled = true;
|
||||
vnstatStatus.textContent = "Cleaning VnStat history…";
|
||||
try {
|
||||
const res = await api("/api/vnstat/reset", { method:"POST" });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
vnstatStatus.textContent = "VnStat history cleaned.";
|
||||
showPanelToast(t("VnStat history was cleaned."), "success", t("Traffic history"));
|
||||
loadVnstat();
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
@@ -260,7 +292,13 @@ async function loadSystemLogs() {
|
||||
|
||||
async function clearPanelLog() {
|
||||
const st = document.getElementById("systemLogStatus");
|
||||
if (!confirm("Clean the panel log now? Logs are already auto-cleaned after 1 MiB.")) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Clean panel log"),
|
||||
message:t("Clean the current panel log now?"),
|
||||
detail:t("This only clears the panel log file. Automatic size-based cleanup remains enabled."),
|
||||
confirmLabel:t("Clean log"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
st.textContent = "Cleaning panel log…";
|
||||
try {
|
||||
const res = await api("/api/system/logs/reset", { method:"POST" });
|
||||
|
||||
@@ -161,7 +161,8 @@ function renderWzInbounds() {
|
||||
sshBtn.type = "button";
|
||||
sshBtn.textContent = t(sshRoute ? "SSH /ssh enabled" : "Enable SSH /ssh");
|
||||
sshBtn.disabled = !!sshRoute;
|
||||
sshBtn.onclick = () => enableSSHForWzInbound(i);
|
||||
sshBtn.title = sshRoute ? t("This endpoint already has an SSH /ssh route.") : t("Add SSH /ssh without rebuilding this inbound.");
|
||||
sshBtn.onclick = () => enableSSHForWzInbound(i, true, sshBtn);
|
||||
actions.appendChild(sshBtn);
|
||||
}
|
||||
const duplicateBtn = document.createElement("button");
|
||||
@@ -178,8 +179,14 @@ function renderWzInbounds() {
|
||||
delBtn.className = "btn btn-danger btn-sm";
|
||||
delBtn.type = "button";
|
||||
delBtn.textContent = "Remover";
|
||||
delBtn.onclick = () => {
|
||||
if (!confirm(`Remover o inbound ${ib.tag || "untagged"}?`)) return;
|
||||
delBtn.onclick = async () => {
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"×", title:t("Remove inbound"),
|
||||
message:t("Remove inbound {name}?", {name:ib.tag || "untagged"}),
|
||||
detail:t("Clients attached only to this inbound will stop connecting after the configuration is saved."),
|
||||
confirmLabel:t("Remove inbound"),
|
||||
});
|
||||
if (!accepted) return;
|
||||
wzInbounds.splice(i,1);
|
||||
if (wzEditingIndex === i) wzCancelInbound();
|
||||
else if (wzEditingIndex > i) wzEditingIndex--;
|
||||
@@ -346,30 +353,38 @@ function uniqueLegacySSHTag(source) {
|
||||
// modifying the original inbound. The companion inherits its listen address,
|
||||
// port, host, XHTTP mode, advanced transport options, TLS certificate, and key;
|
||||
// only its protocol, empty SSH settings, tag, and /ssh path differ.
|
||||
async function enableSSHForWzInbound(index, applyNow = true) {
|
||||
function reportSSHMigration(message, tone = "error") {
|
||||
const st = document.getElementById("wzStatus");
|
||||
if (st) st.textContent = message;
|
||||
if (typeof showPanelToast === "function") {
|
||||
showPanelToast(message, tone, tone === "success" ? t("SSH /ssh enabled") : tone === "warning" ? t("SSH migration attention") : t("Could not enable SSH /ssh"));
|
||||
}
|
||||
}
|
||||
|
||||
async function enableSSHForWzInbound(index, applyNow = true, trigger = null) {
|
||||
const st = document.getElementById("wzStatus");
|
||||
const source = wzInbounds[index];
|
||||
const selectedID = selectedXrayServer() || "local";
|
||||
if (!source || !visualXHTTPSettings(source) || !["vless", "vmess"].includes(String(source.protocol || "").toLowerCase())) {
|
||||
if (st) st.textContent = "Selecione um inbound VLESS/VMess com transporte XHTTP.";
|
||||
reportSSHMigration(t("Select a VLESS/VMess inbound using XHTTP."));
|
||||
return false;
|
||||
}
|
||||
if (!wzLoadedConfigText || String(wzLoadedServerID || "") !== String(selectedID)) {
|
||||
if (st) st.textContent = "Carregue a configuração do servidor selecionado antes de ativar SSH.";
|
||||
reportSSHMigration(t("Load the selected server configuration before enabling SSH."));
|
||||
return false;
|
||||
}
|
||||
if ((document.getElementById("xCoreMode")?.value || "native") !== "native") {
|
||||
if (st) st.textContent = "SSH compartilhado requer o modo Xray nativo.";
|
||||
reportSSHMigration(t("Shared SSH requires native Xray mode. Select Internal native emulator and save the mode first."));
|
||||
return false;
|
||||
}
|
||||
const security = String(source.streamSettings?.security || "").toLowerCase();
|
||||
if (security && security !== "none" && security !== "tls") {
|
||||
if (st) st.textContent = `O inbound ${source.tag || "selecionado"} usa ${security}, que não é suportado pelo SSH XHTTP nativo. Use TLS ou sem TLS.`;
|
||||
reportSSHMigration(t("Inbound {name} uses {security}, which native SSH XHTTP does not support. Use TLS or no TLS.", {name:source.tag || t("selected"), security}));
|
||||
return false;
|
||||
}
|
||||
const existingSSH = findSSHRouteForInbound(source);
|
||||
if (existingSSH) {
|
||||
if (st) st.textContent = `SSH já está ativado em /ssh pelo inbound ${existingSSH.tag}.`;
|
||||
reportSSHMigration(t("SSH is already enabled on /ssh by inbound {name}.", {name:existingSSH.tag}), "success");
|
||||
return true;
|
||||
}
|
||||
const pathConflict = wzInbounds.find(candidate => {
|
||||
@@ -377,17 +392,37 @@ async function enableSSHForWzInbound(index, applyNow = true) {
|
||||
return sameVisualEndpoint(source, candidate) && !!xh && normalizeVisualPath(xh.path) === "/ssh";
|
||||
});
|
||||
if (pathConflict) {
|
||||
if (st) st.textContent = `O caminho /ssh já é usado pelo inbound ${pathConflict.tag || "sem tag"}. Edite esse caminho primeiro.`;
|
||||
reportSSHMigration(t("Path /ssh is already used by inbound {name}. Edit that path first.", {name:pathConflict.tag || t("untagged")}));
|
||||
return false;
|
||||
}
|
||||
if (security === "tls") {
|
||||
const certificate = source.streamSettings?.tlsSettings?.certificates?.[0] || {};
|
||||
if (!certificate.certificateFile || !certificate.keyFile) {
|
||||
if (st) st.textContent = "O inbound usa TLS, mas não possui arquivos de certificado e chave reutilizáveis.";
|
||||
reportSSHMigration(t("This inbound uses TLS but has no reusable certificate and key file paths."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (applyNow && !confirm(`Ativar SSH em /ssh no mesmo endpoint de ${source.tag || "este inbound"}?\n\nA configuração antiga e todos os clientes serão mantidos. O Xray será reiniciado para aplicar.`)) return false;
|
||||
if (applyNow) {
|
||||
const xhttp = visualXHTTPSettings(source) || {};
|
||||
const clients = Array.isArray(source.settings?.clients) ? source.settings.clients.length : 0;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"success", icon:"SSH", eyebrow:t("Safe XHTTP migration"), title:t("Enable SSH on /ssh"),
|
||||
message:t("Add SSH to the same endpoint without rebuilding {name}?", {name:source.tag || t("this inbound")}),
|
||||
detail:[
|
||||
`${t("Listener")}: ${source.listen || "0.0.0.0"}:${source.port}`,
|
||||
`${t("Existing path preserved")}: ${normalizeVisualPath(xhttp.path)}`,
|
||||
`${t("New SSH path")}: /ssh`,
|
||||
`${t("Clients preserved")}: ${clients}`,
|
||||
`${t("Security")}: ${security === "tls" ? "TLS" : t("No TLS")}`,
|
||||
].join("\n"),
|
||||
confirmLabel:t("Enable SSH /ssh"),
|
||||
});
|
||||
if (!accepted) return false;
|
||||
}
|
||||
if (trigger) {
|
||||
trigger.disabled = true;
|
||||
trigger.textContent = t("Enabling SSH…");
|
||||
}
|
||||
|
||||
const streamSettings = cloneJsonSafe(source.streamSettings || {});
|
||||
const settingsKey = streamSettings.xhttpSettings ? "xhttpSettings" : (streamSettings.splithttpSettings ? "splithttpSettings" : "xhttpSettings");
|
||||
@@ -400,28 +435,45 @@ async function enableSSHForWzInbound(index, applyNow = true) {
|
||||
settings: {},
|
||||
streamSettings,
|
||||
};
|
||||
const previousDirty = wzDirty;
|
||||
const before = cloneJsonSafe(source);
|
||||
wzInbounds.push(sshInbound);
|
||||
try {
|
||||
validateVisualInbounds(wzInbounds);
|
||||
} catch (error) {
|
||||
wzInbounds.pop();
|
||||
if (st) st.textContent = `Não foi possível ativar SSH: ${error.message}`;
|
||||
if (trigger) { trigger.disabled = false; trigger.textContent = t("Enable SSH /ssh"); }
|
||||
reportSSHMigration(t("Could not enable SSH: {error}", {error:error.message}));
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(source) !== JSON.stringify(before)) {
|
||||
wzInbounds.pop();
|
||||
if (st) st.textContent = "A migração foi cancelada porque alteraria o inbound antigo.";
|
||||
if (trigger) { trigger.disabled = false; trigger.textContent = t("Enable SSH /ssh"); }
|
||||
reportSSHMigration(t("Migration was cancelled because it would alter the old inbound."));
|
||||
return false;
|
||||
}
|
||||
wzDirty = true;
|
||||
renderWzInbounds();
|
||||
if (!applyNow) {
|
||||
if (st) st.textContent = `SSH /ssh adicionado ao rascunho sem alterar ${source.tag || "o inbound antigo"}.`;
|
||||
if (st) st.textContent = t("SSH /ssh added to the draft without changing {name}.", {name:source.tag || t("the old inbound")});
|
||||
return true;
|
||||
}
|
||||
if (st) st.textContent = `Ativando SSH /ssh sem alterar ${source.tag || "o inbound antigo"}…`;
|
||||
return await applyWizardConfig();
|
||||
if (st) st.textContent = t("Enabling SSH /ssh without changing {name}…", {name:source.tag || t("the old inbound")});
|
||||
const result = await applyWizardConfig();
|
||||
if (!result?.saved) {
|
||||
const addedIndex = wzInbounds.indexOf(sshInbound);
|
||||
if (addedIndex >= 0) wzInbounds.splice(addedIndex, 1);
|
||||
wzDirty = previousDirty;
|
||||
renderWzInbounds();
|
||||
reportSSHMigration(result?.error || t("The SSH route could not be saved. The old inbound was not changed."));
|
||||
return false;
|
||||
}
|
||||
if (!result.restarted) {
|
||||
reportSSHMigration(t("SSH /ssh was saved, but Xray could not restart. Use the Restart button after checking the Xray log."), "warning");
|
||||
return true;
|
||||
}
|
||||
reportSSHMigration(t("SSH /ssh is active. The old inbound and all clients were preserved."), "success");
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateVisualInbounds(inbounds) {
|
||||
@@ -866,7 +918,7 @@ async function applyWizardConfig() {
|
||||
if (String(wzLoadedServerID || "") !== String(selectedID) || !wzLoadedConfigText) {
|
||||
if (st) st.textContent = `Reloading config from ${target} before saving...`;
|
||||
loadWizardFromConfig();
|
||||
return false;
|
||||
return { saved:false, restarted:false, error:t("Configuration for this server was not loaded.") };
|
||||
}
|
||||
|
||||
let cfg;
|
||||
@@ -874,7 +926,7 @@ async function applyWizardConfig() {
|
||||
cfg = buildConfigFromVisualEditor();
|
||||
} catch(e) {
|
||||
if (st) st.textContent = `Invalid visual config: ${e.message}`;
|
||||
return false;
|
||||
return { saved:false, restarted:false, error:t("Invalid visual config: {error}", {error:e.message}) };
|
||||
}
|
||||
|
||||
if (st) st.textContent = `Saving config to ${target}...`;
|
||||
@@ -892,10 +944,10 @@ async function applyWizardConfig() {
|
||||
? `Config saved on ${target} and Xray restarted.`
|
||||
: `Config saved on ${target}, but Xray could not restart. Check Xray logs before editing again.`;
|
||||
setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700);
|
||||
return !!restarted;
|
||||
return { saved:true, restarted:!!restarted, error:restarted ? "" : t("Xray could not restart.") };
|
||||
} catch (e) {
|
||||
if (e.message==="auth") doAuthError();
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
return false;
|
||||
return { saved:false, restarted:false, error:t("Could not save configuration: {error}", {error:e.message}) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,8 @@ async function botSavePlan(event) {
|
||||
}
|
||||
|
||||
async function botDeletePlan(id) {
|
||||
if (!confirm("Excluir este plano? Esta ação não pode ser desfeita.")) return;
|
||||
const accepted = await panelConfirm({ tone:"danger", icon:"×", title:"Excluir plano", message:"Excluir este plano?", detail:"Esta ação não pode ser desfeita.", confirmLabel:"Excluir plano" });
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await botRequest(`/api/bot/plans?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
await loadBotPlans();
|
||||
@@ -396,7 +397,8 @@ async function botSavePkg(event) {
|
||||
}
|
||||
|
||||
async function botDeletePkg(id) {
|
||||
if (!confirm("Excluir este pacote de créditos?")) return;
|
||||
const accepted = await panelConfirm({ tone:"danger", icon:"×", title:"Excluir pacote", message:"Excluir este pacote de créditos?", confirmLabel:"Excluir pacote" });
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await botRequest(`/api/bot/credit-packages?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
await loadBotPkgs();
|
||||
@@ -491,7 +493,13 @@ async function botSaveUserAction(event) {
|
||||
|
||||
async function botToggleBlock(user) {
|
||||
const isBlocked = user.Role === "blocked";
|
||||
if (!confirm(isBlocked ? "Desbloquear este cliente?" : "Bloquear este cliente no bot?")) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:isBlocked ? "default" : "danger", icon:isBlocked ? "✓" : "!",
|
||||
title:isBlocked ? "Desbloquear cliente" : "Bloquear cliente",
|
||||
message:isBlocked ? "Desbloquear este cliente?" : "Bloquear este cliente no bot?",
|
||||
confirmLabel:isBlocked ? "Desbloquear" : "Bloquear",
|
||||
});
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await botRequest("/api/bot/users", { method: "POST", body: JSON.stringify({ telegram_id: user.TelegramID, action: isBlocked ? "unblock" : "block" }) });
|
||||
await loadBotUsers();
|
||||
@@ -551,7 +559,13 @@ async function botReprocess(id) {
|
||||
}
|
||||
|
||||
async function botRefund(id) {
|
||||
if (!confirm(`Isso apenas marca o pagamento #${id} como estornado no painel. Não envia um estorno financeiro ao Mercado Pago. Continuar?`)) return;
|
||||
const accepted = await panelConfirm({
|
||||
tone:"danger", icon:"!", title:"Marcar como estornado",
|
||||
message:`Marcar o pagamento #${id} como estornado no painel?`,
|
||||
detail:"Esta ação não envia um estorno financeiro ao Mercado Pago; ela altera somente o status interno.",
|
||||
confirmLabel:"Marcar estornado",
|
||||
});
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await botRequest("/api/bot/transactions", { method: "POST", body: JSON.stringify({ id, action: "refund" }) });
|
||||
await loadBotTxns();
|
||||
|
||||
+94
-93
@@ -16,7 +16,7 @@
|
||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260713legacyssh3"/>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260713infra4"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -52,10 +52,7 @@
|
||||
<button class="tab-btn" data-tab="xray"><span class="nav-icon">◇</span><span>Xray Users</span></button>
|
||||
<div class="nav-group-label superadmin-only hidden">Administração</div>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="resellers"><span class="nav-icon">🏪</span><span>Revendedores</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="servers"><span class="nav-icon">▣</span><span>Servidores</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="servers-status"><span class="nav-icon">▥</span><span>Status Servidores</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="stats"><span class="nav-icon">📊</span><span>Servidor</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="vnstat"><span class="nav-icon">⇅</span><span>Tráfego</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="servers"><span class="nav-icon">▣</span><span>Infraestrutura</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="logs"><span class="nav-icon">☰</span><span>Logs</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="bot"><span class="nav-icon">🤖</span><span>Bot / Vendas</span></button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="server"><span class="nav-icon">⚙</span><span>Configurações</span></button>
|
||||
@@ -213,9 +210,15 @@
|
||||
|
||||
<!-- ═══════════ SSH Users Tab ═══════════ -->
|
||||
<div class="tab-pane" id="tab-ssh">
|
||||
<section class="page-hero" data-tone="green">
|
||||
<section class="page-hero status-hero" data-tone="green">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Access workspace</span><h2>Contas SSH</h2><p>Crie, limite e acompanhe acessos SSH e SlowDNS com segurança.</p></div>
|
||||
<div class="page-hero-pills"><span>SSH</span><span>TOTP</span><span>SlowDNS</span></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status" id="sshLiveStatus">Aguardando dados</span><button class="btn btn-ghost btn-sm" id="sshHeroRefreshBtn" type="button">Atualizar</button></div>
|
||||
<div class="workspace-overview-grid">
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">●</span><div><small>Operação</small><strong id="sshMetricState">Carregando</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon purple">◆</span><div><small>Contas</small><strong id="sshMetricTotal">0</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon cyan">✓</span><div><small>Ativas</small><strong id="sshMetricActive">0</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon amber">↗</span><div><small>Online agora</small><strong id="sshMetricOnline">0</strong></div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reseller info card (visible to resellers only) -->
|
||||
@@ -312,9 +315,27 @@
|
||||
|
||||
<!-- ═══════════ Xray Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-xray">
|
||||
<section class="page-hero" data-tone="purple">
|
||||
<section class="page-hero status-hero" data-tone="purple">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Proxy studio</span><h2>Xray visual</h2><p>Gerencie clientes, inbounds e um endpoint XHTTP compartilhado sem editar JSON.</p></div>
|
||||
<div class="page-hero-pills"><span>VLESS</span><span>VMess</span><span>XHTTP</span><span>SSH</span></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status" id="xrayChip">Aguardando dados</span><button class="btn btn-ghost btn-sm" id="xRefreshBtn" type="button">Atualizar</button></div>
|
||||
<div class="workspace-overview-grid five">
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">●</span><div><small>Status</small><strong id="xRunning">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon cyan">↗</span><div><small>Online</small><strong id="xOnlineUsers">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon purple">#</span><div><small>PID</small><strong id="xPID">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon amber">◷</span><div><small>Uptime</small><strong id="xUptime">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon blue">API</span><div><small>Contadores</small><strong id="xStatsConfig">--</strong></div></article>
|
||||
</div>
|
||||
<div class="workspace-hero-toolbar">
|
||||
<span id="xStatus" class="workspace-toolbar-status">Pronto.</span>
|
||||
<div class="workspace-toolbar-actions xray-admin-only">
|
||||
<select id="xCoreMode" class="input-sm" title="Xray runtime mode"><option value="native">Internal native emulator</option><option value="external">External xray binary</option></select>
|
||||
<button class="btn btn-ghost btn-sm" id="xSaveModeBtn">Salvar modo</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xStartBtn">Iniciar</button>
|
||||
<button class="btn btn-danger btn-sm" id="xStopBtn">Parar</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xRestartBtn">Reiniciar</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xRepairStatsBtn">Reparar contadores</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Edit Client Panel (hidden by default) -->
|
||||
<div id="editXrayClientPanel" class="card hidden" style="margin-bottom:12px;border-color:rgba(245,158,11,.4);">
|
||||
@@ -356,33 +377,6 @@
|
||||
<div class="hint hidden" id="xrayServerHint"></div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Xray Core <span class="chip" id="xrayChip">--</span></div>
|
||||
<div class="card-actions xray-admin-only">
|
||||
<select id="xCoreMode" class="input-sm" title="Xray runtime mode">
|
||||
<option value="native">Internal native emulator</option>
|
||||
<option value="external">External xray binary</option>
|
||||
</select>
|
||||
<button class="btn btn-ghost btn-sm" id="xSaveModeBtn">Save mode</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xStartBtn">Start</button>
|
||||
<button class="btn btn-danger btn-sm" id="xStopBtn">Stop</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xRestartBtn">Restart</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xRepairStatsBtn">Repair counters</button>
|
||||
<button class="btn btn-ghost btn-sm" id="xRefreshBtn">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="m-label">Status</div><div class="m-val" id="xRunning">--</div></div>
|
||||
<div class="metric"><div class="m-label">Online</div><div class="m-val" id="xOnlineUsers">--</div></div>
|
||||
<div class="metric"><div class="m-label">PID</div><div class="m-val" id="xPID">--</div></div>
|
||||
<div class="metric"><div class="m-label">Uptime</div><div class="m-val" id="xUptime">--</div></div>
|
||||
<div class="metric"><div class="m-label">Counters API</div><div class="m-val" id="xStatsConfig">--</div></div>
|
||||
</div>
|
||||
<div class="statusbar"><span id="xStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Inbounds & clients -->
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div class="card-hdr">
|
||||
@@ -657,13 +651,22 @@
|
||||
|
||||
<!-- ═══════════ Servers Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-servers">
|
||||
<section class="page-hero" data-tone="blue"><div class="page-hero-copy"><span class="page-kicker">Fleet control</span><h2>Servidores</h2><p>Adicione nós, teste credenciais e configure a infraestrutura remota.</p></div><div class="page-hero-pills"><span>Master</span><span>Nodes</span><span>Encrypted</span></div></section>
|
||||
<section class="page-hero status-hero" data-tone="blue">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Fleet control</span><h2>Infraestrutura</h2><p>Gerencie nós, acompanhe a saúde do sistema e consulte tráfego em um único espaço.</p></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status" id="fleetLiveStatus">Aguardando dados</span><button class="btn btn-ghost btn-sm" id="refreshServersBtn" type="button">Atualizar</button></div>
|
||||
<div class="workspace-overview-grid">
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon blue">▣</span><div><small>Nós</small><strong id="fleetMetricNodes">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">●</span><div><small>Ativos</small><strong id="fleetMetricActive">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon cyan">SSH</span><div><small>SSH habilitado</small><strong id="fleetMetricSSH">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon purple">◇</span><div><small>Xray habilitado</small><strong id="fleetMetricXray">--</strong></div></article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="infra-nav-mount"></div>
|
||||
<div id="serversListView">
|
||||
<div class="grid2 servers-grid">
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Managed servers <span class="chip" id="serversCountChip">0</span></div>
|
||||
<div class="card-actions"><button class="btn btn-ghost btn-sm" id="refreshServersBtn" type="button">Reload</button></div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
@@ -885,40 +888,33 @@
|
||||
|
||||
<!-- ═══════════ Servers Status Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-servers-status">
|
||||
<section class="page-hero" data-tone="green"><div class="page-hero-copy"><span class="page-kicker">Live fleet</span><h2>Status dos servidores</h2><p>Saúde, carga e sessões ativas de cada nó gerenciado.</p></div><div class="page-hero-pills"><span>CPU</span><span>RAM</span><span>Uptime</span></div></section>
|
||||
<div class="card servers-status-toolbar">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Servers Status <span class="chip" id="serversStatusCountChip">0</span></div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-ghost btn-sm" id="refreshServersStatusBtn" type="button">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statusbar">
|
||||
<span id="serversStatusPageStatus">Ready.</span>
|
||||
</div>
|
||||
<section class="page-hero status-hero" data-tone="green">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Live fleet</span><h2>Status dos servidores</h2><p>Saúde, carga e sessões ativas de cada nó gerenciado.</p></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status" id="serversStatusPageStatus">Aguardando dados</span><button class="btn btn-ghost btn-sm" id="refreshServersStatusBtn" type="button">Atualizar</button></div>
|
||||
<div class="workspace-overview-grid">
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon blue">▣</span><div><small>Total</small><strong id="serversStatusCountChip">0</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">●</span><div><small>Online</small><strong id="fleetStatusOnline">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon red">●</span><div><small>Offline</small><strong id="fleetStatusOffline">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon amber">↗</span><div><small>Sessões</small><strong id="fleetStatusSessions">--</strong></div></article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="infra-nav-mount"></div>
|
||||
<div class="servers-status-grid" id="serversStatusGrid"></div>
|
||||
</div><!-- /tab-servers-status -->
|
||||
|
||||
<!-- ═══════════ Stats Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-stats">
|
||||
<section class="page-hero" data-tone="cyan"><div class="page-hero-copy"><span class="page-kicker">Observability</span><h2>Monitoramento</h2><p>Recursos, interfaces e capacidade do servidor em tempo real.</p></div><div class="page-hero-pills"><span>2s live</span><span>Network</span><span>Load</span></div></section>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-hdr"><div class="card-title">Server Load</div><span class="hint" id="statsUpdated">--</span></div>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="m-label">CPU</div>
|
||||
<div class="m-val"><span id="cpuVal">--%</span></div>
|
||||
<div class="bar"><div class="bar-inner" id="cpuBar" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="m-label">RAM</div>
|
||||
<div class="m-val"><span id="memVal">--%</span> <span class="hint" id="memDetail"></span></div>
|
||||
<div class="bar"><div class="bar-inner" id="memBar" style="width:0%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="page-hero status-hero" data-tone="cyan">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Observability</span><h2>Monitoramento</h2><p>Recursos, interfaces e capacidade do servidor em tempo real.</p></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status is-ok" id="statsUpdated">Atualização ao vivo</span><button class="btn btn-ghost btn-sm" id="refreshStatsBtn" type="button">Atualizar</button></div>
|
||||
<div class="workspace-overview-grid">
|
||||
<article class="workspace-overview-card has-meter"><span class="workspace-overview-icon cyan">CPU</span><div><small>Processador</small><strong id="cpuVal">--%</strong><div class="workspace-mini-meter"><span id="cpuBar"></span></div></div></article>
|
||||
<article class="workspace-overview-card has-meter"><span class="workspace-overview-icon purple">RAM</span><div><small>Memória</small><strong id="memVal">--%</strong><span class="workspace-card-note" id="memDetail">--</span><div class="workspace-mini-meter"><span id="memBar"></span></div></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">⇅</span><div><small>Rede agora</small><strong id="statsNetVal">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon amber">#</span><div><small>Interfaces</small><strong id="statsIfaceVal">--</strong></div></article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="infra-nav-mount"></div>
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Interfaces <span class="chip">rx/tx Mbps</span><span class="chip warn">30-day rolling</span></div>
|
||||
@@ -932,27 +928,21 @@
|
||||
</div>
|
||||
<div class="statusbar"><span id="ifaceSummary"></span><span class="hint">Totals can be cleaned here and auto-clean every 30 days. VnStat history is separate.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /tab-stats -->
|
||||
|
||||
<!-- ═══════════ VnStat Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-vnstat">
|
||||
<section class="page-hero" data-tone="purple"><div class="page-hero-copy"><span class="page-kicker">Traffic intelligence</span><h2>Tráfego de rede</h2><p>Histórico diário e mensal para entender o consumo da infraestrutura.</p></div><div class="page-hero-pills"><span>RX</span><span>TX</span><span>History</span></div></section>
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">VnStat Usage <span class="chip">daily / monthly</span></div>
|
||||
<div class="form-actions" style="margin-top:0">
|
||||
<button class="btn btn-ghost btn-sm" id="reloadVnstatBtn" type="button">Refresh</button>
|
||||
<button class="btn btn-danger btn-sm" id="resetVnstatBtn" type="button">Clean VnStat history</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="m-label">Today total</div><div class="m-val" id="vnTodayTotal">--</div></div>
|
||||
<div class="metric"><div class="m-label">This month total</div><div class="m-val" id="vnMonthTotal">--</div></div>
|
||||
<div class="metric"><div class="m-label">Interfaces tracked</div><div class="m-val" id="vnIfaceCount">--</div></div>
|
||||
</div>
|
||||
<div class="statusbar"><span id="vnstatStatus">VnStat history does not auto-clean. Use the button when you want to reset it.</span></div>
|
||||
<section class="page-hero status-hero" data-tone="purple">
|
||||
<div class="page-hero-copy"><span class="page-kicker">Traffic intelligence</span><h2>Tráfego de rede</h2><p>Histórico diário e mensal para entender o consumo da infraestrutura.</p></div>
|
||||
<div class="workspace-hero-actions"><span class="workspace-live-status" id="vnstatStatus">Aguardando dados</span><button class="btn btn-ghost btn-sm" id="reloadVnstatBtn" type="button">Atualizar</button><button class="btn btn-danger btn-sm" id="resetVnstatBtn" type="button">Limpar histórico</button></div>
|
||||
<div class="workspace-overview-grid">
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon cyan">D</span><div><small>Hoje</small><strong id="vnTodayTotal">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon purple">M</span><div><small>Este mês</small><strong id="vnMonthTotal">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon green">⇅</span><div><small>Interfaces</small><strong id="vnIfaceCount">--</strong></div></article>
|
||||
<article class="workspace-overview-card"><span class="workspace-overview-icon amber">◷</span><div><small>Período recente</small><strong id="vnLatestPeriod">--</strong></div></article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="infra-nav-mount"></div>
|
||||
|
||||
<div class="grid2" style="margin-top:12px;">
|
||||
<div class="card">
|
||||
@@ -1442,20 +1432,31 @@
|
||||
</div><!-- /shell -->
|
||||
</div><!-- /app -->
|
||||
|
||||
<div class="panel-dialog hidden" id="panelConfirmDialog" aria-hidden="true">
|
||||
<div class="panel-dialog-backdrop" data-panel-dialog-close></div>
|
||||
<section class="panel-dialog-card" role="dialog" aria-modal="true" aria-labelledby="panelDialogTitle" aria-describedby="panelDialogMessage">
|
||||
<div class="panel-dialog-head"><span class="panel-dialog-icon" id="panelDialogIcon" aria-hidden="true">?</span><div><span class="page-kicker" id="panelDialogEyebrow">Confirmação</span><h3 id="panelDialogTitle">Confirmar ação</h3></div></div>
|
||||
<p id="panelDialogMessage"></p>
|
||||
<div class="panel-dialog-detail hidden" id="panelDialogDetail"></div>
|
||||
<div class="panel-dialog-actions"><button class="btn btn-ghost" id="panelDialogCancelBtn" type="button">Cancelar</button><button class="btn" id="panelDialogConfirmBtn" type="button">Confirmar</button></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="panel-toast-stack" id="panelToastStack" aria-live="polite" aria-atomic="false"></div>
|
||||
|
||||
<!-- 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=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/02-shell.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/03-ssh-users.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/04-xray.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/05-resellers.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/06-servers.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/07-stats-logs.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/08-server-config.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/11-update-status.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/12-bot.js?v=20260713legacyssh3"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260713legacyssh3"></script>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user