mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-06 04:44:21 +00:00
fix(node): show the activated first-use deadline on the Clients page
With "start after first use" on a node inbound, the node activates the absolute deadline and the master adopts it into client_traffics via the sync CASE merge — but the client record (what the Clients page reads) was only refreshed by SyncInbound from the snapshot's settings JSON. A node whose JSON still carried the negative duration (stale conversion, older node build, or a mixed local+node attachment) kept rewriting the record back to "not started" even though the DB held the real deadline (#5714). Lift the activated deadline from client_traffics onto still-negative client records at the end of every node sync, after SyncInbound has run. Intentional resets back to delayed start are unaffected: editing a client also resets client_traffics to the negative duration, so the lift's expiry_time > 0 guard never matches. Closes #5714
This commit is contained in:
@@ -191,6 +191,17 @@ func mergeActivationExpiry(existing, node int64) int64 {
|
||||
return node
|
||||
}
|
||||
|
||||
// liftActivatedClientRecordExpiries copies a node-activated deadline from
|
||||
// client_traffics onto client records still holding the negative duration (#5714).
|
||||
func liftActivatedClientRecordExpiries(tx *gorm.DB) error {
|
||||
return tx.Exec(
|
||||
`UPDATE clients
|
||||
SET expiry_time = (SELECT ct.expiry_time FROM client_traffics ct WHERE ct.email = clients.email AND ct.expiry_time > 0 LIMIT 1)
|
||||
WHERE clients.expiry_time < 0
|
||||
AND EXISTS (SELECT 1 FROM client_traffics ct WHERE ct.email = clients.email AND ct.expiry_time > 0)`,
|
||||
).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) SetRemoteTraffic(nodeID int, snap *runtime.TrafficSnapshot, dirty bool) (bool, error) {
|
||||
var structuralChange bool
|
||||
err := submitTrafficWrite(func() error {
|
||||
@@ -847,6 +858,10 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
}
|
||||
}
|
||||
|
||||
if err := liftActivatedClientRecordExpiries(tx); err != nil {
|
||||
logger.Warning("setRemoteTraffic: lift activated expiries failed:", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
@@ -139,3 +140,44 @@ func TestNodeRenewExtendsExpiry(t *testing.T) {
|
||||
t.Fatalf("node renewal did not propagate: expiry = %d, want %d", got, renewed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeActivationLiftsClientRecordExpiry reproduces #5714: the node activates
|
||||
// the deadline (positive ClientStats) while its settings JSON still carries the
|
||||
// negative duration, so SyncInbound keeps writing the stale value into the
|
||||
// client record and the Clients page shows "not started" forever.
|
||||
func TestNodeActivationLiftsClientRecordExpiry(t *testing.T) {
|
||||
db := initTrafficTestDB(t)
|
||||
createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "delayed")
|
||||
svc := &InboundService{}
|
||||
|
||||
const email = "delayed"
|
||||
const duration = int64(-2592000000)
|
||||
const activated = int64(1798448344010)
|
||||
negSettings := `{"clients":[{"email":"delayed","enable":true,"expiryTime":-2592000000}]}`
|
||||
|
||||
if err := db.Create(&model.ClientRecord{Email: email, Enable: true, ExpiryTime: duration}).Error; err != nil {
|
||||
t.Fatalf("seed client record: %v", err)
|
||||
}
|
||||
|
||||
readRecordExpiry := func() int64 {
|
||||
t.Helper()
|
||||
var rec model.ClientRecord
|
||||
if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
|
||||
t.Fatalf("read client record: %v", err)
|
||||
}
|
||||
return rec.ExpiryTime
|
||||
}
|
||||
|
||||
syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, ExpiryTime: duration, Enable: true})
|
||||
if got := readRecordExpiry(); got != duration {
|
||||
t.Fatalf("before activation: record expiry = %d, want %d", got, duration)
|
||||
}
|
||||
|
||||
syncNodeWithSettings(t, svc, 1, "n1-in", negSettings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, ExpiryTime: activated, Enable: true})
|
||||
if got := readTraffic(t, db, email).ExpiryTime; got != activated {
|
||||
t.Fatalf("client_traffics not activated: expiry = %d, want %d", got, activated)
|
||||
}
|
||||
if got := readRecordExpiry(); got != activated {
|
||||
t.Fatalf("client record kept stale duration (#5714): expiry = %d, want %d", got, activated)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user