mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(expiry): show delayed-start countdown in subscribe and client info (#4535)
A client with "start after first use" expiry stores the duration as a negative number of milliseconds (e.g. -86400000 = 1 day after first connect). The clients page row already renders this correctly as "Delayed start: 1d", but two other surfaces treated negative values as zero and rendered them as unlimited: - Subscription header: the index==0 / index>0 branches in subService, subClashService and subJsonService only carried ExpiryTime forward when > 0, so traffic.ExpiryTime stayed at zero and the header sent expire=0. Every imported client appeared to have no expiry, and the built-in subscribe page rendered the "unlimited" tag. - ClientInfoModal: both the expiryLabel helper and the rendering check treated <= 0 as the "no expiry" branch, so the modal showed an infinity tag instead of "Delayed start: Nd". Add subscriptionExpiryFromClient to map negative durations onto a "now + |value|" timestamp so subscription clients see an actual expiry they can count down from. Update ClientInfoModal's helper and render to match the clients-page convention. Regression test in subService_test.go covers the helper. Refs #4535
This commit is contained in:
parent
97967535b6
commit
64122ad80f
5 changed files with 47 additions and 16 deletions
|
|
@ -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<string[]>([]);
|
||||
|
||||
|
|
@ -195,9 +202,9 @@ export default function ClientInfoModal({
|
|||
<tr>
|
||||
<td>{t('pages.inbounds.expireDate')}</td>
|
||||
<td>
|
||||
{!client.expiryTime || client.expiryTime <= 0
|
||||
{!client.expiryTime
|
||||
? <Tag color="purple">∞</Tag>
|
||||
: <Tag>{expiryLabel(client.expiryTime)}</Tag>}
|
||||
: <Tag color={client.expiryTime < 0 ? 'blue' : undefined}>{expiryLabel(client.expiryTime)}</Tag>}
|
||||
{(client.expiryTime ?? 0) > 0 && (
|
||||
<span className="hint">{IntlUtil.formatRelativeTime(client.expiryTime)}</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue