fix(inbounds): allow negative subSortIndex for subscription order (#6465)

* fix(inbounds): allow negative subSortIndex for subscription order

Preserve explicitly set negative indices so primary inbounds can sort
ahead of the default without renumbering peers; keep 0/omitted → 1.

* fix(inbounds): gofumpt model.go and trim subSortIndex comments

---------

Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
mrchatam
2026-09-12 12:44:53 +03:30
committed by GitHub
parent b467d4c676
commit 51e0afdd90
16 changed files with 114 additions and 50 deletions
+1 -2
View File
@@ -2630,9 +2630,8 @@
"sniffing": {},
"streamSettings": {},
"subSortIndex": {
"description": "1-based sort order of this inbound's links in subscription output only (lower first; ties by id)",
"description": "Sort order of this inbound's links in subscription output only (lower first; negatives allowed; 0/omitted → 1; ties by id)",
"example": 1,
"minimum": 1,
"type": "integer"
},
"tag": {
+1 -2
View File
@@ -2604,9 +2604,8 @@ export const SCHEMAS: Record<string, unknown> = {
"sniffing": {},
"streamSettings": {},
"subSortIndex": {
"description": "1-based sort order of this inbound's links in subscription output only (lower first; ties by id)",
"description": "Sort order of this inbound's links in subscription output only (lower first; negatives allowed; 0/omitted → 1; ties by id)",
"example": 1,
"minimum": 1,
"type": "integer"
},
"tag": {
+1 -1
View File
@@ -638,7 +638,7 @@ export const InboundSchema = z.object({
shareAddrStrategy: z.enum(['node', 'listen', 'custom']),
sniffing: z.unknown(),
streamSettings: z.unknown(),
subSortIndex: z.number().int().min(1),
subSortIndex: z.number().int(),
tag: z.string(),
total: z.number().int(),
trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']),
@@ -210,7 +210,7 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
nodeId: row.nodeId ?? null,
shareAddrStrategy: coerceShareAddrStrategy(row.shareAddrStrategy),
shareAddr: row.shareAddr ?? '',
subSortIndex: Math.max(1, row.subSortIndex ?? 1),
subSortIndex: row.subSortIndex == null || row.subSortIndex === 0 ? 1 : row.subSortIndex,
disableFlow: row.disableFlow ?? false,
protocol,
settings,
@@ -697,7 +697,7 @@ export default function InboundFormModal({
t('pages.inbounds.form.subSortIndexHelp'),
)}
>
<InputNumber min={1} />
<InputNumber />
</FormField>
{protocol === Protocols.VLESS && (
@@ -119,7 +119,7 @@ export default function InboundList({
);
const hasAnySubSortIndex = useMemo(
() => dbInbounds.some((i) => (i.subSortIndex ?? 1) > 1),
() => dbInbounds.some((i) => (i.subSortIndex ?? 1) !== 1),
[dbInbounds],
);
+1 -1
View File
@@ -80,7 +80,7 @@ export const InboundDbFieldsSchema = z.object({
nodeId: z.number().int().nullable().optional(),
shareAddrStrategy: ShareAddrStrategySchema.default('node'),
shareAddr: z.string().default(''),
subSortIndex: z.number().int().min(1).default(1),
subSortIndex: z.number().int().default(1),
disableFlow: z.boolean().default(false),
});
export type InboundDbFields = z.infer<typeof InboundDbFieldsSchema>;
@@ -329,10 +329,10 @@ describe('subSortIndex', () => {
expect(values.subSortIndex).toBe(1);
});
it('rawInboundToFormValues preserves valid values and clamps below-minimum ones to 1', () => {
it('rawInboundToFormValues preserves positives and negatives; maps 0/absent to 1', () => {
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: 5 }).subSortIndex).toBe(5);
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: 0 }).subSortIndex).toBe(1);
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: -10 }).subSortIndex).toBe(1);
expect(rawInboundToFormValues({ ...vlessRow, subSortIndex: -10 }).subSortIndex).toBe(-10);
});
it('formValuesToWirePayload includes subSortIndex in the payload', () => {
@@ -348,18 +348,15 @@ describe('subSortIndex', () => {
expect(replay.subSortIndex).toBe(42);
});
it('InboundDbFieldsSchema enforces an integer minimum of 1 and defaults to 1', () => {
it('InboundDbFieldsSchema accepts integers including negatives and defaults to 1', () => {
// Reject for the RIGHT reason: the issue must be about subSortIndex, not some
// unrelated field — otherwise a schema that rejects everything would pass.
const nonInt = InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 1.5 });
expect(nonInt.success).toBe(false);
if (!nonInt.success) expect(nonInt.error.issues[0]?.path).toContain('subSortIndex');
const belowMin = InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 0 });
expect(belowMin.success).toBe(false);
if (!belowMin.success) expect(belowMin.error.issues[0]?.path).toContain('subSortIndex');
// A valid integer >= 1 must pass (guards against a mutant rejecting all values).
expect(InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 0 }).success).toBe(true);
expect(InboundDbFieldsSchema.partial().safeParse({ subSortIndex: -1 }).success).toBe(true);
expect(InboundDbFieldsSchema.partial().safeParse({ subSortIndex: 5 }).success).toBe(true);
expect(InboundDbFieldsSchema.parse({}).subSortIndex).toBe(1);
});