mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +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,
|
onOpenChange,
|
||||||
}: ClientInfoModalProps) {
|
}: ClientInfoModalProps) {
|
||||||
const { datepicker } = useDatepicker();
|
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 { 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 [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const [links, setLinks] = useState<string[]>([]);
|
const [links, setLinks] = useState<string[]>([]);
|
||||||
|
|
||||||
|
|
@ -195,9 +202,9 @@ export default function ClientInfoModal({
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t('pages.inbounds.expireDate')}</td>
|
<td>{t('pages.inbounds.expireDate')}</td>
|
||||||
<td>
|
<td>
|
||||||
{!client.expiryTime || client.expiryTime <= 0
|
{!client.expiryTime
|
||||||
? <Tag color="purple">∞</Tag>
|
? <Tag color="purple">∞</Tag>
|
||||||
: <Tag>{expiryLabel(client.expiryTime)}</Tag>}
|
: <Tag color={client.expiryTime < 0 ? 'blue' : undefined}>{expiryLabel(client.expiryTime)}</Tag>}
|
||||||
{(client.expiryTime ?? 0) > 0 && (
|
{(client.expiryTime ?? 0) > 0 && (
|
||||||
<span className="hint">{IntlUtil.formatRelativeTime(client.expiryTime)}</span>
|
<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.Up = clientTraffic.Up
|
||||||
traffic.Down = clientTraffic.Down
|
traffic.Down = clientTraffic.Down
|
||||||
traffic.Total = clientTraffic.Total
|
traffic.Total = clientTraffic.Total
|
||||||
if clientTraffic.ExpiryTime > 0 {
|
traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
traffic.Up += clientTraffic.Up
|
traffic.Up += clientTraffic.Up
|
||||||
traffic.Down += clientTraffic.Down
|
traffic.Down += clientTraffic.Down
|
||||||
|
|
@ -79,7 +77,8 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
|
||||||
} else {
|
} else {
|
||||||
traffic.Total += clientTraffic.Total
|
traffic.Total += clientTraffic.Total
|
||||||
}
|
}
|
||||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
|
if normalized != traffic.ExpiryTime {
|
||||||
traffic.ExpiryTime = 0
|
traffic.ExpiryTime = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||||
traffic.Up = clientTraffic.Up
|
traffic.Up = clientTraffic.Up
|
||||||
traffic.Down = clientTraffic.Down
|
traffic.Down = clientTraffic.Down
|
||||||
traffic.Total = clientTraffic.Total
|
traffic.Total = clientTraffic.Total
|
||||||
if clientTraffic.ExpiryTime > 0 {
|
traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
traffic.Up += clientTraffic.Up
|
traffic.Up += clientTraffic.Up
|
||||||
traffic.Down += clientTraffic.Down
|
traffic.Down += clientTraffic.Down
|
||||||
|
|
@ -141,7 +139,8 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
|
||||||
} else {
|
} else {
|
||||||
traffic.Total += clientTraffic.Total
|
traffic.Total += clientTraffic.Total
|
||||||
}
|
}
|
||||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
|
if normalized != traffic.ExpiryTime {
|
||||||
traffic.ExpiryTime = 0
|
traffic.ExpiryTime = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
|
||||||
traffic.Up = clientTraffic.Up
|
traffic.Up = clientTraffic.Up
|
||||||
traffic.Down = clientTraffic.Down
|
traffic.Down = clientTraffic.Down
|
||||||
traffic.Total = clientTraffic.Total
|
traffic.Total = clientTraffic.Total
|
||||||
if clientTraffic.ExpiryTime > 0 {
|
traffic.ExpiryTime = subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
traffic.Up += clientTraffic.Up
|
traffic.Up += clientTraffic.Up
|
||||||
traffic.Down += clientTraffic.Down
|
traffic.Down += clientTraffic.Down
|
||||||
|
|
@ -125,7 +123,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
|
||||||
} else {
|
} else {
|
||||||
traffic.Total += clientTraffic.Total
|
traffic.Total += clientTraffic.Total
|
||||||
}
|
}
|
||||||
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
normalized := subscriptionExpiryFromClient(clientTraffic.ExpiryTime)
|
||||||
|
if normalized != traffic.ExpiryTime {
|
||||||
traffic.ExpiryTime = 0
|
traffic.ExpiryTime = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +133,16 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
|
||||||
return result, lastOnline, traffic, nil
|
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) {
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,27 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
"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) {
|
func TestFindClientIndex(t *testing.T) {
|
||||||
clients := []model.Client{
|
clients := []model.Client{
|
||||||
{Email: "a@example.com"},
|
{Email: "a@example.com"},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue