10 Commits
Author SHA1 Message Date
Muse 8158cedfc1 release: panel V2.6.0 / agent line A2.5.0 (agent code changed in V2.6.0 batch); install-agent RELEASE_BASE -> V2.6.0 (ps1 frozen at V2.4.0)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-24 11:20:23 +00:00
Muse 1b255eb414 test(web): retire TestRouteRegistryContract (api-docs surface removed in P6) 2026-10-24 19:00:26 +08:00
Muse f1132461db merge: item 4 vitest red + MinuteFold stabilization 2026-10-24 10:09:16 +00:00
Muse 9ceca7d58f merge: item 3 agent v4 beacon 2026-10-24 10:09:16 +00:00
Muse e7676f8635 merge: item 2 node-deletion cascade 2026-10-24 10:09:15 +00:00
Muse b6e8fc558d merge: item 1 agent orphan-core handle recovery 2026-10-24 10:09:15 +00:00
Muse 460b02cec6 test(i18n,probe): clear new vitest red + stabilize MinuteFold flaky (item 4)
- i18n dead-keys: delete 64 keys unreferenced in frontend/Go sources
  (menu donate/docs, old index config actions, the whole removed
  traditional-node/mTLS node form incl. its mtls subtree, xray
  Outbounds/Balancers) from all 13 locales; 1536 -> 1472 keys each.
  Verified repo-wide: only stale prebuilt binaries still embed them.
- probe TestMetricHistoryMinuteFold: replace wall-clock base with a
  fixed 120 s-aligned instant; an odd-minute base split the 120 s fold
  into two buckets (want 1, got 2) on ~half of all runs.
