diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index 7e827ddb..eea4fe85 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -40,9 +40,16 @@ export default function ClientInfoModal({ onOpenChange, }: ClientInfoModalProps) { const { datepicker } = useDatepicker(); - const expiryLabel = (ts?: number) => (!ts || ts <= 0 ? '∞' : IntlUtil.formatDate(ts, datepicker)); - const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker)); const { t } = useTranslation(); + const expiryLabel = (ts?: number) => { + if (!ts) return '∞'; + if (ts < 0) { + const days = Math.round(ts / -86400000); + return `${t('pages.clients.delayedStart')}: ${days}d`; + } + return IntlUtil.formatDate(ts, datepicker); + }; + const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker)); const [messageApi, messageContextHolder] = message.useMessage(); const [links, setLinks] = useState([]); @@ -195,9 +202,9 @@ export default function ClientInfoModal({ {t('pages.inbounds.expireDate')} - {!client.expiryTime || client.expiryTime <= 0 + {!client.expiryTime ? - : {expiryLabel(client.expiryTime)}} + : {expiryLabel(client.expiryTime)}} {(client.expiryTime ?? 0) > 0 && ( {IntlUtil.formatRelativeTime(client.expiryTime)} )} diff --git a/sub/subClashService.go b/sub/subClashService.go index 68f0c3bc..e01fa57c 100644 --- a/sub/subClashService.go +++ b/sub/subClashService.go @@ -68,9 +68,7 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e traffic.Up = clientTraffic.Up traffic.Down = clientTraffic.Down traffic.Total = clientTraffic.Total - if clientTraffic.ExpiryTime > 0 { - traffic.ExpiryTime = clientTraffic.ExpiryTime - } + traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime) } else { traffic.Up += clientTraffic.Up traffic.Down += clientTraffic.Down @@ -79,7 +77,8 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e } else { traffic.Total += clientTraffic.Total } - if clientTraffic.ExpiryTime != traffic.ExpiryTime { + normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime) + if normalized != traffic.ExpiryTime { traffic.ExpiryTime = 0 } } diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 28a1a9ff..29dbd987 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -130,9 +130,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err traffic.Up = clientTraffic.Up traffic.Down = clientTraffic.Down traffic.Total = clientTraffic.Total - if clientTraffic.ExpiryTime > 0 { - traffic.ExpiryTime = clientTraffic.ExpiryTime - } + traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime) } else { traffic.Up += clientTraffic.Up traffic.Down += clientTraffic.Down @@ -141,7 +139,8 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err } else { traffic.Total += clientTraffic.Total } - if clientTraffic.ExpiryTime != traffic.ExpiryTime { + normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime) + if normalized != traffic.ExpiryTime { traffic.ExpiryTime = 0 } } diff --git a/sub/subService.go b/sub/subService.go index 49a05938..706ef3b8 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -114,9 +114,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C traffic.Up = clientTraffic.Up traffic.Down = clientTraffic.Down traffic.Total = clientTraffic.Total - if clientTraffic.ExpiryTime > 0 { - traffic.ExpiryTime = clientTraffic.ExpiryTime - } + traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime) } else { traffic.Up += clientTraffic.Up traffic.Down += clientTraffic.Down @@ -125,7 +123,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C } else { traffic.Total += clientTraffic.Total } - if clientTraffic.ExpiryTime != traffic.ExpiryTime { + normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime) + if normalized != traffic.ExpiryTime { traffic.ExpiryTime = 0 } } @@ -134,6 +133,16 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C return result, lastOnline, traffic, nil } +func subscriptionExpiryFromClient(expiryTime int64) int64 { + if expiryTime > 0 { + return expiryTime + } + if expiryTime < 0 { + return time.Now().UnixMilli() + (-expiryTime) + } + return 0 +} + func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound diff --git a/sub/subService_test.go b/sub/subService_test.go index 91512d7f..32f013fd 100644 --- a/sub/subService_test.go +++ b/sub/subService_test.go @@ -5,10 +5,27 @@ import ( "encoding/json" "strings" "testing" + "time" "github.com/mhsanaei/3x-ui/v3/database/model" ) +func TestSubscriptionExpiryFromClient(t *testing.T) { + if got := subscriptionExpiryFromClient(0); got != 0 { + t.Fatalf("zero expiry should stay zero, got %d", got) + } + if got := subscriptionExpiryFromClient(1_700_000_000_000); got != 1_700_000_000_000 { + t.Fatalf("positive expiry should pass through, got %d", got) + } + const oneDayMs = int64(86_400_000) + before := time.Now().UnixMilli() + got := subscriptionExpiryFromClient(-oneDayMs) + after := time.Now().UnixMilli() + if got < before+oneDayMs || got > after+oneDayMs { + t.Fatalf("delayed-start expiry should land ~1 day from now, got %d (window %d..%d)", got, before+oneDayMs, after+oneDayMs) + } +} + func TestFindClientIndex(t *testing.T) { clients := []model.Client{ {Email: "a@example.com"},