mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
Fix NTP server config not reaching betterdesk-server (Fixes #223).
Wire billing/NTP env from console .env into Go on Linux, Windows, and Docker; add OS NTP fallback and Commercialization settings UI to configure and restart the server.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **NTP servers for billing clock (#223)**: `betterdesk-server` now loads `NTP_SERVERS` and billing clock vars from console `.env` (Linux `EnvironmentFile` + update migration). Added `BILLING_TRUST_OS_NTP` fallback when OS time sync is healthy but public NTP is blocked. Commercialization → Settings exposes NTP configuration with Go server restart.
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
|
||||
@@ -195,6 +195,12 @@ sudo firewall-cmd --reload
|
||||
| `TLS_KEY` | - | TLS private key path |
|
||||
| `PEER_TIMEOUT_SECS` | 15 | Seconds before peer marked offline |
|
||||
| `TRUST_PROXY` | N | Trust X-Forwarded-For header |
|
||||
| `NTP_SERVERS` | public pools | Comma-separated NTP hostnames/IPs for billing clock checks |
|
||||
| `BILLING_MAX_CLOCK_SKEW_MS` | 2000 | Max allowed clock offset vs NTP (ms) |
|
||||
| `BILLING_REQUIRE_SYNCED_CLOCK` | Y (Linux default) | Block billable sessions when clock unsynced |
|
||||
| `BILLING_TRUST_OS_NTP` | Y on Linux, N elsewhere | Trust `timedatectl` when direct NTP queries fail |
|
||||
|
||||
NTP/billing variables are stored in the web console `.env` on native installs. Linux `betterdesk-server.service` loads that file via `EnvironmentFile`. After changing NTP settings, restart **betterdesk-server** (not only the console). The panel path: **Commercialization → Settings → Server time (NTP)**.
|
||||
|
||||
For NGINX `stream` or other UDP/TCP reverse proxy deployments, remember that signal traffic on `21116` is not HTTP and cannot carry `X-Forwarded-For`. `TRUST_PROXY` only applies to HTTP/API handlers. If many legitimate devices share the same proxy address, raise `SIGNAL_RATE_LIMIT_PER_IP` or set it to `0` only on trusted private networks.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -138,6 +139,7 @@ type Config struct {
|
||||
NTPServers string // Comma-separated NTP servers
|
||||
BillingMaxClockSkewMS int // Max allowed clock offset vs NTP (default 2000)
|
||||
BillingRequireSyncedClock bool // Block billable sessions when clock unsynced
|
||||
BillingTrustOSNTP bool // Trust OS NTP (timedatectl) when direct queries fail
|
||||
BillingRoundingMinutes int // Billable minute rounding (1, 10, 15)
|
||||
BillingRequireWorkReport bool // Require technician report before session close
|
||||
}
|
||||
@@ -167,6 +169,7 @@ func DefaultConfig() *Config {
|
||||
P2PFallbackMs: 2000, // grace period for target hole punch before relay fallback
|
||||
BillingMaxClockSkewMS: 2000,
|
||||
BillingRequireSyncedClock: true,
|
||||
BillingTrustOSNTP: runtime.GOOS == "linux",
|
||||
BillingRoundingMinutes: 1,
|
||||
}
|
||||
}
|
||||
@@ -389,6 +392,14 @@ func (c *Config) LoadEnv() {
|
||||
c.BillingRequireSyncedClock = false
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("BILLING_TRUST_OS_NTP"); v != "" {
|
||||
switch strings.ToUpper(v) {
|
||||
case "Y", "YES", "1", "TRUE", "ON":
|
||||
c.BillingTrustOSNTP = true
|
||||
case "N", "NO", "0", "FALSE", "OFF":
|
||||
c.BillingTrustOSNTP = false
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("BILLING_ROUNDING_MINUTES"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
c.BillingRoundingMinutes = n
|
||||
|
||||
@@ -293,10 +293,23 @@ func main() {
|
||||
QueryTimeout: 5 * time.Second,
|
||||
MaxSkew: time.Duration(cfg.BillingMaxClockSkewMS) * time.Millisecond,
|
||||
RequireSync: cfg.BillingRequireSyncedClock,
|
||||
TrustOSNTP: cfg.BillingTrustOSNTP,
|
||||
})
|
||||
log.Printf("[timesync] NTP servers: %v (trust OS NTP when queries fail: %v)",
|
||||
cfg.GetNTPServers(), cfg.BillingTrustOSNTP)
|
||||
timeSyncSvc.Start(ctx)
|
||||
defer timeSyncSvc.Stop()
|
||||
|
||||
reloadHandler.OnReload(func() error {
|
||||
timeSyncSvc.ApplyConfig(timesync.Config{
|
||||
Servers: cfg.GetNTPServers(),
|
||||
MaxSkew: time.Duration(cfg.BillingMaxClockSkewMS) * time.Millisecond,
|
||||
RequireSync: cfg.BillingRequireSyncedClock,
|
||||
TrustOSNTP: cfg.BillingTrustOSNTP,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
billingSvc := billing.NewService(database, timeSyncSvc, cfg.BillingRoundingMinutes, cfg.BillingRequireWorkReport)
|
||||
billingSvc.Start(ctx)
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ type Status struct {
|
||||
|
||||
// Config controls periodic NTP checks.
|
||||
type Config struct {
|
||||
Servers []string
|
||||
Interval time.Duration
|
||||
QueryTimeout time.Duration
|
||||
MaxSkew time.Duration
|
||||
RequireSync bool
|
||||
Servers []string
|
||||
Interval time.Duration
|
||||
QueryTimeout time.Duration
|
||||
MaxSkew time.Duration
|
||||
RequireSync bool
|
||||
TrustOSNTP bool
|
||||
}
|
||||
|
||||
// Service periodically queries NTP and exposes clock health for billing.
|
||||
@@ -103,18 +104,50 @@ func (s *Service) loop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyConfig hot-reloads NTP/billing clock settings (e.g. after SIGHUP).
|
||||
func (s *Service) ApplyConfig(cfg Config) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(cfg.Servers) > 0 {
|
||||
s.cfg.Servers = append([]string(nil), cfg.Servers...)
|
||||
} else {
|
||||
s.cfg.Servers = []string{"pool.ntp.org", "time.google.com", "time.cloudflare.com"}
|
||||
}
|
||||
if cfg.Interval > 0 {
|
||||
s.cfg.Interval = cfg.Interval
|
||||
}
|
||||
if cfg.QueryTimeout > 0 {
|
||||
s.cfg.QueryTimeout = cfg.QueryTimeout
|
||||
}
|
||||
if cfg.MaxSkew > 0 {
|
||||
s.cfg.MaxSkew = cfg.MaxSkew
|
||||
}
|
||||
s.cfg.RequireSync = cfg.RequireSync
|
||||
s.cfg.TrustOSNTP = cfg.TrustOSNTP
|
||||
s.status.MaxSkewMS = s.cfg.MaxSkew.Milliseconds()
|
||||
s.status.RequireSync = s.cfg.RequireSync
|
||||
}
|
||||
|
||||
// CheckNow runs an immediate NTP check against configured servers.
|
||||
func (s *Service) CheckNow() Status {
|
||||
var (
|
||||
bestOffset time.Duration
|
||||
bestServer string
|
||||
bestOffset time.Duration
|
||||
bestServer string
|
||||
bestStratum uint8
|
||||
lastErr error
|
||||
found bool
|
||||
lastErr error
|
||||
found bool
|
||||
)
|
||||
|
||||
for _, server := range s.cfg.Servers {
|
||||
offset, stratum, err := QueryNTP(server, s.cfg.QueryTimeout)
|
||||
s.mu.RLock()
|
||||
servers := append([]string(nil), s.cfg.Servers...)
|
||||
queryTimeout := s.cfg.QueryTimeout
|
||||
maxSkew := s.cfg.MaxSkew
|
||||
requireSync := s.cfg.RequireSync
|
||||
trustOSNTP := s.cfg.TrustOSNTP
|
||||
s.mu.RUnlock()
|
||||
|
||||
for _, server := range servers {
|
||||
offset, stratum, err := QueryNTP(server, queryTimeout)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
log.Printf("[timesync] NTP query %s failed: %v", server, err)
|
||||
@@ -130,14 +163,22 @@ func (s *Service) CheckNow() Status {
|
||||
|
||||
st := Status{
|
||||
LastCheckAt: time.Now().UTC(),
|
||||
MaxSkewMS: s.cfg.MaxSkew.Milliseconds(),
|
||||
RequireSync: s.cfg.RequireSync,
|
||||
MaxSkewMS: maxSkew.Milliseconds(),
|
||||
RequireSync: requireSync,
|
||||
}
|
||||
if osSync := readOSClockSynced(); osSync != nil {
|
||||
if osSync := osClockSyncedReader(); osSync != nil {
|
||||
st.OSClockSynced = osSync
|
||||
}
|
||||
|
||||
if !found {
|
||||
if trustOSNTP && st.OSClockSynced != nil && *st.OSClockSynced {
|
||||
st.Synced = true
|
||||
st.NTPServer = "os:systemd-timesyncd"
|
||||
st.LastError = ""
|
||||
s.setStatus(st)
|
||||
s.persistStatus(st)
|
||||
return st
|
||||
}
|
||||
st.Synced = false
|
||||
if lastErr != nil {
|
||||
st.LastError = lastErr.Error()
|
||||
@@ -153,7 +194,7 @@ func (s *Service) CheckNow() Status {
|
||||
st.OffsetMS = offsetMS
|
||||
st.NTPServer = bestServer
|
||||
st.Stratum = bestStratum
|
||||
st.Synced = absDuration(bestOffset) <= s.cfg.MaxSkew
|
||||
st.Synced = absDuration(bestOffset) <= maxSkew
|
||||
if !st.Synced {
|
||||
st.LastError = "clock skew exceeds threshold"
|
||||
}
|
||||
@@ -162,7 +203,7 @@ func (s *Service) CheckNow() Status {
|
||||
s.persistStatus(st)
|
||||
if !st.Synced {
|
||||
log.Printf("[timesync] WARNING: clock skew %dms (max %dms) via %s",
|
||||
offsetMS, s.cfg.MaxSkew.Milliseconds(), bestServer)
|
||||
offsetMS, maxSkew.Milliseconds(), bestServer)
|
||||
}
|
||||
return st
|
||||
}
|
||||
@@ -219,3 +260,6 @@ func absDuration(d time.Duration) time.Duration {
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// osClockSyncedReader is overridden in tests.
|
||||
var osClockSyncedReader = readOSClockSynced
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package timesync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestApplyConfigUpdatesServers(t *testing.T) {
|
||||
svc := NewService(nil, Config{
|
||||
Servers: []string{"pool.ntp.org"},
|
||||
MaxSkew: 2 * time.Second,
|
||||
RequireSync: true,
|
||||
})
|
||||
svc.ApplyConfig(Config{
|
||||
Servers: []string{"10.0.0.1", "ntp.example.com"},
|
||||
MaxSkew: 3 * time.Second,
|
||||
RequireSync: false,
|
||||
TrustOSNTP: true,
|
||||
})
|
||||
|
||||
svc.mu.RLock()
|
||||
defer svc.mu.RUnlock()
|
||||
if len(svc.cfg.Servers) != 2 || svc.cfg.Servers[0] != "10.0.0.1" {
|
||||
t.Fatalf("servers = %#v", svc.cfg.Servers)
|
||||
}
|
||||
if svc.cfg.MaxSkew != 3*time.Second {
|
||||
t.Fatalf("max skew = %v", svc.cfg.MaxSkew)
|
||||
}
|
||||
if svc.cfg.RequireSync {
|
||||
t.Fatal("expected RequireSync=false")
|
||||
}
|
||||
if !svc.cfg.TrustOSNTP {
|
||||
t.Fatal("expected TrustOSNTP=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckNowTrustOSWhenNTPFails(t *testing.T) {
|
||||
old := osClockSyncedReader
|
||||
defer func() { osClockSyncedReader = old }()
|
||||
synced := true
|
||||
osClockSyncedReader = func() *bool { return &synced }
|
||||
|
||||
svc := NewService(nil, Config{
|
||||
Servers: []string{"127.0.0.1:9"},
|
||||
QueryTimeout: 100 * time.Millisecond,
|
||||
MaxSkew: 2 * time.Second,
|
||||
TrustOSNTP: true,
|
||||
})
|
||||
st := svc.CheckNow()
|
||||
if !st.Synced {
|
||||
t.Fatalf("expected synced via OS fallback, got %+v", st)
|
||||
}
|
||||
if st.NTPServer != "os:systemd-timesyncd" {
|
||||
t.Fatalf("ntp_server = %q", st.NTPServer)
|
||||
}
|
||||
if st.LastError != "" {
|
||||
t.Fatalf("last_error = %q", st.LastError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckNowNoTrustOSWhenNTPFails(t *testing.T) {
|
||||
old := osClockSyncedReader
|
||||
defer func() { osClockSyncedReader = old }()
|
||||
synced := true
|
||||
osClockSyncedReader = func() *bool { return &synced }
|
||||
|
||||
svc := NewService(nil, Config{
|
||||
Servers: []string{"127.0.0.1:9"},
|
||||
QueryTimeout: 100 * time.Millisecond,
|
||||
MaxSkew: 2 * time.Second,
|
||||
TrustOSNTP: false,
|
||||
})
|
||||
st := svc.CheckNow()
|
||||
if st.Synced {
|
||||
t.Fatal("expected unsynced when TrustOSNTP=false")
|
||||
}
|
||||
if st.LastError == "" {
|
||||
t.Fatal("expected last_error")
|
||||
}
|
||||
}
|
||||
@@ -2121,6 +2121,31 @@ function Setup-Services {
|
||||
Print-Info "Services: $script:SERVER_SERVICE, $script:CONSOLE_SERVICE"
|
||||
}
|
||||
|
||||
function Get-GoBillingEnvLauncherLines {
|
||||
param([string]$EnvFilePath)
|
||||
$lines = @()
|
||||
$keys = @(
|
||||
'NTP_SERVERS',
|
||||
'BILLING_MAX_CLOCK_SKEW_MS',
|
||||
'BILLING_REQUIRE_SYNCED_CLOCK',
|
||||
'BILLING_TRUST_OS_NTP'
|
||||
)
|
||||
if (-not $EnvFilePath -or -not (Test-Path $EnvFilePath)) {
|
||||
return $lines
|
||||
}
|
||||
foreach ($rawLine in Get-Content $EnvFilePath) {
|
||||
$line = $rawLine.Trim()
|
||||
if (-not $line -or $line.StartsWith('#')) { continue }
|
||||
$parts = $line -split '=', 2
|
||||
if ($parts.Count -lt 2) { continue }
|
||||
$key = $parts[0].Trim()
|
||||
if ($keys -notcontains $key) { continue }
|
||||
$value = $parts[1].Trim()
|
||||
$lines += "set `"$key=$value`""
|
||||
}
|
||||
return $lines
|
||||
}
|
||||
|
||||
function Setup-ScheduledTasks {
|
||||
param([string]$ServerIP)
|
||||
|
||||
@@ -2201,6 +2226,10 @@ function Setup-ScheduledTasks {
|
||||
if ($adminPass) { $launcherLines += "set `"INIT_ADMIN_PASS=$adminPass`"" }
|
||||
# New installs default to "managed" enrollment; existing installs untouched.
|
||||
if ($script:FRESH_INSTALL) { $launcherLines += "set `"ENROLLMENT_MODE=managed`"" }
|
||||
$billingEnvFile = Join-Path $script:CONSOLE_PATH ".env"
|
||||
foreach ($billingLine in (Get-GoBillingEnvLauncherLines -EnvFilePath $billingEnvFile)) {
|
||||
$launcherLines += $billingLine
|
||||
}
|
||||
$launcherLines += "`"$serverExe`" $serverArgs"
|
||||
Set-Content -Path $serverLauncher -Value $launcherLines -Encoding ASCII
|
||||
& icacls $serverLauncher /inheritance:r /grant:r "*S-1-5-32-544:F" "*S-1-5-18:F" 2>$null | Out-Null
|
||||
|
||||
@@ -2749,6 +2749,7 @@ After=network.target postgresql.service
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$RUSTDESK_PATH
|
||||
EnvironmentFile=-$CONSOLE_PATH/.env
|
||||
Environment=AUTH_DB_PATH=$CONSOLE_PATH/data/auth.db
|
||||
Environment=MESH_ENABLED=Y
|
||||
$CONNECTION_MODE_ENV_BLOCK
|
||||
@@ -3150,6 +3151,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
$( [ -n "$CONSOLE_PATH" ] && [ -f "$CONSOLE_PATH/.env" ] && echo "EnvironmentFile=-$CONSOLE_PATH/.env" )
|
||||
ExecStart=$GO_BINARY_PATH $SERVER_ARGS
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
@@ -74,6 +74,10 @@ services:
|
||||
# new devices); pre-existing volumes keep their current mode. Override with
|
||||
# "open", "managed", or "locked".
|
||||
- ENROLLMENT_MODE=${ENROLLMENT_MODE:-}
|
||||
- NTP_SERVERS=${NTP_SERVERS:-pool.ntp.org,time.google.com,time.cloudflare.com}
|
||||
- BILLING_MAX_CLOCK_SKEW_MS=${BILLING_MAX_CLOCK_SKEW_MS:-2000}
|
||||
- BILLING_REQUIRE_SYNCED_CLOCK=${BILLING_REQUIRE_SYNCED_CLOCK:-1}
|
||||
- BILLING_TRUST_OS_NTP=${BILLING_TRUST_OS_NTP:-Y}
|
||||
# Panel folders/groups for RustDesk client (SQLite — same container as console).
|
||||
- AUTH_DB_PATH=/app/data/auth.db
|
||||
# Admin credentials (first run only; existing users are not overwritten):
|
||||
|
||||
@@ -49,6 +49,10 @@ services:
|
||||
# new devices); pre-existing volumes keep their current mode. Set to
|
||||
# "open", "managed", or "locked" to override.
|
||||
- ENROLLMENT_MODE=${ENROLLMENT_MODE:-}
|
||||
- NTP_SERVERS=${NTP_SERVERS:-pool.ntp.org,time.google.com,time.cloudflare.com}
|
||||
- BILLING_MAX_CLOCK_SKEW_MS=${BILLING_MAX_CLOCK_SKEW_MS:-2000}
|
||||
- BILLING_REQUIRE_SYNCED_CLOCK=${BILLING_REQUIRE_SYNCED_CLOCK:-1}
|
||||
- BILLING_TRUST_OS_NTP=${BILLING_TRUST_OS_NTP:-Y}
|
||||
networks:
|
||||
- betterdesk-net
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -60,5 +60,5 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=SIGNAL_PORT="21116",SIGNAL_RATE_LIMIT_PER_IP="%(ENV_SIGNAL_RATE_LIMIT_PER_IP)s",ENCRYPTED_ONLY="%(ENV_ENCRYPTED_ONLY)s",DB_URL="%(ENV_DB_URL)s",AUTH_DB_PATH="%(ENV_AUTH_DB_PATH)s",RELAY_SERVERS="%(ENV_RELAY_SERVERS)s",ENROLLMENT_MODE="%(ENV_ENROLLMENT_MODE)s"
|
||||
environment=SIGNAL_PORT="21116",SIGNAL_RATE_LIMIT_PER_IP="%(ENV_SIGNAL_RATE_LIMIT_PER_IP)s",ENCRYPTED_ONLY="%(ENV_ENCRYPTED_ONLY)s",DB_URL="%(ENV_DB_URL)s",AUTH_DB_PATH="%(ENV_AUTH_DB_PATH)s",RELAY_SERVERS="%(ENV_RELAY_SERVERS)s",ENROLLMENT_MODE="%(ENV_ENROLLMENT_MODE)s",NTP_SERVERS="%(ENV_NTP_SERVERS)s",BILLING_MAX_CLOCK_SKEW_MS="%(ENV_BILLING_MAX_CLOCK_SKEW_MS)s",BILLING_REQUIRE_SYNCED_CLOCK="%(ENV_BILLING_REQUIRE_SYNCED_CLOCK)s",BILLING_TRUST_OS_NTP="%(ENV_BILLING_TRUST_OS_NTP)s"
|
||||
priority=200
|
||||
|
||||
@@ -76,10 +76,13 @@ TRUST_PROXY=false
|
||||
# Console→Go API: keep on 127.0.0.1 (native install) or http://betterdesk-server:21114/api (Docker).
|
||||
# Do not expose the Go API port directly to WAN — use the console proxy on :21121 for RustDesk clients.
|
||||
|
||||
# Billing / NTP (commercialization module — merged on update, missing keys only)
|
||||
# Billing / NTP (commercialization module — read by betterdesk-server Go process)
|
||||
# Stored in console .env; Linux installs load this via EnvironmentFile on betterdesk-server.service.
|
||||
# After changes, restart betterdesk-server (Commercialization → Settings can apply + restart).
|
||||
NTP_SERVERS=pool.ntp.org,time.google.com,time.cloudflare.com
|
||||
BILLING_MAX_CLOCK_SKEW_MS=2000
|
||||
BILLING_REQUIRE_SYNCED_CLOCK=1
|
||||
BILLING_TRUST_OS_NTP=1
|
||||
BILLING_ROUNDING_MINUTES=1
|
||||
BILLING_REQUIRE_WORK_REPORT=1
|
||||
|
||||
|
||||
+17
-1
@@ -3835,7 +3835,23 @@
|
||||
"clock": {
|
||||
"title": "وقت الخادم (NTP)",
|
||||
"check_now": "تحقق الآن",
|
||||
"unsynced": "ساعة الخادم غير متزامنة — قد تُحظر الجلسات القابلة للفوترة"
|
||||
"unsynced": "ساعة الخادم غير متزامنة — قد تُحظر الجلسات القابلة للفوترة",
|
||||
"synced_detail": "الساعة متزامنة للفوترة",
|
||||
"ntp_servers": "خوادم NTP",
|
||||
"ntp_servers_hint": "أسماء مضيفة أو عناوين IP مفصولة بفواصل. تُطبَّق على خادم Go (betterdesk-server).",
|
||||
"max_skew_ms": "أقصى انحراف للساعة (مللي ثانية)",
|
||||
"require_sync": "اشتراط ساعة متزامنة للجلسات القابلة للفوترة",
|
||||
"trust_os": "الثقة بـ NTP للنظام عند فشل الاستعلامات المباشرة",
|
||||
"trust_os_hint": "استخدم حالة systemd-timesyncd/chrony عند حظر UDP NTP (DMZ/جدار ناري).",
|
||||
"save": "حفظ إعدادات NTP",
|
||||
"saved": "تم حفظ إعدادات الساعة. بدء إعادة تشغيل خادم Go.",
|
||||
"save_restart": "حفظ إعدادات NTP وإعادة تشغيل خادم Go؟ قد تنقطع الجلسات النشطة briefly.",
|
||||
"status_offset": "الانحراف",
|
||||
"status_server": "مصدر NTP",
|
||||
"status_os": "ساعة OS متزامنة",
|
||||
"status_error": "آخر خطأ",
|
||||
"os_yes": "نعم",
|
||||
"os_no": "لا"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "جلسات قابلة للفوترة نشطة"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Čas serveru (NTP)",
|
||||
"check_now": "Zkontrolovat",
|
||||
"unsynced": "Hodiny serveru nejsou synchronizovány — fakturovatelné relace mohou být blokovány"
|
||||
"unsynced": "Hodiny serveru nejsou synchronizovány — fakturovatelné relace mohou být blokovány",
|
||||
"synced_detail": "Hodiny synchronizovány pro fakturaci",
|
||||
"ntp_servers": "NTP servery",
|
||||
"ntp_servers_hint": "Názvy hostitelů nebo IP oddělené čárkou. Platí pro Go server (betterdesk-server).",
|
||||
"max_skew_ms": "Maximální odchylka hodin (ms)",
|
||||
"require_sync": "Vyžadovat synchronizované hodiny pro fakturovatelné relace",
|
||||
"trust_os": "Důvěřovat OS NTP při selhání přímých dotazů",
|
||||
"trust_os_hint": "Použít stav systemd-timesyncd/chrony, když je UDP NTP blokováno (DMZ/firewall).",
|
||||
"save": "Uložit nastavení NTP",
|
||||
"saved": "Nastavení hodin uloženo. Restart Go serveru spuštěn.",
|
||||
"save_restart": "Uložit NTP a restartovat Go server? Aktivní relace se mohou krátce odpojit.",
|
||||
"status_offset": "Odchylka",
|
||||
"status_server": "Zdroj NTP",
|
||||
"status_os": "OS hodiny synchronizovány",
|
||||
"status_error": "Poslední chyba",
|
||||
"os_yes": "Ano",
|
||||
"os_no": "Ne"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktivní fakturovatelné relace"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Servertid (NTP)",
|
||||
"check_now": "Tjek nu",
|
||||
"unsynced": "Serveruret er ikke synkroniseret — fakturerbare sessioner kan blive blokeret"
|
||||
"unsynced": "Serveruret er ikke synkroniseret — fakturerbare sessioner kan blive blokeret",
|
||||
"synced_detail": "Ur synkroniseret til fakturering",
|
||||
"ntp_servers": "NTP-servere",
|
||||
"ntp_servers_hint": "Kommaseparerede værtsnavne eller IP. Gælder Go-serveren (betterdesk-server).",
|
||||
"max_skew_ms": "Maks. urafvigelse (ms)",
|
||||
"require_sync": "Kræv synkroniseret ur for fakturerbare sessioner",
|
||||
"trust_os": "Stol på OS NTP når direkte forespørgsler fejler",
|
||||
"trust_os_hint": "Brug systemd-timesyncd/chrony-status når UDP NTP er blokeret (DMZ/firewall).",
|
||||
"save": "Gem NTP-indstillinger",
|
||||
"saved": "Urindstillinger gemt. Genstart af Go-server startet.",
|
||||
"save_restart": "Gem NTP og genstart Go-serveren? Aktive sessioner kan kort afbrydes.",
|
||||
"status_offset": "Afvigelse",
|
||||
"status_server": "NTP-kilde",
|
||||
"status_os": "OS-ur synkroniseret",
|
||||
"status_error": "Seneste fejl",
|
||||
"os_yes": "Ja",
|
||||
"os_no": "Nej"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktive fakturerbare sessioner"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Serverzeit (NTP)",
|
||||
"check_now": "Jetzt prüfen",
|
||||
"unsynced": "Serveruhr nicht synchronisiert — abrechenbare Sitzungen können blockiert werden"
|
||||
"unsynced": "Serveruhr nicht synchronisiert — abrechenbare Sitzungen können blockiert werden",
|
||||
"synced_detail": "Uhr für Abrechnung synchronisiert",
|
||||
"ntp_servers": "NTP-Server",
|
||||
"ntp_servers_hint": "Kommagetrennte Hostnamen oder IPs. Gilt für den Go-Server (betterdesk-server).",
|
||||
"max_skew_ms": "Maximale Uhrabweichung (ms)",
|
||||
"require_sync": "Synchronisierte Uhr für abrechenbare Sitzungen erforderlich",
|
||||
"trust_os": "OS-NTP vertrauen, wenn direkte Abfragen fehlschlagen",
|
||||
"trust_os_hint": "systemd-timesyncd/chrony-Status nutzen, wenn UDP-NTP blockiert ist (DMZ/Firewall).",
|
||||
"save": "NTP-Einstellungen speichern",
|
||||
"saved": "Uhreinstellungen gespeichert. Go-Server-Neustart gestartet.",
|
||||
"save_restart": "NTP speichern und Go-Server neu starten? Aktive Sitzungen können kurz trennen.",
|
||||
"status_offset": "Abweichung",
|
||||
"status_server": "NTP-Quelle",
|
||||
"status_os": "OS-Uhr synchronisiert",
|
||||
"status_error": "Letzter Fehler",
|
||||
"os_yes": "Ja",
|
||||
"os_no": "Nein"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktive abrechenbare Sitzungen"
|
||||
|
||||
+17
-1
@@ -3874,7 +3874,23 @@
|
||||
"clock": {
|
||||
"title": "Server time (NTP)",
|
||||
"check_now": "Check now",
|
||||
"unsynced": "Server clock is not synchronized — billable sessions may be blocked"
|
||||
"unsynced": "Server clock is not synchronized — billable sessions may be blocked",
|
||||
"synced_detail": "Clock synchronized for billing",
|
||||
"ntp_servers": "NTP servers",
|
||||
"ntp_servers_hint": "Comma-separated hostnames or IPs. Applied to the Go server (betterdesk-server), not only the web console.",
|
||||
"max_skew_ms": "Maximum clock skew (ms)",
|
||||
"require_sync": "Require synchronized clock for billable sessions",
|
||||
"trust_os": "Trust OS NTP when direct queries fail",
|
||||
"trust_os_hint": "Use systemd-timesyncd/chrony status when UDP NTP to public servers is blocked (DMZ/firewall).",
|
||||
"save": "Save NTP settings",
|
||||
"saved": "Clock settings saved. Go server restart initiated.",
|
||||
"save_restart": "Save NTP settings and restart the BetterDesk Go server? Active sessions may disconnect briefly.",
|
||||
"status_offset": "Offset",
|
||||
"status_server": "NTP source",
|
||||
"status_os": "OS clock synced",
|
||||
"status_error": "Last error",
|
||||
"os_yes": "Yes",
|
||||
"os_no": "No"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Active billable sessions"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Hora del servidor (NTP)",
|
||||
"check_now": "Comprobar ahora",
|
||||
"unsynced": "El reloj del servidor no está sincronizado — las sesiones facturables pueden bloquearse"
|
||||
"unsynced": "El reloj del servidor no está sincronizado — las sesiones facturables pueden bloquearse",
|
||||
"synced_detail": "Reloj sincronizado para facturación",
|
||||
"ntp_servers": "Servidores NTP",
|
||||
"ntp_servers_hint": "Nombres de host o IP separados por comas. Aplica al servidor Go (betterdesk-server).",
|
||||
"max_skew_ms": "Desviación máxima del reloj (ms)",
|
||||
"require_sync": "Exigir reloj sincronizado para sesiones facturables",
|
||||
"trust_os": "Confiar en NTP del SO si fallan las consultas directas",
|
||||
"trust_os_hint": "Usar estado systemd-timesyncd/chrony cuando UDP NTP esté bloqueado (DMZ/firewall).",
|
||||
"save": "Guardar ajustes NTP",
|
||||
"saved": "Ajustes de reloj guardados. Reinicio del servidor Go iniciado.",
|
||||
"save_restart": "¿Guardar NTP y reiniciar el servidor Go? Las sesiones activas pueden desconectarse brevemente.",
|
||||
"status_offset": "Desviación",
|
||||
"status_server": "Fuente NTP",
|
||||
"status_os": "Reloj del SO sincronizado",
|
||||
"status_error": "Último error",
|
||||
"os_yes": "Sí",
|
||||
"os_no": "No"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Sesiones facturables activas"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Palvelinaika (NTP)",
|
||||
"check_now": "Tarkista nyt",
|
||||
"unsynced": "Palvelimen kello ei ole synkronoitu — laskutettavat istunnot voidaan estää"
|
||||
"unsynced": "Palvelimen kello ei ole synkronoitu — laskutettavat istunnot voidaan estää",
|
||||
"synced_detail": "Kello synkronoitu laskutusta varten",
|
||||
"ntp_servers": "NTP-palvelimet",
|
||||
"ntp_servers_hint": "Pilkuilla erotetut isäntänimet tai IP:t. Koskee Go-palvelinta (betterdesk-server).",
|
||||
"max_skew_ms": "Suurin kelloero (ms)",
|
||||
"require_sync": "Vaadi synkronoitu kello laskutettaville istunnoille",
|
||||
"trust_os": "Luota OS NTP:hen kun suorat kyselyt epäonnistuvat",
|
||||
"trust_os_hint": "Käytä systemd-timesyncd/chrony-tilaa kun UDP NTP on estetty (DMZ/palomuuri).",
|
||||
"save": "Tallenna NTP-asetukset",
|
||||
"saved": "Kelloasetukset tallennettu. Go-palvelimen uudelleenkäynnistys käynnistetty.",
|
||||
"save_restart": "Tallenna NTP ja käynnistä Go-palvelin uudelleen? Aktiiviset istunnot voivat katketa hetkeksi.",
|
||||
"status_offset": "Ero",
|
||||
"status_server": "NTP-lähde",
|
||||
"status_os": "OS-kello synkronoitu",
|
||||
"status_error": "Viimeisin virhe",
|
||||
"os_yes": "Kyllä",
|
||||
"os_no": "Ei"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktiiviset laskutettavat istunnot"
|
||||
|
||||
+17
-1
@@ -3832,7 +3832,23 @@
|
||||
"clock": {
|
||||
"title": "Serverzeit (NTP)",
|
||||
"check_now": "Jetzt prüfen",
|
||||
"unsynced": "Serveruhr nicht synchronisiert — abrechenbare Sitzungen können blockiert werden"
|
||||
"unsynced": "Serveruhr nicht synchronisiert — abrechenbare Sitzungen können blockiert werden",
|
||||
"synced_detail": "Horloge synchronisée pour la facturation",
|
||||
"ntp_servers": "Serveurs NTP",
|
||||
"ntp_servers_hint": "Noms d'hôte ou IP séparés par des virgules. Appliqué au serveur Go (betterdesk-server).",
|
||||
"max_skew_ms": "Décalage horaire max. (ms)",
|
||||
"require_sync": "Exiger une horloge synchronisée pour les sessions facturables",
|
||||
"trust_os": "Faire confiance au NTP OS si les requêtes directes échouent",
|
||||
"trust_os_hint": "Utiliser l'état systemd-timesyncd/chrony quand l'UDP NTP est bloqué (DMZ/pare-feu).",
|
||||
"save": "Enregistrer les paramètres NTP",
|
||||
"saved": "Paramètres d'horloge enregistrés. Redémarrage du serveur Go lancé.",
|
||||
"save_restart": "Enregistrer le NTP et redémarrer le serveur Go ? Les sessions actives peuvent se déconnecter brièvement.",
|
||||
"status_offset": "Décalage",
|
||||
"status_server": "Source NTP",
|
||||
"status_os": "Horloge OS synchronisée",
|
||||
"status_error": "Dernière erreur",
|
||||
"os_yes": "Oui",
|
||||
"os_no": "Non"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktive abrechenbare Sitzungen"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "सर्वर समय (NTP)",
|
||||
"check_now": "अभी जाँचें",
|
||||
"unsynced": "सर्वर घड़ी सिंक नहीं है — बिल योग्य सत्र अवरुद्ध हो सकते हैं"
|
||||
"unsynced": "सर्वर घड़ी सिंक नहीं है — बिल योग्य सत्र अवरुद्ध हो सकते हैं",
|
||||
"synced_detail": "बिलिंग के लिए घड़ी सिंक",
|
||||
"ntp_servers": "NTP सर्वर",
|
||||
"ntp_servers_hint": "अल्पविराम से अलग होस्टनाम या IP। Go सर्वर (betterdesk-server) पर लागू।",
|
||||
"max_skew_ms": "अधिकतम घड़ी विचलन (ms)",
|
||||
"require_sync": "बिल योग्य सत्रों के लिए सिंक घड़ी आवश्यक",
|
||||
"trust_os": "प्रत्यक्ष क्वेरी विफल होने पर OS NTP पर भरोसा करें",
|
||||
"trust_os_hint": "UDP NTP अवरुद्ध होने पर systemd-timesyncd/chrony स्थिति का उपयोग करें।",
|
||||
"save": "NTP सेटिंग्स सहेजें",
|
||||
"saved": "घड़ी सेटिंग्स सहेजी गईं। Go सर्वर पुनः आरंभ शुरू।",
|
||||
"save_restart": "NTP सहेजें और Go सर्वर पुनः आरंभ करें? सक्रिय सत्र briefly डिस्कनेक्ट हो सकते हैं।",
|
||||
"status_offset": "ऑफ़सेट",
|
||||
"status_server": "NTP स्रोत",
|
||||
"status_os": "OS घड़ी सिंक",
|
||||
"status_error": "अंतिम त्रुटि",
|
||||
"os_yes": "हाँ",
|
||||
"os_no": "नहीं"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "सक्रिय बिल योग्य सत्र"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Szerveridő (NTP)",
|
||||
"check_now": "Ellenőrzés",
|
||||
"unsynced": "A szerver órája nincs szinkronban — a számlázható munkamenetek blokkolva lehetnek"
|
||||
"unsynced": "A szerver órája nincs szinkronban — a számlázható munkamenetek blokkolva lehetnek",
|
||||
"synced_detail": "Óra szinkronizálva számlázáshoz",
|
||||
"ntp_servers": "NTP szerverek",
|
||||
"ntp_servers_hint": "Vesszővel elválasztott hostnevek vagy IP-k. A Go szerverre (betterdesk-server) vonatkozik.",
|
||||
"max_skew_ms": "Max. óraeltérés (ms)",
|
||||
"require_sync": "Szinkronizált óra kötelező számlázható munkamenetekhez",
|
||||
"trust_os": "Bízzon az OS NTP-ben, ha a közvetlen lekérdezések sikertelenek",
|
||||
"trust_os_hint": "Használja a systemd-timesyncd/chrony állapotot, ha az UDP NTP blokkolva van.",
|
||||
"save": "NTP beállítások mentése",
|
||||
"saved": "Óra beállítások mentve. Go szerver újraindítás elindítva.",
|
||||
"save_restart": "Mentse az NTP-t és indítsa újra a Go szervert? Az aktív munkamenetek rövid ideig megszakadhatnak.",
|
||||
"status_offset": "Eltérés",
|
||||
"status_server": "NTP forrás",
|
||||
"status_os": "OS óra szinkronizálva",
|
||||
"status_error": "Utolsó hiba",
|
||||
"os_yes": "Igen",
|
||||
"os_no": "Nem"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktív számlázható munkamenetek"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Waktu server (NTP)",
|
||||
"check_now": "Periksa sekarang",
|
||||
"unsynced": "Jam server tidak tersinkron — sesi berbayar dapat diblokir"
|
||||
"unsynced": "Jam server tidak tersinkron — sesi berbayar dapat diblokir",
|
||||
"synced_detail": "Jam disinkronkan untuk penagihan",
|
||||
"ntp_servers": "Server NTP",
|
||||
"ntp_servers_hint": "Nama host atau IP dipisah koma. Berlaku untuk server Go (betterdesk-server).",
|
||||
"max_skew_ms": "Selisih jam maksimum (ms)",
|
||||
"require_sync": "Wajib jam tersinkron untuk sesi berbayar",
|
||||
"trust_os": "Percayai NTP OS jika kueri langsung gagal",
|
||||
"trust_os_hint": "Gunakan status systemd-timesyncd/chrony saat UDP NTP diblokir (DMZ/firewall).",
|
||||
"save": "Simpan pengaturan NTP",
|
||||
"saved": "Pengaturan jam disimpan. Restart server Go dimulai.",
|
||||
"save_restart": "Simpan NTP dan restart server Go? Sesi aktif mungkin terputus sebentar.",
|
||||
"status_offset": "Offset",
|
||||
"status_server": "Sumber NTP",
|
||||
"status_os": "Jam OS tersinkron",
|
||||
"status_error": "Kesalahan terakhir",
|
||||
"os_yes": "Ya",
|
||||
"os_no": "Tidak"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Sesi berbayar aktif"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Ora server (NTP)",
|
||||
"check_now": "Controlla ora",
|
||||
"unsynced": "L'orologio del server non è sincronizzato — le sessioni fatturabili possono essere bloccate"
|
||||
"unsynced": "L'orologio del server non è sincronizzato — le sessioni fatturabili possono essere bloccate",
|
||||
"synced_detail": "Orologio sincronizzato per la fatturazione",
|
||||
"ntp_servers": "Server NTP",
|
||||
"ntp_servers_hint": "Hostname o IP separati da virgola. Si applica al server Go (betterdesk-server).",
|
||||
"max_skew_ms": "Scostamento orario massimo (ms)",
|
||||
"require_sync": "Richiedi orologio sincronizzato per sessioni fatturabili",
|
||||
"trust_os": "Affidati all'NTP del SO se le query dirette falliscono",
|
||||
"trust_os_hint": "Usa lo stato systemd-timesyncd/chrony quando l'UDP NTP è bloccato (DMZ/firewall).",
|
||||
"save": "Salva impostazioni NTP",
|
||||
"saved": "Impostazioni orologio salvate. Riavvio server Go avviato.",
|
||||
"save_restart": "Salvare NTP e riavviare il server Go? Le sessioni attive possono disconnettersi brevemente.",
|
||||
"status_offset": "Scostamento",
|
||||
"status_server": "Fonte NTP",
|
||||
"status_os": "Orologio OS sincronizzato",
|
||||
"status_error": "Ultimo errore",
|
||||
"os_yes": "Sì",
|
||||
"os_no": "No"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Sessioni fatturabili attive"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "サーバー時刻 (NTP)",
|
||||
"check_now": "今すぐ確認",
|
||||
"unsynced": "サーバー時計が同期されていません — 課金セッションがブロックされる場合があります"
|
||||
"unsynced": "サーバー時計が同期されていません — 課金セッションがブロックされる場合があります",
|
||||
"synced_detail": "課金用に時計が同期されています",
|
||||
"ntp_servers": "NTPサーバー",
|
||||
"ntp_servers_hint": "カンマ区切りのホスト名またはIP。Goサーバー(betterdesk-server)に適用されます。",
|
||||
"max_skew_ms": "最大時刻ずれ (ms)",
|
||||
"require_sync": "課金セッションに同期時計を必須にする",
|
||||
"trust_os": "直接クエリ失敗時はOS NTPを信頼",
|
||||
"trust_os_hint": "UDP NTPがブロックされている場合は systemd-timesyncd/chrony の状態を使用。",
|
||||
"save": "NTP設定を保存",
|
||||
"saved": "時計設定を保存しました。Goサーバーの再起動を開始しました。",
|
||||
"save_restart": "NTPを保存してGoサーバーを再起動しますか?アクティブなセッションが一時的に切断される場合があります。",
|
||||
"status_offset": "オフセット",
|
||||
"status_server": "NTPソース",
|
||||
"status_os": "OS時計同期",
|
||||
"status_error": "最後のエラー",
|
||||
"os_yes": "はい",
|
||||
"os_no": "いいえ"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "アクティブな課金セッション"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "서버 시간 (NTP)",
|
||||
"check_now": "지금 확인",
|
||||
"unsynced": "서버 시계가 동기화되지 않음 — 과금 세션이 차단될 수 있습니다"
|
||||
"unsynced": "서버 시계가 동기화되지 않음 — 과금 세션이 차단될 수 있습니다",
|
||||
"synced_detail": "과금을 위해 시계가 동기화됨",
|
||||
"ntp_servers": "NTP 서버",
|
||||
"ntp_servers_hint": "쉼표로 구분된 호스트명 또는 IP. Go 서버(betterdesk-server)에 적용됩니다.",
|
||||
"max_skew_ms": "최대 시계 오차 (ms)",
|
||||
"require_sync": "과금 세션에 동기화된 시계 필요",
|
||||
"trust_os": "직접 쿼리 실패 시 OS NTP 신뢰",
|
||||
"trust_os_hint": "UDP NTP가 차단된 경우 systemd-timesyncd/chrony 상태 사용.",
|
||||
"save": "NTP 설정 저장",
|
||||
"saved": "시계 설정이 저장되었습니다. Go 서버 재시작이 시작되었습니다.",
|
||||
"save_restart": "NTP를 저장하고 Go 서버를 재시작하시겠습니까? 활성 세션이 잠시 끊길 수 있습니다.",
|
||||
"status_offset": "오프셋",
|
||||
"status_server": "NTP 소스",
|
||||
"status_os": "OS 시계 동기화",
|
||||
"status_error": "마지막 오류",
|
||||
"os_yes": "예",
|
||||
"os_no": "아니오"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "활성 과금 세션"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Servertid (NTP)",
|
||||
"check_now": "Sjekk nå",
|
||||
"unsynced": "Serverklokken er ikke synkronisert — fakturerbare økter kan bli blokkert"
|
||||
"unsynced": "Serverklokken er ikke synkronisert — fakturerbare økter kan bli blokkert",
|
||||
"synced_detail": "Klokke synkronisert for fakturering",
|
||||
"ntp_servers": "NTP-servere",
|
||||
"ntp_servers_hint": "Kommaseparerte vertsnavn eller IP. Gjelder Go-serveren (betterdesk-server).",
|
||||
"max_skew_ms": "Maks klokkeavvik (ms)",
|
||||
"require_sync": "Krev synkronisert klokke for fakturerbare økter",
|
||||
"trust_os": "Stol på OS NTP når direkte forespørsler feiler",
|
||||
"trust_os_hint": "Bruk systemd-timesyncd/chrony-status når UDP NTP er blokkert.",
|
||||
"save": "Lagre NTP-innstillinger",
|
||||
"saved": "Klokkeinnstillinger lagret. Omstart av Go-server startet.",
|
||||
"save_restart": "Lagre NTP og start Go-server på nytt? Aktive økter kan kort kobles fra.",
|
||||
"status_offset": "Avvik",
|
||||
"status_server": "NTP-kilde",
|
||||
"status_os": "OS-klokke synkronisert",
|
||||
"status_error": "Siste feil",
|
||||
"os_yes": "Ja",
|
||||
"os_no": "Nei"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktive fakturerbare økter"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Servertijd (NTP)",
|
||||
"check_now": "Nu controleren",
|
||||
"unsynced": "Serverklok niet gesynchroniseerd — factureerbare sessies kunnen worden geblokkeerd"
|
||||
"unsynced": "Serverklok niet gesynchroniseerd — factureerbare sessies kunnen worden geblokkeerd",
|
||||
"synced_detail": "Klok gesynchroniseerd voor facturering",
|
||||
"ntp_servers": "NTP-servers",
|
||||
"ntp_servers_hint": "Kommagescheiden hostnamen of IP's. Geldt voor de Go-server (betterdesk-server).",
|
||||
"max_skew_ms": "Maximale klokafwijking (ms)",
|
||||
"require_sync": "Gesynchroniseerde klok vereisen voor factureerbare sessies",
|
||||
"trust_os": "Vertrouw OS NTP wanneer directe queries falen",
|
||||
"trust_os_hint": "Gebruik systemd-timesyncd/chrony-status wanneer UDP NTP geblokkeerd is.",
|
||||
"save": "NTP-instellingen opslaan",
|
||||
"saved": "Klokinstellingen opgeslagen. Herstart Go-server gestart.",
|
||||
"save_restart": "NTP opslaan en Go-server herstarten? Actieve sessies kunnen kort verbreken.",
|
||||
"status_offset": "Offset",
|
||||
"status_server": "NTP-bron",
|
||||
"status_os": "OS-klok gesynchroniseerd",
|
||||
"status_error": "Laatste fout",
|
||||
"os_yes": "Ja",
|
||||
"os_no": "Nee"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Actieve factureerbare sessies"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Czas serwera (NTP)",
|
||||
"check_now": "Sprawdź teraz",
|
||||
"unsynced": "Zegar serwera nie jest zsynchronizowany — sesje rozliczeniowe mogą być zablokowane"
|
||||
"unsynced": "Zegar serwera nie jest zsynchronizowany — sesje rozliczeniowe mogą być zablokowane",
|
||||
"synced_detail": "Zegar zsynchronizowany do rozliczeń",
|
||||
"ntp_servers": "Serwery NTP",
|
||||
"ntp_servers_hint": "Nazwy hostów lub IP oddzielone przecinkami. Dotyczy serwera Go (betterdesk-server).",
|
||||
"max_skew_ms": "Maksymalne odchylenie zegara (ms)",
|
||||
"require_sync": "Wymagaj zsynchronizowanego zegara dla sesji płatnych",
|
||||
"trust_os": "Ufaj NTP systemu, gdy bezpośrednie zapytania zawiodą",
|
||||
"trust_os_hint": "Użyj statusu systemd-timesyncd/chrony, gdy UDP NTP jest blokowane (DMZ/firewall).",
|
||||
"save": "Zapisz ustawienia NTP",
|
||||
"saved": "Ustawienia zegara zapisane. Uruchomiono restart serwera Go.",
|
||||
"save_restart": "Zapisać NTP i zrestartować serwer Go? Aktywne sesje mogą się chwilowo rozłączyć.",
|
||||
"status_offset": "Odchylenie",
|
||||
"status_server": "Źródło NTP",
|
||||
"status_os": "Zegar OS zsynchronizowany",
|
||||
"status_error": "Ostatni błąd",
|
||||
"os_yes": "Tak",
|
||||
"os_no": "Nie"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktywne sesje rozliczeniowe"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "Hora do servidor (NTP)",
|
||||
"check_now": "Verificar agora",
|
||||
"unsynced": "Relógio do servidor não sincronizado — sessões faturáveis podem ser bloqueadas"
|
||||
"unsynced": "Relógio do servidor não sincronizado — sessões faturáveis podem ser bloqueadas",
|
||||
"synced_detail": "Relógio sincronizado para faturação",
|
||||
"ntp_servers": "Servidores NTP",
|
||||
"ntp_servers_hint": "Nomes de host ou IP separados por vírgula. Aplica-se ao servidor Go (betterdesk-server).",
|
||||
"max_skew_ms": "Desvio máximo do relógio (ms)",
|
||||
"require_sync": "Exigir relógio sincronizado para sessões faturáveis",
|
||||
"trust_os": "Confiar no NTP do SO quando consultas diretas falharem",
|
||||
"trust_os_hint": "Usar estado systemd-timesyncd/chrony quando UDP NTP estiver bloqueado.",
|
||||
"save": "Guardar definições NTP",
|
||||
"saved": "Definições de relógio guardadas. Reinício do servidor Go iniciado.",
|
||||
"save_restart": "Guardar NTP e reiniciar o servidor Go? Sessões ativas podem desligar-se brevemente.",
|
||||
"status_offset": "Desvio",
|
||||
"status_server": "Fonte NTP",
|
||||
"status_os": "Relógio do SO sincronizado",
|
||||
"status_error": "Último erro",
|
||||
"os_yes": "Sim",
|
||||
"os_no": "Não"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Sessões faturáveis ativas"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Ora serverului (NTP)",
|
||||
"check_now": "Verifică acum",
|
||||
"unsynced": "Ceasul serverului nu este sincronizat — sesiunile facturabile pot fi blocate"
|
||||
"unsynced": "Ceasul serverului nu este sincronizat — sesiunile facturabile pot fi blocate",
|
||||
"synced_detail": "Ceas sincronizat pentru facturare",
|
||||
"ntp_servers": "Servere NTP",
|
||||
"ntp_servers_hint": "Nume gazdă sau IP separate prin virgulă. Se aplică serverului Go (betterdesk-server).",
|
||||
"max_skew_ms": "Abatere maximă ceas (ms)",
|
||||
"require_sync": "Cere ceas sincronizat pentru sesiuni facturabile",
|
||||
"trust_os": "Ai încredere în NTP OS când interogările directe eșuează",
|
||||
"trust_os_hint": "Folosește starea systemd-timesyncd/chrony când UDP NTP este blocat.",
|
||||
"save": "Salvează setările NTP",
|
||||
"saved": "Setările ceasului salvate. Repornire server Go inițiată.",
|
||||
"save_restart": "Salvezi NTP și repornești serverul Go? Sesiunile active se pot deconecta scurt.",
|
||||
"status_offset": "Abatere",
|
||||
"status_server": "Sursă NTP",
|
||||
"status_os": "Ceas OS sincronizat",
|
||||
"status_error": "Ultima eroare",
|
||||
"os_yes": "Da",
|
||||
"os_no": "Nu"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Sesiuni facturabile active"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Servertid (NTP)",
|
||||
"check_now": "Kontrollera nu",
|
||||
"unsynced": "Serverklockan är inte synkroniserad — fakturerbara sessioner kan blockeras"
|
||||
"unsynced": "Serverklockan är inte synkroniserad — fakturerbara sessioner kan blockeras",
|
||||
"synced_detail": "Klocka synkroniserad för fakturering",
|
||||
"ntp_servers": "NTP-servrar",
|
||||
"ntp_servers_hint": "Kommaseparerade värdnamn eller IP. Gäller Go-servern (betterdesk-server).",
|
||||
"max_skew_ms": "Max klockavvikelse (ms)",
|
||||
"require_sync": "Kräv synkad klocka för fakturerbara sessioner",
|
||||
"trust_os": "Lita på OS NTP när direkta frågor misslyckas",
|
||||
"trust_os_hint": "Använd systemd-timesyncd/chrony-status när UDP NTP blockeras.",
|
||||
"save": "Spara NTP-inställningar",
|
||||
"saved": "Klockinställningar sparade. Omstart av Go-server startad.",
|
||||
"save_restart": "Spara NTP och starta om Go-servern? Aktiva sessioner kan kopplas från kort.",
|
||||
"status_offset": "Avvikelse",
|
||||
"status_server": "NTP-källa",
|
||||
"status_os": "OS-klocka synkad",
|
||||
"status_error": "Senaste fel",
|
||||
"os_yes": "Ja",
|
||||
"os_no": "Nej"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktiva fakturerbara sessioner"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "เวลาเซิร์ฟเวอร์ (NTP)",
|
||||
"check_now": "ตรวจสอบตอนนี้",
|
||||
"unsynced": "นาฬิกาเซิร์ฟเวอร์ไม่ซิงค์ — เซสชันที่เรียกเก็บเงินอาจถูกบล็อก"
|
||||
"unsynced": "นาฬิกาเซิร์ฟเวอร์ไม่ซิงค์ — เซสชันที่เรียกเก็บเงินอาจถูกบล็อก",
|
||||
"synced_detail": "นาฬิกาซิงค์สำหรับการเรียกเก็บเงิน",
|
||||
"ntp_servers": "เซิร์ฟเวอร์ NTP",
|
||||
"ntp_servers_hint": "ชื่อโฮสต์หรือ IP คั่นด้วยจุลภาค ใช้กับเซิร์ฟเวอร์ Go (betterdesk-server)",
|
||||
"max_skew_ms": "ความคลาดเคลื่อนสูงสุดของนาฬิกา (ms)",
|
||||
"require_sync": "ต้องมีนาฬิกาซิงค์สำหรับเซสชันที่เรียกเก็บเงิน",
|
||||
"trust_os": "เชื่อถือ NTP ของ OS เมื่อการสอบถามโดยตรงล้มเหลว",
|
||||
"trust_os_hint": "ใช้สถานะ systemd-timesyncd/chrony เมื่อ UDP NTP ถูกบล็อก",
|
||||
"save": "บันทึกการตั้งค่า NTP",
|
||||
"saved": "บันทึกการตั้งค่านาฬิกาแล้ว เริ่มรีสตาร์ทเซิร์ฟเวอร์ Go",
|
||||
"save_restart": "บันทึก NTP และรีสตาร์ทเซิร์ฟเวอร์ Go? เซสชันที่ใช้งานอาจหลุดชั่วคราว",
|
||||
"status_offset": "ออฟเซ็ต",
|
||||
"status_server": "แหล่ง NTP",
|
||||
"status_os": "นาฬิกา OS ซิงค์",
|
||||
"status_error": "ข้อผิดพลาดล่าสุด",
|
||||
"os_yes": "ใช่",
|
||||
"os_no": "ไม่"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "เซสชันที่เรียกเก็บเงินที่ใช้งานอยู่"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Sunucu saati (NTP)",
|
||||
"check_now": "Şimdi kontrol et",
|
||||
"unsynced": "Sunucu saati senkronize değil — faturalandırılabilir oturumlar engellenebilir"
|
||||
"unsynced": "Sunucu saati senkronize değil — faturalandırılabilir oturumlar engellenebilir",
|
||||
"synced_detail": "Faturalandırma için saat senkronize",
|
||||
"ntp_servers": "NTP sunucuları",
|
||||
"ntp_servers_hint": "Virgülle ayrılmış ana bilgisayar adları veya IP. Go sunucusuna (betterdesk-server) uygulanır.",
|
||||
"max_skew_ms": "Maksimum saat sapması (ms)",
|
||||
"require_sync": "Faturalandırılabilir oturumlar için senkron saat gerekli",
|
||||
"trust_os": "Doğrudan sorgular başarısız olursa OS NTP'ye güven",
|
||||
"trust_os_hint": "UDP NTP engellendiğinde systemd-timesyncd/chrony durumunu kullanın.",
|
||||
"save": "NTP ayarlarını kaydet",
|
||||
"saved": "Saat ayarları kaydedildi. Go sunucusu yeniden başlatılıyor.",
|
||||
"save_restart": "NTP kaydedilsin ve Go sunucusu yeniden başlatılsın mı? Aktif oturumlar kısa süre kesilebilir.",
|
||||
"status_offset": "Sapma",
|
||||
"status_server": "NTP kaynağı",
|
||||
"status_os": "OS saati senkron",
|
||||
"status_error": "Son hata",
|
||||
"os_yes": "Evet",
|
||||
"os_no": "Hayır"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Aktif faturalandırılabilir oturumlar"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Час сервера (NTP)",
|
||||
"check_now": "Перевірити зараз",
|
||||
"unsynced": "Годинник сервера не синхронізовано — платні сесії можуть бути заблоковані"
|
||||
"unsynced": "Годинник сервера не синхронізовано — платні сесії можуть бути заблоковані",
|
||||
"synced_detail": "Годинник синхронізовано для білінгу",
|
||||
"ntp_servers": "NTP-сервери",
|
||||
"ntp_servers_hint": "Імена хостів або IP через кому. Застосовується до Go-сервера (betterdesk-server).",
|
||||
"max_skew_ms": "Максимальне відхилення годинника (мс)",
|
||||
"require_sync": "Вимагати синхронізований годинник для платних сесій",
|
||||
"trust_os": "Довіряти OS NTP, якщо прямі запити не вдаються",
|
||||
"trust_os_hint": "Використовуйте статус systemd-timesyncd/chrony, коли UDP NTP заблоковано.",
|
||||
"save": "Зберегти налаштування NTP",
|
||||
"saved": "Налаштування годинника збережено. Перезапуск Go-сервера розпочато.",
|
||||
"save_restart": "Зберегти NTP і перезапустити Go-сервер? Активні сесії можуть коротко відключитися.",
|
||||
"status_offset": "Відхилення",
|
||||
"status_server": "Джерело NTP",
|
||||
"status_os": "Годинник OS синхронізовано",
|
||||
"status_error": "Остання помилка",
|
||||
"os_yes": "Так",
|
||||
"os_no": "Ні"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Активні платні сесії"
|
||||
|
||||
+17
-1
@@ -3834,7 +3834,23 @@
|
||||
"clock": {
|
||||
"title": "Giờ máy chủ (NTP)",
|
||||
"check_now": "Kiểm tra ngay",
|
||||
"unsynced": "Đồng hồ máy chủ chưa đồng bộ — phiên tính phí có thể bị chặn"
|
||||
"unsynced": "Đồng hồ máy chủ chưa đồng bộ — phiên tính phí có thể bị chặn",
|
||||
"synced_detail": "Đồng hồ đã đồng bộ cho tính phí",
|
||||
"ntp_servers": "Máy chủ NTP",
|
||||
"ntp_servers_hint": "Tên máy chủ hoặc IP cách nhau bằng dấu phẩy. Áp dụng cho máy chủ Go (betterdesk-server).",
|
||||
"max_skew_ms": "Độ lệch đồng hồ tối đa (ms)",
|
||||
"require_sync": "Yêu cầu đồng hồ đồng bộ cho phiên tính phí",
|
||||
"trust_os": "Tin cậy NTP OS khi truy vấn trực tiếp thất bại",
|
||||
"trust_os_hint": "Dùng trạng thái systemd-timesyncd/chrony khi UDP NTP bị chặn.",
|
||||
"save": "Lưu cài đặt NTP",
|
||||
"saved": "Đã lưu cài đặt đồng hồ. Đang khởi động lại máy chủ Go.",
|
||||
"save_restart": "Lưu NTP và khởi động lại máy chủ Go? Phiên đang hoạt động có thể ngắt tạm thời.",
|
||||
"status_offset": "Độ lệch",
|
||||
"status_server": "Nguồn NTP",
|
||||
"status_os": "Đồng hồ OS đã đồng bộ",
|
||||
"status_error": "Lỗi cuối",
|
||||
"os_yes": "Có",
|
||||
"os_no": "Không"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "Phiên tính phí đang hoạt động"
|
||||
|
||||
@@ -3832,7 +3832,23 @@
|
||||
"clock": {
|
||||
"title": "服務器時間 (NTP)",
|
||||
"check_now": "立即檢查",
|
||||
"unsynced": "服務器時鐘未同步 — 可計費會話可能被阻止"
|
||||
"unsynced": "服務器時鐘未同步 — 可計費會話可能被阻止",
|
||||
"synced_detail": "時鐘已同步,可用於計費",
|
||||
"ntp_servers": "NTP 伺服器",
|
||||
"ntp_servers_hint": "逗號分隔的主機名稱或 IP。套用至 Go 伺服器(betterdesk-server)。",
|
||||
"max_skew_ms": "最大時鐘偏差 (ms)",
|
||||
"require_sync": "可計費工作階段要求時鐘同步",
|
||||
"trust_os": "直接查詢失敗時信任 OS NTP",
|
||||
"trust_os_hint": "UDP NTP 被阻擋時使用 systemd-timesyncd/chrony 狀態。",
|
||||
"save": "儲存 NTP 設定",
|
||||
"saved": "時鐘設定已儲存。已啟動 Go 伺服器重新啟動。",
|
||||
"save_restart": "儲存 NTP 並重新啟動 Go 伺服器?使用中工作階段可能短暫中斷。",
|
||||
"status_offset": "偏移",
|
||||
"status_server": "NTP 來源",
|
||||
"status_os": "OS 時鐘已同步",
|
||||
"status_error": "最後錯誤",
|
||||
"os_yes": "是",
|
||||
"os_no": "否"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "活動可計費會話"
|
||||
|
||||
+17
-1
@@ -3828,7 +3828,23 @@
|
||||
"clock": {
|
||||
"title": "服务器时间 (NTP)",
|
||||
"check_now": "立即检查",
|
||||
"unsynced": "服务器时钟未同步 — 可计费会话可能被阻止"
|
||||
"unsynced": "服务器时钟未同步 — 可计费会话可能被阻止",
|
||||
"synced_detail": "时钟已同步,可用于计费",
|
||||
"ntp_servers": "NTP 服务器",
|
||||
"ntp_servers_hint": "逗号分隔的主机名或 IP。应用于 Go 服务器(betterdesk-server)。",
|
||||
"max_skew_ms": "最大时钟偏差 (ms)",
|
||||
"require_sync": "可计费会话要求时钟同步",
|
||||
"trust_os": "直接查询失败时信任 OS NTP",
|
||||
"trust_os_hint": "UDP NTP 被阻止时使用 systemd-timesyncd/chrony 状态。",
|
||||
"save": "保存 NTP 设置",
|
||||
"saved": "时钟设置已保存。已启动 Go 服务器重启。",
|
||||
"save_restart": "保存 NTP 并重启 Go 服务器?活动会话可能短暂断开。",
|
||||
"status_offset": "偏移",
|
||||
"status_server": "NTP 来源",
|
||||
"status_os": "OS 时钟已同步",
|
||||
"status_error": "最后错误",
|
||||
"os_yes": "是",
|
||||
"os_no": "否"
|
||||
},
|
||||
"stats": {
|
||||
"active_sessions": "活动可计费会话"
|
||||
|
||||
@@ -112,20 +112,51 @@
|
||||
const statusEl = document.getElementById('clock-status');
|
||||
const offsetEl = document.getElementById('clock-offset');
|
||||
const detail = document.getElementById('settings-clock-detail');
|
||||
const ntpServerEl = document.getElementById('clock-ntp-server');
|
||||
const osSyncEl = document.getElementById('clock-os-sync');
|
||||
const lastErrorEl = document.getElementById('clock-last-error');
|
||||
const synced = !!st.synced;
|
||||
if (statusEl) statusEl.textContent = synced ? 'OK' : 'WARN';
|
||||
const offset = `${st.offset_ms || 0} ms`;
|
||||
if (offsetEl) offsetEl.textContent = offset;
|
||||
if (detail) detail.textContent = `${synced ? 'Synced' : 'Not synced'} · offset ${offset}`;
|
||||
if (ntpServerEl) ntpServerEl.textContent = st.ntp_server || '—';
|
||||
if (osSyncEl) {
|
||||
if (st.os_clock_synced === true) osSyncEl.textContent = t('commercialization.clock.os_yes', 'Yes');
|
||||
else if (st.os_clock_synced === false) osSyncEl.textContent = t('commercialization.clock.os_no', 'No');
|
||||
else osSyncEl.textContent = '—';
|
||||
}
|
||||
if (lastErrorEl) lastErrorEl.textContent = st.last_error || '—';
|
||||
if (detail) {
|
||||
detail.textContent = synced
|
||||
? t('commercialization.clock.synced_detail', 'Clock synchronized for billing')
|
||||
: t('commercialization.clock.unsynced', 'Server clock is not synchronized — billable sessions may be blocked');
|
||||
}
|
||||
if (banner) {
|
||||
banner.classList.toggle('hidden', synced);
|
||||
banner.textContent = synced ? '' : t('commercialization.clock.unsynced', 'Clock not synchronized');
|
||||
banner.textContent = synced ? '' : t('commercialization.clock.unsynced', 'Server clock is not synchronized — billable sessions may be blocked');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[commercialization] timesync', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClockSettings() {
|
||||
try {
|
||||
const data = await api('/api/panel/billing/clock/settings');
|
||||
const settings = data.settings || {};
|
||||
const serversEl = document.getElementById('clock-ntp-servers');
|
||||
const skewEl = document.getElementById('clock-max-skew');
|
||||
const requireEl = document.getElementById('clock-require-sync');
|
||||
const trustEl = document.getElementById('clock-trust-os');
|
||||
if (serversEl) serversEl.value = settings.ntp_servers || '';
|
||||
if (skewEl) skewEl.value = settings.max_skew_ms || 2000;
|
||||
if (requireEl) requireEl.checked = settings.require_synced_clock !== false;
|
||||
if (trustEl) trustEl.checked = settings.trust_os_ntp !== false;
|
||||
} catch (e) {
|
||||
console.warn('[commercialization] clock settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPackages() {
|
||||
const tbody = document.querySelector('#packages-table tbody');
|
||||
if (!tbody) return [];
|
||||
@@ -341,6 +372,29 @@
|
||||
await loadTimesync();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-clock-settings')?.addEventListener('click', async () => {
|
||||
const ok = window.confirm(t(
|
||||
'commercialization.clock.save_restart',
|
||||
'Save NTP settings and restart the BetterDesk Go server? Active sessions may disconnect briefly.'
|
||||
));
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api('/api/panel/billing/clock/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
ntp_servers: document.getElementById('clock-ntp-servers')?.value || '',
|
||||
max_skew_ms: Number(document.getElementById('clock-max-skew')?.value || 2000),
|
||||
require_synced_clock: document.getElementById('clock-require-sync')?.checked !== false,
|
||||
trust_os_ntp: document.getElementById('clock-trust-os')?.checked !== false,
|
||||
})
|
||||
});
|
||||
alert(t('commercialization.clock.saved', 'Clock settings saved. Go server restart initiated.'));
|
||||
await loadTimesync();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-new-package')?.addEventListener('click', () => {
|
||||
openPackageModal().catch((e) => alert(e.message));
|
||||
});
|
||||
@@ -417,6 +471,7 @@
|
||||
}
|
||||
if (tab === 'settings') {
|
||||
loadEmailNotificationSettings().catch(console.warn);
|
||||
loadClockSettings().catch(console.warn);
|
||||
}
|
||||
if (tab === 'overview' || tab === 'sessions') {
|
||||
loadSessions().catch(console.warn);
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
CONFIG_KEY,
|
||||
} = require('../services/helpRequestEmailService');
|
||||
const { getSmtpSettings } = require('../lib/smtpSettingsHandlers');
|
||||
const billingClockConfig = require('../services/billingClockConfigService');
|
||||
|
||||
router.get('/commercialization', requireAuth, requirePermission('billing.view'), (req, res) => {
|
||||
const validTabs = ['overview', 'packages', 'sessions', 'reports', 'settings'];
|
||||
@@ -39,6 +40,37 @@ router.post('/api/panel/billing/timesync/check', requireAuth, requirePermission(
|
||||
proxyToGo(apiClient, req, res, 'POST', '/timesync/check');
|
||||
});
|
||||
|
||||
router.get('/api/panel/billing/clock/settings', requireAuth, requirePermission('billing.view'), (req, res) => {
|
||||
try {
|
||||
res.json({ success: true, settings: billingClockConfig.getClockSettings() });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/api/panel/billing/clock/settings', requireAuth, requirePermission('server.config'), async (req, res) => {
|
||||
try {
|
||||
const result = await billingClockConfig.saveClockSettings(req.body || {}, { restart: true });
|
||||
try {
|
||||
await db.logAction(
|
||||
req.session.userId,
|
||||
'billing_clock_settings_updated',
|
||||
'Updated billing/NTP clock settings',
|
||||
req.ip
|
||||
);
|
||||
} catch (_) {}
|
||||
res.json({
|
||||
success: true,
|
||||
settings: result.settings,
|
||||
serviceConfig: result.serviceConfig,
|
||||
restart: result.restart,
|
||||
});
|
||||
} catch (err) {
|
||||
const code = ['invalid_ntp_servers', 'invalid_max_skew'].includes(err.message) ? 400 : 500;
|
||||
res.status(code).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/panel/billing/packages', requireAuth, requirePermission('billing.view'), (req, res) => {
|
||||
proxyToGo(apiClient, req, res, 'GET', '/billing/packages');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const langDir = path.join(__dirname, '..', 'lang');
|
||||
const locales = fs.readdirSync(langDir).filter((f) => f.endsWith('.json'));
|
||||
|
||||
const patches = {
|
||||
ar: {
|
||||
synced_detail: 'الساعة متزامنة للفوترة',
|
||||
ntp_servers: 'خوادم NTP',
|
||||
ntp_servers_hint: 'أسماء مضيفة أو عناوين IP مفصولة بفواصل. تُطبَّق على خادم Go (betterdesk-server).',
|
||||
max_skew_ms: 'أقصى انحراف للساعة (مللي ثانية)',
|
||||
require_sync: 'اشتراط ساعة متزامنة للجلسات القابلة للفوترة',
|
||||
trust_os: 'الثقة بـ NTP للنظام عند فشل الاستعلامات المباشرة',
|
||||
trust_os_hint: 'استخدم حالة systemd-timesyncd/chrony عند حظر UDP NTP (DMZ/جدار ناري).',
|
||||
save: 'حفظ إعدادات NTP',
|
||||
saved: 'تم حفظ إعدادات الساعة. بدء إعادة تشغيل خادم Go.',
|
||||
save_restart: 'حفظ إعدادات NTP وإعادة تشغيل خادم Go؟ قد تنقطع الجلسات النشطة briefly.',
|
||||
status_offset: 'الانحراف',
|
||||
status_server: 'مصدر NTP',
|
||||
status_os: 'ساعة OS متزامنة',
|
||||
status_error: 'آخر خطأ',
|
||||
os_yes: 'نعم',
|
||||
os_no: 'لا',
|
||||
},
|
||||
cs: {
|
||||
synced_detail: 'Hodiny synchronizovány pro fakturaci',
|
||||
ntp_servers: 'NTP servery',
|
||||
ntp_servers_hint: 'Názvy hostitelů nebo IP oddělené čárkou. Platí pro Go server (betterdesk-server).',
|
||||
max_skew_ms: 'Maximální odchylka hodin (ms)',
|
||||
require_sync: 'Vyžadovat synchronizované hodiny pro fakturovatelné relace',
|
||||
trust_os: 'Důvěřovat OS NTP při selhání přímých dotazů',
|
||||
trust_os_hint: 'Použít stav systemd-timesyncd/chrony, když je UDP NTP blokováno (DMZ/firewall).',
|
||||
save: 'Uložit nastavení NTP',
|
||||
saved: 'Nastavení hodin uloženo. Restart Go serveru spuštěn.',
|
||||
save_restart: 'Uložit NTP a restartovat Go server? Aktivní relace se mohou krátce odpojit.',
|
||||
status_offset: 'Odchylka',
|
||||
status_server: 'Zdroj NTP',
|
||||
status_os: 'OS hodiny synchronizovány',
|
||||
status_error: 'Poslední chyba',
|
||||
os_yes: 'Ano',
|
||||
os_no: 'Ne',
|
||||
},
|
||||
da: {
|
||||
synced_detail: 'Ur synkroniseret til fakturering',
|
||||
ntp_servers: 'NTP-servere',
|
||||
ntp_servers_hint: 'Kommaseparerede værtsnavne eller IP. Gælder Go-serveren (betterdesk-server).',
|
||||
max_skew_ms: 'Maks. urafvigelse (ms)',
|
||||
require_sync: 'Kræv synkroniseret ur for fakturerbare sessioner',
|
||||
trust_os: 'Stol på OS NTP når direkte forespørgsler fejler',
|
||||
trust_os_hint: 'Brug systemd-timesyncd/chrony-status når UDP NTP er blokeret (DMZ/firewall).',
|
||||
save: 'Gem NTP-indstillinger',
|
||||
saved: 'Urindstillinger gemt. Genstart af Go-server startet.',
|
||||
save_restart: 'Gem NTP og genstart Go-serveren? Aktive sessioner kan kort afbrydes.',
|
||||
status_offset: 'Afvigelse',
|
||||
status_server: 'NTP-kilde',
|
||||
status_os: 'OS-ur synkroniseret',
|
||||
status_error: 'Seneste fejl',
|
||||
os_yes: 'Ja',
|
||||
os_no: 'Nej',
|
||||
},
|
||||
de: {
|
||||
synced_detail: 'Uhr für Abrechnung synchronisiert',
|
||||
ntp_servers: 'NTP-Server',
|
||||
ntp_servers_hint: 'Kommagetrennte Hostnamen oder IPs. Gilt für den Go-Server (betterdesk-server).',
|
||||
max_skew_ms: 'Maximale Uhrabweichung (ms)',
|
||||
require_sync: 'Synchronisierte Uhr für abrechenbare Sitzungen erforderlich',
|
||||
trust_os: 'OS-NTP vertrauen, wenn direkte Abfragen fehlschlagen',
|
||||
trust_os_hint: 'systemd-timesyncd/chrony-Status nutzen, wenn UDP-NTP blockiert ist (DMZ/Firewall).',
|
||||
save: 'NTP-Einstellungen speichern',
|
||||
saved: 'Uhreinstellungen gespeichert. Go-Server-Neustart gestartet.',
|
||||
save_restart: 'NTP speichern und Go-Server neu starten? Aktive Sitzungen können kurz trennen.',
|
||||
status_offset: 'Abweichung',
|
||||
status_server: 'NTP-Quelle',
|
||||
status_os: 'OS-Uhr synchronisiert',
|
||||
status_error: 'Letzter Fehler',
|
||||
os_yes: 'Ja',
|
||||
os_no: 'Nein',
|
||||
},
|
||||
es: {
|
||||
synced_detail: 'Reloj sincronizado para facturación',
|
||||
ntp_servers: 'Servidores NTP',
|
||||
ntp_servers_hint: 'Nombres de host o IP separados por comas. Aplica al servidor Go (betterdesk-server).',
|
||||
max_skew_ms: 'Desviación máxima del reloj (ms)',
|
||||
require_sync: 'Exigir reloj sincronizado para sesiones facturables',
|
||||
trust_os: 'Confiar en NTP del SO si fallan las consultas directas',
|
||||
trust_os_hint: 'Usar estado systemd-timesyncd/chrony cuando UDP NTP esté bloqueado (DMZ/firewall).',
|
||||
save: 'Guardar ajustes NTP',
|
||||
saved: 'Ajustes de reloj guardados. Reinicio del servidor Go iniciado.',
|
||||
save_restart: '¿Guardar NTP y reiniciar el servidor Go? Las sesiones activas pueden desconectarse brevemente.',
|
||||
status_offset: 'Desviación',
|
||||
status_server: 'Fuente NTP',
|
||||
status_os: 'Reloj del SO sincronizado',
|
||||
status_error: 'Último error',
|
||||
os_yes: 'Sí',
|
||||
os_no: 'No',
|
||||
},
|
||||
fi: {
|
||||
synced_detail: 'Kello synkronoitu laskutusta varten',
|
||||
ntp_servers: 'NTP-palvelimet',
|
||||
ntp_servers_hint: 'Pilkuilla erotetut isäntänimet tai IP:t. Koskee Go-palvelinta (betterdesk-server).',
|
||||
max_skew_ms: 'Suurin kelloero (ms)',
|
||||
require_sync: 'Vaadi synkronoitu kello laskutettaville istunnoille',
|
||||
trust_os: 'Luota OS NTP:hen kun suorat kyselyt epäonnistuvat',
|
||||
trust_os_hint: 'Käytä systemd-timesyncd/chrony-tilaa kun UDP NTP on estetty (DMZ/palomuuri).',
|
||||
save: 'Tallenna NTP-asetukset',
|
||||
saved: 'Kelloasetukset tallennettu. Go-palvelimen uudelleenkäynnistys käynnistetty.',
|
||||
save_restart: 'Tallenna NTP ja käynnistä Go-palvelin uudelleen? Aktiiviset istunnot voivat katketa hetkeksi.',
|
||||
status_offset: 'Ero',
|
||||
status_server: 'NTP-lähde',
|
||||
status_os: 'OS-kello synkronoitu',
|
||||
status_error: 'Viimeisin virhe',
|
||||
os_yes: 'Kyllä',
|
||||
os_no: 'Ei',
|
||||
},
|
||||
fr: {
|
||||
synced_detail: 'Horloge synchronisée pour la facturation',
|
||||
ntp_servers: 'Serveurs NTP',
|
||||
ntp_servers_hint: 'Noms d\'hôte ou IP séparés par des virgules. Appliqué au serveur Go (betterdesk-server).',
|
||||
max_skew_ms: 'Décalage horaire max. (ms)',
|
||||
require_sync: 'Exiger une horloge synchronisée pour les sessions facturables',
|
||||
trust_os: 'Faire confiance au NTP OS si les requêtes directes échouent',
|
||||
trust_os_hint: 'Utiliser l\'état systemd-timesyncd/chrony quand l\'UDP NTP est bloqué (DMZ/pare-feu).',
|
||||
save: 'Enregistrer les paramètres NTP',
|
||||
saved: 'Paramètres d\'horloge enregistrés. Redémarrage du serveur Go lancé.',
|
||||
save_restart: 'Enregistrer le NTP et redémarrer le serveur Go ? Les sessions actives peuvent se déconnecter brièvement.',
|
||||
status_offset: 'Décalage',
|
||||
status_server: 'Source NTP',
|
||||
status_os: 'Horloge OS synchronisée',
|
||||
status_error: 'Dernière erreur',
|
||||
os_yes: 'Oui',
|
||||
os_no: 'Non',
|
||||
},
|
||||
hi: {
|
||||
synced_detail: 'बिलिंग के लिए घड़ी सिंक',
|
||||
ntp_servers: 'NTP सर्वर',
|
||||
ntp_servers_hint: 'अल्पविराम से अलग होस्टनाम या IP। Go सर्वर (betterdesk-server) पर लागू।',
|
||||
max_skew_ms: 'अधिकतम घड़ी विचलन (ms)',
|
||||
require_sync: 'बिल योग्य सत्रों के लिए सिंक घड़ी आवश्यक',
|
||||
trust_os: 'प्रत्यक्ष क्वेरी विफल होने पर OS NTP पर भरोसा करें',
|
||||
trust_os_hint: 'UDP NTP अवरुद्ध होने पर systemd-timesyncd/chrony स्थिति का उपयोग करें।',
|
||||
save: 'NTP सेटिंग्स सहेजें',
|
||||
saved: 'घड़ी सेटिंग्स सहेजी गईं। Go सर्वर पुनः आरंभ शुरू।',
|
||||
save_restart: 'NTP सहेजें और Go सर्वर पुनः आरंभ करें? सक्रिय सत्र briefly डिस्कनेक्ट हो सकते हैं।',
|
||||
status_offset: 'ऑफ़सेट',
|
||||
status_server: 'NTP स्रोत',
|
||||
status_os: 'OS घड़ी सिंक',
|
||||
status_error: 'अंतिम त्रुटि',
|
||||
os_yes: 'हाँ',
|
||||
os_no: 'नहीं',
|
||||
},
|
||||
hu: {
|
||||
synced_detail: 'Óra szinkronizálva számlázáshoz',
|
||||
ntp_servers: 'NTP szerverek',
|
||||
ntp_servers_hint: 'Vesszővel elválasztott hostnevek vagy IP-k. A Go szerverre (betterdesk-server) vonatkozik.',
|
||||
max_skew_ms: 'Max. óraeltérés (ms)',
|
||||
require_sync: 'Szinkronizált óra kötelező számlázható munkamenetekhez',
|
||||
trust_os: 'Bízzon az OS NTP-ben, ha a közvetlen lekérdezések sikertelenek',
|
||||
trust_os_hint: 'Használja a systemd-timesyncd/chrony állapotot, ha az UDP NTP blokkolva van.',
|
||||
save: 'NTP beállítások mentése',
|
||||
saved: 'Óra beállítások mentve. Go szerver újraindítás elindítva.',
|
||||
save_restart: 'Mentse az NTP-t és indítsa újra a Go szervert? Az aktív munkamenetek rövid ideig megszakadhatnak.',
|
||||
status_offset: 'Eltérés',
|
||||
status_server: 'NTP forrás',
|
||||
status_os: 'OS óra szinkronizálva',
|
||||
status_error: 'Utolsó hiba',
|
||||
os_yes: 'Igen',
|
||||
os_no: 'Nem',
|
||||
},
|
||||
id: {
|
||||
synced_detail: 'Jam disinkronkan untuk penagihan',
|
||||
ntp_servers: 'Server NTP',
|
||||
ntp_servers_hint: 'Nama host atau IP dipisah koma. Berlaku untuk server Go (betterdesk-server).',
|
||||
max_skew_ms: 'Selisih jam maksimum (ms)',
|
||||
require_sync: 'Wajib jam tersinkron untuk sesi berbayar',
|
||||
trust_os: 'Percayai NTP OS jika kueri langsung gagal',
|
||||
trust_os_hint: 'Gunakan status systemd-timesyncd/chrony saat UDP NTP diblokir (DMZ/firewall).',
|
||||
save: 'Simpan pengaturan NTP',
|
||||
saved: 'Pengaturan jam disimpan. Restart server Go dimulai.',
|
||||
save_restart: 'Simpan NTP dan restart server Go? Sesi aktif mungkin terputus sebentar.',
|
||||
status_offset: 'Offset',
|
||||
status_server: 'Sumber NTP',
|
||||
status_os: 'Jam OS tersinkron',
|
||||
status_error: 'Kesalahan terakhir',
|
||||
os_yes: 'Ya',
|
||||
os_no: 'Tidak',
|
||||
},
|
||||
it: {
|
||||
synced_detail: 'Orologio sincronizzato per la fatturazione',
|
||||
ntp_servers: 'Server NTP',
|
||||
ntp_servers_hint: 'Hostname o IP separati da virgola. Si applica al server Go (betterdesk-server).',
|
||||
max_skew_ms: 'Scostamento orario massimo (ms)',
|
||||
require_sync: 'Richiedi orologio sincronizzato per sessioni fatturabili',
|
||||
trust_os: 'Affidati all\'NTP del SO se le query dirette falliscono',
|
||||
trust_os_hint: 'Usa lo stato systemd-timesyncd/chrony quando l\'UDP NTP è bloccato (DMZ/firewall).',
|
||||
save: 'Salva impostazioni NTP',
|
||||
saved: 'Impostazioni orologio salvate. Riavvio server Go avviato.',
|
||||
save_restart: 'Salvare NTP e riavviare il server Go? Le sessioni attive possono disconnettersi brevemente.',
|
||||
status_offset: 'Scostamento',
|
||||
status_server: 'Fonte NTP',
|
||||
status_os: 'Orologio OS sincronizzato',
|
||||
status_error: 'Ultimo errore',
|
||||
os_yes: 'Sì',
|
||||
os_no: 'No',
|
||||
},
|
||||
ja: {
|
||||
synced_detail: '課金用に時計が同期されています',
|
||||
ntp_servers: 'NTPサーバー',
|
||||
ntp_servers_hint: 'カンマ区切りのホスト名またはIP。Goサーバー(betterdesk-server)に適用されます。',
|
||||
max_skew_ms: '最大時刻ずれ (ms)',
|
||||
require_sync: '課金セッションに同期時計を必須にする',
|
||||
trust_os: '直接クエリ失敗時はOS NTPを信頼',
|
||||
trust_os_hint: 'UDP NTPがブロックされている場合は systemd-timesyncd/chrony の状態を使用。',
|
||||
save: 'NTP設定を保存',
|
||||
saved: '時計設定を保存しました。Goサーバーの再起動を開始しました。',
|
||||
save_restart: 'NTPを保存してGoサーバーを再起動しますか?アクティブなセッションが一時的に切断される場合があります。',
|
||||
status_offset: 'オフセット',
|
||||
status_server: 'NTPソース',
|
||||
status_os: 'OS時計同期',
|
||||
status_error: '最後のエラー',
|
||||
os_yes: 'はい',
|
||||
os_no: 'いいえ',
|
||||
},
|
||||
ko: {
|
||||
synced_detail: '과금을 위해 시계가 동기화됨',
|
||||
ntp_servers: 'NTP 서버',
|
||||
ntp_servers_hint: '쉼표로 구분된 호스트명 또는 IP. Go 서버(betterdesk-server)에 적용됩니다.',
|
||||
max_skew_ms: '최대 시계 오차 (ms)',
|
||||
require_sync: '과금 세션에 동기화된 시계 필요',
|
||||
trust_os: '직접 쿼리 실패 시 OS NTP 신뢰',
|
||||
trust_os_hint: 'UDP NTP가 차단된 경우 systemd-timesyncd/chrony 상태 사용.',
|
||||
save: 'NTP 설정 저장',
|
||||
saved: '시계 설정이 저장되었습니다. Go 서버 재시작이 시작되었습니다.',
|
||||
save_restart: 'NTP를 저장하고 Go 서버를 재시작하시겠습니까? 활성 세션이 잠시 끊길 수 있습니다.',
|
||||
status_offset: '오프셋',
|
||||
status_server: 'NTP 소스',
|
||||
status_os: 'OS 시계 동기화',
|
||||
status_error: '마지막 오류',
|
||||
os_yes: '예',
|
||||
os_no: '아니오',
|
||||
},
|
||||
nb: {
|
||||
synced_detail: 'Klokke synkronisert for fakturering',
|
||||
ntp_servers: 'NTP-servere',
|
||||
ntp_servers_hint: 'Kommaseparerte vertsnavn eller IP. Gjelder Go-serveren (betterdesk-server).',
|
||||
max_skew_ms: 'Maks klokkeavvik (ms)',
|
||||
require_sync: 'Krev synkronisert klokke for fakturerbare økter',
|
||||
trust_os: 'Stol på OS NTP når direkte forespørsler feiler',
|
||||
trust_os_hint: 'Bruk systemd-timesyncd/chrony-status når UDP NTP er blokkert.',
|
||||
save: 'Lagre NTP-innstillinger',
|
||||
saved: 'Klokkeinnstillinger lagret. Omstart av Go-server startet.',
|
||||
save_restart: 'Lagre NTP og start Go-server på nytt? Aktive økter kan kort kobles fra.',
|
||||
status_offset: 'Avvik',
|
||||
status_server: 'NTP-kilde',
|
||||
status_os: 'OS-klokke synkronisert',
|
||||
status_error: 'Siste feil',
|
||||
os_yes: 'Ja',
|
||||
os_no: 'Nei',
|
||||
},
|
||||
nl: {
|
||||
synced_detail: 'Klok gesynchroniseerd voor facturering',
|
||||
ntp_servers: 'NTP-servers',
|
||||
ntp_servers_hint: 'Kommagescheiden hostnamen of IP\'s. Geldt voor de Go-server (betterdesk-server).',
|
||||
max_skew_ms: 'Maximale klokafwijking (ms)',
|
||||
require_sync: 'Gesynchroniseerde klok vereisen voor factureerbare sessies',
|
||||
trust_os: 'Vertrouw OS NTP wanneer directe queries falen',
|
||||
trust_os_hint: 'Gebruik systemd-timesyncd/chrony-status wanneer UDP NTP geblokkeerd is.',
|
||||
save: 'NTP-instellingen opslaan',
|
||||
saved: 'Klokinstellingen opgeslagen. Herstart Go-server gestart.',
|
||||
save_restart: 'NTP opslaan en Go-server herstarten? Actieve sessies kunnen kort verbreken.',
|
||||
status_offset: 'Offset',
|
||||
status_server: 'NTP-bron',
|
||||
status_os: 'OS-klok gesynchroniseerd',
|
||||
status_error: 'Laatste fout',
|
||||
os_yes: 'Ja',
|
||||
os_no: 'Nee',
|
||||
},
|
||||
pl: {
|
||||
synced_detail: 'Zegar zsynchronizowany do rozliczeń',
|
||||
ntp_servers: 'Serwery NTP',
|
||||
ntp_servers_hint: 'Nazwy hostów lub IP oddzielone przecinkami. Dotyczy serwera Go (betterdesk-server).',
|
||||
max_skew_ms: 'Maksymalne odchylenie zegara (ms)',
|
||||
require_sync: 'Wymagaj zsynchronizowanego zegara dla sesji płatnych',
|
||||
trust_os: 'Ufaj NTP systemu, gdy bezpośrednie zapytania zawiodą',
|
||||
trust_os_hint: 'Użyj statusu systemd-timesyncd/chrony, gdy UDP NTP jest blokowane (DMZ/firewall).',
|
||||
save: 'Zapisz ustawienia NTP',
|
||||
saved: 'Ustawienia zegara zapisane. Uruchomiono restart serwera Go.',
|
||||
save_restart: 'Zapisać NTP i zrestartować serwer Go? Aktywne sesje mogą się chwilowo rozłączyć.',
|
||||
status_offset: 'Odchylenie',
|
||||
status_server: 'Źródło NTP',
|
||||
status_os: 'Zegar OS zsynchronizowany',
|
||||
status_error: 'Ostatni błąd',
|
||||
os_yes: 'Tak',
|
||||
os_no: 'Nie',
|
||||
},
|
||||
pt: {
|
||||
synced_detail: 'Relógio sincronizado para faturação',
|
||||
ntp_servers: 'Servidores NTP',
|
||||
ntp_servers_hint: 'Nomes de host ou IP separados por vírgula. Aplica-se ao servidor Go (betterdesk-server).',
|
||||
max_skew_ms: 'Desvio máximo do relógio (ms)',
|
||||
require_sync: 'Exigir relógio sincronizado para sessões faturáveis',
|
||||
trust_os: 'Confiar no NTP do SO quando consultas diretas falharem',
|
||||
trust_os_hint: 'Usar estado systemd-timesyncd/chrony quando UDP NTP estiver bloqueado.',
|
||||
save: 'Guardar definições NTP',
|
||||
saved: 'Definições de relógio guardadas. Reinício do servidor Go iniciado.',
|
||||
save_restart: 'Guardar NTP e reiniciar o servidor Go? Sessões ativas podem desligar-se brevemente.',
|
||||
status_offset: 'Desvio',
|
||||
status_server: 'Fonte NTP',
|
||||
status_os: 'Relógio do SO sincronizado',
|
||||
status_error: 'Último erro',
|
||||
os_yes: 'Sim',
|
||||
os_no: 'Não',
|
||||
},
|
||||
ro: {
|
||||
synced_detail: 'Ceas sincronizat pentru facturare',
|
||||
ntp_servers: 'Servere NTP',
|
||||
ntp_servers_hint: 'Nume gazdă sau IP separate prin virgulă. Se aplică serverului Go (betterdesk-server).',
|
||||
max_skew_ms: 'Abatere maximă ceas (ms)',
|
||||
require_sync: 'Cere ceas sincronizat pentru sesiuni facturabile',
|
||||
trust_os: 'Ai încredere în NTP OS când interogările directe eșuează',
|
||||
trust_os_hint: 'Folosește starea systemd-timesyncd/chrony când UDP NTP este blocat.',
|
||||
save: 'Salvează setările NTP',
|
||||
saved: 'Setările ceasului salvate. Repornire server Go inițiată.',
|
||||
save_restart: 'Salvezi NTP și repornești serverul Go? Sesiunile active se pot deconecta scurt.',
|
||||
status_offset: 'Abatere',
|
||||
status_server: 'Sursă NTP',
|
||||
status_os: 'Ceas OS sincronizat',
|
||||
status_error: 'Ultima eroare',
|
||||
os_yes: 'Da',
|
||||
os_no: 'Nu',
|
||||
},
|
||||
sv: {
|
||||
synced_detail: 'Klocka synkroniserad för fakturering',
|
||||
ntp_servers: 'NTP-servrar',
|
||||
ntp_servers_hint: 'Kommaseparerade värdnamn eller IP. Gäller Go-servern (betterdesk-server).',
|
||||
max_skew_ms: 'Max klockavvikelse (ms)',
|
||||
require_sync: 'Kräv synkad klocka för fakturerbara sessioner',
|
||||
trust_os: 'Lita på OS NTP när direkta frågor misslyckas',
|
||||
trust_os_hint: 'Använd systemd-timesyncd/chrony-status när UDP NTP blockeras.',
|
||||
save: 'Spara NTP-inställningar',
|
||||
saved: 'Klockinställningar sparade. Omstart av Go-server startad.',
|
||||
save_restart: 'Spara NTP och starta om Go-servern? Aktiva sessioner kan kopplas från kort.',
|
||||
status_offset: 'Avvikelse',
|
||||
status_server: 'NTP-källa',
|
||||
status_os: 'OS-klocka synkad',
|
||||
status_error: 'Senaste fel',
|
||||
os_yes: 'Ja',
|
||||
os_no: 'Nej',
|
||||
},
|
||||
th: {
|
||||
synced_detail: 'นาฬิกาซิงค์สำหรับการเรียกเก็บเงิน',
|
||||
ntp_servers: 'เซิร์ฟเวอร์ NTP',
|
||||
ntp_servers_hint: 'ชื่อโฮสต์หรือ IP คั่นด้วยจุลภาค ใช้กับเซิร์ฟเวอร์ Go (betterdesk-server)',
|
||||
max_skew_ms: 'ความคลาดเคลื่อนสูงสุดของนาฬิกา (ms)',
|
||||
require_sync: 'ต้องมีนาฬิกาซิงค์สำหรับเซสชันที่เรียกเก็บเงิน',
|
||||
trust_os: 'เชื่อถือ NTP ของ OS เมื่อการสอบถามโดยตรงล้มเหลว',
|
||||
trust_os_hint: 'ใช้สถานะ systemd-timesyncd/chrony เมื่อ UDP NTP ถูกบล็อก',
|
||||
save: 'บันทึกการตั้งค่า NTP',
|
||||
saved: 'บันทึกการตั้งค่านาฬิกาแล้ว เริ่มรีสตาร์ทเซิร์ฟเวอร์ Go',
|
||||
save_restart: 'บันทึก NTP และรีสตาร์ทเซิร์ฟเวอร์ Go? เซสชันที่ใช้งานอาจหลุดชั่วคราว',
|
||||
status_offset: 'ออฟเซ็ต',
|
||||
status_server: 'แหล่ง NTP',
|
||||
status_os: 'นาฬิกา OS ซิงค์',
|
||||
status_error: 'ข้อผิดพลาดล่าสุด',
|
||||
os_yes: 'ใช่',
|
||||
os_no: 'ไม่',
|
||||
},
|
||||
tr: {
|
||||
synced_detail: 'Faturalandırma için saat senkronize',
|
||||
ntp_servers: 'NTP sunucuları',
|
||||
ntp_servers_hint: 'Virgülle ayrılmış ana bilgisayar adları veya IP. Go sunucusuna (betterdesk-server) uygulanır.',
|
||||
max_skew_ms: 'Maksimum saat sapması (ms)',
|
||||
require_sync: 'Faturalandırılabilir oturumlar için senkron saat gerekli',
|
||||
trust_os: 'Doğrudan sorgular başarısız olursa OS NTP\'ye güven',
|
||||
trust_os_hint: 'UDP NTP engellendiğinde systemd-timesyncd/chrony durumunu kullanın.',
|
||||
save: 'NTP ayarlarını kaydet',
|
||||
saved: 'Saat ayarları kaydedildi. Go sunucusu yeniden başlatılıyor.',
|
||||
save_restart: 'NTP kaydedilsin ve Go sunucusu yeniden başlatılsın mı? Aktif oturumlar kısa süre kesilebilir.',
|
||||
status_offset: 'Sapma',
|
||||
status_server: 'NTP kaynağı',
|
||||
status_os: 'OS saati senkron',
|
||||
status_error: 'Son hata',
|
||||
os_yes: 'Evet',
|
||||
os_no: 'Hayır',
|
||||
},
|
||||
uk: {
|
||||
synced_detail: 'Годинник синхронізовано для білінгу',
|
||||
ntp_servers: 'NTP-сервери',
|
||||
ntp_servers_hint: 'Імена хостів або IP через кому. Застосовується до Go-сервера (betterdesk-server).',
|
||||
max_skew_ms: 'Максимальне відхилення годинника (мс)',
|
||||
require_sync: 'Вимагати синхронізований годинник для платних сесій',
|
||||
trust_os: 'Довіряти OS NTP, якщо прямі запити не вдаються',
|
||||
trust_os_hint: 'Використовуйте статус systemd-timesyncd/chrony, коли UDP NTP заблоковано.',
|
||||
save: 'Зберегти налаштування NTP',
|
||||
saved: 'Налаштування годинника збережено. Перезапуск Go-сервера розпочато.',
|
||||
save_restart: 'Зберегти NTP і перезапустити Go-сервер? Активні сесії можуть коротко відключитися.',
|
||||
status_offset: 'Відхилення',
|
||||
status_server: 'Джерело NTP',
|
||||
status_os: 'Годинник OS синхронізовано',
|
||||
status_error: 'Остання помилка',
|
||||
os_yes: 'Так',
|
||||
os_no: 'Ні',
|
||||
},
|
||||
vi: {
|
||||
synced_detail: 'Đồng hồ đã đồng bộ cho tính phí',
|
||||
ntp_servers: 'Máy chủ NTP',
|
||||
ntp_servers_hint: 'Tên máy chủ hoặc IP cách nhau bằng dấu phẩy. Áp dụng cho máy chủ Go (betterdesk-server).',
|
||||
max_skew_ms: 'Độ lệch đồng hồ tối đa (ms)',
|
||||
require_sync: 'Yêu cầu đồng hồ đồng bộ cho phiên tính phí',
|
||||
trust_os: 'Tin cậy NTP OS khi truy vấn trực tiếp thất bại',
|
||||
trust_os_hint: 'Dùng trạng thái systemd-timesyncd/chrony khi UDP NTP bị chặn.',
|
||||
save: 'Lưu cài đặt NTP',
|
||||
saved: 'Đã lưu cài đặt đồng hồ. Đang khởi động lại máy chủ Go.',
|
||||
save_restart: 'Lưu NTP và khởi động lại máy chủ Go? Phiên đang hoạt động có thể ngắt tạm thời.',
|
||||
status_offset: 'Độ lệch',
|
||||
status_server: 'Nguồn NTP',
|
||||
status_os: 'Đồng hồ OS đã đồng bộ',
|
||||
status_error: 'Lỗi cuối',
|
||||
os_yes: 'Có',
|
||||
os_no: 'Không',
|
||||
},
|
||||
zh: {
|
||||
synced_detail: '时钟已同步,可用于计费',
|
||||
ntp_servers: 'NTP 服务器',
|
||||
ntp_servers_hint: '逗号分隔的主机名或 IP。应用于 Go 服务器(betterdesk-server)。',
|
||||
max_skew_ms: '最大时钟偏差 (ms)',
|
||||
require_sync: '可计费会话要求时钟同步',
|
||||
trust_os: '直接查询失败时信任 OS NTP',
|
||||
trust_os_hint: 'UDP NTP 被阻止时使用 systemd-timesyncd/chrony 状态。',
|
||||
save: '保存 NTP 设置',
|
||||
saved: '时钟设置已保存。已启动 Go 服务器重启。',
|
||||
save_restart: '保存 NTP 并重启 Go 服务器?活动会话可能短暂断开。',
|
||||
status_offset: '偏移',
|
||||
status_server: 'NTP 来源',
|
||||
status_os: 'OS 时钟已同步',
|
||||
status_error: '最后错误',
|
||||
os_yes: '是',
|
||||
os_no: '否',
|
||||
},
|
||||
'zh-TW': {
|
||||
synced_detail: '時鐘已同步,可用於計費',
|
||||
ntp_servers: 'NTP 伺服器',
|
||||
ntp_servers_hint: '逗號分隔的主機名稱或 IP。套用至 Go 伺服器(betterdesk-server)。',
|
||||
max_skew_ms: '最大時鐘偏差 (ms)',
|
||||
require_sync: '可計費工作階段要求時鐘同步',
|
||||
trust_os: '直接查詢失敗時信任 OS NTP',
|
||||
trust_os_hint: 'UDP NTP 被阻擋時使用 systemd-timesyncd/chrony 狀態。',
|
||||
save: '儲存 NTP 設定',
|
||||
saved: '時鐘設定已儲存。已啟動 Go 伺服器重新啟動。',
|
||||
save_restart: '儲存 NTP 並重新啟動 Go 伺服器?使用中工作階段可能短暫中斷。',
|
||||
status_offset: '偏移',
|
||||
status_server: 'NTP 來源',
|
||||
status_os: 'OS 時鐘已同步',
|
||||
status_error: '最後錯誤',
|
||||
os_yes: '是',
|
||||
os_no: '否',
|
||||
},
|
||||
};
|
||||
|
||||
let updated = 0;
|
||||
for (const file of locales) {
|
||||
const locale = file.replace(/\.json$/, '');
|
||||
const patch = patches[locale];
|
||||
if (!patch) continue;
|
||||
const filePath = path.join(langDir, file);
|
||||
const json = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
if (!json.commercialization?.clock) continue;
|
||||
Object.assign(json.commercialization.clock, patch);
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
||||
updated += 1;
|
||||
}
|
||||
|
||||
console.log(`Patched commercialization.clock in ${updated} locale files`);
|
||||
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const { upsertEnvKey } = require('../lib/envMerge');
|
||||
const updateService = require('./updateService');
|
||||
const serverConnectionConfigService = require('./serverConnectionConfigService');
|
||||
|
||||
const CONSOLE_ROOT = path.join(__dirname, '..');
|
||||
const ENV_PATH = path.join(CONSOLE_ROOT, '.env');
|
||||
|
||||
const ENV_KEYS = {
|
||||
ntp_servers: 'NTP_SERVERS',
|
||||
max_skew_ms: 'BILLING_MAX_CLOCK_SKEW_MS',
|
||||
require_synced_clock: 'BILLING_REQUIRE_SYNCED_CLOCK',
|
||||
trust_os_ntp: 'BILLING_TRUST_OS_NTP',
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
ntp_servers: 'pool.ntp.org,time.google.com,time.cloudflare.com',
|
||||
max_skew_ms: 2000,
|
||||
require_synced_clock: true,
|
||||
trust_os_ntp: process.platform === 'linux',
|
||||
};
|
||||
|
||||
function parseEnvFile(content) {
|
||||
const out = {};
|
||||
if (!content) return out;
|
||||
for (const line of String(content).split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function envToBool(value, defaultValue) {
|
||||
if (value === undefined || value === null || value === '') return defaultValue;
|
||||
const v = String(value).trim().toUpperCase();
|
||||
if (['Y', 'YES', '1', 'TRUE', 'ON'].includes(v)) return true;
|
||||
if (['N', 'NO', '0', 'FALSE', 'OFF'].includes(v)) return false;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function boolToEnv(value) {
|
||||
return value ? '1' : '0';
|
||||
}
|
||||
|
||||
function isValidNTPServerEntry(entry) {
|
||||
const s = String(entry || '').trim();
|
||||
if (!s || s.length > 253) return false;
|
||||
if (net.isIP(s)) return true;
|
||||
return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s);
|
||||
}
|
||||
|
||||
function normalizeSettings(raw = {}) {
|
||||
const servers = String(raw.ntp_servers ?? DEFAULTS.ntp_servers)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const maxSkew = Number.parseInt(raw.max_skew_ms, 10);
|
||||
return {
|
||||
ntp_servers: servers.join(','),
|
||||
max_skew_ms: Number.isFinite(maxSkew) && maxSkew > 0 ? maxSkew : DEFAULTS.max_skew_ms,
|
||||
require_synced_clock: raw.require_synced_clock !== undefined
|
||||
? !!raw.require_synced_clock
|
||||
: DEFAULTS.require_synced_clock,
|
||||
trust_os_ntp: raw.trust_os_ntp !== undefined
|
||||
? !!raw.trust_os_ntp
|
||||
: DEFAULTS.trust_os_ntp,
|
||||
};
|
||||
}
|
||||
|
||||
function validateSettings(settings) {
|
||||
const normalized = normalizeSettings(settings);
|
||||
const servers = normalized.ntp_servers.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (!servers.length) {
|
||||
throw new Error('invalid_ntp_servers');
|
||||
}
|
||||
for (const server of servers) {
|
||||
if (!isValidNTPServerEntry(server)) {
|
||||
throw new Error('invalid_ntp_servers');
|
||||
}
|
||||
}
|
||||
if (normalized.max_skew_ms <= 0 || normalized.max_skew_ms > 600000) {
|
||||
throw new Error('invalid_max_skew');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function settingsFromEnvContent(content) {
|
||||
const env = parseEnvFile(content);
|
||||
return normalizeSettings({
|
||||
ntp_servers: env[ENV_KEYS.ntp_servers] || DEFAULTS.ntp_servers,
|
||||
max_skew_ms: env[ENV_KEYS.max_skew_ms] || DEFAULTS.max_skew_ms,
|
||||
require_synced_clock: envToBool(env[ENV_KEYS.require_synced_clock], DEFAULTS.require_synced_clock),
|
||||
trust_os_ntp: envToBool(env[ENV_KEYS.trust_os_ntp], DEFAULTS.trust_os_ntp),
|
||||
});
|
||||
}
|
||||
|
||||
function getClockSettings() {
|
||||
const content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
|
||||
return settingsFromEnvContent(content);
|
||||
}
|
||||
|
||||
function writeClockSettingsToEnv(settings) {
|
||||
const normalized = validateSettings(settings);
|
||||
let content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
|
||||
content = upsertEnvKey(content, ENV_KEYS.ntp_servers, normalized.ntp_servers);
|
||||
content = upsertEnvKey(content, ENV_KEYS.max_skew_ms, String(normalized.max_skew_ms));
|
||||
content = upsertEnvKey(content, ENV_KEYS.require_synced_clock, boolToEnv(normalized.require_synced_clock));
|
||||
content = upsertEnvKey(content, ENV_KEYS.trust_os_ntp, boolToEnv(normalized.trust_os_ntp));
|
||||
fs.writeFileSync(ENV_PATH, content, { encoding: 'utf8', mode: 0o600 });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function saveClockSettings(settings, opts = {}) {
|
||||
const normalized = writeClockSettingsToEnv(settings);
|
||||
const serviceConfig = updateService.sanitizeGoServerServiceConfig();
|
||||
const result = {
|
||||
settings: normalized,
|
||||
serviceConfig,
|
||||
restart: null,
|
||||
};
|
||||
if (opts.restart !== false) {
|
||||
result.restart = serverConnectionConfigService.restartServer();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULTS,
|
||||
ENV_PATH,
|
||||
ENV_KEYS,
|
||||
parseEnvFile,
|
||||
envToBool,
|
||||
boolToEnv,
|
||||
isValidNTPServerEntry,
|
||||
normalizeSettings,
|
||||
validateSettings,
|
||||
settingsFromEnvContent,
|
||||
getClockSettings,
|
||||
saveClockSettings,
|
||||
};
|
||||
@@ -742,6 +742,75 @@ function runPrivileged(command, options = {}) {
|
||||
/**
|
||||
* Ensure MESH_ENABLED=Y is present in Go server service environment (one-time migration).
|
||||
*/
|
||||
const BILLING_ENV_KEYS = [
|
||||
'NTP_SERVERS',
|
||||
'BILLING_MAX_CLOCK_SKEW_MS',
|
||||
'BILLING_REQUIRE_SYNCED_CLOCK',
|
||||
'BILLING_TRUST_OS_NTP',
|
||||
];
|
||||
|
||||
function parseEnvFileKeys(content, keys) {
|
||||
const out = {};
|
||||
if (!content || typeof content !== 'string') return out;
|
||||
const wanted = new Set(keys);
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
if (!wanted.has(key)) continue;
|
||||
out[key] = trimmed.slice(eq + 1).trim();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mergeBillingEnvIntoWindowsServiceExtra(existingExtra, billingVars) {
|
||||
const lines = (existingExtra || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const map = new Map();
|
||||
for (const line of lines) {
|
||||
const eq = line.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
map.set(line.slice(0, eq), line.slice(eq + 1));
|
||||
}
|
||||
let changed = false;
|
||||
for (const key of BILLING_ENV_KEYS) {
|
||||
if (billingVars[key] === undefined) continue;
|
||||
const nextVal = billingVars[key];
|
||||
if (map.get(key) !== nextVal) {
|
||||
map.set(key, nextVal);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return { text: existingExtra, changed: false };
|
||||
const merged = [...map.entries()].map(([k, v]) => `${k}=${v}`).join('\n');
|
||||
return { text: merged, changed: true };
|
||||
}
|
||||
|
||||
function syncBillingEnvToWindowsGoServer() {
|
||||
const envPath = path.join(ROOT_DIR, '.env');
|
||||
if (!fs.existsSync(envPath)) return { changed: false };
|
||||
const billingVars = parseEnvFileKeys(fs.readFileSync(envPath, 'utf8'), BILLING_ENV_KEYS);
|
||||
if (!Object.keys(billingVars).length) return { changed: false };
|
||||
|
||||
const serviceName = COMPONENTS.server.service;
|
||||
const serverEnvRaw = execSync(`nssm get "${serviceName}" AppEnvironmentExtra 2>nul`, {
|
||||
timeout: 5000,
|
||||
stdio: 'pipe'
|
||||
}).toString();
|
||||
const patch = mergeBillingEnvIntoWindowsServiceExtra(serverEnvRaw, billingVars);
|
||||
if (!patch.changed) return { changed: false };
|
||||
execFileSync('nssm', ['set', serviceName, 'AppEnvironmentExtra', patch.text], {
|
||||
timeout: 5000,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
return { changed: true };
|
||||
}
|
||||
|
||||
function ensureMeshEnabledInServiceEnv(envText) {
|
||||
if (!envText || typeof envText !== 'string') return { text: envText, changed: false };
|
||||
if (/^MESH_ENABLED=/m.test(envText)) return { text: envText, changed: false };
|
||||
@@ -750,6 +819,28 @@ function ensureMeshEnabledInServiceEnv(envText) {
|
||||
return { text: trimmed ? `${trimmed}\n${line}` : line, changed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Go server systemd unit loads console .env (NTP / billing keys for timesync).
|
||||
*/
|
||||
function ensureGoServerEnvironmentFile(unitText, envFilePath) {
|
||||
if (!unitText || typeof unitText !== 'string') return { text: unitText, changed: false };
|
||||
if (/^EnvironmentFile=/m.test(unitText)) return { text: unitText, changed: false };
|
||||
const envLine = `EnvironmentFile=-${envFilePath}`;
|
||||
if (/^Environment=AUTH_DB_PATH=/m.test(unitText)) {
|
||||
return {
|
||||
text: unitText.replace(/^(Environment=AUTH_DB_PATH=.*)$/m, `${envLine}\n$1`),
|
||||
changed: true
|
||||
};
|
||||
}
|
||||
if (/^\[Service\]/m.test(unitText)) {
|
||||
return {
|
||||
text: unitText.replace(/^\[Service\]/m, `[Service]\n${envLine}`),
|
||||
changed: true
|
||||
};
|
||||
}
|
||||
return { text: `${envLine}\n${unitText}`, changed: true };
|
||||
}
|
||||
|
||||
function sanitizeGoServerServiceConfig() {
|
||||
const result = { changed: false, changes: [], error: null, needsRestart: false };
|
||||
|
||||
@@ -787,6 +878,15 @@ function sanitizeGoServerServiceConfig() {
|
||||
}
|
||||
} catch (_e) { /* server service may not exist */ }
|
||||
|
||||
try {
|
||||
const billingPatch = syncBillingEnvToWindowsGoServer();
|
||||
if (billingPatch.changed) {
|
||||
result.changed = true;
|
||||
result.needsRestart = true;
|
||||
result.changes.push('synced billing/NTP env to BetterDesk Go Server NSSM environment');
|
||||
}
|
||||
} catch (_e) { /* server service may not exist */ }
|
||||
|
||||
try {
|
||||
const consoleService = COMPONENTS.console.service;
|
||||
const envRaw = execSync(`nssm get "${consoleService}" AppEnvironmentExtra 2>nul`, {
|
||||
@@ -835,6 +935,14 @@ function sanitizeGoServerServiceConfig() {
|
||||
result.changes.push('set MESH_ENABLED=Y in betterdesk-server systemd unit');
|
||||
}
|
||||
|
||||
const consoleEnvPath = path.join(ROOT_DIR, '.env');
|
||||
const envFilePatch = ensureGoServerEnvironmentFile(clean, consoleEnvPath);
|
||||
if (envFilePatch.changed) {
|
||||
clean = envFilePatch.text;
|
||||
result.needsRestart = true;
|
||||
result.changes.push('set EnvironmentFile for console .env on betterdesk-server systemd unit');
|
||||
}
|
||||
|
||||
if (clean !== original) {
|
||||
writeTextFilePrivileged(fragmentPath, clean);
|
||||
runPrivileged('systemctl daemon-reload', { timeout: 10000, stdio: 'pipe' });
|
||||
@@ -3087,6 +3195,9 @@ module.exports = {
|
||||
createPreUpdateBackup,
|
||||
applyUpdate,
|
||||
runUpdatePreflight,
|
||||
sanitizeGoServerServiceConfig,
|
||||
syncBillingEnvToWindowsGoServer,
|
||||
BILLING_ENV_KEYS,
|
||||
restartService,
|
||||
daemonReload,
|
||||
listBackups,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
validateSettings,
|
||||
normalizeSettings,
|
||||
settingsFromEnvContent,
|
||||
isValidNTPServerEntry,
|
||||
envToBool,
|
||||
boolToEnv,
|
||||
} = require('../services/billingClockConfigService');
|
||||
|
||||
describe('billingClockConfigService', () => {
|
||||
test('normalizeSettings trims NTP server list', () => {
|
||||
const out = normalizeSettings({
|
||||
ntp_servers: ' 10.0.0.1 , ntp.example.com , ',
|
||||
max_skew_ms: '3000',
|
||||
});
|
||||
expect(out.ntp_servers).toBe('10.0.0.1,ntp.example.com');
|
||||
expect(out.max_skew_ms).toBe(3000);
|
||||
});
|
||||
|
||||
test('validateSettings rejects invalid hostnames', () => {
|
||||
expect(() => validateSettings({ ntp_servers: 'bad host name' }))
|
||||
.toThrow('invalid_ntp_servers');
|
||||
});
|
||||
|
||||
test('validateSettings accepts IPv4 NTP servers', () => {
|
||||
const out = validateSettings({ ntp_servers: '192.168.1.1' });
|
||||
expect(out.ntp_servers).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
test('settingsFromEnvContent reads billing keys', () => {
|
||||
const content = [
|
||||
'NTP_SERVERS=10.0.0.1',
|
||||
'BILLING_MAX_CLOCK_SKEW_MS=1500',
|
||||
'BILLING_REQUIRE_SYNCED_CLOCK=0',
|
||||
'BILLING_TRUST_OS_NTP=1',
|
||||
].join('\n');
|
||||
const settings = settingsFromEnvContent(content);
|
||||
expect(settings.ntp_servers).toBe('10.0.0.1');
|
||||
expect(settings.max_skew_ms).toBe(1500);
|
||||
expect(settings.require_synced_clock).toBe(false);
|
||||
expect(settings.trust_os_ntp).toBe(true);
|
||||
});
|
||||
|
||||
test('isValidNTPServerEntry validates IP and hostname', () => {
|
||||
expect(isValidNTPServerEntry('10.0.0.1')).toBe(true);
|
||||
expect(isValidNTPServerEntry('time.google.com')).toBe(true);
|
||||
expect(isValidNTPServerEntry('')).toBe(false);
|
||||
});
|
||||
|
||||
test('envToBool and boolToEnv round-trip', () => {
|
||||
expect(envToBool('Y', false)).toBe(true);
|
||||
expect(envToBool('0', true)).toBe(false);
|
||||
expect(boolToEnv(true)).toBe('1');
|
||||
expect(boolToEnv(false)).toBe('0');
|
||||
});
|
||||
});
|
||||
@@ -109,8 +109,31 @@
|
||||
<section id="tab-settings" class="tab-panel ${activeTab === 'settings' ? '' : 'hidden'}">
|
||||
<div class="card">
|
||||
<h3>${_('commercialization.clock.title')}</h3>
|
||||
<p id="settings-clock-detail"></p>
|
||||
<p id="settings-clock-detail" class="text-muted"></p>
|
||||
<dl class="clock-status-grid">
|
||||
<div><dt>${_('commercialization.clock.status_offset')}</dt><dd id="clock-offset">—</dd></div>
|
||||
<div><dt>${_('commercialization.clock.status_server')}</dt><dd id="clock-ntp-server">—</dd></div>
|
||||
<div><dt>${_('commercialization.clock.status_os')}</dt><dd id="clock-os-sync">—</dd></div>
|
||||
<div><dt>${_('commercialization.clock.status_error')}</dt><dd id="clock-last-error">—</dd></div>
|
||||
</dl>
|
||||
<button type="button" class="btn btn-secondary" id="btn-timesync-check">${_('commercialization.clock.check_now')}</button>
|
||||
<form id="clock-settings-form" class="settings-form" style="margin-top: 1rem;">
|
||||
<label class="form-label">${_('commercialization.clock.ntp_servers')}
|
||||
<textarea id="clock-ntp-servers" class="form-control" rows="2" placeholder="pool.ntp.org,time.google.com"></textarea>
|
||||
</label>
|
||||
<p class="text-muted form-hint">${_('commercialization.clock.ntp_servers_hint')}</p>
|
||||
<label class="form-label">${_('commercialization.clock.max_skew_ms')}
|
||||
<input type="number" id="clock-max-skew" class="form-control" min="100" max="600000" step="100" value="2000">
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="clock-require-sync"> ${_('commercialization.clock.require_sync')}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="clock-trust-os"> ${_('commercialization.clock.trust_os')}</label>
|
||||
</div>
|
||||
<p class="text-muted form-hint">${_('commercialization.clock.trust_os_hint')}</p>
|
||||
<button type="button" class="btn btn-primary" id="btn-save-clock-settings">${_('commercialization.clock.save')}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 1rem;">
|
||||
<h3>${_('commercialization.notifications.title')}</h3>
|
||||
|
||||
Reference in New Issue
Block a user