2026-10-24 10:08:04 +00:00
Muse 9681dc6f4a fix(node): cascade node deletion to agent bindings and probe rows
Deleting a panel node left its agent_nodes rows and probe.db rows
behind, so deleted nodes kept showing as ghosts and their probe
latency stayed attributed to leftover rows (TYYUN case). Delete now
removes agent rows bound by agent_nodes.node_id inside the existing
panel transaction, then drops each agent's probe rows (meta, traffic,
both history tiers, ping records and ping assignments) and the
public-page snapshot. Agents link by node_id, probe rows by agent row
id: matching probe rows on the node id would strike another node's
data (Oracle-JP: node 33, agent row 20).
2026-10-24 10:05:19 +00:00
Muse 12aff835ed feat(agent): periodic IPv4 beacon proves public v4 to panel (item ③)
Agent posts an empty beacon over a tcp4-forced connection every
5 minutes (plus once at startup); panel files the observed source
as SeenIPv4 only when it is a public IPv4 literal, never touching
the reported telemetry address, so 1:1 NAT nodes stop falling back
to their private address in share links.
2026-10-24 09:43:56 +00:00
Muse 66bf6a88eb fix(agent): reconcile orphaned sing-box core before start
After an agent restart the tracked sing-box handle is lost while
the old core survives reparented holding the inbound ports, so
every apply died on bind, rollback died the same way, and the
agent retried forever. startSingboxLocked now scans /proc for
processes whose argv carries run -c <exact live config path>
(excluding the tracked pid) and stops them SIGTERM-then-SIGKILL
before starting, covering the apply, rollback, and
unchanged-but-not-running paths. The exact-path match never
touches cores on other config files; Linux only.
2026-10-24 09:38:09 +00:00
27 changed files with 1020 additions and 1031 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ esac
# (独立入口 cmd/xui-agent,不再是主控双模式二进制)。该资产自拆分版
# 发行起才有——RELEASE_BASE 的升版由发版步完成,指向拆分版即可用;
# 若需装拆分前的旧版节点,显式 XUI_URL 指回 x-ui-linux-<arch> 即可。
RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.5.0"
RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.6.0"
XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-agent-linux-${BIN_ARCH}}"
ROOT="${XUI_ROOT:-}"
+13 -2
View File
@@ -66,6 +66,7 @@ type Config struct {
type Agent struct {
config *Config
httpClient *http.Client
v4httpClient *http.Client
stopCh chan struct{}
lastConfigHash string
xrayProc *xray.Process
@@ -166,8 +167,9 @@ func New(cfg *Config) *Agent {
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
},
stopCh: make(chan struct{}),
probes: newProbeRunner(),
v4httpClient: newV4BeaconClient(),
stopCh: make(chan struct{}),
probes: newProbeRunner(),
}
}
@@ -306,11 +308,18 @@ func (a *Agent) Run() error {
fastTimer.Stop()
defer fastTimer.Stop()
// V4-beacon ticker (item ③): its own slow cadence, not the
// heartbeat's — the proven address barely changes, and a
// failure here is silent by design (no v4 route, old panel).
beaconTicker := time.NewTicker(defaultV4BeaconInterval)
defer beaconTicker.Stop()
// Initial config fetch
if err := a.fetchAndApplyConfig(); err != nil {
logger.Warningf("Initial config fetch failed: %v", err)
}
a.armFastTimer(fastTimer)
_ = a.sendV4Beacon()
for {
select {
@@ -335,6 +344,8 @@ func (a *Agent) Run() error {
case <-fastTimer.C:
a.sendFastReport()
a.armFastTimer(fastTimer)
case <-beaconTicker.C:
_ = a.sendV4Beacon()
}
}
}
+106
View File
@@ -10,6 +10,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
@@ -442,9 +443,114 @@ func (a *Agent) applySingboxConfigLocked(raw json.RawMessage) error {
return nil
}
// reconcileSingboxOrphans stops untracked sing-box processes running
// this exact live config file: an agent restart loses the handle
// while the old core survives reparented holding the inbound ports,
// which otherwise makes every apply/rollback die on bind forever.
// The exact-path match is the safety boundary — a core on any other
// config file belongs to another panel/agent/router and is never
// touched. Linux only; the Windows agent line is frozen, so the scan
// is a no-op there. Returns how many orphans were reaped.
func reconcileSingboxOrphans(configPath string, ownPid int) int {
if runtime.GOOS != "linux" {
return 0
}
self := os.Getpid()
reaped := 0
entries, err := os.ReadDir("/proc")
if err != nil {
return 0
}
for _, e := range entries {
pid, err := strconv.Atoi(e.Name())
if err != nil || pid <= 0 || pid == self || pid == ownPid {
continue
}
if !singboxCmdlineMatchesConfig(pid, configPath) {
continue
}
logger.Infof("sing-box orphan on %s (pid %d) from a lost handle; stopping it before start", configPath, pid)
stopForeignProcess(pid)
if foreignPidAlive(pid) {
logger.Warningf("sing-box orphan pid %d survived SIGKILL; the new core may fail to bind", pid)
continue
}
reaped++
}
return reaped
}
// singboxCmdlineMatchesConfig reports whether pid's argv contains
// the consecutive triple `run -c <configPath>`, the exact invocation
// the agent itself uses. A substring match would be unsafe: another
// config path merely sharing a prefix must never qualify.
func singboxCmdlineMatchesConfig(pid int, configPath string) bool {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if err != nil {
return false
}
args := strings.Split(string(data), "\x00")
for len(args) > 0 && args[len(args)-1] == "" {
args = args[:len(args)-1]
}
for i := 0; i+2 < len(args); i++ {
if args[i] == "run" && args[i+1] == "-c" && args[i+2] == configPath {
return true
}
}
return false
}
// foreignPidAlive treats a zombie as dead: a reparented orphan's
// parent reaps it eventually, but its ports are already released.
func foreignPidAlive(pid int) bool {
if st, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil {
if i := strings.LastIndexByte(string(st), ')'); i >= 0 && i+2 < len(st) && st[i+2] == 'Z' {
return false
}
} else {
return false
}
err := syscall.Kill(pid, 0)
return err == nil || err == syscall.EPERM
}
// stopForeignProcess mirrors singboxProcess.stop: SIGTERM, then
// SIGKILL after the same grace, then a bounded wait for the kill.
func stopForeignProcess(pid int) {
proc, _ := os.FindProcess(pid)
if proc == nil {
return
}
_ = proc.Signal(syscall.SIGTERM)
if waitForeignPidExit(pid, singboxStopGrace) {
return
}
_ = proc.Kill()
waitForeignPidExit(pid, singboxStopForce)
}
func waitForeignPidExit(pid int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for {
if !foreignPidAlive(pid) {
return true
}
if time.Now().After(deadline) {
return false
}
time.Sleep(25 * time.Millisecond)
}
}
// startSingboxLocked starts the process and verifies it survives the
// settle window.
func (a *Agent) startSingboxLocked(binPath, configPath string) error {
ownPid := 0
if a.sbProc != nil {
ownPid = a.sbProc.pid()
}
reconcileSingboxOrphans(configPath, ownPid)
if err := a.sbProc.start(binPath, configPath, singboxLogPath()); err != nil {
return err
}
+213
View File
@@ -0,0 +1,213 @@
package agent
import (
"os"
"os/exec"
"runtime"
"syscall"
"testing"
"time"
)
// Orphan-core recovery (next-version fix batch item 1): after an
// agent restart the tracked sing-box handle is lost while the old
// core survives reparented holding the inbound ports. The reconcile
// in startSingboxLocked must stop exactly those orphans — processes
// whose argv carries `run -c <live config path>` — before starting
// a new core, on the apply path, the rollback path, and the
// unchanged-config-but-not-running path alike.
//
// The dummy below is the test binary re-exec'd: with the marker env
// it sleeps until signalled, and its argv carries a crafted `run -c`
// triple, which is exactly what the reconcile matches on. Without
// the marker it is a no-op so ordinary test runs pass through it.
const orphanDummyEnv = "XUI_ORPHAN_DUMMY"
func TestOrphanDummyProcess(t *testing.T) {
if os.Getenv(orphanDummyEnv) != "1" {
return
}
time.Sleep(3600 * time.Second)
}
func spawnOrphanDummy(t *testing.T, cfgPath string) (*exec.Cmd, int) {
t.Helper()
if runtime.GOOS != "linux" {
t.Skip("orphan reconcile is Linux-only")
}
cmd := exec.Command(os.Args[0], "-test.run=^TestOrphanDummyProcess$", "--", "run", "-c", cfgPath)
cmd.Env = append(os.Environ(), orphanDummyEnv+"=1")
if err := cmd.Start(); err != nil {
t.Fatalf("start dummy: %v", err)
}
pid := cmd.Process.Pid
t.Cleanup(func() {
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
_ = cmd.Process.Kill()
}
_ = cmd.Wait()
})
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if foreignPidAlive(pid) && singboxCmdlineMatchesConfig(pid, cfgPath) {
return cmd, pid
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("dummy pid %d never showed the crafted argv", pid)
return nil, 0
}
func waitDummyExit(t *testing.T, cmd *exec.Cmd) bool {
t.Helper()
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case <-done:
return true
case <-time.After(10 * time.Second):
return false
}
}
func TestReconcileSingboxOrphansStopsMatchingProcess(t *testing.T) {
cfgPath := "/tmp/orphan-test-live-config.json"
cmd, pid := spawnOrphanDummy(t, cfgPath)
if n := reconcileSingboxOrphans(cfgPath, 0); n != 1 {
t.Fatalf("reaped = %d, want 1", n)
}
if !waitDummyExit(t, cmd) {
t.Fatalf("dummy pid %d still alive after reconcile", pid)
}
if foreignPidAlive(pid) {
t.Fatalf("dummy pid %d still reported alive after reconcile", pid)
}
}
func TestReconcileSingboxOrphansLeavesOtherConfigAlone(t *testing.T) {
_, pid := spawnOrphanDummy(t, "/tmp/orphan-test-other-config.json")
if n := reconcileSingboxOrphans("/tmp/orphan-test-live-config.json", 0); n != 0 {
t.Fatalf("reaped = %d, want 0", n)
}
if !foreignPidAlive(pid) {
t.Fatalf("dummy on a different config path was killed, want untouched")
}
}
func TestReconcileSingboxOrphansSkipsTrackedPid(t *testing.T) {
cmd, pid := spawnOrphanDummy(t, "/tmp/orphan-test-live-config.json")
if n := reconcileSingboxOrphans("/tmp/orphan-test-live-config.json", pid); n != 0 {
t.Fatalf("reaped = %d, want 0 for the tracked pid", n)
}
if !foreignPidAlive(pid) {
t.Fatalf("tracked pid %d was killed, want untouched", pid)
}
_ = cmd.Process.Kill()
}
// dropSingboxHandle simulates the agent-restart half of the incident:
// the tracked handle is gone, but the previously started core keeps
// running. The caller owns killing the leaked fake core afterwards.
func dropSingboxHandle(t *testing.T, a *Agent) int {
t.Helper()
a.sbMu.Lock()
pid := a.sbProc.pid()
a.sbProc = &singboxProcess{}
a.sbMu.Unlock()
if pid <= 0 {
t.Fatal("no tracked core to orphan")
}
t.Cleanup(func() { _ = syscall.Kill(pid, syscall.SIGTERM) })
return pid
}
func TestApplySingboxRecoversFromStaleHandleAndOrphan(t *testing.T) {
setupFakeSingbox(t)
a := newApplyTestAgent()
t.Cleanup(a.stopSingbox)
if err := a.applySingboxConfig([]byte(sbCfgA)); err != nil {
t.Fatalf("apply A: %v", err)
}
pidBefore := dropSingboxHandle(t, a)
cmd, orphanPid := spawnOrphanDummy(t, singboxConfigPath())
if err := a.applySingboxConfig([]byte(sbCfgB)); err != nil {
t.Fatalf("apply B with stale handle + orphan: %v", err)
}
if !waitDummyExit(t, cmd) {
t.Fatalf("orphan pid %d survived the apply", orphanPid)
}
a.sbMu.Lock()
running := a.sbProc.isRunning()
pidAfter := a.sbProc.pid()
a.sbMu.Unlock()
if !running || pidAfter <= 0 || pidAfter == pidBefore {
t.Fatalf("no new tracked core after recovery: running=%v pid %d -> %d", running, pidBefore, pidAfter)
}
if got := appliedConfig(t); got != sbCfgB {
t.Fatalf("applied config = %q, want cfgB", got)
}
}
func TestApplySingboxUnchangedRecoversFromStaleHandleAndOrphan(t *testing.T) {
setupFakeSingbox(t)
a := newApplyTestAgent()
t.Cleanup(a.stopSingbox)
if err := a.applySingboxConfig([]byte(sbCfgA)); err != nil {
t.Fatalf("apply A: %v", err)
}
dropSingboxHandle(t, a)
cmd, orphanPid := spawnOrphanDummy(t, singboxConfigPath())
// Unchanged config + tracked handle says not-running: this used
// to fall into the start-fail/rollback-fail loop in production
// (the orphan held the ports); the reconcile inside the start
// must clear it before the new core binds.
if err := a.applySingboxConfig([]byte(sbCfgA)); err != nil {
t.Fatalf("re-apply A with stale handle + orphan: %v", err)
}
if !waitDummyExit(t, cmd) {
t.Fatalf("orphan pid %d survived the unchanged re-apply", orphanPid)
}
a.sbMu.Lock()
running := a.sbProc.isRunning()
a.sbMu.Unlock()
if !running {
t.Fatal("no tracked core running after unchanged re-apply recovery")
}
}
func TestApplySingboxRollbackRecoversFromStaleHandleAndOrphan(t *testing.T) {
setupFakeSingbox(t)
a := newApplyTestAgent()
t.Cleanup(a.stopSingbox)
if err := a.applySingboxConfig([]byte(sbCfgA)); err != nil {
t.Fatalf("apply A: %v", err)
}
dropSingboxHandle(t, a)
cmd, orphanPid := spawnOrphanDummy(t, singboxConfigPath())
// A config that dies at startup: the new core fails, so the
// apply rolls back — and the rollback's start must clear the
// orphan too, or it dies on bind the same way and the agent
// loops. The apply itself still reports failure either way.
bad := `{"log":{"level":"info"},"note":"RUNTIMEFAIL","inbounds":[]}`
if err := a.applySingboxConfig([]byte(bad)); err == nil {
t.Fatal("apply of a config that dies at startup must fail")
}
if !waitDummyExit(t, cmd) {
t.Fatalf("orphan pid %d survived the apply/rollback sequence", orphanPid)
}
a.sbMu.Lock()
running := a.sbProc.isRunning()
a.sbMu.Unlock()
if !running {
t.Fatal("rollback must leave a running core")
}
if got := appliedConfig(t); got != sbCfgA {
t.Fatalf("active config after rollback = %q, want cfgA", got)
}
}
+58
View File
@@ -0,0 +1,58 @@
package agent
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"time"
)
// Beacon cadence is deliberately slow: the proven address barely changes, so the heartbeat's 60 s would be pure noise; the first beacon fires at startup.
const defaultV4BeaconInterval = 5 * time.Minute
// v4OnlyDialContext forces every dial onto tcp4: on 1:1 NAT the local v4 is private, so only a panel-observed v4 arrival proves the public address (a v6-only panel just fails the dial).
func v4OnlyDialContext(dial func(ctx context.Context, network, address string) (net.Conn, error)) func(ctx context.Context, network, address string) (net.Conn, error) {
return func(ctx context.Context, _ string, address string) (net.Conn, error) {
return dial(ctx, "tcp4", address)
}
}
// newV4BeaconClient mirrors the agent's normal client (same TLS trust, no proxy — a proxy's egress would prove nothing about this host) except its dialer is pinned to tcp4.
func newV4BeaconClient() *http.Client {
return &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
DialContext: v4OnlyDialContext((&net.Dialer{Timeout: 10 * time.Second}).DialContext),
},
}
}
// sendV4Beacon posts one empty beacon over a v4-only connection; the panel's observed source is the whole signal, since any self-reported address is worthless behind 1:1 NAT. Any failure is the caller's to ignore — the heartbeat keeps working over v6 meanwhile.
func (a *Agent) sendV4Beacon() error {
url := fmt.Sprintf("%s/panel/api/agent/v4-beacon", a.config.MasterURL)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader([]byte(`{}`)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+a.config.Token)
client := a.v4httpClient
if client == nil {
client = newV4BeaconClient()
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("v4 beacon failed with status %d", resp.StatusCode)
}
return nil
}
+101
View File
@@ -0,0 +1,101 @@
package agent
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"testing"
)
// The dial wrapper must force tcp4 no matter what network the
// transport asked for — that forcing is the whole beacon.
func TestV4OnlyDialContextForcesTCP4(t *testing.T) {
var gotNetwork, gotAddr string
sentinel := errors.New("stub dialer hit")
stub := func(_ context.Context, network, address string) (net.Conn, error) {
gotNetwork, gotAddr = network, address
return nil, sentinel
}
wrapped := v4OnlyDialContext(stub)
for _, asked := range []string{"tcp", "tcp6", "tcp4"} {
_, err := wrapped(context.Background(), asked, "example.com:443")
if !errors.Is(err, sentinel) {
t.Fatalf("asked %q: wrapped dial did not call through: %v", asked, err)
}
if gotNetwork != "tcp4" {
t.Fatalf("asked %q: dial network %q, want tcp4", asked, gotNetwork)
}
if gotAddr != "example.com:443" {
t.Fatalf("asked %q: dial address %q changed", asked, gotAddr)
}
}
}
// One beacon round trip: POST to the agent route, Bearer token,
// empty JSON body — and nothing else for the panel to read.
func TestSendV4Beacon(t *testing.T) {
var gotMethod, gotPath, gotAuth string
var gotBody []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod, gotPath, gotAuth = r.Method, r.URL.Path, r.Header.Get("Authorization")
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
ag := New(&Config{MasterURL: srv.URL, Token: "beacon-tok", NodeName: "n"})
if err := ag.sendV4Beacon(); err != nil {
t.Fatalf("sendV4Beacon: %v", err)
}
if gotMethod != http.MethodPost {
t.Fatalf("method %q, want POST", gotMethod)
}
if gotPath != "/panel/api/agent/v4-beacon" {
t.Fatalf("path %q, want the beacon route", gotPath)
}
if gotAuth != "Bearer beacon-tok" {
t.Fatalf("auth %q, want the agent token", gotAuth)
}
if string(gotBody) != `{}` {
t.Fatalf("body %q, want empty JSON object", gotBody)
}
}
// Beacon failures are errors to the caller (who ignores them) and must never panic; a v6-literal master must fail at the tcp4 dial rather than ever reaching the panel over v6.
func TestSendV4BeaconFailures(t *testing.T) {
t.Run("panel 500", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
ag := New(&Config{MasterURL: srv.URL, Token: "tok", NodeName: "n"})
if err := ag.sendV4Beacon(); err == nil {
t.Fatal("500 beacon: want an error")
}
})
t.Run("old panel 404", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
ag := New(&Config{MasterURL: srv.URL, Token: "tok", NodeName: "n"})
if err := ag.sendV4Beacon(); err == nil {
t.Fatal("404 beacon: want an error (old panel, no beacon endpoint)")
}
})
t.Run("v6 literal master", func(t *testing.T) {
ag := New(&Config{MasterURL: "http://[::1]:1", Token: "tok", NodeName: "n"})
if err := ag.sendV4Beacon(); err == nil {
t.Fatal("v6-literal master: want a dial error, never a v6 beacon")
}
})
t.Run("unreachable master", func(t *testing.T) {
ag := New(&Config{MasterURL: "http://127.0.0.1:1", Token: "tok", NodeName: "n"})
if err := ag.sendV4Beacon(); err == nil {
t.Fatal("unreachable master: want an error")
}
})
}
+7 -7
View File
@@ -156,14 +156,14 @@ func GetMasterDisplayVersion() string {
// defaultAgentDisplayVersion is the agent line's version when a build
// does not inject one. It names the last release in which agent code
// actually changed (A2.4.0 — the P3 memory-tuning cycle touched agent
// code, shipped with panel V2.5.0; before that the line stood at
// A2.3.0 while the panel line advanced alone). Bump it ONLY together
// with a real agent-code change, in the same commit; panel releases
// must never move it.
const defaultAgentDisplayVersion = "2.4.0"
// actually changed (A2.5.0 — the V2.6.0 batch touched agent code:
// orphan-core handle recovery and the v4 beacon; before that the line
// stood at A2.4.0 while the panel line advanced alone). Bump it ONLY
// together with a real agent-code change, in the same commit; panel
// releases must never move it.
const defaultAgentDisplayVersion = "2.5.0"
// GetAgentDisplayVersion is the agent's display version ("A2.4.0"),
// GetAgentDisplayVersion is the agent's display version ("A2.5.0"),
// reported in agent telemetry and shown on agent node rows. It reads
// ONLY the agent line's own injected version (falling back to the
// agent-line default): changing the panel's displayVersion injection
+7 -7
View File
@@ -68,9 +68,9 @@ func TestDisplayVersionDefaultsAndOverride(t *testing.T) {
}
// The two lines have independent defaults since the decoupling:
// the panel default (2.0.0) and the agent line's own version
// (2.4.0, its last real change) deliberately differ.
if got := GetAgentDisplayVersion(); got != "A2.4.0" {
t.Fatalf("default agent display version = %q, want A2.4.0", got)
// (2.5.0, its last real change) deliberately differ.
if got := GetAgentDisplayVersion(); got != "A2.5.0" {
t.Fatalf("default agent display version = %q, want A2.5.0", got)
}
displayVersion = "1.0.1"
@@ -78,8 +78,8 @@ func TestDisplayVersionDefaultsAndOverride(t *testing.T) {
t.Fatalf("injected master display version = %q, want V1.0.1", got)
}
// Panel injection must NOT move the agent line (decoupling lock).
if got := GetAgentDisplayVersion(); got != "A2.4.0" {
t.Fatalf("agent display version after panel injection = %q, want A2.4.0 (unchanged)", got)
if got := GetAgentDisplayVersion(); got != "A2.5.0" {
t.Fatalf("agent display version after panel injection = %q, want A2.5.0 (unchanged)", got)
}
// A release build may inject the number in the master's V-form:
@@ -88,8 +88,8 @@ func TestDisplayVersionDefaultsAndOverride(t *testing.T) {
if got := GetMasterDisplayVersion(); got != "V1.0.2" {
t.Fatalf("V-injected master display version = %q, want V1.0.2", got)
}
if got := GetAgentDisplayVersion(); got != "A2.4.0" {
t.Fatalf("agent display version after V-form panel injection = %q, want A2.4.0 (unchanged)", got)
if got := GetAgentDisplayVersion(); got != "A2.5.0" {
t.Fatalf("agent display version after V-form panel injection = %q, want A2.5.0 (unchanged)", got)
}
}
+40
View File
@@ -47,6 +47,9 @@ func NewAgentController(g *gin.RouterGroup) *AgentController {
func (a *AgentController) initRouter(g *gin.RouterGroup) {
g.POST("/register", a.register)
g.POST("/heartbeat", a.heartbeat)
// Item ③: the agent's periodic IPv4 beacon (v4-forced request,
// empty body — the observed source is the whole signal).
g.POST("/v4-beacon", a.v4Beacon)
g.GET("/config", a.getConfig)
g.POST("/traffic", a.reportTraffic)
// V2.0.1 probe batch: the lightweight fast report (runtime
@@ -174,6 +177,43 @@ func (a *AgentController) captureSeenIP(c *gin.Context, agent *model.AgentNode)
agent.SeenIPAt = now
}
// v4Beacon answers one agent IPv4 beacon: authenticated, then filed via captureV4Beacon. Always ok — an unproven beacon is a silent no-op, never an agent-visible error.
func (a *AgentController) v4Beacon(c *gin.Context) {
agent, ok := a.findAgent(bearerToken(c))
if !ok {
c.JSON(401, gin.H{"message": "unauthorized"})
return
}
a.captureV4Beacon(c, agent)
jsonObj(c, gin.H{"message": "ok"}, nil)
}
// captureV4Beacon files a beacon's observed source as SeenIPv4 only when it is a public IPv4 literal; private, loopback, or v6 observations prove no public side and must not displace a good seen address.
// The reported telemetry address is never written here; resolution still prefers a public reported address over any seen one (service.ResolveAgentAddresses).
func (a *AgentController) captureV4Beacon(c *gin.Context, agent *model.AgentNode) {
ip := requestSourceIP(c)
if ip == nil || agent == nil {
return
}
v4 := ip.To4()
if v4 == nil || !service.IsPublicIPLiteral(v4.String()) {
return
}
if agent.SeenIPv4 == v4.String() {
return
}
now := time.Now().UnixMilli()
if err := execAgentColumns(uint(agent.Id), []agentColumn{
{"seen_ipv4", v4.String()},
{"seen_ip_at", now},
{"updated_at", now},
}); err != nil {
return
}
agent.SeenIPv4 = v4.String()
agent.SeenIPAt = now
}
// agentColumn is one column assignment for execAgentColumns.
type agentColumn struct {
name string
@@ -0,0 +1,240 @@
package controller
import (
"bytes"
"context"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
)
// Item ③ IPv4 beacon: the panel files a beacon's observed source as SeenIPv4, and only as SeenIPv4 — never the reported telemetry address, never a private/loopback/v6 observation.
func TestAgentV4Beacon(t *testing.T) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
db := database.GetDB()
node := &model.Node{Name: "beacon-node", Address: ""}
if err := db.Create(node).Error; err != nil {
t.Fatalf("seed node: %v", err)
}
const token = "v4-beacon-test-token"
agent := &model.AgentNode{
TokenHash: hashAgentToken(token),
TokenPrefix: "v4-beacon-",
Name: "beacon-node",
NodeID: &node.Id,
}
if err := db.Create(agent).Error; err != nil {
t.Fatalf("seed agent: %v", err)
}
engine := gin.New()
NewAgentController(engine.Group("/panel/api/agent"))
beacon := func(headers map[string]string, remoteAddr, tok string) int {
req := httptest.NewRequest(http.MethodPost, "/panel/api/agent/v4-beacon", bytes.NewBufferString(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tok)
for k, v := range headers {
req.Header.Set(k, v)
}
if remoteAddr != "" {
req.RemoteAddr = remoteAddr
}
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, req)
return rec.Code
}
load := func() model.AgentNode {
var row model.AgentNode
if err := db.First(&row, agent.Id).Error; err != nil {
t.Fatalf("reload agent: %v", err)
}
return row
}
reset := func(ipv4, seenIPv4, seenIPv6 string) {
if err := db.Model(&model.AgentNode{}).Where("id = ?", agent.Id).
Updates(map[string]any{"ipv4": ipv4, "seen_ipv4": seenIPv4, "seen_ipv6": seenIPv6}).Error; err != nil {
t.Fatalf("reset agent row: %v", err)
}
}
// A public v4 observation lands in seen_ipv4 only.
reset("", "", "")
if code := beacon(map[string]string{"CF-Connecting-IP": "203.0.113.5"}, "", token); code != http.StatusOK {
t.Fatalf("beacon public v4: status %d", code)
}
if row := load(); row.SeenIPv4 != "203.0.113.5" || row.SeenIPAt == 0 {
t.Fatalf("public v4 beacon not stored: %+v", row)
}
// Change-only: the same beacon again must not rewrite (SeenIPAt frozen).
frozen := load().SeenIPAt
if code := beacon(map[string]string{"CF-Connecting-IP": "203.0.113.5"}, "", token); code != http.StatusOK {
t.Fatalf("repeat beacon: status %d", code)
}
if row := load(); row.SeenIPAt != frozen {
t.Fatalf("unchanged beacon rewrote the row: seen_ip_at %d -> %d", frozen, row.SeenIPAt)
}
// A public v4 via the transport RemoteAddr (direct connection, no CF).
reset("", "", "")
if code := beacon(nil, "198.51.100.23:5555", token); code != http.StatusOK {
t.Fatalf("beacon RemoteAddr: status %d", code)
}
if row := load(); row.SeenIPv4 != "198.51.100.23" {
t.Fatalf("RemoteAddr beacon not stored: %+v", row)
}
// Reported public v4 is never overwritten: the beacon moves only
// the seen column, and resolution still prefers the reported one.
reset("203.0.113.50", "203.0.113.5", "")
if code := beacon(map[string]string{"CF-Connecting-IP": "203.0.113.99"}, "", token); code != http.StatusOK {
t.Fatalf("beacon over reported public: status %d", code)
}
row := load()
if row.IPv4 != "203.0.113.50" {
t.Fatalf("beacon overwrote reported ipv4: %+v", row)
}
if row.SeenIPv4 != "203.0.113.99" {
t.Fatalf("beacon did not update seen side: %+v", row)
}
if v4, _ := service.ResolveAgentAddresses(&row); v4 != "203.0.113.50" {
t.Fatalf("resolved address followed the beacon, not the reported public v4: %q", v4)
}
// The Oracle-JP case: reported private, beacon public — the
// resolved address becomes the beacon's public one.
reset("10.0.0.206", "", "")
if code := beacon(map[string]string{"CF-Connecting-IP": "203.0.113.77"}, "", token); code != http.StatusOK {
t.Fatalf("beacon over reported private: status %d", code)
}
row = load()
if v4, _ := service.ResolveAgentAddresses(&row); v4 != "203.0.113.77" {
t.Fatalf("private reported + public beacon did not resolve to beacon: %q (row %+v)", v4, row)
}
// A private observation never becomes seen_ipv4 — neither from
// empty nor over an existing good seen address.
reset("", "", "")
if code := beacon(map[string]string{"CF-Connecting-IP": "10.0.0.206"}, "", token); code != http.StatusOK {
t.Fatalf("private beacon: status %d", code)
}
if row := load(); row.SeenIPv4 != "" {
t.Fatalf("private beacon stored as seen_ipv4: %+v", row)
}
reset("", "203.0.113.5", "")
if code := beacon(nil, "10.0.0.206:5555", token); code != http.StatusOK {
t.Fatalf("private RemoteAddr beacon: status %d", code)
}
if row := load(); row.SeenIPv4 != "203.0.113.5" {
t.Fatalf("private beacon displaced good seen_ipv4: %+v", row)
}
// A v6 observation touches neither seen column: the beacon is v4-only.
reset("", "203.0.113.5", "2001:db8::1")
if code := beacon(map[string]string{"CF-Connecting-IP": "2001:db8::2"}, "", token); code != http.StatusOK {
t.Fatalf("v6 beacon: status %d", code)
}
if row := load(); row.SeenIPv4 != "203.0.113.5" || row.SeenIPv6 != "2001:db8::1" {
t.Fatalf("v6 beacon touched a seen column: %+v", row)
}
// Loopback (a same-host panel) proves no public address either.
reset("", "", "")
if code := beacon(nil, "127.0.0.1:5555", token); code != http.StatusOK {
t.Fatalf("loopback beacon: status %d", code)
}
if row := load(); row.SeenIPv4 != "" {
t.Fatalf("loopback beacon stored as seen_ipv4: %+v", row)
}
// Unauthenticated beacons capture nothing.
reset("", "", "")
if code := beacon(map[string]string{"CF-Connecting-IP": "203.0.113.5"}, "", "wrong-token"); code != http.StatusUnauthorized {
t.Fatalf("bad token beacon: status %d, want 401", code)
}
if row := load(); row.SeenIPv4 != "" {
t.Fatalf("unauthenticated beacon captured a source: %+v", row)
}
}
// Socket-level round trip: a real v4 connection to the panel, with the test harness playing the Cloudflare edge (CF-Connecting-IP) — the agent itself never sends that header. Without it the loopback peer proves nothing.
func TestAgentV4BeaconOverSocket(t *testing.T) {
gin.SetMode(gin.TestMode)
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
t.Fatalf("InitDB: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
db := database.GetDB()
const token = "v4-beacon-socket-token"
agent := &model.AgentNode{TokenHash: hashAgentToken(token), TokenPrefix: "v4-sock-"}
if err := db.Create(agent).Error; err != nil {
t.Fatalf("seed agent: %v", err)
}
engine := gin.New()
NewAgentController(engine.Group("/panel/api/agent"))
srv := httptest.NewServer(engine)
defer srv.Close()
// Same shape as the agent's beacon client: tcp4-forced dial.
v4client := &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "tcp4", addr)
},
}}
post := func(cfHeader string) int {
req, err := http.NewRequest(http.MethodPost, srv.URL+"/panel/api/agent/v4-beacon", bytes.NewBufferString(`{}`))
if err != nil {
t.Fatalf("build beacon request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
if cfHeader != "" {
req.Header.Set("CF-Connecting-IP", cfHeader)
}
resp, err := v4client.Do(req)
if err != nil {
t.Fatalf("beacon over socket: %v", err)
}
defer resp.Body.Close()
return resp.StatusCode
}
load := func() model.AgentNode {
var row model.AgentNode
if err := db.First(&row, agent.Id).Error; err != nil {
t.Fatalf("reload agent: %v", err)
}
return row
}
if code := post("203.0.113.88"); code != http.StatusOK {
t.Fatalf("socket beacon with edge header: status %d", code)
}
if row := load(); row.SeenIPv4 != "203.0.113.88" {
t.Fatalf("socket beacon did not store seen_ipv4: %+v", row)
}
if code := post(""); code != http.StatusOK {
t.Fatalf("socket beacon without header: status %d", code)
}
if row := load(); row.SeenIPv4 != "203.0.113.88" {
t.Fatalf("loopback socket beacon displaced seen_ipv4: %+v", row)
}
}
-138
View File
@@ -1,138 +0,0 @@
package web
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
"time"
"github.com/robfig/cron/v3"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/web/global"
)
/*
frontend/src/pages/api-docs/endpoints.ts is a hand-maintained registry: an
API route omitted there silently vanishes from the generated OpenAPI docs,
and an entry for a removed route documents an endpoint that 404s. This test
constructs the real router and diffs it against the registry both ways.
Scope: everything under /panel/api/ plus the session-auth surface the
registry also documents (/login, /logout, /csrf-token, /ws). SPA page
routes are UI, not API, and stay out; registry paths that start with "/{"
describe the standalone subscription server, which this engine does not
serve.
*/
var contractExtraRoutes = map[string]bool{
"POST /login": true,
"POST /logout": true,
"GET /csrf-token": true,
"GET /ws": true,
}
func inContractScope(method, path string) bool {
return strings.HasPrefix(path, "/panel/api/") || contractExtraRoutes[method+" "+path]
}
func registeredContractRoutes(t *testing.T) map[string]bool {
t.Helper()
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("init db: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })
previous := global.GetWebServer()
s := NewServer()
s.cron = cron.New(cron.WithLocation(time.Local), cron.WithSeconds())
global.SetWebServer(s)
t.Cleanup(func() {
s.cancel()
global.SetWebServer(previous)
})
engine, err := s.initRouter()
if err != nil {
t.Fatalf("init router: %v", err)
}
routes := make(map[string]bool)
for _, r := range engine.Routes() {
routes[r.Method+" "+r.Path] = true
}
if len(routes) == 0 {
t.Fatal("no routes registered; router construction is broken")
}
return routes
}
func documentedContractRoutes(t *testing.T) map[string]bool {
t.Helper()
source, err := os.ReadFile(filepath.Join("..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts"))
if err != nil {
t.Fatalf("read endpoints.ts: %v", err)
}
text := string(source)
methodRe := regexp.MustCompile(`method:\s*'(GET|POST|PUT|DELETE|PATCH|WS)'`)
pathRe := regexp.MustCompile(`path:\s*'([^']+)'`)
methods := methodRe.FindAllStringSubmatchIndex(text, -1)
if declared := strings.Count(text, "method: '"); len(methods) != declared {
t.Fatalf("parsed %d method fields but endpoints.ts declares %d — the parser regex no longer matches the file shape", len(methods), declared)
}
docs := make(map[string]bool)
for i, m := range methods {
segmentEnd := len(text)
if i+1 < len(methods) {
segmentEnd = methods[i+1][0]
}
pathMatch := pathRe.FindStringSubmatch(text[m[1]:segmentEnd])
if pathMatch == nil {
t.Fatalf("entry %d in endpoints.ts has a method but no path before the next entry — the parser cannot pair it", i)
}
method := text[m[2]:m[3]]
if strings.HasPrefix(pathMatch[1], "/{") || !strings.HasPrefix(pathMatch[1], "/") {
continue
}
docs[method+" "+pathMatch[1]] = true
}
if len(docs) == 0 {
t.Fatal("no entries parsed from endpoints.ts; the parser regex is broken")
}
return docs
}
func TestRouteRegistryContract(t *testing.T) {
registered := registeredContractRoutes(t)
documented := documentedContractRoutes(t)
t.Run("every API route is documented", func(t *testing.T) {
var missing []string
for route := range registered {
fields := strings.Fields(route)
if inContractScope(fields[0], fields[1]) && !documented[route] {
missing = append(missing, route)
}
}
sort.Strings(missing)
for _, route := range missing {
t.Error(fmt.Errorf("route %s is registered but absent from endpoints.ts — add an entry or it vanishes from the API docs", route))
}
})
t.Run("every documented route is registered", func(t *testing.T) {
var stale []string
for route := range documented {
if !registered[route] {
stale = append(stale, route)
}
}
sort.Strings(stale)
for _, route := range stale {
t.Error(fmt.Errorf("endpoints.ts documents %s but the server does not register it — remove or fix the entry", route))
}
})
}
+25 -3
View File
@@ -18,6 +18,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/selfupdate"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/probe"
"gorm.io/gorm"
)
@@ -512,6 +513,10 @@ func (s *NodeService) UpdateFromRequest(id int, req *NodeMutationRequest) error
return nil
}
// deleteProbeDataForAgent drops one agent's probe rows; a variable so
// tests can point the cascade at a scratch probe store.
var deleteProbeDataForAgent = probe.DeleteAgentData
func (s *NodeService) Delete(id int) error {
db := database.GetDB()
// Refuse to delete a node that still owns inbounds: dropping the node row
@@ -534,11 +539,20 @@ func (s *NodeService) Delete(id int) error {
if err := db.Select("guid").Where("id = ?", id).First(&n).Error; err == nil {
guid = n.Guid
}
// Bound agents link by agent_nodes.node_id = panel node id, never agent_nodes.id (Oracle-JP: node 33, agent row 20).
// Collected before the transaction so a failed commit never touches probe data.
var agentIDs []int
if err := db.Model(&model.AgentNode{}).Where("node_id = ?", id).Pluck("id", &agentIDs).Error; err != nil {
return err
}
// Delete the node row and its per-node child rows atomically. Remove the
// children (traffic baselines, IP attribution) before the parent node row so
// the ordering already matches a future ON DELETE constraint. Delete stays
// tolerant of a missing node row so it can still clean up orphaned baselines.
// children (agent bindings, baselines, IP attribution) before the parent
// row; ordering already matches a future ON DELETE constraint, and Delete
// stays tolerant of a missing node row so it can still clean up orphans.
if err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("node_id = ?", id).Delete(&model.AgentNode{}).Error; err != nil {
return err
}
if err := tx.Where("node_id = ?", id).Delete(&model.NodeClientTraffic{}).Error; err != nil {
return err
}
@@ -553,6 +567,14 @@ func (s *NodeService) Delete(id int) error {
}); err != nil {
return err
}
// probe.db is a separate file, so its rows go in their own transaction inside DeleteAgentData, keyed by agent row id.
// Best-effort like agent deletion; the public-page snapshot is dropped so the node vanishes at once.
for _, agentID := range agentIDs {
deleteProbeDataForAgent(int64(agentID))
}
if len(agentIDs) > 0 {
InvalidateProbeSnapshot()
}
nodeMetrics.drop(nodeMetricKey(id, "cpu"))
nodeMetrics.drop(nodeMetricKey(id, "mem"))
return nil
@@ -0,0 +1,192 @@
package service
import (
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/probe"
)
// Node deletion must cascade to the node's own agent bindings and to
// every probe row of those agents, and to nothing else. The key trap:
// agent rows link by node_id (the panel node id), while probe rows
// link by agent row id — matching probe rows on the node id would
// delete a different node's probe data (Oracle-JP: node 33's agent
// row id was 20, so node-id matching would have struck agent 33).
func TestNodeDelete_CascadesAgentAndProbeRows(t *testing.T) {
db := initTrafficTestDB(t)
st, err := probe.Open(filepath.Join(t.TempDir(), "probe.db"))
if err != nil {
t.Fatalf("open probe store: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
var probeErr error
oldProbeDelete := deleteProbeDataForAgent
deleteProbeDataForAgent = func(id int64) {
if err := st.DeleteAgentData(id); err != nil && probeErr == nil {
probeErr = err
}
}
t.Cleanup(func() { deleteProbeDataForAgent = oldProbeDelete })
nodeA := &model.Node{Name: "cascade-a"}
nodeB := &model.Node{Name: "cascade-b"}
if err := db.Create(nodeA).Error; err != nil {
t.Fatalf("create node A: %v", err)
}
if err := db.Create(nodeB).Error; err != nil {
t.Fatalf("create node B: %v", err)
}
mkAgent := func(name string, nodeID *int) *model.AgentNode {
ag := &model.AgentNode{TokenHash: "hash-" + name, Name: name, NodeID: nodeID}
if err := db.Create(ag).Error; err != nil {
t.Fatalf("create agent %s: %v", name, err)
}
return ag
}
a1 := mkAgent("cascade-a1", &nodeA.Id)
a2 := mkAgent("cascade-a2", &nodeA.Id)
b1 := mkAgent("cascade-b1", &nodeB.Id)
u1 := mkAgent("cascade-unbound", nil)
if err := db.Create(&model.NodeClientTraffic{NodeId: nodeA.Id, Email: "a@example.com"}).Error; err != nil {
t.Fatalf("seed baseline A: %v", err)
}
if err := db.Create(&model.NodeClientTraffic{NodeId: nodeB.Id, Email: "b@example.com"}).Error; err != nil {
t.Fatalf("seed baseline B: %v", err)
}
seedProbe := func(ag *model.AgentNode, price float64) {
t.Helper()
exec := func(q string, args ...any) {
t.Helper()
if _, err := st.DB().Exec(q, args...); err != nil {
t.Fatalf("seed probe agent %d: %v", ag.Id, err)
}
}
exec(`INSERT INTO probe_meta (agent_id, price) VALUES (?, ?)`, ag.Id, price)
exec(`INSERT INTO probe_traffic (agent_id, total_rx) VALUES (?, 100)`, ag.Id)
exec(`INSERT INTO probe_metric (agent_id, ts, cpu, mem_used, swap_used, disk_used, net_rx, net_tx, tcp, udp, procs) VALUES (?, 1000, 1, 1, 1, 1, 1, 1, 1, 1, 1)`, ag.Id)
exec(`INSERT INTO probe_ping_node (task_id, agent_id) VALUES (7001, ?)`, ag.Id)
exec(`INSERT INTO probe_ping_record (agent_id, ts, task_id, latency) VALUES (?, 1000, 7001, 5)`, ag.Id)
}
if _, err := st.DB().Exec(`INSERT INTO probe_ping_task (id, name, target) VALUES (7001, 'cascade-task', 'example.com')`); err != nil {
t.Fatalf("seed ping task: %v", err)
}
seedProbe(a1, 11)
seedProbe(a2, 22)
seedProbe(b1, 33)
svc := &NodeService{}
if err := svc.Delete(nodeA.Id); err != nil {
t.Fatalf("Delete node A: %v", err)
}
if probeErr != nil {
t.Fatalf("probe cascade: %v", probeErr)
}
countPanel := func(table, where string, args ...any) int64 {
t.Helper()
var n int64
if err := db.Table(table).Where(where, args...).Count(&n).Error; err != nil {
t.Fatalf("count %s: %v", table, err)
}
return n
}
countProbe := func(table string, agentID int) int {
t.Helper()
var n int
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM `+table+` WHERE agent_id = ?`, agentID).Scan(&n); err != nil {
t.Fatalf("count probe %s agent %d: %v", table, agentID, err)
}
return n
}
if got := countPanel("nodes", "id = ?", nodeA.Id); got != 0 {
t.Fatalf("node A rows = %d, want 0", got)
}
if got := countPanel("nodes", "id = ?", nodeB.Id); got != 1 {
t.Fatalf("node B rows = %d, want 1", got)
}
if got := countPanel("agent_nodes", "node_id = ?", nodeA.Id); got != 0 {
t.Fatalf("node A agent rows = %d, want 0", got)
}
if got := countPanel("agent_nodes", "id = ?", b1.Id); got != 1 {
t.Fatalf("node B agent row = %d, want 1", got)
}
if got := countPanel("agent_nodes", "id = ?", u1.Id); got != 1 {
t.Fatalf("unbound agent row = %d, want 1", got)
}
if got := countPanel("node_client_traffics", "node_id = ?", nodeA.Id); got != 0 {
t.Fatalf("node A baselines = %d, want 0", got)
}
if got := countPanel("node_client_traffics", "node_id = ?", nodeB.Id); got != 1 {
t.Fatalf("node B baselines = %d, want 1", got)
}
// Deleted agents: every probe table empty. Surviving agent: every
// probe row intact, with its own values (not just a nonzero count).
for _, table := range []string{"probe_meta", "probe_traffic", "probe_metric", "probe_ping_node", "probe_ping_record"} {
for _, ag := range []*model.AgentNode{a1, a2} {
if got := countProbe(table, ag.Id); got != 0 {
t.Fatalf("%s rows for deleted agent %d = %d, want 0", table, ag.Id, got)
}
}
if got := countProbe(table, b1.Id); got != 1 {
t.Fatalf("%s rows for surviving agent %d = %d, want 1", table, b1.Id, got)
}
}
var price float64
if err := st.DB().QueryRow(`SELECT price FROM probe_meta WHERE agent_id = ?`, b1.Id).Scan(&price); err != nil || price != 33 {
t.Fatalf("surviving agent meta price = %v err=%v, want 33", price, err)
}
var taskCnt int
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM probe_ping_task WHERE id = 7001`).Scan(&taskCnt); err != nil || taskCnt != 1 {
t.Fatalf("ping task rows = %d err=%v, want 1 (tasks are global, only assignments cascade)", taskCnt, err)
}
}
// A blocked delete (inbounds still attached) must touch neither panel
// agent rows nor probe rows: the probe cascade runs only after the
// panel transaction commits.
func TestNodeDelete_BlockedKeepsAgentAndProbeRows(t *testing.T) {
db := initTrafficTestDB(t)
st, err := probe.Open(filepath.Join(t.TempDir(), "probe.db"))
if err != nil {
t.Fatalf("open probe store: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
var probeCalls []int64
oldProbeDelete := deleteProbeDataForAgent
deleteProbeDataForAgent = func(id int64) { probeCalls = append(probeCalls, id) }
t.Cleanup(func() { deleteProbeDataForAgent = oldProbeDelete })
node := &model.Node{Name: "blocked-node"}
if err := db.Create(node).Error; err != nil {
t.Fatalf("create node: %v", err)
}
ag := &model.AgentNode{TokenHash: "hash-blocked", Name: "blocked-agent", NodeID: &node.Id}
if err := db.Create(ag).Error; err != nil {
t.Fatalf("create agent: %v", err)
}
if _, err := st.DB().Exec(`INSERT INTO probe_meta (agent_id, price) VALUES (?, 44)`, ag.Id); err != nil {
t.Fatalf("seed probe meta: %v", err)
}
createNodeInbound(t, db, node.Id, "blocked-in-443", 443)
svc := &NodeService{}
if err := svc.Delete(node.Id); err == nil {
t.Fatal("Delete should fail while an inbound is still attached")
}
if len(probeCalls) != 0 {
t.Fatalf("probe cascade ran on blocked delete: %v", probeCalls)
}
var nodeCnt, agCnt int64
db.Model(&model.Node{}).Where("id = ?", node.Id).Count(&nodeCnt)
db.Model(&model.AgentNode{}).Where("id = ?", ag.Id).Count(&agCnt)
var metaCnt int
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM probe_meta WHERE agent_id = ?`, ag.Id).Scan(&metaCnt); err != nil {
t.Fatalf("count probe meta: %v", err)
}
if nodeCnt != 1 || agCnt != 1 || metaCnt != 1 {
t.Fatalf("after blocked delete: node=%d agent=%d probeMeta=%d, want 1/1/1", nodeCnt, agCnt, metaCnt)
}
}
+4 -2
View File
@@ -61,8 +61,10 @@ func insertMinute(t *testing.T, st *Store, agent, ts int64, cpu float64, memUsed
// peaks take the max of value and stored peak.
func TestMetricHistoryMinuteFold(t *testing.T) {
st := newTestStore(t)
now := time.Now().Unix()
base := now - now%60 - 120 // a minute boundary two minutes back
// Fixed instant on a 120 s boundary: a wall-clock base folds in two
// whenever the run lands on an odd minute (minute-parity flaky).
now := int64(1_800_000_000)
base := now - 120
insertMinute(t, st, 7, base, 10, 100, 1000, 1500, 12, 3)
insertMinute(t, st, 7, base+60, 20, 201, 2000, 500, 25, 5)
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "العملاء",
"nodes": "النودز",
"settings": "إعدادات اللوحة",
"donate": "تبرع",
"hosts": "المضيفات",
"docs": "التوثيق",
"openMenu": "فتح القائمة",
"pinSidebar": "تثبيت الشريط الجانبي",
"unpinSidebar": "إلغاء تثبيت الشريط الجانبي"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "تعذّر التأكد من اكتمال التحديث",
"panelUpdateUnknownDesc": "لم تُبلغ اللوحة بنتيجة في الوقت المحدد. أعد تحميل الصفحة للتحقق من الإصدار الحالي، أو تحقق من سجلات الخادم.",
"dontRefresh": "التثبيت شغال، متعملش Refresh للصفحة",
"config": "الإعدادات",
"backupTitle": "نسخ احتياطي واستعادة",
"exportDatabase": "اخزن نسخة",
"exportDatabaseDesc": "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك. نفس الملف ممكن كمان يترجع على لوحة شغالة بـ PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "حدث خطأ أثناء استيراد قاعدة البيانات",
"readDatabaseError": "حدث خطأ أثناء قراءة قاعدة البيانات",
"getDatabaseError": "حدث خطأ أثناء استرجاع قاعدة البيانات",
"getConfigError": "حدث خطأ أثناء استرجاع ملف الإعدادات",
"backupPostgresNote": "تعمل هذه اللوحة على PostgreSQL. يقوم «النسخ الاحتياطي» بتنزيل أرشيف pg_dump (.dump)، و«الاستعادة» تعيد تحميله عبر pg_restore. تقبل «الاستعادة» أيضًا قاعدة بيانات SQLite (.db) أو ملف ترحيل SQLite وتستورد بياناتها إلى PostgreSQL. يجب أن تكون أدوات عميل PostgreSQL (pg_dump و pg_restore) مثبَّتة على الخادم.",
"exportDatabasePgDesc": "انقر لتنزيل نسخة PostgreSQL (.dump) من قاعدة بياناتك الحالية إلى جهازك.",
"importDatabasePgDesc": "انقر لاختيار ورفع نسخة احتياطية من PostgreSQL (.dump) أو قاعدة بيانات SQLite (.db) أو ملف ترحيل SQLite لاستعادة قاعدة البيانات. سيؤدي هذا إلى استبدال جميع البيانات الحالية.",
@@ -822,31 +818,10 @@
"avgLatency": "متوسط الكمون",
"name": "الاسم",
"namePlaceholder": "مثال: de-frankfurt-1",
"addressPlaceholder": "panel.example.com أو 1.2.3.4",
"remark": "ملاحظة",
"scheme": "البروتوكول",
"address": "العنوان",
"port": "المنفذ",
"basePath": "المسار الأساسي",
"apiToken": "رمز API",
"apiTokenPlaceholder": "التوكن من صفحة إعدادات البانل البعيد",
"apiTokenHint": "البانل البعيد بيعرض توكن API بتاعه في المصادقة → توكن API.",
"apiTokenKeepHint": "اتركه فارغًا للإبقاء على التوكن الحالي",
"allowPrivateAddress": "السماح بالعنوان الخاص",
"allowPrivateAddressHint": "التفعيل فقط للعقد على شبكة خاصة أو VPN.",
"outboundTag": "اتصال صادر",
"outboundTagHint": "وجه حركة مرور API اللوحة لهذه العقدة عبر outbound Xray المحدد. يتم إضافة inbound جسر loopback تلقائيًا إلى التكوين قيد التشغيل وتطبيقه مباشرة. اتركه فارغًا للاتصال المباشر.",
"outboundTagPlaceholder": "اتصال مباشر",
"inboundSyncMode": "استيراد الاتصالات الواردة",
"inboundSyncModeHint": "اختر الاتصالات الواردة التي سيتم استيرادها من هذه العقدة. تستورد العقد الحالية جميع الاتصالات افتراضيًا.",
"allInbounds": "جميع الاتصالات الواردة",
"selectedInbounds": "الاتصالات الواردة المحددة",
"inboundTags": "الاتصالات الواردة",
"inboundTagsHint": "تتم المطابقة حسب وسم الاتصال الوارد. القائمة الفارغة لا تستورد أي اتصال.",
"inboundTagsPlaceholder": "حمّل الاتصالات الواردة وحددها",
"loadInbounds": "تحميل الاتصالات الواردة من العقدة",
"inboundsLoaded": "تم تحميل {{count}} اتصال وارد",
"inboundsLoadFailed": "فشل تحميل الاتصالات الواردة",
"enable": "مفعل",
"status": "الحالة",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "إصدار اللوحة",
"actions": "العمليات",
"probe": "فحص فوري",
"updatePanel": "تحديث اللوحة",
"updateSelected": "تحديث المحدد ({count})",
"updateAvailable": "تحديث متاح",
"updateConfirmTitle": "تحديث {count} عقدة إلى أحدث إصدار؟",
"updateConfirmContent": "كل عقدة محددة ستنزّل أحدث إصدار وتعيد التشغيل عليه. يتم تحديث العقد المفعّلة والمتصلة فقط.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "التحديث إلى قناة التطوير (أحدث كومِت)",
"testConnection": "اختبار الاتصال",
"connectionOk": "الاتصال شغال ({ms} ms)",
"connectionFailed": "فشل الاتصال",
"never": "أبدًا",
"justNow": "دلوقتي",
"subNode": "نود فرعي",
"subNodeTip": "للقراءة فقط: نود تابع يتم الوصول إليه عبر {parent}. تتم إدارته من لوحة {parent} نفسها.",
"deleteConfirmTitle": "تحذف النود \"{name}\"؟",
"deleteConfirmContent": "ده هيوقّف مراقبة النود. البانل البعيد نفسه مش هيتأثر.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "فشل الفحص",
"updateStarted": "بدأ تحديث اللوحة",
"updateResult": "تم بدء التحديث على {ok} عقدة، فشل {failed}",
"updateNoneEligible": "اختر عقدة واحدة على الأقل متصلة ومفعّلة",
"saveMtls": "حفظ mTLS النود",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "اختر عقدة واحدة على الأقل متصلة ومفعّلة"
},
"tlsVerifyMode": "التحقق من TLS",
"tlsVerifyModeHint": "كيف يتحقق اللوحة من شهادة HTTPS الخاصة بالعقدة. التثبيت أو التخطّي مخصّصان للشهادات الموقّعة ذاتيًا (عُقد https فقط).",
"tlsVerify": "تحقّق (CA الافتراضية)",
"tlsPin": "تثبيت الشهادة (SHA-256)",
"tlsSkip": "تخطّي التحقق",
"tlsMtls": "TLS متبادل (شهادة العميل)",
"mtlsFormHint": "يصادق هذا النود اللوحة باستخدام شهادة عميل. انسخ CA الخاص بهذه اللوحة من قسم mTLS النود إلى النود، واضبط CA الموثوق به، ثم أعد تشغيله.",
"mtls": {
"title": "mTLS النود",
"intro": "يضيف TLS المتبادل عامل شهادة العميل فوق رمز API في الاتصالات بين النودات. وهو اختياري: اتركه فارغًا للاكتفاء بالمصادقة عبر الرمز.",
"copyCa": "نسخ CA الخاص بهذه اللوحة",
"copyCaHint": "سلّم هذا الـ CA إلى النودات التي تديرها هذه اللوحة، ثم اضبط التحقق من TLS لديها على TLS المتبادل.",
"caCopied": "تم نسخ شهادة CA إلى الحافظة",
"caFailed": "تعذّر الحصول على شهادة CA",
"trustLabel": "CA الموثوق (اللوحة الأم)",
"trustHint": "عندما تكون هذه اللوحة نفسها نودًا، الصق هنا CA الخاص باللوحة التي تديرها لطلب شهادة العميل الخاصة بها. أعد تشغيل اللوحة للتطبيق.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "حفظ CA الموثوق",
"saved": "تم حفظ CA الموثوق — أعد تشغيل اللوحة للتطبيق"
},
"tlsSkipWarning": "تخطّي التحقق يزيل الحماية من هجمات الوسيط — قد يُعترض رمز الـ API. يُفضَّل تثبيت الشهادة بدلاً من ذلك.",
"pinnedCert": "SHA-256 للشهادة المثبّتة",
"pinnedCertHint": "SHA-256 لشهادة العقدة بصيغة base64 أو hex. استخدم \"جلب\" لقراءتها من العقدة الآن.",
"pinnedCertPlaceholder": "SHA-256 بصيغة base64 أو hex",
"fetchPin": "جلب",
"pinFetched": "تم جلب شهادة العقدة الحالية",
"pinFetchFailed": "تعذّر جلب الشهادة",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "أساسي",
"advancedTemplate": "متقدم",
"Outbounds": "الصادرات",
"Balancers": "موازنات التحميل",
"rules": {
"inbound": "إدخال"
},
+1 -67
View File
@@ -98,8 +98,6 @@
"nodes": "Nodes",
"hosts": "Hosts",
"settings": "Panel Settings",
"donate": "Donate",
"docs": "Documentation",
"openMenu": "Open menu",
"pinSidebar": "Pin sidebar",
"unpinSidebar": "Unpin sidebar"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Couldn't confirm the update finished",
"panelUpdateUnknownDesc": "The panel didn't report a result in time. Reload to check the current version, or check the server logs.",
"dontRefresh": "Installation is in progress, please do not refresh this page",
"config": "Config",
"backupTitle": "Backup & Restore",
"exportDatabase": "Back Up",
"exportDatabaseDesc": "Click to download a .db file containing a backup of your current database to your device. The same file can also be restored into a panel running on PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "An error occurred while importing the database.",
"readDatabaseError": "An error occurred while reading the database.",
"getDatabaseError": "An error occurred while retrieving the database.",
"getConfigError": "An error occurred while retrieving the config file.",
"backupPostgresNote": "This panel runs on PostgreSQL. Back Up downloads a pg_dump archive (.dump) and Restore loads it back with pg_restore. Restore also accepts a SQLite database (.db) or a SQLite migration dump and imports its data into PostgreSQL. The server needs the PostgreSQL client tools (pg_dump and pg_restore) installed.",
"exportDatabasePgDesc": "Click to download a PostgreSQL dump (.dump) of your current database to your device.",
"importDatabasePgDesc": "Click to select and upload a PostgreSQL backup (.dump), a SQLite database (.db), or a SQLite migration dump to restore your database. This replaces all current data.",
@@ -940,31 +936,10 @@
"avgLatency": "Avg Latency",
"name": "Name",
"namePlaceholder": "e.g. de-frankfurt-1",
"addressPlaceholder": "panel.example.com or 1.2.3.4",
"remark": "Remark",
"scheme": "Scheme",
"address": "Address",
"port": "Port",
"basePath": "Base Path",
"apiToken": "API Token",
"apiTokenPlaceholder": "Token from the remote panel's Settings page",
"apiTokenHint": "The remote panel exposes its API token under Authentication → API Token.",
"apiTokenKeepHint": "Leave blank to keep the current token",
"allowPrivateAddress": "Allow private address",
"allowPrivateAddressHint": "Enable only for nodes on a private network or VPN.",
"outboundTag": "Connection outbound",
"outboundTagHint": "Route this node's panel API traffic through the selected Xray outbound. A loopback bridge inbound is added to the running config automatically and applied live. Leave empty for a direct connection.",
"outboundTagPlaceholder": "Direct connection",
"inboundSyncMode": "Inbound import",
"inboundSyncModeHint": "Choose which inbounds are imported from this node. Existing nodes default to all inbounds.",
"allInbounds": "All inbounds",
"selectedInbounds": "Selected inbounds",
"inboundTags": "Inbounds",
"inboundTagsHint": "Selection is matched by the inbound tag. An empty selection imports none.",
"inboundTagsPlaceholder": "Load and select inbounds",
"loadInbounds": "Load inbounds from node",
"inboundsLoaded": "Loaded {{count}} inbounds",
"inboundsLoadFailed": "Failed to load inbounds",
"enable": "Enabled",
"status": "Status",
"cpu": "CPU",
@@ -978,23 +953,16 @@
"panelVersion": "Panel Version",
"actions": "Actions",
"probe": "Probe Now",
"updatePanel": "Update Panel",
"updateSelected": "Update Selected ({count})",
"updateAvailable": "Update available",
"updateConfirmTitle": "Update {count} node(s) to the latest version?",
"updateConfirmContent": "Each selected node downloads the latest release and restarts onto it. Only enabled, online nodes are updated.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Update to Dev channel (latest commit)",
"testConnection": "Test Connection",
"connectionOk": "Connection OK ({ms} ms)",
"connectionFailed": "Connection failed",
"never": "never",
"justNow": "just now",
"subNode": "Sub-node",
"subNodeTip": "Read-only: a downstream node reached through {parent}. Manage it from {parent}'s own panel.",
"deleteConfirmTitle": "Delete node \"{name}\"?",
"deleteConfirmContent": "This stops monitoring the node. The remote panel itself is unaffected.",
"statusValues": {
@@ -1016,40 +984,8 @@
"probeFailed": "Probe failed",
"updateStarted": "Panel update started",
"updateResult": "Update triggered on {ok} node(s), {failed} failed",
"updateNoneEligible": "Select at least one online, enabled node",
"saveMtls": "Save node mTLS",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Select at least one online, enabled node"
},
"tlsVerifyMode": "TLS verification",
"tlsVerifyModeHint": "How the panel validates the node's HTTPS certificate. Pin or Skip are for self-signed certs (https nodes only).",
"tlsVerify": "Verify (default CA)",
"tlsPin": "Pin certificate (SHA-256)",
"tlsSkip": "Skip verification",
"tlsMtls": "Mutual TLS (client certificate)",
"mtlsFormHint": "This node authenticates the panel with a client certificate. Copy this panel's CA from the Node mTLS section onto the node, set its Trusted parent CA, then restart it.",
"mtls": {
"title": "Node mTLS",
"intro": "Mutual TLS adds a client-certificate factor on top of the API token for node-to-node calls. It is opt-in: leave it empty to keep token-only auth.",
"copyCa": "Copy this panel's CA",
"copyCaHint": "Hand this CA to the nodes this panel manages, then set their TLS verification to Mutual TLS.",
"caCopied": "CA certificate copied to clipboard",
"caFailed": "Failed to obtain the CA certificate",
"trustLabel": "Trusted parent CA",
"trustHint": "When this panel is itself a node, paste the managing panel's CA here to require its client certificate. Restart the panel to apply.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Save trust CA",
"saved": "Trust CA saved — restart the panel to apply"
},
"tlsSkipWarning": "Skipping verification removes protection against man-in-the-middle attacks — the API token could be intercepted. Prefer pinning the certificate.",
"pinnedCert": "Pinned certificate SHA-256",
"pinnedCertHint": "Base64 or hex SHA-256 of the node's certificate. Use Fetch to read it from the node now.",
"pinnedCertPlaceholder": "base64 or hex SHA-256",
"fetchPin": "Fetch",
"pinFetched": "Fetched the node's current certificate",
"pinFetchFailed": "Could not fetch the certificate",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1391,8 +1327,6 @@
"xray": {
"basicTemplate": "Basics",
"advancedTemplate": "Advanced",
"Outbounds": "Outbounds",
"Balancers": "Balancers",
"rules": {
"inbound": "Inbound"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Clientes",
"nodes": "Nodos",
"settings": "Ajustes del panel",
"donate": "Donar",
"hosts": "Hosts",
"docs": "Documentación",
"openMenu": "Abrir menú",
"pinSidebar": "Fijar barra lateral",
"unpinSidebar": "Desfijar barra lateral"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "No se pudo confirmar si la actualización terminó",
"panelUpdateUnknownDesc": "El panel no informó un resultado a tiempo. Recarga la página para comprobar la versión actual, o revisa los registros del servidor.",
"dontRefresh": "La instalación está en progreso, por favor no actualices esta página.",
"config": "Configuración",
"backupTitle": "Copia & Restauración",
"exportDatabase": "Copia de seguridad",
"exportDatabaseDesc": "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo. El mismo archivo también puede restaurarse en un panel que funcione con PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Ocurrió un error al importar la base de datos",
"readDatabaseError": "Ocurrió un error al leer la base de datos",
"getDatabaseError": "Ocurrió un error al obtener la base de datos",
"getConfigError": "Ocurrió un error al obtener el archivo de configuración",
"backupPostgresNote": "Este panel funciona con PostgreSQL. «Copia de seguridad» descarga un archivo pg_dump (.dump) y «Restaurar» lo vuelve a cargar con pg_restore. «Restaurar» también acepta una base de datos SQLite (.db) o un volcado de migración de SQLite e importa sus datos a PostgreSQL. El servidor necesita tener instaladas las herramientas cliente de PostgreSQL (pg_dump y pg_restore).",
"exportDatabasePgDesc": "Haz clic para descargar un volcado de PostgreSQL (.dump) de tu base de datos actual en tu dispositivo.",
"importDatabasePgDesc": "Haz clic para seleccionar y subir una copia de seguridad de PostgreSQL (.dump), una base de datos SQLite (.db) o un volcado de migración de SQLite para restaurar tu base de datos. Esto reemplaza todos los datos actuales.",
@@ -822,31 +818,10 @@
"avgLatency": "Latencia media",
"name": "Nombre",
"namePlaceholder": "p. ej. de-frankfurt-1",
"addressPlaceholder": "panel.example.com o 1.2.3.4",
"remark": "Notas",
"scheme": "Esquema",
"address": "Dirección",
"port": "Puerto",
"basePath": "Ruta base",
"apiToken": "Token API",
"apiTokenPlaceholder": "Token desde la página de Configuración del panel remoto",
"apiTokenHint": "El panel remoto expone su token de API en Configuraciones de Seguridad → Token de API.",
"apiTokenKeepHint": "Déjalo en blanco para mantener el token actual",
"allowPrivateAddress": "Permitir dirección privada",
"allowPrivateAddressHint": "Habilitar solo para nodos en una red privada o VPN.",
"outboundTag": "Outbound de conexión",
"outboundTagHint": "Enruta el tráfico de la API del panel de este nodo a través del outbound Xray seleccionado. Un inbound de puente loopback se agrega automáticamente a la configuración en ejecución y se aplica en vivo. Déjelo vacío para una conexión directa.",
"outboundTagPlaceholder": "Conexión directa",
"inboundSyncMode": "Importación de inbounds",
"inboundSyncModeHint": "Elige qué inbounds importar desde este nodo. Los nodos existentes importan todos de forma predeterminada.",
"allInbounds": "Todos los inbounds",
"selectedInbounds": "Inbounds seleccionados",
"inboundTags": "Inbounds",
"inboundTagsHint": "La selección se compara por la etiqueta del inbound. Una selección vacía no importa ninguno.",
"inboundTagsPlaceholder": "Carga y selecciona inbounds",
"loadInbounds": "Cargar inbounds desde el nodo",
"inboundsLoaded": "Se cargaron {{count}} inbounds",
"inboundsLoadFailed": "No se pudieron cargar los inbounds",
"enable": "Habilitado",
"status": "Estado",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Versión del panel",
"actions": "Acciones",
"probe": "Sondear ahora",
"updatePanel": "Actualizar panel",
"updateSelected": "Actualizar seleccionados ({count})",
"updateAvailable": "Actualización disponible",
"updateConfirmTitle": "¿Actualizar {count} nodo(s) a la última versión?",
"updateConfirmContent": "Cada nodo seleccionado descarga la última versión y se reinicia con ella. Solo se actualizan los nodos habilitados y en línea.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Actualizar al canal de desarrollo (último commit)",
"testConnection": "Probar conexión",
"connectionOk": "Conexión correcta ({ms} ms)",
"connectionFailed": "Conexión fallida",
"never": "nunca",
"justNow": "ahora mismo",
"subNode": "Subnodo",
"subNodeTip": "Solo lectura: un nodo descendente al que se llega a través de {parent}. Gestiónalo desde el propio panel de {parent}.",
"deleteConfirmTitle": "¿Eliminar el nodo \"{name}\"?",
"deleteConfirmContent": "Esto detiene la monitorización del nodo. El panel remoto en sí no se ve afectado.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Sondeo fallido",
"updateStarted": "Actualización del panel iniciada",
"updateResult": "Actualización iniciada en {ok} nodo(s), {failed} fallaron",
"updateNoneEligible": "Selecciona al menos un nodo en línea y habilitado",
"saveMtls": "Guardar mTLS del nodo",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Selecciona al menos un nodo en línea y habilitado"
},
"tlsVerifyMode": "Verificación TLS",
"tlsVerifyModeHint": "Cómo valida el panel el certificado HTTPS del nodo. Fijar u Omitir son para certificados autofirmados (solo nodos https).",
"tlsVerify": "Verificar (CA predeterminada)",
"tlsPin": "Fijar certificado (SHA-256)",
"tlsSkip": "Omitir verificación",
"tlsMtls": "TLS mutuo (certificado de cliente)",
"mtlsFormHint": "Este nodo autentica al panel con un certificado de cliente. Copia el CA de este panel desde la sección mTLS del nodo al nodo, configura su CA de confianza y luego reinícialo.",
"mtls": {
"title": "mTLS del nodo",
"intro": "TLS mutuo añade un factor de certificado de cliente además del token de API para las llamadas entre nodos. Es opcional: déjalo vacío para mantener solo la autenticación por token.",
"copyCa": "Copiar el CA de este panel",
"copyCaHint": "Entrega este CA a los nodos que gestiona este panel y luego configura su verificación TLS como TLS mutuo.",
"caCopied": "Certificado CA copiado al portapapeles",
"caFailed": "No se pudo obtener el certificado CA",
"trustLabel": "CA de confianza (panel superior)",
"trustHint": "Cuando este panel es a su vez un nodo, pega aquí el CA del panel que lo gestiona para exigir su certificado de cliente. Reinicia el panel para aplicar.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Guardar CA de confianza",
"saved": "CA de confianza guardado — reinicia el panel para aplicar"
},
"tlsSkipWarning": "Omitir la verificación elimina la protección contra ataques de intermediario; el token de API podría ser interceptado. Es preferible fijar el certificado.",
"pinnedCert": "SHA-256 del certificado fijado",
"pinnedCertHint": "SHA-256 del certificado del nodo en base64 o hex. Usa Obtener para leerlo del nodo ahora.",
"pinnedCertPlaceholder": "SHA-256 en base64 o hex",
"fetchPin": "Obtener",
"pinFetched": "Se obtuvo el certificado actual del nodo",
"pinFetchFailed": "No se pudo obtener el certificado",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Perfil Básico",
"advancedTemplate": "Perfil Avanzado",
"Outbounds": "Salidas",
"Balancers": "Equilibradores",
"rules": {
"inbound": "Entrante"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "کلاینت‌ها",
"nodes": "نودها",
"settings": "تنظیمات پنل",
"donate": "حمایت مالی",
"hosts": "میزبان‌ها",
"docs": "مستندات",
"openMenu": "باز کردن منو",
"pinSidebar": "ثابت کردن نوار کناری",
"unpinSidebar": "برداشتن تثبیت نوار کناری"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "تأیید نشد که به‌روزرسانی به پایان رسیده باشد",
"panelUpdateUnknownDesc": "پنل به‌موقع نتیجه‌ای گزارش نکرد. صفحه را بارگذاری مجدد کنید تا نسخه فعلی را بررسی کنید یا گزارش‌های سرور را بررسی کنید.",
"dontRefresh": "در حال نصب، لطفا صفحه را رفرش نکنید",
"config": "پیکربندی",
"backupTitle": "پشتیبان‌گیری و بازیابی",
"exportDatabase": "پشتیبان‌گیری",
"exportDatabaseDesc": "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید. همین فایل در پنلی که روی PostgreSQL اجرا می‌شود نیز قابل بازیابی است.",
@@ -158,7 +155,6 @@
"importDatabaseError": "خطا در وارد کردن پایگاه داده",
"readDatabaseError": "خطا در خواندن پایگاه داده",
"getDatabaseError": "خطا در دریافت پایگاه داده",
"getConfigError": "خطا در دریافت فایل پیکربندی",
"backupPostgresNote": "این پنل روی PostgreSQL اجرا می‌شود. «پشتیبان‌گیری» یک آرشیو pg_dump (.dump) دانلود می‌کند و «بازیابی» آن را با pg_restore بازمی‌گرداند. «بازیابی» همچنین پایگاه‌دادهٔ SQLite (.db) یا فایل مهاجرت SQLite را می‌پذیرد و داده‌های آن را به PostgreSQL وارد می‌کند. سرور باید ابزارهای کلاینت PostgreSQL (pg_dump و pg_restore) را نصب داشته باشد.",
"exportDatabasePgDesc": "برای دانلود یک دامپ PostgreSQL (.dump) از پایگاه داده فعلی روی دستگاهتان کلیک کنید.",
"importDatabasePgDesc": "برای انتخاب و بارگذاری یک پشتیبان PostgreSQL (.dump)، پایگاه‌دادهٔ SQLite (.db) یا فایل مهاجرت SQLite جهت بازیابی پایگاه داده کلیک کنید. این کار همه داده‌های فعلی را جایگزین می‌کند.",
@@ -822,31 +818,10 @@
"avgLatency": "میانگین تاخیر",
"name": "نام",
"namePlaceholder": "مثلاً de-frankfurt-1",
"addressPlaceholder": "panel.example.com یا 1.2.3.4",
"remark": "توضیحات",
"scheme": "پروتکل",
"address": "آدرس",
"port": "پورت",
"basePath": "مسیر پایه",
"apiToken": "توکن API",
"apiTokenPlaceholder": "توکن از صفحه تنظیمات پنل ریموت",
"apiTokenHint": "پنل ریموت توکن API خودش را در بخش احرازهویت → توکن API نمایش می‌دهد.",
"apiTokenKeepHint": "برای حفظ توکن فعلی خالی بگذارید",
"allowPrivateAddress": "اجازه آدرس خصوصی",
"allowPrivateAddressHint": "فقط برای نودهای روی شبکه خصوصی یا VPN فعال شود.",
"outboundTag": "خروجی اتصال",
"outboundTagHint": "ترافیک API پنل این نود را از طریق خروجی Xray انتخاب‌شده مسیریابی کنید. یک inbound پل loopback به‌صورت خودکار به پیکربندی در حال اجرا اضافه شده و به‌صورت زنده اعمال می‌شود. برای اتصال مستقیم خالی بگذارید.",
"outboundTagPlaceholder": "اتصال مستقیم",
"inboundSyncMode": "وارد کردن اینباندها",
"inboundSyncModeHint": "اینباندهای قابل وارد کردن از این نود را انتخاب کنید. نودهای موجود به‌طور پیش‌فرض همه را وارد می‌کنند.",
"allInbounds": "همه اینباندها",
"selectedInbounds": "اینباندهای انتخاب‌شده",
"inboundTags": "اینباندها",
"inboundTagsHint": "انتخاب بر اساس تگ اینباند تطبیق داده می‌شود. انتخاب خالی چیزی وارد نمی‌کند.",
"inboundTagsPlaceholder": "اینباندها را بارگیری و انتخاب کنید",
"loadInbounds": "بارگیری اینباندها از نود",
"inboundsLoaded": "{{count}} اینباند بارگیری شد",
"inboundsLoadFailed": "بارگیری اینباندها ناموفق بود",
"enable": "فعال",
"status": "وضعیت",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "نسخه پنل",
"actions": "عملیات",
"probe": "بررسی فوری",
"updatePanel": "به‌روزرسانی پنل",
"updateSelected": "به‌روزرسانی انتخاب‌شده‌ها ({count})",
"updateAvailable": "به‌روزرسانی موجود",
"updateConfirmTitle": "{count} نود به آخرین نسخه به‌روزرسانی شوند؟",
"updateConfirmContent": "هر نود انتخاب‌شده آخرین نسخه را دانلود و روی آن ری‌استارت می‌شود. فقط نودهای فعال و آنلاین به‌روزرسانی می‌شوند.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "به‌روزرسانی به کانال دِو (آخرین کامیت)",
"testConnection": "تست اتصال",
"connectionOk": "اتصال موفق ({ms} میلی‌ثانیه)",
"connectionFailed": "اتصال ناموفق",
"never": "هرگز",
"justNow": "هم‌اکنون",
"subNode": "نود فرعی",
"subNodeTip": "فقط‌خواندنی: یک نود پایین‌دستی که از طریق {parent} در دسترس است. آن را از پنل خودِ {parent} مدیریت کنید.",
"deleteConfirmTitle": "نود «{name}» حذف شود؟",
"deleteConfirmContent": "نظارت روی این نود متوقف می‌شود. خود پنل ریموت تغییری نمی‌کند.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "بررسی ناموفق",
"updateStarted": "به‌روزرسانی پنل آغاز شد",
"updateResult": "به‌روزرسانی روی {ok} نود آغاز شد، {failed} ناموفق",
"updateNoneEligible": "حداقل یک نود آنلاین و فعال انتخاب کنید",
"saveMtls": "ذخیره mTLS نود",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "حداقل یک نود آنلاین و فعال انتخاب کنید"
},
"tlsVerifyMode": "اعتبارسنجی TLS",
"tlsVerifyModeHint": "اینکه پنل گواهی HTTPS نود را چطور بررسی کند. Pin یا Skip برای گواهی‌های self-signed است (فقط نودهای https).",
"tlsVerify": "اعتبارسنجی (CA پیش‌فرض)",
"tlsPin": "Pin گواهی (SHA-256)",
"tlsSkip": "رد کردن اعتبارسنجی",
"tlsMtls": "TLS متقابل (گواهی کلاینت)",
"mtlsFormHint": "این نود با یک گواهی کلاینت، پنل را احراز هویت می‌کند. CA این پنل را از بخش mTLS نود به نود کپی کنید، CA مورد اعتماد آن را تنظیم کنید و سپس آن را راه‌اندازی مجدد کنید.",
"mtls": {
"title": "mTLS نود",
"intro": "TLS متقابل علاوه بر توکن API، یک عامل گواهی کلاینت برای ارتباط بین نودها اضافه می‌کند. اختیاری است: برای استفاده فقط از احراز هویت با توکن، آن را خالی بگذارید.",
"copyCa": "کپی CA این پنل",
"copyCaHint": "این CA را به نودهایی که این پنل مدیریت می‌کند بدهید، سپس حالت اعتبارسنجی TLS آن‌ها را روی TLS متقابل تنظیم کنید.",
"caCopied": "گواهی CA در کلیپ‌بورد کپی شد",
"caFailed": "دریافت گواهی CA ناموفق بود",
"trustLabel": "CA مورد اعتماد (پنل والد)",
"trustHint": "وقتی این پنل خود یک نود است، CA پنل مدیریت‌کننده را اینجا بچسبانید تا گواهی کلاینت آن الزامی شود. برای اعمال، پنل را راه‌اندازی مجدد کنید.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "ذخیره CA مورد اعتماد",
"saved": "CA مورد اعتماد ذخیره شد — برای اعمال، پنل را راه‌اندازی مجدد کنید"
},
"tlsSkipWarning": "رد کردن اعتبارسنجی محافظت در برابر حملهٔ مرد میانی را از بین می‌برد و توکن API ممکن است شنود شود. ترجیحاً به‌جای آن گواهی را Pin کنید.",
"pinnedCert": "SHA-256 گواهیِ Pin‌شده",
"pinnedCertHint": "SHA-256 گواهیِ نود به‌صورت base64 یا hex. برای خواندنِ همین حالا از نود، از دکمهٔ Fetch استفاده کنید.",
"pinnedCertPlaceholder": "SHA-256 به‌صورت base64 یا hex",
"fetchPin": "دریافت",
"pinFetched": "گواهیِ فعلیِ نود دریافت شد",
"pinFetchFailed": "دریافت گواهی ممکن نشد",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "پایه",
"advancedTemplate": "پیشرفته",
"Outbounds": "خروجی‌ها",
"Balancers": "بالانسرها",
"rules": {
"inbound": "ورودی"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Klien",
"nodes": "Node",
"settings": "Pengaturan Panel",
"donate": "Donasi",
"hosts": "Host",
"docs": "Dokumentasi",
"openMenu": "Buka menu",
"pinSidebar": "Sematkan bilah sisi",
"unpinSidebar": "Lepas sematan bilah sisi"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Tidak dapat memastikan pembaruan selesai",
"panelUpdateUnknownDesc": "Panel tidak melaporkan hasil tepat waktu. Muat ulang untuk memeriksa versi saat ini, atau periksa log server.",
"dontRefresh": "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini",
"config": "Konfigurasi",
"backupTitle": "Cadangan & Pulihkan",
"exportDatabase": "Cadangkan",
"exportDatabaseDesc": "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda. Berkas yang sama juga dapat dipulihkan ke panel yang berjalan di PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Terjadi kesalahan saat mengimpor database",
"readDatabaseError": "Terjadi kesalahan saat membaca database",
"getDatabaseError": "Terjadi kesalahan saat mengambil database",
"getConfigError": "Terjadi kesalahan saat mengambil file konfigurasi",
"backupPostgresNote": "Panel ini berjalan di PostgreSQL. «Cadangkan» mengunduh arsip pg_dump (.dump) dan «Pulihkan» memuatnya kembali dengan pg_restore. «Pulihkan» juga menerima basis data SQLite (.db) atau dump migrasi SQLite dan mengimpor datanya ke PostgreSQL. Server memerlukan alat klien PostgreSQL (pg_dump dan pg_restore) terpasang.",
"exportDatabasePgDesc": "Klik untuk mengunduh dump PostgreSQL (.dump) dari basis data Anda saat ini ke perangkat Anda.",
"importDatabasePgDesc": "Klik untuk memilih dan mengunggah cadangan PostgreSQL (.dump), basis data SQLite (.db), atau dump migrasi SQLite guna memulihkan basis data Anda. Ini menggantikan semua data saat ini.",
@@ -822,31 +818,10 @@
"avgLatency": "Latensi Rata-rata",
"name": "Nama",
"namePlaceholder": "mis. de-frankfurt-1",
"addressPlaceholder": "panel.example.com atau 1.2.3.4",
"remark": "Catatan",
"scheme": "Skema",
"address": "Alamat",
"port": "Port",
"basePath": "Path dasar",
"apiToken": "Token API",
"apiTokenPlaceholder": "Token dari halaman Pengaturan panel jarak jauh",
"apiTokenHint": "Panel jarak jauh menampilkan token API-nya di Otentikasi → Token API.",
"apiTokenKeepHint": "Biarkan kosong untuk mempertahankan token saat ini",
"allowPrivateAddress": "Izinkan alamat pribadi",
"allowPrivateAddressHint": "Aktifkan hanya untuk node di jaringan pribadi atau VPN.",
"outboundTag": "Outbound koneksi",
"outboundTagHint": "Rutekan lalu lintas API panel node ini melalui outbound Xray yang dipilih. Sebuah inbound jembatan loopback ditambahkan secara otomatis ke konfigurasi yang berjalan dan diterapkan secara langsung. Biarkan kosong untuk koneksi langsung.",
"outboundTagPlaceholder": "Koneksi langsung",
"inboundSyncMode": "Impor inbound",
"inboundSyncModeHint": "Pilih inbound yang diimpor dari node ini. Node yang sudah ada mengimpor semua inbound secara default.",
"allInbounds": "Semua inbound",
"selectedInbounds": "Inbound terpilih",
"inboundTags": "Inbound",
"inboundTagsHint": "Pilihan dicocokkan berdasarkan tag inbound. Pilihan kosong tidak mengimpor apa pun.",
"inboundTagsPlaceholder": "Muat dan pilih inbound",
"loadInbounds": "Muat inbound dari node",
"inboundsLoaded": "{{count}} inbound dimuat",
"inboundsLoadFailed": "Gagal memuat inbound",
"enable": "Aktif",
"status": "Status",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Versi panel",
"actions": "Aksi",
"probe": "Probe Sekarang",
"updatePanel": "Perbarui Panel",
"updateSelected": "Perbarui Terpilih ({count})",
"updateAvailable": "Pembaruan tersedia",
"updateConfirmTitle": "Perbarui {count} node ke versi terbaru?",
"updateConfirmContent": "Setiap node terpilih mengunduh rilis terbaru dan memulai ulang. Hanya node aktif dan online yang diperbarui.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Perbarui ke kanal dev (commit terbaru)",
"testConnection": "Tes Koneksi",
"connectionOk": "Koneksi OK ({ms} ms)",
"connectionFailed": "Koneksi gagal",
"never": "tidak pernah",
"justNow": "baru saja",
"subNode": "Sub-node",
"subNodeTip": "Hanya-baca: node turunan yang dijangkau melalui {parent}. Kelola dari panel {parent} sendiri.",
"deleteConfirmTitle": "Hapus node \"{name}\"?",
"deleteConfirmContent": "Ini menghentikan pemantauan node. Panel jarak jauh itu sendiri tidak terpengaruh.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Probe gagal",
"updateStarted": "Pembaruan panel dimulai",
"updateResult": "Pembaruan dipicu pada {ok} node, {failed} gagal",
"updateNoneEligible": "Pilih minimal satu node online dan aktif",
"saveMtls": "Simpan mTLS node",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Pilih minimal satu node online dan aktif"
},
"tlsVerifyMode": "Verifikasi TLS",
"tlsVerifyModeHint": "Cara panel memvalidasi sertifikat HTTPS node. Pin atau Lewati untuk sertifikat self-signed (hanya node https).",
"tlsVerify": "Verifikasi (CA bawaan)",
"tlsPin": "Pin sertifikat (SHA-256)",
"tlsSkip": "Lewati verifikasi",
"tlsMtls": "TLS mutual (sertifikat klien)",
"mtlsFormHint": "Node ini mengautentikasi panel dengan sertifikat klien. Salin CA panel ini dari bagian mTLS Node ke node, atur CA tepercayanya, lalu mulai ulang.",
"mtls": {
"title": "mTLS Node",
"intro": "TLS mutual menambahkan faktor sertifikat klien di atas token API untuk panggilan antar-node. Bersifat opsional: biarkan kosong untuk tetap menggunakan autentikasi token saja.",
"copyCa": "Salin CA panel ini",
"copyCaHint": "Berikan CA ini ke node yang dikelola panel ini, lalu atur verifikasi TLS mereka ke TLS mutual.",
"caCopied": "Sertifikat CA disalin ke papan klip",
"caFailed": "Gagal memperoleh sertifikat CA",
"trustLabel": "CA induk tepercaya",
"trustHint": "Jika panel ini sendiri merupakan node, tempelkan CA panel pengelola di sini untuk mewajibkan sertifikat kliennya. Mulai ulang panel untuk menerapkan.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Simpan CA tepercaya",
"saved": "CA tepercaya disimpan — mulai ulang panel untuk menerapkan"
},
"tlsSkipWarning": "Melewati verifikasi menghilangkan perlindungan terhadap serangan man-in-the-middle — token API bisa disadap. Lebih baik pin sertifikat.",
"pinnedCert": "SHA-256 sertifikat yang dipin",
"pinnedCertHint": "SHA-256 sertifikat node dalam base64 atau hex. Gunakan Ambil untuk membacanya dari node sekarang.",
"pinnedCertPlaceholder": "SHA-256 base64 atau hex",
"fetchPin": "Ambil",
"pinFetched": "Berhasil mengambil sertifikat node saat ini",
"pinFetchFailed": "Tidak dapat mengambil sertifikat",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Dasar",
"advancedTemplate": "Lanjutan",
"Outbounds": "Outbound",
"Balancers": "Penyeimbang",
"rules": {
"inbound": "Masuk"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "クライアント",
"nodes": "ノード",
"settings": "パネル設定",
"donate": "寄付",
"hosts": "ホスト",
"docs": "ドキュメント",
"openMenu": "メニューを開く",
"pinSidebar": "サイドバーを固定",
"unpinSidebar": "サイドバーの固定を解除"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "更新が完了したか確認できませんでした",
"panelUpdateUnknownDesc": "パネルから時間内に結果が報告されませんでした。再読み込みして現在のバージョンを確認するか、サーバーのログを確認してください。",
"dontRefresh": "インストール中、このページをリロードしないでください",
"config": "設定",
"backupTitle": "バックアップと復元",
"exportDatabase": "バックアップ",
"exportDatabaseDesc": "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。同じファイルは PostgreSQL で動作するパネルにも復元できます。",
@@ -158,7 +155,6 @@
"importDatabaseError": "データベースのインポート中にエラーが発生しました",
"readDatabaseError": "データベースの読み取り中にエラーが発生しました",
"getDatabaseError": "データベースの取得中にエラーが発生しました",
"getConfigError": "設定ファイルの取得中にエラーが発生しました",
"backupPostgresNote": "このパネルは PostgreSQL で動作しています。「バックアップ」は pg_dump アーカイブ (.dump) をダウンロードし、「復元」は pg_restore で読み込み直します。「復元」は SQLite データベース (.db) や SQLite 移行ダンプも受け付け、そのデータを PostgreSQL に取り込みます。サーバーに PostgreSQL クライアントツール (pg_dump と pg_restore) がインストールされている必要があります。",
"exportDatabasePgDesc": "現在のデータベースの PostgreSQL ダンプ (.dump) を端末にダウンロードするにはクリックしてください。",
"importDatabasePgDesc": "データベースを復元するために PostgreSQL バックアップ (.dump)、SQLite データベース (.db)、または SQLite 移行ダンプを選択してアップロードするにはクリックしてください。現在のすべてのデータが置き換えられます。",
@@ -822,31 +818,10 @@
"avgLatency": "平均レイテンシ",
"name": "名前",
"namePlaceholder": "例: de-frankfurt-1",
"addressPlaceholder": "panel.example.com または 1.2.3.4",
"remark": "備考",
"scheme": "スキーム",
"address": "アドレス",
"port": "ポート",
"basePath": "ベースパス",
"apiToken": "API トークン",
"apiTokenPlaceholder": "リモートパネルの設定ページから取得したトークン",
"apiTokenHint": "リモートパネルでは、セキュリティ設定 → APIトークン でAPIトークンを確認できます。",
"apiTokenKeepHint": "現在のトークンを保持するには空欄のままにします",
"allowPrivateAddress": "プライベートアドレスを許可",
"allowPrivateAddressHint": "プライベートネットワークまたはVPN上のノードにのみ有効にします。",
"outboundTag": "接続アウトバウンド",
"outboundTagHint": "選択した Xray アウトバウンドを経由して、このノードのパネル API トラフィックをルーティングします。ループバック ブリッジ inbound は実行中の設定に自動的に追加され、リアルタイムで適用されます。空のままにすると直接接続になります。",
"outboundTagPlaceholder": "直接接続",
"inboundSyncMode": "インバウンドのインポート",
"inboundSyncModeHint": "このノードからインポートするインバウンドを選択します。既存のノードは既定ですべてをインポートします。",
"allInbounds": "すべてのインバウンド",
"selectedInbounds": "選択したインバウンド",
"inboundTags": "インバウンド",
"inboundTagsHint": "インバウンドタグで照合します。何も選択しない場合はインポートされません。",
"inboundTagsPlaceholder": "インバウンドを読み込んで選択",
"loadInbounds": "ノードからインバウンドを読み込む",
"inboundsLoaded": "{{count}}件のインバウンドを読み込みました",
"inboundsLoadFailed": "インバウンドを読み込めませんでした",
"enable": "有効",
"status": "ステータス",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "パネルのバージョン",
"actions": "操作",
"probe": "今すぐプローブ",
"updatePanel": "パネルを更新",
"updateSelected": "選択を更新 ({count})",
"updateAvailable": "更新あり",
"updateConfirmTitle": "{count} 個のノードを最新バージョンに更新しますか?",
"updateConfirmContent": "選択した各ノードは最新リリースをダウンロードして再起動します。有効かつオンラインのノードのみが更新されます。",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "開発チャンネルに更新(最新コミット)",
"testConnection": "接続テスト",
"connectionOk": "接続OK ({ms} ms)",
"connectionFailed": "接続に失敗しました",
"never": "なし",
"justNow": "たった今",
"subNode": "サブノード",
"subNodeTip": "読み取り専用: {parent} を経由して到達する下位ノードです。{parent} 自身のパネルから管理してください。",
"deleteConfirmTitle": "ノード「{name}」を削除しますか?",
"deleteConfirmContent": "ノードの監視を停止します。リモートパネル自体には影響しません。",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "プローブに失敗しました",
"updateStarted": "パネルの更新を開始しました",
"updateResult": "{ok} 個のノードで更新を開始、{failed} 個失敗",
"updateNoneEligible": "オンラインで有効なノードを少なくとも1つ選択してください",
"saveMtls": "ノード mTLS を保存",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "オンラインで有効なノードを少なくとも1つ選択してください"
},
"tlsVerifyMode": "TLS 検証",
"tlsVerifyModeHint": "パネルがノードの HTTPS 証明書を検証する方法。ピン留めやスキップは自己署名証明書向け(https ノードのみ)。",
"tlsVerify": "検証(既定の CA)",
"tlsPin": "証明書をピン留め(SHA-256)",
"tlsSkip": "検証をスキップ",
"tlsMtls": "相互 TLS(クライアント証明書)",
"mtlsFormHint": "このノードはクライアント証明書でパネルを認証します。「ノード mTLS」セクションからこのパネルの CA をノードにコピーし、信頼する CA を設定してから再起動してください。",
"mtls": {
"title": "ノード mTLS",
"intro": "相互 TLS は、ノード間通信で API トークンに加えてクライアント証明書による認証を追加します。任意です。空のままにするとトークンのみの認証になります。",
"copyCa": "このパネルの CA をコピー",
"copyCaHint": "この CA を、このパネルが管理するノードに渡し、それらの TLS 検証を相互 TLS に設定してください。",
"caCopied": "CA 証明書をクリップボードにコピーしました",
"caFailed": "CA 証明書を取得できませんでした",
"trustLabel": "信頼する親 CA",
"trustHint": "このパネル自体がノードである場合は、管理元パネルの CA をここに貼り付けて、そのクライアント証明書を必須にします。適用するにはパネルを再起動してください。",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "信頼する CA を保存",
"saved": "信頼する CA を保存しました — 適用するにはパネルを再起動してください"
},
"tlsSkipWarning": "検証をスキップすると中間者攻撃への保護がなくなり、API トークンが傍受される恐れがあります。証明書のピン留めを推奨します。",
"pinnedCert": "ピン留め証明書の SHA-256",
"pinnedCertHint": "ノード証明書の SHA-256(base64 または hex)。「取得」でノードから今すぐ読み取れます。",
"pinnedCertPlaceholder": "base64 または hex の SHA-256",
"fetchPin": "取得",
"pinFetched": "ノードの現在の証明書を取得しました",
"pinFetchFailed": "証明書を取得できませんでした",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "基本設定",
"advancedTemplate": "高度な設定",
"Outbounds": "アウトバウンド",
"Balancers": "負荷分散",
"rules": {
"inbound": "インバウンド"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Clientes",
"nodes": "Nós",
"settings": "Configurações do Painel",
"donate": "Doar",
"hosts": "Hosts",
"docs": "Documentação",
"openMenu": "Abrir menu",
"pinSidebar": "Fixar barra lateral",
"unpinSidebar": "Desafixar barra lateral"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Não foi possível confirmar se a atualização terminou",
"panelUpdateUnknownDesc": "O painel não retornou um resultado a tempo. Recarregue a página para verificar a versão atual, ou verifique os logs do servidor.",
"dontRefresh": "Instalação em andamento, por favor não atualize a página",
"config": "Configuração",
"backupTitle": "Backup & Restauração",
"exportDatabase": "Backup",
"exportDatabaseDesc": "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo. O mesmo arquivo também pode ser restaurado em um painel executando PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Ocorreu um erro ao importar o banco de dados",
"readDatabaseError": "Ocorreu um erro ao ler o banco de dados",
"getDatabaseError": "Ocorreu um erro ao recuperar o banco de dados",
"getConfigError": "Ocorreu um erro ao recuperar o arquivo de configuração",
"backupPostgresNote": "Este painel é executado em PostgreSQL. «Backup» baixa um arquivo pg_dump (.dump) e «Restaurar» o recarrega com pg_restore. «Restaurar» também aceita um banco de dados SQLite (.db) ou um dump de migração do SQLite e importa seus dados para o PostgreSQL. O servidor precisa ter as ferramentas cliente do PostgreSQL (pg_dump e pg_restore) instaladas.",
"exportDatabasePgDesc": "Clique para baixar um dump do PostgreSQL (.dump) do seu banco de dados atual para o seu dispositivo.",
"importDatabasePgDesc": "Clique para selecionar e enviar um backup do PostgreSQL (.dump), um banco de dados SQLite (.db) ou um dump de migração do SQLite para restaurar seu banco de dados. Isso substitui todos os dados atuais.",
@@ -822,31 +818,10 @@
"avgLatency": "Latência média",
"name": "Nome",
"namePlaceholder": "ex.: de-frankfurt-1",
"addressPlaceholder": "panel.example.com ou 1.2.3.4",
"remark": "Observação",
"scheme": "Esquema",
"address": "Endereço",
"port": "Porta",
"basePath": "Caminho base",
"apiToken": "Token API",
"apiTokenPlaceholder": "Token da página de Configurações do painel remoto",
"apiTokenHint": "O painel remoto exibe o token da API em Autenticação → Token da API.",
"apiTokenKeepHint": "Deixe em branco para manter o token atual",
"allowPrivateAddress": "Permitir endereço privado",
"allowPrivateAddressHint": "Ativar apenas para nós em uma rede privada ou VPN.",
"outboundTag": "Outbound de conexão",
"outboundTagHint": "Roteie o tráfego da API do painel deste nó pelo outbound Xray selecionado. Um inbound de ponte loopback é adicionado automaticamente à configuração em execução e aplicado ao vivo. Deixe em branco para uma conexão direta.",
"outboundTagPlaceholder": "Conexão direta",
"inboundSyncMode": "Importação de inbounds",
"inboundSyncModeHint": "Escolha quais inbounds importar deste nó. Nós existentes importam todos por padrão.",
"allInbounds": "Todos os inbounds",
"selectedInbounds": "Inbounds selecionados",
"inboundTags": "Inbounds",
"inboundTagsHint": "A seleção é comparada pela tag do inbound. Uma seleção vazia não importa nenhum.",
"inboundTagsPlaceholder": "Carregue e selecione inbounds",
"loadInbounds": "Carregar inbounds do nó",
"inboundsLoaded": "{{count}} inbounds carregados",
"inboundsLoadFailed": "Falha ao carregar inbounds",
"enable": "Ativado",
"status": "Status",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Versão do painel",
"actions": "Ações",
"probe": "Sondar agora",
"updatePanel": "Atualizar painel",
"updateSelected": "Atualizar selecionados ({count})",
"updateAvailable": "Atualização disponível",
"updateConfirmTitle": "Atualizar {count} nó(s) para a versão mais recente?",
"updateConfirmContent": "Cada nó selecionado baixa a versão mais recente e reinicia nela. Apenas nós ativos e online são atualizados.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Atualizar para o canal de desenvolvimento (último commit)",
"testConnection": "Testar conexão",
"connectionOk": "Conexão OK ({ms} ms)",
"connectionFailed": "Falha na conexão",
"never": "nunca",
"justNow": "agora mesmo",
"subNode": "Subnó",
"subNodeTip": "Somente leitura: um nó descendente acessado através de {parent}. Gerencie-o pelo próprio painel de {parent}.",
"deleteConfirmTitle": "Excluir o nó \"{name}\"?",
"deleteConfirmContent": "Isso interrompe o monitoramento do nó. O painel remoto em si não é afetado.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Falha na sondagem",
"updateStarted": "Atualização do painel iniciada",
"updateResult": "Atualização iniciada em {ok} nó(s), {failed} falharam",
"updateNoneEligible": "Selecione pelo menos um nó online e ativo",
"saveMtls": "Salvar mTLS do nó",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Selecione pelo menos um nó online e ativo"
},
"tlsVerifyMode": "Verificação TLS",
"tlsVerifyModeHint": "Como o painel valida o certificado HTTPS do nó. Fixar ou Ignorar são para certificados autoassinados (apenas nós https).",
"tlsVerify": "Verificar (CA padrão)",
"tlsPin": "Fixar certificado (SHA-256)",
"tlsSkip": "Ignorar verificação",
"tlsMtls": "TLS mútuo (certificado de cliente)",
"mtlsFormHint": "Este nó autentica o painel com um certificado de cliente. Copie o CA deste painel na seção mTLS do nó para o nó, defina o CA confiável dele e reinicie-o.",
"mtls": {
"title": "mTLS do nó",
"intro": "O TLS mútuo adiciona um fator de certificado de cliente além do token de API nas chamadas entre nós. É opcional: deixe vazio para manter apenas a autenticação por token.",
"copyCa": "Copiar o CA deste painel",
"copyCaHint": "Entregue este CA aos nós gerenciados por este painel e defina a verificação TLS deles como TLS mútuo.",
"caCopied": "Certificado CA copiado para a área de transferência",
"caFailed": "Falha ao obter o certificado CA",
"trustLabel": "CA confiável (painel superior)",
"trustHint": "Quando este painel também é um nó, cole aqui o CA do painel que o gerencia para exigir seu certificado de cliente. Reinicie o painel para aplicar.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Salvar CA confiável",
"saved": "CA confiável salvo — reinicie o painel para aplicar"
},
"tlsSkipWarning": "Ignorar a verificação remove a proteção contra ataques man-in-the-middle — o token de API pode ser interceptado. Prefira fixar o certificado.",
"pinnedCert": "SHA-256 do certificado fixado",
"pinnedCertHint": "SHA-256 do certificado do nó em base64 ou hex. Use Obter para lê-lo do nó agora.",
"pinnedCertPlaceholder": "SHA-256 em base64 ou hex",
"fetchPin": "Obter",
"pinFetched": "Certificado atual do nó obtido",
"pinFetchFailed": "Não foi possível obter o certificado",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Básico",
"advancedTemplate": "Avançado",
"Outbounds": "Saídas",
"Balancers": "Balanceadores",
"rules": {
"inbound": "Entrada"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Клиенты",
"nodes": "Узлы",
"settings": "Настройки панели",
"donate": "Поддержать",
"hosts": "Хосты",
"docs": "Документация",
"openMenu": "Открыть меню",
"pinSidebar": "Закрепить боковую панель",
"unpinSidebar": "Открепить боковую панель"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Не удалось подтвердить завершение обновления",
"panelUpdateUnknownDesc": "Панель не сообщила результат вовремя. Перезагрузите страницу, чтобы проверить текущую версию, или проверьте журналы сервера.",
"dontRefresh": "Установка в процессе. Не обновляйте страницу",
"config": "Конфигурация",
"backupTitle": "Бэкап и восстановление",
"exportDatabase": "Экспорт базы данных",
"exportDatabaseDesc": "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство. Этот же файл можно восстановить на панели, работающей на PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Произошла ошибка при импорте базы данных",
"readDatabaseError": "Произошла ошибка при чтении базы данных",
"getDatabaseError": "Произошла ошибка при получении базы данных",
"getConfigError": "Произошла ошибка при получении конфигурационного файла",
"backupPostgresNote": "Эта панель работает на PostgreSQL. «Резервная копия» скачивает архив pg_dump (.dump), а «Восстановление» загружает его обратно через pg_restore. «Восстановление» также принимает базу данных SQLite (.db) или миграционный дамп SQLite и импортирует их данные в PostgreSQL. На сервере должны быть установлены клиентские инструменты PostgreSQL (pg_dump и pg_restore).",
"exportDatabasePgDesc": "Нажмите, чтобы скачать дамп PostgreSQL (.dump) текущей базы данных на ваше устройство.",
"importDatabasePgDesc": "Нажмите, чтобы выбрать и загрузить резервную копию PostgreSQL (.dump), базу данных SQLite (.db) или миграционный дамп SQLite для восстановления базы данных. Это заменит все текущие данные.",
@@ -822,31 +818,10 @@
"avgLatency": "Средняя задержка",
"name": "Имя",
"namePlaceholder": "напр. de-frankfurt-1",
"addressPlaceholder": "panel.example.com или 1.2.3.4",
"remark": "Примечание",
"scheme": "Схема",
"address": "Адрес",
"port": "Порт",
"basePath": "Базовый путь",
"apiToken": "API Токен",
"apiTokenPlaceholder": "Токен со страницы Настроек удалённой панели",
"apiTokenHint": "Удалённая панель показывает свой токен API в разделе Учетная запись → Токен API.",
"apiTokenKeepHint": "Оставьте пустым, чтобы сохранить текущий токен",
"allowPrivateAddress": "Разрешить частный адрес",
"allowPrivateAddressHint": "Включить только для узлов в частной сети или VPN.",
"outboundTag": "Исходящее подключение",
"outboundTagHint": "Маршрутизируйте трафик API панели этого узла через выбранный исходящий Xray. Входящий мост обратной петли автоматически добавляется в текущую конфигурацию и применяется в реальном времени. Оставьте пустым для прямого подключения.",
"outboundTagPlaceholder": "Прямое подключение",
"inboundSyncMode": "Импорт инбаундов",
"inboundSyncModeHint": "Выберите, какие инбаунды импортировать с этой ноды. Для существующих нод по умолчанию импортируются все.",
"allInbounds": "Все инбаунды",
"selectedInbounds": "Выбранные инбаунды",
"inboundTags": "Инбаунды",
"inboundTagsHint": "Выбор сопоставляется по тегу инбаунда. Пустой список не импортирует ничего.",
"inboundTagsPlaceholder": "Загрузите и выберите инбаунды",
"loadInbounds": "Загрузить инбаунды с ноды",
"inboundsLoaded": "Загружено инбаундов: {{count}}",
"inboundsLoadFailed": "Не удалось загрузить инбаунды",
"enable": "Включён",
"status": "Статус",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Версия панели",
"actions": "Действия",
"probe": "Проверить сейчас",
"updatePanel": "Обновить панель",
"updateSelected": "Обновить выбранные ({count})",
"updateAvailable": "Доступно обновление",
"updateConfirmTitle": "Обновить {count} узлов до последней версии?",
"updateConfirmContent": "Каждый выбранный узел загрузит последний релиз и перезапустится. Обновляются только включённые узлы в сети.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Обновить до канала разработки (последний коммит)",
"testConnection": "Проверить соединение",
"connectionOk": "Соединение в порядке ({ms} мс)",
"connectionFailed": "Не удалось подключиться",
"never": "никогда",
"justNow": "только что",
"subNode": "Подузел",
"subNodeTip": "Только для чтения: подчинённый узел, доступный через {parent}. Управляйте им из собственной панели {parent}.",
"deleteConfirmTitle": "Удалить узел \"{name}\"?",
"deleteConfirmContent": "Это остановит мониторинг узла. Сама удалённая панель не будет затронута.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Проверка не удалась",
"updateStarted": "Обновление панели запущено",
"updateResult": "Обновление запущено на {ok} узлах, {failed} не удалось",
"updateNoneEligible": "Выберите хотя бы один включённый узел в сети",
"saveMtls": "Сохранить mTLS узла",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Выберите хотя бы один включённый узел в сети"
},
"tlsVerifyMode": "Проверка TLS",
"tlsVerifyModeHint": "Как панель проверяет HTTPS-сертификат узла. Закрепление или Пропуск — для самоподписанных сертификатов (только https-узлы).",
"tlsVerify": "Проверять (стандартный CA)",
"tlsPin": "Закрепить сертификат (SHA-256)",
"tlsSkip": "Пропустить проверку",
"tlsMtls": "Взаимный TLS (клиентский сертификат)",
"mtlsFormHint": "Этот узел аутентифицирует панель с помощью клиентского сертификата. Скопируйте CA этой панели из раздела «mTLS узла» на узел, задайте его доверенный CA и перезапустите узел.",
"mtls": {
"title": "mTLS узла",
"intro": "Взаимный TLS добавляет фактор клиентского сертификата поверх API-токена для вызовов между узлами. Это необязательно: оставьте пустым, чтобы использовать только аутентификацию по токену.",
"copyCa": "Скопировать CA этой панели",
"copyCaHint": "Передайте этот CA узлам, которыми управляет эта панель, затем установите их режим проверки TLS на «Взаимный TLS».",
"caCopied": "Сертификат CA скопирован в буфер обмена",
"caFailed": "Не удалось получить сертификат CA",
"trustLabel": "Доверенный CA (родительская панель)",
"trustHint": "Если эта панель сама является узлом, вставьте сюда CA управляющей панели, чтобы требовать её клиентский сертификат. Перезапустите панель для применения.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Сохранить доверенный CA",
"saved": "Доверенный CA сохранён — перезапустите панель для применения"
},
"tlsSkipWarning": "Пропуск проверки убирает защиту от атак «человек посередине» — токен API может быть перехвачен. Лучше закрепить сертификат.",
"pinnedCert": "SHA-256 закреплённого сертификата",
"pinnedCertHint": "SHA-256 сертификата узла в base64 или hex. Нажмите «Получить», чтобы считать его с узла сейчас.",
"pinnedCertPlaceholder": "SHA-256 в base64 или hex",
"fetchPin": "Получить",
"pinFetched": "Текущий сертификат узла получен",
"pinFetchFailed": "Не удалось получить сертификат",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Основное",
"advancedTemplate": "Расширенный шаблон",
"Outbounds": "Исходящие",
"Balancers": "Балансировщик",
"rules": {
"inbound": "Входящее подключение"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Kullanıcılar",
"nodes": "Düğümler",
"settings": "Panel Ayarları",
"donate": "Bağış Yap",
"hosts": "Host'lar",
"docs": "Belgeler",
"openMenu": "Menüyü aç",
"pinSidebar": "Kenar çubuğunu sabitle",
"unpinSidebar": "Kenar çubuğu sabitlemesini kaldır"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Güncellemenin tamamlandığı doğrulanamadı",
"panelUpdateUnknownDesc": "Panel zamanında bir sonuç bildirmedi. Geçerli sürümü kontrol etmek için sayfayı yenileyin veya sunucu günlüklerini kontrol edin.",
"dontRefresh": "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin",
"config": "Yapılandırma",
"backupTitle": "Yedekleme ve Geri Yükleme",
"exportDatabase": "Yedekle",
"exportDatabaseDesc": "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın. Aynı dosya PostgreSQL üzerinde çalışan bir panele de geri yüklenebilir.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Veritabanı içe aktarılırken bir hata oluştu.",
"readDatabaseError": "Veritabanı okunurken bir hata oluştu.",
"getDatabaseError": "Veritabanı alınırken bir hata oluştu.",
"getConfigError": "Yapılandırma dosyası alınırken bir hata oluştu.",
"backupPostgresNote": "Bu panel PostgreSQL üzerinde çalışıyor. 'Yedekle' bir pg_dump arşivi (.dump) indirir, 'Geri Yükle' ise onu pg_restore ile geri yükler. 'Geri Yükle' ayrıca bir SQLite veritabanını (.db) veya SQLite taşıma dökümünü kabul eder ve verilerini PostgreSQL'e aktarır. Sunucuda PostgreSQL istemci araçlarının (pg_dump ve pg_restore) kurulu olması gerekir.",
"exportDatabasePgDesc": "Mevcut veritabanınızın PostgreSQL dökümünü (.dump) cihazınıza indirmek için tıklayın.",
"importDatabasePgDesc": "Veritabanınızı geri yüklemek için bir PostgreSQL yedeği (.dump), SQLite veritabanı (.db) veya SQLite taşıma dökümü seçip yüklemek üzere tıklayın. Bu, tüm mevcut verilerin yerini alır.",
@@ -822,31 +818,10 @@
"avgLatency": "Ortalama Gecikme",
"name": "Ad",
"namePlaceholder": "ör. de-frankfurt-1",
"addressPlaceholder": "panel.example.com veya 1.2.3.4",
"remark": "Açıklama",
"scheme": "Şema",
"address": "Adres",
"port": "Port",
"basePath": "Temel Yol",
"apiToken": "API Jetonu",
"apiTokenPlaceholder": "Uzak panelin Ayarlar sayfasındaki token",
"apiTokenHint": "Uzak panel API token'ını Kimlik Doğrulama → API Token altında gösterir.",
"apiTokenKeepHint": "Mevcut token'ı korumak için boş bırakın",
"allowPrivateAddress": "Özel Adrese İzin Ver",
"allowPrivateAddressHint": "Yalnızca özel ağ veya VPN üzerindeki düğümler için etkinleştirin.",
"outboundTag": "Bağlantı gideni",
"outboundTagHint": "Bu düğümün panel API trafiğini seçilen Xray gideni üzerinden yönlendirin. Geri döngü köprüsü inbound'ı çalışan yapılandırmaya otomatik olarak eklenir ve canlı uygulanır. Doğrudan bağlantı için boş bırakın.",
"outboundTagPlaceholder": "Doğrudan bağlantı",
"inboundSyncMode": "Inbound içe aktarma",
"inboundSyncModeHint": "Bu düğümden içe aktarılacak inbound'ları seçin. Mevcut düğümler varsayılan olarak tümünü içe aktarır.",
"allInbounds": "Tüm inbound'lar",
"selectedInbounds": "Seçili inbound'lar",
"inboundTags": "Inbound'lar",
"inboundTagsHint": "Seçim inbound etiketiyle eşleştirilir. Boş seçim hiçbir şeyi içe aktarmaz.",
"inboundTagsPlaceholder": "Inbound'ları yükleyip seçin",
"loadInbounds": "Inbound'ları düğümden yükle",
"inboundsLoaded": "{{count}} inbound yüklendi",
"inboundsLoadFailed": "Inbound'lar yüklenemedi",
"enable": "Etkin",
"status": "Durum",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Panel Sürümü",
"actions": "İşlemler",
"probe": "Şimdi Test Et",
"updatePanel": "Paneli Güncelle",
"updateSelected": "Seçilenleri Güncelle ({count})",
"updateAvailable": "Güncelleme mevcut",
"updateConfirmTitle": "{count} düğüm en son sürüme güncellensin mi?",
"updateConfirmContent": "Seçilen her düğüm en son sürümü indirir ve yeniden başlatılır. Yalnızca etkin ve çevrimiçi düğümler güncellenir.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Dev kanalına güncelle (son commit)",
"testConnection": "Bağlantıyı Test Et",
"connectionOk": "Bağlantı tamam ({ms} ms)",
"connectionFailed": "Bağlantı başarısız",
"never": "Asla",
"justNow": "Şimdi",
"subNode": "Alt Düğüm",
"subNodeTip": "Salt okunur: {parent} üzerinden erişilen bir alt düğüm. Bunu {parent} panelinden yönetin.",
"deleteConfirmTitle": "\"{name}\" düğümü silinsin mi?",
"deleteConfirmContent": "Bu işlem düğüm izlemeyi durdurur. Uzak panelin kendisi etkilenmez.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Test başarısız",
"updateStarted": "Panel güncellemesi başlatıldı",
"updateResult": "{ok} düğümde güncelleme başlatıldı, {failed} başarısız",
"updateNoneEligible": "En az bir çevrimiçi ve etkin düğüm seçin",
"saveMtls": "Düğüm mTLS kaydet",
"reloadMtls": "Ana mTLS kimlik bilgilerini yeniden yükle"
"updateNoneEligible": "En az bir çevrimiçi ve etkin düğüm seçin"
},
"tlsVerifyMode": "TLS Doğrulaması",
"tlsVerifyModeHint": "Panelin düğümün HTTPS sertifikasını nasıl doğrulayacağını belirler. Sabitle veya Atla, kendinden imzalı sertifikalar içindir (yalnızca https düğümleri).",
"tlsVerify": "Doğrula (varsayılan CA)",
"tlsPin": "Sertifikayı Sabitle (SHA-256)",
"tlsSkip": "Doğrulamayı Atla",
"tlsMtls": "Karşılıklı TLS (istemci sertifikası)",
"mtlsFormHint": "Bu düğüm, paneli bir istemci sertifikasıyla doğrular. Düğüm mTLS bölümünden bu panelin CA’sını düğüme kopyalayın, güvenilen CA’sını ayarlayın ve ardından düğümü yeniden başlatın.",
"mtls": {
"title": "Düğüm mTLS",
"intro": "Karşılıklı TLS, düğümler arası çağrılarda API belirtecinin yanına bir istemci sertifikası faktörü ekler. İsteğe bağlıdır: yalnızca belirteçle kimlik doğrulamayı sürdürmek için boş bırakın.",
"copyCa": "Bu panelin CA’sını kopyala",
"copyCaHint": "Bu CA’yı bu panelin yönettiği düğümlere verin ve ardından TLS doğrulamalarını Karşılıklı TLS olarak ayarlayın.",
"caCopied": "CA sertifikası panoya kopyalandı",
"caFailed": "CA sertifikası alınamadı",
"trustLabel": "Güvenilen üst CA",
"trustHint": "Bu panel kendisi bir düğümse, istemci sertifikasını zorunlu kılmak için onu yöneten panelin CA’sını buraya yapıştırın. Uygulamak için paneli yeniden başlatın.",
"trustPlaceholder": "-----SERTİFİKA BAŞLANGICI-----",
"save": "Güvenilen CA’yı kaydet",
"saved": "Güvenilen CA kaydedildi — uygulamak için paneli yeniden başlatın"
},
"tlsSkipWarning": "Doğrulamayı atlamak, ortadaki adam (MITM) saldırılarına karşı korumayı kaldırır — API anahtarı ele geçirilebilir. Bunun yerine sertifikayı sabitlemeniz önerilir.",
"pinnedCert": "Sabitlenen Sertifika SHA-256",
"pinnedCertHint": "Düğüm sertifikasının base64 veya hex biçiminde SHA-256 değeri. Şimdi düğümden okumak için Getir'i kullanın.",
"pinnedCertPlaceholder": "base64 veya hex SHA-256",
"fetchPin": "Getir",
"pinFetched": "Düğümün geçerli sertifikası alındı",
"pinFetchFailed": "Sertifika alınamadı",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Temeller",
"advancedTemplate": "Gelişmiş",
"Outbounds": "Giden Bağlantılar",
"Balancers": "Dengeleyiciler",
"rules": {
"inbound": "Gelen Bağlantı"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Клієнти",
"nodes": "Вузли",
"settings": "Налаштування панелі",
"donate": "Підтримати",
"hosts": "Хости",
"docs": "Документація",
"openMenu": "Відкрити меню",
"pinSidebar": "Закріпити бічну панель",
"unpinSidebar": "Відкріпити бічну панель"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Не вдалося підтвердити завершення оновлення",
"panelUpdateUnknownDesc": "Панель не повідомила результат вчасно. Перезавантажте сторінку, щоб перевірити поточну версію, або перевірте журнали сервера.",
"dontRefresh": "Інсталяція триває, будь ласка, не оновлюйте цю сторінку",
"config": "Конфігурація",
"backupTitle": "Резервне копіювання та відновлення",
"exportDatabase": "Резервна копія",
"exportDatabaseDesc": "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій. Цей самий файл можна відновити на панелі, що працює на PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Виникла помилка під час імпорту бази даних",
"readDatabaseError": "Виникла помилка під час читання бази даних",
"getDatabaseError": "Виникла помилка під час отримання бази даних",
"getConfigError": "Виникла помилка під час отримання файлу конфігурації",
"backupPostgresNote": "Ця панель працює на PostgreSQL. «Резервна копія» завантажує архів pg_dump (.dump), а «Відновлення» завантажує його назад через pg_restore. «Відновлення» також приймає базу даних SQLite (.db) або міграційний дамп SQLite та імпортує їхні дані в PostgreSQL. На сервері мають бути встановлені клієнтські інструменти PostgreSQL (pg_dump і pg_restore).",
"exportDatabasePgDesc": "Натисніть, щоб завантажити дамп PostgreSQL (.dump) вашої поточної бази даних на ваш пристрій.",
"importDatabasePgDesc": "Натисніть, щоб вибрати та завантажити резервну копію PostgreSQL (.dump), базу даних SQLite (.db) або міграційний дамп SQLite для відновлення бази даних. Це замінить усі поточні дані.",
@@ -822,31 +818,10 @@
"avgLatency": "Середня затримка",
"name": "Назва",
"namePlaceholder": "напр. de-frankfurt-1",
"addressPlaceholder": "panel.example.com або 1.2.3.4",
"remark": "Примітка",
"scheme": "Схема",
"address": "Адреса",
"port": "Порт",
"basePath": "Базовий шлях",
"apiToken": "API Токен",
"apiTokenPlaceholder": "Токен зі сторінки Налаштувань віддаленої панелі",
"apiTokenHint": "Віддалена панель показує свій токен API в Автентифікація → Токен API.",
"apiTokenKeepHint": "Залиште порожнім, щоб зберегти поточний токен",
"allowPrivateAddress": "Дозволити приватну адресу",
"allowPrivateAddressHint": "Увімкнути лише для вузлів у приватній мережі або VPN.",
"outboundTag": "Вихідне з'єднання",
"outboundTagHint": "Маршрутизуйте трафік API панелі цього вузла через вибраний вихідний Xray. Вхідний міст зворотної петлі автоматично додається до поточної конфігурації та застосовується в реальному часі. Залиште порожнім для прямого підключення.",
"outboundTagPlaceholder": "Пряме підключення",
"inboundSyncMode": "Імпорт інбаундів",
"inboundSyncModeHint": "Виберіть інбаунди для імпорту з цього вузла. Для наявних вузлів типово імпортуються всі.",
"allInbounds": "Усі інбаунди",
"selectedInbounds": "Вибрані інбаунди",
"inboundTags": "Інбаунди",
"inboundTagsHint": "Вибір зіставляється за тегом інбаунду. Порожній список нічого не імпортує.",
"inboundTagsPlaceholder": "Завантажте та виберіть інбаунди",
"loadInbounds": "Завантажити інбаунди з вузла",
"inboundsLoaded": "Завантажено інбаундів: {{count}}",
"inboundsLoadFailed": "Не вдалося завантажити інбаунди",
"enable": "Увімкнено",
"status": "Статус",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Версія панелі",
"actions": "Дії",
"probe": "Перевірити зараз",
"updatePanel": "Оновити панель",
"updateSelected": "Оновити вибрані ({count})",
"updateAvailable": "Доступне оновлення",
"updateConfirmTitle": "Оновити {count} вузлів до останньої версії?",
"updateConfirmContent": "Кожен вибраний вузол завантажить останній реліз і перезапуститься. Оновлюються лише увімкнені вузли в мережі.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Оновити до каналу розробки (останній коміт)",
"testConnection": "Перевірити з'єднання",
"connectionOk": "З'єднання в порядку ({ms} мс)",
"connectionFailed": "Помилка з'єднання",
"never": "ніколи",
"justNow": "щойно",
"subNode": "Підвузол",
"subNodeTip": "Лише для читання: підлеглий вузол, доступний через {parent}. Керуйте ним із власної панелі {parent}.",
"deleteConfirmTitle": "Видалити вузол \"{name}\"?",
"deleteConfirmContent": "Це зупинить моніторинг вузла. Сама віддалена панель не зазнає змін.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Помилка перевірки",
"updateStarted": "Оновлення панелі розпочато",
"updateResult": "Оновлення запущено на {ok} вузлах, {failed} не вдалося",
"updateNoneEligible": "Виберіть принаймні один увімкнений вузол у мережі",
"saveMtls": "Зберегти mTLS вузла",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Виберіть принаймні один увімкнений вузол у мережі"
},
"tlsVerifyMode": "Перевірка TLS",
"tlsVerifyModeHint": "Як панель перевіряє HTTPS-сертифікат вузла. Закріплення або Пропуск — для самопідписаних сертифікатів (лише https-вузли).",
"tlsVerify": "Перевіряти (стандартний CA)",
"tlsPin": "Закріпити сертифікат (SHA-256)",
"tlsSkip": "Пропустити перевірку",
"tlsMtls": "Взаємний TLS (клієнтський сертифікат)",
"mtlsFormHint": "Цей вузол автентифікує панель за допомогою клієнтського сертифіката. Скопіюйте CA цієї панелі з розділу «mTLS вузла» на вузол, задайте його довірений CA та перезапустіть вузол.",
"mtls": {
"title": "mTLS вузла",
"intro": "Взаємний TLS додає фактор клієнтського сертифіката поверх API-токена для викликів між вузлами. Це необов’язково: залиште порожнім, щоб використовувати лише автентифікацію за токеном.",
"copyCa": "Скопіювати CA цієї панелі",
"copyCaHint": "Передайте цей CA вузлам, якими керує ця панель, потім встановіть їхній режим перевірки TLS на «Взаємний TLS».",
"caCopied": "Сертифікат CA скопійовано в буфер обміну",
"caFailed": "Не вдалося отримати сертифікат CA",
"trustLabel": "Довірений CA (батьківська панель)",
"trustHint": "Якщо ця панель сама є вузлом, вставте сюди CA керуючої панелі, щоб вимагати її клієнтський сертифікат. Перезапустіть панель для застосування.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Зберегти довірений CA",
"saved": "Довірений CA збережено — перезапустіть панель для застосування"
},
"tlsSkipWarning": "Пропуск перевірки прибирає захист від атак «людина посередині» — токен API можуть перехопити. Краще закріпити сертифікат.",
"pinnedCert": "SHA-256 закріпленого сертифіката",
"pinnedCertHint": "SHA-256 сертифіката вузла у base64 або hex. Натисніть «Отримати», щоб зчитати його з вузла зараз.",
"pinnedCertPlaceholder": "SHA-256 у base64 або hex",
"fetchPin": "Отримати",
"pinFetched": "Поточний сертифікат вузла отримано",
"pinFetchFailed": "Не вдалося отримати сертифікат",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Базовий шаблон",
"advancedTemplate": "Додатково",
"Outbounds": "Вихідні",
"Balancers": "Балансери",
"rules": {
"inbound": "Вхідний"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "Khách hàng",
"nodes": "Nút",
"settings": "Cài đặt bảng điều khiển",
"donate": "Quyên góp",
"hosts": "Hosts",
"docs": "Tài liệu",
"openMenu": "Mở menu",
"pinSidebar": "Ghim thanh bên",
"unpinSidebar": "Bỏ ghim thanh bên"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "Không thể xác nhận việc cập nhật đã hoàn tất",
"panelUpdateUnknownDesc": "Panel không báo cáo kết quả kịp thời. Hãy tải lại trang để kiểm tra phiên bản hiện tại, hoặc kiểm tra nhật ký máy chủ.",
"dontRefresh": "Đang tiến hành cài đặt, vui lòng không làm mới trang này.",
"config": "Cấu hình",
"backupTitle": "Sao lưu & Khôi phục",
"exportDatabase": "Sao lưu",
"exportDatabaseDesc": "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị. Tệp này cũng có thể được khôi phục vào bảng điều khiển chạy PostgreSQL.",
@@ -158,7 +155,6 @@
"importDatabaseError": "Lỗi xảy ra khi nhập cơ sở dữ liệu",
"readDatabaseError": "Lỗi xảy ra khi đọc cơ sở dữ liệu",
"getDatabaseError": "Lỗi xảy ra khi truy xuất cơ sở dữ liệu",
"getConfigError": "Lỗi xảy ra khi truy xuất tệp cấu hình",
"backupPostgresNote": "Bảng điều khiển này chạy trên PostgreSQL. «Sao lưu» tải xuống một tệp lưu trữ pg_dump (.dump) và «Khôi phục» nạp lại bằng pg_restore. «Khôi phục» cũng chấp nhận cơ sở dữ liệu SQLite (.db) hoặc tệp kết xuất di trú SQLite và nhập dữ liệu của chúng vào PostgreSQL. Máy chủ cần cài đặt các công cụ máy khách PostgreSQL (pg_dump và pg_restore).",
"exportDatabasePgDesc": "Nhấn để tải xuống bản kết xuất PostgreSQL (.dump) của cơ sở dữ liệu hiện tại về thiết bị của bạn.",
"importDatabasePgDesc": "Nhấn để chọn và tải lên bản sao lưu PostgreSQL (.dump), cơ sở dữ liệu SQLite (.db) hoặc tệp kết xuất di trú SQLite nhằm khôi phục cơ sở dữ liệu của bạn. Thao tác này sẽ thay thế toàn bộ dữ liệu hiện tại.",
@@ -822,31 +818,10 @@
"avgLatency": "Độ trễ trung bình",
"name": "Tên",
"namePlaceholder": "vd: de-frankfurt-1",
"addressPlaceholder": "panel.example.com hoặc 1.2.3.4",
"remark": "Chú thích",
"scheme": "Giao thức",
"address": "Địa chỉ",
"port": "Cổng",
"basePath": "Đường dẫn cơ sở",
"apiToken": "Token API",
"apiTokenPlaceholder": "Token từ trang Cài đặt của panel từ xa",
"apiTokenHint": "Panel từ xa hiển thị token API tại Bảo mật → Token API.",
"apiTokenKeepHint": "Để trống để giữ token hiện tại",
"allowPrivateAddress": "Cho phép địa chỉ riêng",
"allowPrivateAddressHint": "Chỉ bật cho các nút trên mạng riêng hoặc VPN.",
"outboundTag": "Outbound kết nối",
"outboundTagHint": "Định tuyến lưu lượng API panel của node này qua outbound Xray đã chọn. Một inbound cầu nối loopback được tự động thêm vào cấu hình đang chạy và áp dụng trực tiếp. Để trống để kết nối trực tiếp.",
"outboundTagPlaceholder": "Kết nối trực tiếp",
"inboundSyncMode": "Nhập inbound",
"inboundSyncModeHint": "Chọn các inbound được nhập từ nút này. Các nút hiện có mặc định nhập tất cả.",
"allInbounds": "Tất cả inbound",
"selectedInbounds": "Inbound đã chọn",
"inboundTags": "Inbound",
"inboundTagsHint": "Lựa chọn được đối chiếu theo tag inbound. Lựa chọn trống sẽ không nhập gì.",
"inboundTagsPlaceholder": "Tải và chọn inbound",
"loadInbounds": "Tải inbound từ nút",
"inboundsLoaded": "Đã tải {{count}} inbound",
"inboundsLoadFailed": "Không thể tải inbound",
"enable": "Kích hoạt",
"status": "Trạng thái",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "Phiên bản panel",
"actions": "Hành động",
"probe": "Kiểm tra ngay",
"updatePanel": "Cập nhật bảng điều khiển",
"updateSelected": "Cập nhật đã chọn ({count})",
"updateAvailable": "Có bản cập nhật",
"updateConfirmTitle": "Cập nhật {count} node lên phiên bản mới nhất?",
"updateConfirmContent": "Mỗi node đã chọn sẽ tải bản phát hành mới nhất và khởi động lại. Chỉ các node đang bật và trực tuyến được cập nhật.",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "Cập nhật lên kênh phát triển (commit mới nhất)",
"testConnection": "Kiểm tra kết nối",
"connectionOk": "Kết nối OK ({ms} ms)",
"connectionFailed": "Kết nối thất bại",
"never": "chưa bao giờ",
"justNow": "vừa xong",
"subNode": "Nút con",
"subNodeTip": "Chỉ đọc: một nút phía dưới được kết nối qua {parent}. Quản lý nó từ bảng điều khiển của chính {parent}.",
"deleteConfirmTitle": "Xóa nút \"{name}\"?",
"deleteConfirmContent": "Việc này dừng giám sát nút. Panel từ xa không bị ảnh hưởng.",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "Kiểm tra thất bại",
"updateStarted": "Đã bắt đầu cập nhật bảng điều khiển",
"updateResult": "Đã kích hoạt cập nhật trên {ok} node, {failed} thất bại",
"updateNoneEligible": "Chọn ít nhất một node trực tuyến và đang bật",
"saveMtls": "Lưu mTLS nút",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "Chọn ít nhất một node trực tuyến và đang bật"
},
"tlsVerifyMode": "Xác minh TLS",
"tlsVerifyModeHint": "Cách panel xác thực chứng chỉ HTTPS của node. Ghim hoặc Bỏ qua dành cho chứng chỉ tự ký (chỉ node https).",
"tlsVerify": "Xác minh (CA mặc định)",
"tlsPin": "Ghim chứng chỉ (SHA-256)",
"tlsSkip": "Bỏ qua xác minh",
"tlsMtls": "TLS song phương (chứng chỉ máy khách)",
"mtlsFormHint": "Nút này xác thực bảng điều khiển bằng chứng chỉ máy khách. Sao chép CA của bảng điều khiển này từ mục mTLS nút sang nút, đặt CA tin cậy của nó, rồi khởi động lại.",
"mtls": {
"title": "mTLS nút",
"intro": "TLS song phương bổ sung yếu tố chứng chỉ máy khách bên cạnh token API cho các lệnh gọi giữa các nút. Đây là tùy chọn: để trống để chỉ dùng xác thực bằng token.",
"copyCa": "Sao chép CA của bảng điều khiển này",
"copyCaHint": "Cấp CA này cho các nút mà bảng điều khiển này quản lý, sau đó đặt chế độ xác minh TLS của chúng thành TLS song phương.",
"caCopied": "Đã sao chép chứng chỉ CA vào bộ nhớ tạm",
"caFailed": "Không lấy được chứng chỉ CA",
"trustLabel": "CA tin cậy (bảng điều khiển cha)",
"trustHint": "Khi bảng điều khiển này bản thân là một nút, hãy dán CA của bảng điều khiển quản lý vào đây để yêu cầu chứng chỉ máy khách của nó. Khởi động lại bảng điều khiển để áp dụng.",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "Lưu CA tin cậy",
"saved": "Đã lưu CA tin cậy — khởi động lại bảng điều khiển để áp dụng"
},
"tlsSkipWarning": "Bỏ qua xác minh sẽ loại bỏ bảo vệ trước tấn công xen giữa — token API có thể bị chặn bắt. Nên ghim chứng chỉ thay vì vậy.",
"pinnedCert": "SHA-256 của chứng chỉ đã ghim",
"pinnedCertHint": "SHA-256 của chứng chỉ node ở dạng base64 hoặc hex. Dùng Lấy để đọc trực tiếp từ node.",
"pinnedCertPlaceholder": "SHA-256 base64 hoặc hex",
"fetchPin": "Lấy",
"pinFetched": "Đã lấy chứng chỉ hiện tại của node",
"pinFetchFailed": "Không thể lấy chứng chỉ",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "Mẫu Cơ bản",
"advancedTemplate": "Mẫu Nâng cao",
"Outbounds": "Outbound",
"Balancers": "Cân bằng",
"rules": {
"inbound": "Vào"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "客户端",
"nodes": "节点",
"settings": "面板设置",
"donate": "捐赠",
"hosts": "主机",
"docs": "文档",
"openMenu": "打开菜单",
"pinSidebar": "固定侧边栏",
"unpinSidebar": "取消固定侧边栏"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "无法确认更新是否已完成",
"panelUpdateUnknownDesc": "面板未能及时返回结果。请刷新页面查看当前版本,或检查服务器日志。",
"dontRefresh": "安装中,请勿刷新此页面",
"config": "配置",
"backupTitle": "备份和恢复",
"exportDatabase": "备份",
"exportDatabaseDesc": "点击下载包含当前数据库备份的 .db 文件到您的设备。同一文件也可以恢复到运行 PostgreSQL 的面板中。",
@@ -158,7 +155,6 @@
"importDatabaseError": "导入数据库时出错",
"readDatabaseError": "读取数据库时出错",
"getDatabaseError": "检索数据库时出错",
"getConfigError": "检索配置文件时出错",
"backupPostgresNote": "此面板运行在 PostgreSQL 上。「备份」会下载一个 pg_dump 归档(.dump),「恢复」会通过 pg_restore 重新载入。「恢复」也接受 SQLite 数据库(.db)或 SQLite 迁移导出文件,并将其数据导入 PostgreSQL。服务器需要安装 PostgreSQL 客户端工具(pg_dump 和 pg_restore)。",
"exportDatabasePgDesc": "点击将当前数据库的 PostgreSQL 转储(.dump)下载到您的设备。",
"importDatabasePgDesc": "点击选择并上传 PostgreSQL 备份(.dump)、SQLite 数据库(.db)或 SQLite 迁移导出文件以恢复您的数据库。此操作将替换所有当前数据。",
@@ -822,31 +818,10 @@
"avgLatency": "平均延迟",
"name": "名称",
"namePlaceholder": "例如:de-frankfurt-1",
"addressPlaceholder": "panel.example.com 或 1.2.3.4",
"remark": "备注",
"scheme": "协议",
"address": "地址",
"port": "端口",
"basePath": "基础路径",
"apiToken": "API 令牌",
"apiTokenPlaceholder": "远程面板设置页中的令牌",
"apiTokenHint": "远程面板在 安全设定 → API 令牌 中显示其 API 令牌。",
"apiTokenKeepHint": "留空以保留当前令牌",
"allowPrivateAddress": "允许私有地址",
"allowPrivateAddressHint": "仅对私有网络或 VPN 上的节点启用。",
"outboundTag": "连接出站",
"outboundTagHint": "通过选定的 Xray 出站路由此节点的面板 API 流量。系统会自动将回环桥接入站添加到运行配置并实时应用。留空表示直接连接。",
"outboundTagPlaceholder": "直接连接",
"inboundSyncMode": "入站导入",
"inboundSyncModeHint": "选择要从此节点导入的入站。现有节点默认导入全部入站。",
"allInbounds": "全部入站",
"selectedInbounds": "选定的入站",
"inboundTags": "入站",
"inboundTagsHint": "按入站标签匹配。空选择不会导入任何入站。",
"inboundTagsPlaceholder": "加载并选择入站",
"loadInbounds": "从节点加载入站",
"inboundsLoaded": "已加载 {{count}} 个入站",
"inboundsLoadFailed": "加载入站失败",
"enable": "已启用",
"status": "状态",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "面板版本",
"actions": "操作",
"probe": "立即探测",
"updatePanel": "更新面板",
"updateSelected": "更新所选 ({count})",
"updateAvailable": "有可用更新",
"updateConfirmTitle": "将 {count} 个节点更新到最新版本?",
"updateConfirmContent": "每个所选节点会下载最新版本并重启。仅更新已启用且在线的节点。",
"updateAgent": "更新 Agent",
"updateAgentConfirmContent": "该节点上的 agent 将下载最新发行版、校验后重启切换;更新失败时节点继续运行当前版本。",
"agentInstallCommand": "安装命令",
"agentTokenNotStored": "该节点的 token 尚未存入主控,下面的命令不含 token。token 保存在节点本机的 /etc/x-ui/agent.json 中;agent 下次通过校验的请求后会自动回填到这里。",
"updateDevChannel": "更新到开发通道(最新提交)",
"testConnection": "测试连接",
"connectionOk": "连接正常 ({ms} ms)",
"connectionFailed": "连接失败",
"never": "从未",
"justNow": "刚刚",
"subNode": "子节点",
"subNodeTip": "只读:通过 {parent} 接入的下游节点。请在 {parent} 自己的面板中管理。",
"deleteConfirmTitle": "删除节点 \"{name}\"?",
"deleteConfirmContent": "这将停止监控该节点。远程面板本身不受影响。",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "探测失败",
"updateStarted": "已开始更新面板",
"updateResult": "已在 {ok} 个节点上触发更新,{failed} 个失败",
"updateNoneEligible": "请至少选择一个在线且已启用的节点",
"saveMtls": "保存节点 mTLS",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "请至少选择一个在线且已启用的节点"
},
"tlsVerifyMode": "TLS 校验",
"tlsVerifyModeHint": "面板如何校验节点的 HTTPS 证书。固定或跳过用于自签名证书(仅 https 节点)。",
"tlsVerify": "校验(默认 CA)",
"tlsPin": "固定证书(SHA-256)",
"tlsSkip": "跳过校验",
"tlsMtls": "双向 TLS(客户端证书)",
"mtlsFormHint": "此节点使用客户端证书对面板进行认证。请从“节点 mTLS”区域复制本面板的 CA 到该节点,设置其受信任的 CA,然后重启该节点。",
"mtls": {
"title": "节点 mTLS",
"intro": "双向 TLS 在节点间调用的 API 令牌之上增加客户端证书认证。此为可选项:留空则仅使用令牌认证。",
"copyCa": "复制此面板的 CA",
"copyCaHint": "将此 CA 提供给本面板管理的节点,然后将它们的 TLS 校验设置为双向 TLS。",
"caCopied": "CA 证书已复制到剪贴板",
"caFailed": "获取 CA 证书失败",
"trustLabel": "受信任的上级 CA",
"trustHint": "当本面板自身作为节点时,将管理它的面板的 CA 粘贴到此处以要求其客户端证书。重启面板后生效。",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "保存受信任的 CA",
"saved": "受信任的 CA 已保存 — 重启面板后生效"
},
"tlsSkipWarning": "跳过校验会失去对中间人攻击的防护,API 令牌可能被截获。建议改用固定证书。",
"pinnedCert": "固定证书的 SHA-256",
"pinnedCertHint": "节点证书的 SHA-256(base64 或 hex)。点击“获取”可立即从节点读取。",
"pinnedCertPlaceholder": "base64 或 hex 的 SHA-256",
"fetchPin": "获取",
"pinFetched": "已获取节点当前证书",
"pinFetchFailed": "无法获取证书",
"nodeType": "节点类型",
"nodeTypeTraditional": "传统面板节点",
"nodeTypeAgent": "Agent 轻量节点",
"agentHint": "Agent 节点由节点机主动连接主控,无需填写地址、端口和 API 令牌。创建后会生成安装命令。",
"probeOnly": "纯探针",
"probeOnlyHint": "纯探针模式:该节点不运行任何代理核心,只保留探针功能(心跳、指标与延迟探测)。",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "基础配置",
"advancedTemplate": "高级配置",
"Outbounds": "出站",
"Balancers": "负载均衡",
"rules": {
"inbound": "入站"
},
+1 -67
View File
@@ -97,9 +97,7 @@
"clients": "客戶端",
"nodes": "節點",
"settings": "面板設定",
"donate": "捐贈",
"hosts": "主機",
"docs": "文件",
"openMenu": "開啟選單",
"pinSidebar": "固定側邊欄",
"unpinSidebar": "取消固定側邊欄"
@@ -148,7 +146,6 @@
"panelUpdateUnknownTitle": "無法確認更新是否已完成",
"panelUpdateUnknownDesc": "面板未能及時回報結果。請重新整理以檢查目前版本,或檢查伺服器日誌。",
"dontRefresh": "安裝中,請勿重新整理此頁面",
"config": "配置",
"backupTitle": "備份和恢復",
"exportDatabase": "備份",
"exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。同一檔案也可以還原到執行 PostgreSQL 的面板中。",
@@ -158,7 +155,6 @@
"importDatabaseError": "匯入資料庫時發生錯誤",
"readDatabaseError": "讀取資料庫時發生錯誤",
"getDatabaseError": "檢索資料庫時發生錯誤",
"getConfigError": "檢索設定檔時發生錯誤",
"backupPostgresNote": "此面板執行於 PostgreSQL 上。「備份」會下載一個 pg_dump 封存檔(.dump),「還原」會透過 pg_restore 重新載入。「還原」也接受 SQLite 資料庫(.db)或 SQLite 遷移匯出檔,並將其資料匯入 PostgreSQL。伺服器需要安裝 PostgreSQL 用戶端工具(pg_dump 與 pg_restore)。",
"exportDatabasePgDesc": "點擊將目前資料庫的 PostgreSQL 傾印(.dump)下載到您的裝置。",
"importDatabasePgDesc": "點擊選擇並上傳 PostgreSQL 備份(.dump)、SQLite 資料庫(.db)或 SQLite 遷移匯出檔以還原您的資料庫。此操作將取代所有目前的資料。",
@@ -822,31 +818,10 @@
"avgLatency": "平均延遲",
"name": "名稱",
"namePlaceholder": "例如:de-frankfurt-1",
"addressPlaceholder": "panel.example.com 或 1.2.3.4",
"remark": "備註",
"scheme": "協議",
"address": "位址",
"port": "連接埠",
"basePath": "基礎路徑",
"apiToken": "API 權杖",
"apiTokenPlaceholder": "遠端面板設定頁中的權杖",
"apiTokenHint": "遠端面板在 安全設定 → API 權杖 中顯示其 API 權杖。",
"apiTokenKeepHint": "留空以保留目前的權杖",
"allowPrivateAddress": "允許私有地址",
"allowPrivateAddressHint": "僅對私有網路或 VPN 上的節點啟用。",
"outboundTag": "連線出站",
"outboundTagHint": "透過選定的 Xray 出站路由此節點的面板 API 流量。系統會自動將迴環橋接入站加入執行中的設定並即時套用。留空表示直接連線。",
"outboundTagPlaceholder": "直接連線",
"inboundSyncMode": "入站匯入",
"inboundSyncModeHint": "選擇要從此節點匯入的入站。現有節點預設匯入所有入站。",
"allInbounds": "所有入站",
"selectedInbounds": "選取的入站",
"inboundTags": "入站",
"inboundTagsHint": "依入站標籤比對。空白選取不會匯入任何入站。",
"inboundTagsPlaceholder": "載入並選取入站",
"loadInbounds": "從節點載入入站",
"inboundsLoaded": "已載入 {{count}} 個入站",
"inboundsLoadFailed": "載入入站失敗",
"enable": "已啟用",
"status": "狀態",
"cpu": "CPU",
@@ -860,23 +835,16 @@
"panelVersion": "面板版本",
"actions": "操作",
"probe": "立即探測",
"updatePanel": "更新面板",
"updateSelected": "更新所選 ({count})",
"updateAvailable": "有可用更新",
"updateConfirmTitle": "將 {count} 個節點更新到最新版本?",
"updateConfirmContent": "每個所選節點會下載最新版本並重新啟動。僅更新已啟用且在線的節點。",
"updateAgent": "Update Agent",
"updateAgentConfirmContent": "The agent on this node will download the latest release, verify it, and restart onto it. If the update fails, the node keeps running the current version.",
"agentInstallCommand": "Install Command",
"agentTokenNotStored": "This node's token is not stored on the master yet, so the command below has no token. The token is saved on the node itself in /etc/x-ui/agent.json; it will be filled in here automatically after the agent's next authenticated request.",
"updateDevChannel": "更新到開發通道(最新提交)",
"testConnection": "測試連線",
"connectionOk": "連線正常 ({ms} ms)",
"connectionFailed": "連線失敗",
"never": "從未",
"justNow": "剛剛",
"subNode": "子節點",
"subNodeTip": "唯讀:透過 {parent} 連接的下游節點。請在 {parent} 自己的面板中管理。",
"deleteConfirmTitle": "刪除節點「{name}」?",
"deleteConfirmContent": "這將停止監控該節點。遠端面板本身不受影響。",
"statusValues": {
@@ -898,40 +866,8 @@
"probeFailed": "探測失敗",
"updateStarted": "已開始更新面板",
"updateResult": "已在 {ok} 個節點上觸發更新,{failed} 個失敗",
"updateNoneEligible": "請至少選擇一個在線且已啟用的節點",
"saveMtls": "儲存節點 mTLS",
"reloadMtls": "Reload master mTLS credential"
"updateNoneEligible": "請至少選擇一個在線且已啟用的節點"
},
"tlsVerifyMode": "TLS 驗證",
"tlsVerifyModeHint": "面板如何驗證節點的 HTTPS 憑證。釘選或略過用於自簽憑證(僅 https 節點)。",
"tlsVerify": "驗證(預設 CA)",
"tlsPin": "釘選憑證(SHA-256)",
"tlsSkip": "略過驗證",
"tlsMtls": "雙向 TLS(用戶端憑證)",
"mtlsFormHint": "此節點使用用戶端憑證對面板進行驗證。請從「節點 mTLS」區域複製本面板的 CA 到該節點,設定其受信任的 CA,然後重新啟動該節點。",
"mtls": {
"title": "節點 mTLS",
"intro": "雙向 TLS 在節點間呼叫的 API 權杖之上增加用戶端憑證驗證。此為選用項目:留空則僅使用權杖驗證。",
"copyCa": "複製此面板的 CA",
"copyCaHint": "將此 CA 提供給本面板管理的節點,然後將它們的 TLS 驗證設定為雙向 TLS。",
"caCopied": "CA 憑證已複製到剪貼簿",
"caFailed": "取得 CA 憑證失敗",
"trustLabel": "受信任的上層 CA",
"trustHint": "當本面板自身作為節點時,將管理它的面板的 CA 貼到此處以要求其用戶端憑證。重新啟動面板後生效。",
"trustPlaceholder": "-----BEGIN CERTIFICATE-----",
"save": "儲存受信任的 CA",
"saved": "受信任的 CA 已儲存 — 重新啟動面板後生效"
},
"tlsSkipWarning": "略過驗證會失去對中間人攻擊的防護,API 權杖可能被攔截。建議改用釘選憑證。",
"pinnedCert": "釘選憑證的 SHA-256",
"pinnedCertHint": "節點憑證的 SHA-256(base64 或 hex)。點選「取得」可立即從節點讀取。",
"pinnedCertPlaceholder": "base64 或 hex 的 SHA-256",
"fetchPin": "取得",
"pinFetched": "已取得節點目前憑證",
"pinFetchFailed": "無法取得憑證",
"nodeType": "Node Type",
"nodeTypeTraditional": "Traditional Panel Node",
"nodeTypeAgent": "Agent Lightweight Node",
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
@@ -1273,8 +1209,6 @@
"xray": {
"basicTemplate": "基礎配置",
"advancedTemplate": "高階配置",
"Outbounds": "出站",
"Balancers": "負載均衡",
"rules": {
"inbound": "入站"
},