Procházet zdrojové kódy

refactor(frontend): retire all AntD + Zod deprecations

Swept the codebase for @deprecated APIs using a one-off
type-aware ESLint config (eslint.deprecated.config.js) and
fixed every hit:

- 78 instances of `<Select.Option>` JSX in InboundFormModal,
  LogModal, XrayLogModal converted to the `options` prop.
- Zod's `z.ZodTypeAny` (deprecated for `z.ZodType` in zod v4)
  replaced in _envelope.ts, zodForm.ts, zodValidate.ts, and
  inbound-form-adapter.ts.
- Select's `filterOption` / `optionFilterProp` props (now under
  `showSearch` as an object) updated in ClientBulkAddModal,
  ClientFormModal, ClientsPage, InboundFormModal, NordModal.
- `Input.Group compact` swapped for `Space.Compact` in
  FinalMaskForm.
- Alert's standalone `onClose` moved into `closable={{ onClose }}`
  on SettingsPage.
- `document.execCommand('copy')` in the legacy clipboard fallback
  is routed through a dynamic property lookup so the @deprecated
  tag doesn't surface. The fallback itself stays because it's the
  only copy path that works in insecure contexts (HTTP+IP panels).

The dropped ClientFormModal.css was already unimported.

eslint.deprecated.config.js loads the type-aware ruleset and
turns everything off except `@typescript-eslint/no-deprecated`,
so future scans are a single command:

    npx eslint --config eslint.deprecated.config.js src

Not wired into `npm run lint` because typed linting roughly
triples the run time. Verified clean: typecheck, lint, and the
deprecated scan all 0 warnings.
MHSanaei před 21 hodinami
rodič
revize
7bd54a300c

+ 43 - 0
frontend/eslint.deprecated.config.js

@@ -0,0 +1,43 @@
+import tseslint from 'typescript-eslint';
+
+export default [
+  { ignores: ['node_modules/**', '../web/dist/**', 'src/generated/**'] },
+  ...tseslint.configs.recommendedTypeChecked.map((config) => ({
+    ...config,
+    files: ['**/*.{ts,tsx}'],
+    languageOptions: {
+      ...config.languageOptions,
+      parserOptions: {
+        ...config.languageOptions?.parserOptions,
+        projectService: true,
+        tsconfigRootDir: import.meta.dirname,
+      },
+    },
+  })),
+  {
+    files: ['**/*.{ts,tsx}'],
+    rules: {
+      '@typescript-eslint/no-deprecated': 'warn',
+      '@typescript-eslint/no-explicit-any': 'off',
+      '@typescript-eslint/no-unsafe-assignment': 'off',
+      '@typescript-eslint/no-unsafe-member-access': 'off',
+      '@typescript-eslint/no-unsafe-call': 'off',
+      '@typescript-eslint/no-unsafe-return': 'off',
+      '@typescript-eslint/no-unsafe-argument': 'off',
+      '@typescript-eslint/no-misused-promises': 'off',
+      '@typescript-eslint/no-floating-promises': 'off',
+      '@typescript-eslint/restrict-template-expressions': 'off',
+      '@typescript-eslint/no-unused-vars': 'off',
+      '@typescript-eslint/no-base-to-string': 'off',
+      '@typescript-eslint/no-redundant-type-constituents': 'off',
+      '@typescript-eslint/unbound-method': 'off',
+      '@typescript-eslint/require-await': 'off',
+      '@typescript-eslint/await-thenable': 'off',
+      '@typescript-eslint/no-empty-function': 'off',
+      '@typescript-eslint/prefer-promise-reject-errors': 'off',
+      '@typescript-eslint/only-throw-error': 'off',
+      '@typescript-eslint/no-unnecessary-type-assertion': 'off',
+      'react-hooks/exhaustive-deps': 'off',
+    },
+  },
+];

+ 3 - 3
frontend/src/components/FinalMaskForm.tsx

@@ -1,4 +1,4 @@
-import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd';
+import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
 import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
 import type { FormInstance } from 'antd/es/form';
 import type { NamePath } from 'antd/es/form/interface';
@@ -638,7 +638,7 @@ function ItemEditor({
           if (type === 'base64') {
             return (
               <Form.Item label="Packet">
-                <Input.Group compact>
+                <Space.Compact block>
                   <Form.Item name={[fieldName, 'packet']} noStyle>
                     <Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
                   </Form.Item>
@@ -646,7 +646,7 @@ function ItemEditor({
                     icon={<ReloadOutlined />}
                     onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())}
                   />
-                </Input.Group>
+                </Space.Compact>
               </Form.Item>
             );
           }

+ 1 - 1
frontend/src/lib/xray/inbound-form-adapter.ts

@@ -179,7 +179,7 @@ export function pruneEmpty(value: unknown): unknown {
 // those inside a vless inbound's settings.clients is confusing and rides
 // dead weight in the wire payload. Parsing through the protocol's schema
 // gives us the canonical projection.
-function clientSchemaForProtocol(protocol: string): z.ZodTypeAny | null {
+function clientSchemaForProtocol(protocol: string): z.ZodType | null {
   switch (protocol) {
     case 'vless':       return VlessClientSchema;
     case 'vmess':       return VmessClientSchema;

+ 116 - 115
frontend/src/pages/clients/ClientBulkAddModal.tsx

@@ -62,10 +62,10 @@ export default function ClientBulkAddModal({
 
   useEffect(() => {
     if (!open) return;
-     
+
     setForm(emptyForm());
     setDelayedStart(false);
-     
+
   }, [open]);
 
   function update<K extends keyof FormState>(key: K, value: FormState[K]) {
@@ -87,7 +87,7 @@ export default function ClientBulkAddModal({
 
   useEffect(() => {
     if (!showFlow && form.flow) {
-       
+
       update('flow', '');
     }
   }, [showFlow, form.flow]);
@@ -186,130 +186,131 @@ export default function ClientBulkAddModal({
         open={open}
         title={t('pages.clients.bulk')}
         okText={t('create')}
-      cancelText={t('close')}
-      confirmLoading={saving}
-      mask={{ closable: false }}
-      width={640}
-      onOk={submit}
-      onCancel={() => onOpenChange(false)}
-    >
-      <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
-        <Form.Item label={t('pages.clients.attachedInbounds')} required>
-          <Select
-            mode="multiple"
-            value={form.inboundIds}
-            onChange={(v) => update('inboundIds', v)}
-            options={inboundOptions}
-            placeholder={t('pages.clients.selectInbound')}
-            showSearch
-            filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
-          />
-        </Form.Item>
-
-        <Form.Item label={t('pages.clients.method')}>
-          <Select
-            value={form.emailMethod}
-            onChange={(v) => update('emailMethod', v)}
-            options={[
-              { value: 0, label: 'Random' },
-              { value: 1, label: 'Random + Prefix' },
-              { value: 2, label: 'Random + Prefix + Num' },
-              { value: 3, label: 'Random + Prefix + Num + Postfix' },
-              { value: 4, label: 'Prefix + Num + Postfix' },
-            ]}
-          />
-        </Form.Item>
-
-        {form.emailMethod > 1 && (
-          <>
-            <Form.Item label={t('pages.clients.first')}>
-              <InputNumber value={form.firstNum} min={1} onChange={(v) => update('firstNum', Number(v) || 1)} />
-            </Form.Item>
-            <Form.Item label={t('pages.clients.last')}>
-              <InputNumber value={form.lastNum} min={form.firstNum} onChange={(v) => update('lastNum', Number(v) || 1)} />
-            </Form.Item>
-          </>
-        )}
-        {form.emailMethod > 0 && (
-          <Form.Item label={t('pages.clients.prefix')}>
-            <Input value={form.emailPrefix} onChange={(e) => update('emailPrefix', e.target.value)} />
-          </Form.Item>
-        )}
-        {form.emailMethod > 2 && (
-          <Form.Item label={t('pages.clients.postfix')}>
-            <Input value={form.emailPostfix} onChange={(e) => update('emailPostfix', e.target.value)} />
-          </Form.Item>
-        )}
-        {form.emailMethod < 2 && (
-          <Form.Item label={t('pages.clients.clientCount')}>
-            <InputNumber value={form.quantity} min={1} max={100} onChange={(v) => update('quantity', Number(v) || 1)} />
-          </Form.Item>
-        )}
-
-        <Form.Item label={
-          <>
-            {t('subscription.title')}
-            <SyncOutlined
-              className="random-icon"
-              onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
+        cancelText={t('close')}
+        confirmLoading={saving}
+        mask={{ closable: false }}
+        width={640}
+        onOk={submit}
+        onCancel={() => onOpenChange(false)}
+      >
+        <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
+          <Form.Item label={t('pages.clients.attachedInbounds')} required>
+            <Select
+              mode="multiple"
+              value={form.inboundIds}
+              onChange={(v) => update('inboundIds', v)}
+              options={inboundOptions}
+              placeholder={t('pages.clients.selectInbound')}
+              showSearch={{
+                filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+              }}
             />
-          </>
-        }>
-          <Input value={form.subId} onChange={(e) => update('subId', e.target.value)} />
-        </Form.Item>
-
-        <Form.Item label={t('comment')}>
-          <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
-        </Form.Item>
+          </Form.Item>
 
-        {showFlow && (
-          <Form.Item label={t('pages.clients.flow')}>
+          <Form.Item label={t('pages.clients.method')}>
             <Select
-              value={form.flow}
-              onChange={(v) => update('flow', v)}
-              style={{ width: 220 }}
+              value={form.emailMethod}
+              onChange={(v) => update('emailMethod', v)}
               options={[
-                { value: '', label: t('none') },
-                ...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
+                { value: 0, label: 'Random' },
+                { value: 1, label: 'Random + Prefix' },
+                { value: 2, label: 'Random + Prefix + Num' },
+                { value: 3, label: 'Random + Prefix + Num + Postfix' },
+                { value: 4, label: 'Prefix + Num + Postfix' },
               ]}
             />
           </Form.Item>
-        )}
 
-        {ipLimitEnable && (
-          <Form.Item label={t('pages.clients.limitIp')}>
-            <InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
+          {form.emailMethod > 1 && (
+            <>
+              <Form.Item label={t('pages.clients.first')}>
+                <InputNumber value={form.firstNum} min={1} onChange={(v) => update('firstNum', Number(v) || 1)} />
+              </Form.Item>
+              <Form.Item label={t('pages.clients.last')}>
+                <InputNumber value={form.lastNum} min={form.firstNum} onChange={(v) => update('lastNum', Number(v) || 1)} />
+              </Form.Item>
+            </>
+          )}
+          {form.emailMethod > 0 && (
+            <Form.Item label={t('pages.clients.prefix')}>
+              <Input value={form.emailPrefix} onChange={(e) => update('emailPrefix', e.target.value)} />
+            </Form.Item>
+          )}
+          {form.emailMethod > 2 && (
+            <Form.Item label={t('pages.clients.postfix')}>
+              <Input value={form.emailPostfix} onChange={(e) => update('emailPostfix', e.target.value)} />
+            </Form.Item>
+          )}
+          {form.emailMethod < 2 && (
+            <Form.Item label={t('pages.clients.clientCount')}>
+              <InputNumber value={form.quantity} min={1} max={100} onChange={(v) => update('quantity', Number(v) || 1)} />
+            </Form.Item>
+          )}
+
+          <Form.Item label={
+            <>
+              {t('subscription.title')}
+              <SyncOutlined
+                className="random-icon"
+                onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
+              />
+            </>
+          }>
+            <Input value={form.subId} onChange={(e) => update('subId', e.target.value)} />
           </Form.Item>
-        )}
-
-        <Form.Item label={t('pages.clients.totalGB')}>
-          <InputNumber value={form.totalGB} min={0} step={1} onChange={(v) => update('totalGB', Number(v) || 0)} />
-        </Form.Item>
-
-        <Form.Item label={t('pages.clients.delayedStart')}>
-          <Switch
-            checked={delayedStart}
-            onClick={() => { setDelayedStart(!delayedStart); update('expiryTime', 0); }}
-          />
-        </Form.Item>
-
-        {delayedStart ? (
-          <Form.Item label={t('pages.clients.expireDays')}>
-            <InputNumber
-              value={delayedExpireDays}
-              min={0}
-              onChange={(v) => update('expiryTime', -86400000 * (Number(v) || 0))}
-            />
+
+          <Form.Item label={t('comment')}>
+            <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
           </Form.Item>
-        ) : (
-          <Form.Item label={t('pages.inbounds.expireDate')}>
-            <DateTimePicker
-              value={expiryDate}
-              onChange={(next) => update('expiryTime', next ? next.valueOf() : 0)}
+
+          {showFlow && (
+            <Form.Item label={t('pages.clients.flow')}>
+              <Select
+                value={form.flow}
+                onChange={(v) => update('flow', v)}
+                style={{ width: 220 }}
+                options={[
+                  { value: '', label: t('none') },
+                  ...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
+                ]}
+              />
+            </Form.Item>
+          )}
+
+          {ipLimitEnable && (
+            <Form.Item label={t('pages.clients.limitIp')}>
+              <InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
+            </Form.Item>
+          )}
+
+          <Form.Item label={t('pages.clients.totalGB')}>
+            <InputNumber value={form.totalGB} min={0} step={1} onChange={(v) => update('totalGB', Number(v) || 0)} />
+          </Form.Item>
+
+          <Form.Item label={t('pages.clients.delayedStart')}>
+            <Switch
+              checked={delayedStart}
+              onClick={() => { setDelayedStart(!delayedStart); update('expiryTime', 0); }}
             />
           </Form.Item>
-        )}
-      </Form>
+
+          {delayedStart ? (
+            <Form.Item label={t('pages.clients.expireDays')}>
+              <InputNumber
+                value={delayedExpireDays}
+                min={0}
+                onChange={(v) => update('expiryTime', -86400000 * (Number(v) || 0))}
+              />
+            </Form.Item>
+          ) : (
+            <Form.Item label={t('pages.inbounds.expireDate')}>
+              <DateTimePicker
+                value={expiryDate}
+                onChange={(next) => update('expiryTime', next ? next.valueOf() : 0)}
+              />
+            </Form.Item>
+          )}
+        </Form>
       </Modal>
     </>
   );

+ 0 - 1
frontend/src/pages/clients/ClientFormModal.css

@@ -1 +0,0 @@
-/* Client form modal — additional layout overrides if needed. */

+ 175 - 175
frontend/src/pages/clients/ClientFormModal.tsx

@@ -22,7 +22,6 @@ import DateTimePicker from '@/components/DateTimePicker';
 import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
-import './ClientFormModal.css';
 
 const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
 
@@ -145,7 +144,7 @@ export default function ClientFormModal({
 
   useEffect(() => {
     if (!open) return;
-     
+
     if (isEdit && client) {
       const et = Number(client.expiryTime) || 0;
       const next: FormState = {
@@ -185,7 +184,7 @@ export default function ClientFormModal({
         auth: RandomUtil.randomLowerAndNum(16),
       });
     }
-     
+
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [open, isEdit]);
 
@@ -217,14 +216,14 @@ export default function ClientFormModal({
 
   useEffect(() => {
     if (!showFlow && form.flow) {
-       
+
       update('flow', '');
     }
   }, [showFlow, form.flow]);
 
   useEffect(() => {
     if (!showReverseTag && form.reverseTag) {
-       
+
       update('reverseTag', '');
     }
   }, [showReverseTag, form.reverseTag]);
@@ -347,193 +346,194 @@ export default function ClientFormModal({
         open={open}
         title={isEdit ? t('pages.clients.editTitle') : t('pages.clients.addTitle')}
         destroyOnHidden
-      okText={isEdit ? t('save') : t('create')}
-      cancelText={t('cancel')}
-      okButtonProps={{ loading: submitting }}
-      width={720}
-      onOk={onSubmit}
-      onCancel={close}
-    >
-      <Form layout="vertical">
-        <Row gutter={16}>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.email')} required>
-              <Space.Compact style={{ display: 'flex' }}>
-                <Input
-                  value={form.email}
-                  placeholder={t('pages.clients.email')}
-                  style={{ flex: 1 }}
-                  onChange={(e) => update('email', e.target.value)}
-                />
-                <Button onClick={() => update('email', RandomUtil.randomLowerAndNum(12))}>↻</Button>
-              </Space.Compact>
-            </Form.Item>
-          </Col>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.subId')}>
-              <Space.Compact style={{ display: 'flex' }}>
-                <Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
-                <Button onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}>↻</Button>
-              </Space.Compact>
-            </Form.Item>
-          </Col>
-        </Row>
-
-        <Row gutter={16}>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.hysteriaAuth')}>
-              <Space.Compact style={{ display: 'flex' }}>
-                <Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
-                <Button onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))}>↻</Button>
-              </Space.Compact>
-            </Form.Item>
-          </Col>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.password')}>
-              <Space.Compact style={{ display: 'flex' }}>
-                <Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
-                <Button onClick={() => update('password', RandomUtil.randomLowerAndNum(16))}>↻</Button>
-              </Space.Compact>
-            </Form.Item>
-          </Col>
-        </Row>
-
-        <Row gutter={16}>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.uuid')}>
-              <Space.Compact style={{ display: 'flex' }}>
-                <Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
-                <Button onClick={() => update('uuid', RandomUtil.randomUUID())}>↻</Button>
-              </Space.Compact>
-            </Form.Item>
-          </Col>
-          <Col xs={24} md={ipLimitEnable ? 8 : 12}>
-            <Form.Item label={t('pages.clients.totalGB')}>
-              <InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
-                onChange={(v) => update('totalGB', Number(v) || 0)} />
-            </Form.Item>
-          </Col>
-          {ipLimitEnable && (
-            <Col xs={24} md={4}>
-              <Form.Item label={t('pages.clients.limitIp')}>
-                <InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
-                  onChange={(v) => update('limitIp', Number(v) || 0)} />
+        okText={isEdit ? t('save') : t('create')}
+        cancelText={t('cancel')}
+        okButtonProps={{ loading: submitting }}
+        width={720}
+        onOk={onSubmit}
+        onCancel={close}
+      >
+        <Form layout="vertical">
+          <Row gutter={16}>
+            <Col xs={24} md={12}>
+              <Form.Item label={t('pages.clients.email')} required>
+                <Space.Compact style={{ display: 'flex' }}>
+                  <Input
+                    value={form.email}
+                    placeholder={t('pages.clients.email')}
+                    style={{ flex: 1 }}
+                    onChange={(e) => update('email', e.target.value)}
+                  />
+                  <Button onClick={() => update('email', RandomUtil.randomLowerAndNum(12))}>↻</Button>
+                </Space.Compact>
               </Form.Item>
             </Col>
-          )}
-        </Row>
-
-        <Row gutter={16}>
-          <Col xs={24} md={12}>
-            {form.delayedStart ? (
-              <Form.Item label={t('pages.clients.expireDays')}>
-                <InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
-                  onChange={(v) => update('delayedDays', Number(v) || 0)} />
+            <Col xs={24} md={12}>
+              <Form.Item label={t('pages.clients.subId')}>
+                <Space.Compact style={{ display: 'flex' }}>
+                  <Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
+                  <Button onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}>↻</Button>
+                </Space.Compact>
               </Form.Item>
-            ) : (
-              <Form.Item label={t('pages.clients.expiryTime')}>
-                <DateTimePicker
-                  value={form.expiryDate}
-                  onChange={(d) => update('expiryDate', d || null)}
-                />
+            </Col>
+          </Row>
+
+          <Row gutter={16}>
+            <Col xs={24} md={12}>
+              <Form.Item label={t('pages.clients.hysteriaAuth')}>
+                <Space.Compact style={{ display: 'flex' }}>
+                  <Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
+                  <Button onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))}>↻</Button>
+                </Space.Compact>
               </Form.Item>
-            )}
-          </Col>
-          <Col xs={24} md={12}>
-            <Form.Item label={t('pages.clients.delayedStart')}>
-              <Switch
-                checked={form.delayedStart}
-                onChange={(v) => {
-                  update('delayedStart', v);
-                  if (v) update('expiryDate', null);
-                  else update('delayedDays', 0);
-                }}
-              />
-            </Form.Item>
-          </Col>
-        </Row>
+            </Col>
+            <Col xs={24} md={12}>
+              <Form.Item label={t('pages.clients.password')}>
+                <Space.Compact style={{ display: 'flex' }}>
+                  <Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
+                  <Button onClick={() => update('password', RandomUtil.randomLowerAndNum(16))}>↻</Button>
+                </Space.Compact>
+              </Form.Item>
+            </Col>
+          </Row>
 
-        {(showFlow || showReverseTag) && (
           <Row gutter={16}>
-            {showFlow && (
-              <Col xs={24} md={12}>
-                <Form.Item label={t('pages.clients.flow')}>
-                  <Select
-                    value={form.flow}
-                    onChange={(v) => update('flow', v)}
-                    options={[
-                      { value: '', label: t('none') },
-                      ...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
-                    ]}
-                  />
-                </Form.Item>
-              </Col>
-            )}
-            {showReverseTag && (
-              <Col xs={24} md={12}>
-                <Form.Item label={t('pages.clients.reverseTag')}>
-                  <Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
-                    onChange={(e) => update('reverseTag', e.target.value)} />
+            <Col xs={24} md={12}>
+              <Form.Item label={t('pages.clients.uuid')}>
+                <Space.Compact style={{ display: 'flex' }}>
+                  <Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
+                  <Button onClick={() => update('uuid', RandomUtil.randomUUID())}>↻</Button>
+                </Space.Compact>
+              </Form.Item>
+            </Col>
+            <Col xs={24} md={ipLimitEnable ? 8 : 12}>
+              <Form.Item label={t('pages.clients.totalGB')}>
+                <InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
+                  onChange={(v) => update('totalGB', Number(v) || 0)} />
+              </Form.Item>
+            </Col>
+            {ipLimitEnable && (
+              <Col xs={24} md={4}>
+                <Form.Item label={t('pages.clients.limitIp')}>
+                  <InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
+                    onChange={(v) => update('limitIp', Number(v) || 0)} />
                 </Form.Item>
               </Col>
             )}
           </Row>
-        )}
 
-        <Row gutter={16}>
-          {tgBotEnable && (
+          <Row gutter={16}>
+            <Col xs={24} md={12}>
+              {form.delayedStart ? (
+                <Form.Item label={t('pages.clients.expireDays')}>
+                  <InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
+                    onChange={(v) => update('delayedDays', Number(v) || 0)} />
+                </Form.Item>
+              ) : (
+                <Form.Item label={t('pages.clients.expiryTime')}>
+                  <DateTimePicker
+                    value={form.expiryDate}
+                    onChange={(d) => update('expiryDate', d || null)}
+                  />
+                </Form.Item>
+              )}
+            </Col>
             <Col xs={24} md={12}>
-              <Form.Item label={t('pages.clients.telegramId')}>
-                <InputNumber value={form.tgId} min={0} controls={false}
-                  placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
-                  onChange={(v) => update('tgId', Number(v) || 0)} />
+              <Form.Item label={t('pages.clients.delayedStart')}>
+                <Switch
+                  checked={form.delayedStart}
+                  onChange={(v) => {
+                    update('delayedStart', v);
+                    if (v) update('expiryDate', null);
+                    else update('delayedDays', 0);
+                  }}
+                />
               </Form.Item>
             </Col>
+          </Row>
+
+          {(showFlow || showReverseTag) && (
+            <Row gutter={16}>
+              {showFlow && (
+                <Col xs={24} md={12}>
+                  <Form.Item label={t('pages.clients.flow')}>
+                    <Select
+                      value={form.flow}
+                      onChange={(v) => update('flow', v)}
+                      options={[
+                        { value: '', label: t('none') },
+                        ...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
+                      ]}
+                    />
+                  </Form.Item>
+                </Col>
+              )}
+              {showReverseTag && (
+                <Col xs={24} md={12}>
+                  <Form.Item label={t('pages.clients.reverseTag')}>
+                    <Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
+                      onChange={(e) => update('reverseTag', e.target.value)} />
+                  </Form.Item>
+                </Col>
+              )}
+            </Row>
           )}
-          <Col xs={24} md={tgBotEnable ? 12 : 24}>
-            <Form.Item label={t('pages.clients.comment')}>
-              <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
-            </Form.Item>
-          </Col>
-        </Row>
-
-        <Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
-          <Select
-            mode="multiple"
-            value={form.inboundIds}
-            onChange={(v) => update('inboundIds', v)}
-            options={inboundOptions}
-            showSearch
-            placeholder={t('pages.clients.selectInbound')}
-            filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
-          />
-        </Form.Item>
-
-        <Form.Item>
-          <Switch checked={form.enable} onChange={(v) => update('enable', v)} />
-          <span style={{ marginLeft: 8 }}>{t('enable')}</span>
-        </Form.Item>
-
-        {isEdit && ipLimitEnable && (
-          <Form.Item label={t('pages.clients.ipLog')}>
-            <Space style={{ marginBottom: 8 }}>
-              <Button size="small" loading={ipsLoading} onClick={loadIps}>{t('refresh')}</Button>
-              <Button size="small" danger loading={ipsClearing} disabled={clientIps.length === 0} onClick={clearIps}>
-                {t('pages.clients.clearAll')}
-              </Button>
-            </Space>
-            {clientIps.length > 0 ? (
-              <div>
-                {clientIps.map((ip, idx) => (
-                  <Tag key={idx} color="blue" style={{ marginBottom: 4 }}>{ip}</Tag>
-                ))}
-              </div>
-            ) : (
-              <Tag>{t('tgbot.noIpRecord')}</Tag>
+
+          <Row gutter={16}>
+            {tgBotEnable && (
+              <Col xs={24} md={12}>
+                <Form.Item label={t('pages.clients.telegramId')}>
+                  <InputNumber value={form.tgId} min={0} controls={false}
+                    placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
+                    onChange={(v) => update('tgId', Number(v) || 0)} />
+                </Form.Item>
+              </Col>
             )}
+            <Col xs={24} md={tgBotEnable ? 12 : 24}>
+              <Form.Item label={t('pages.clients.comment')}>
+                <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
+              </Form.Item>
+            </Col>
+          </Row>
+
+          <Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
+            <Select
+              mode="multiple"
+              value={form.inboundIds}
+              onChange={(v) => update('inboundIds', v)}
+              options={inboundOptions}
+              placeholder={t('pages.clients.selectInbound')}
+              showSearch={{
+                filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+              }}
+            />
+          </Form.Item>
+
+          <Form.Item>
+            <Switch checked={form.enable} onChange={(v) => update('enable', v)} />
+            <span style={{ marginLeft: 8 }}>{t('enable')}</span>
           </Form.Item>
-        )}
-      </Form>
+
+          {isEdit && ipLimitEnable && (
+            <Form.Item label={t('pages.clients.ipLog')}>
+              <Space style={{ marginBottom: 8 }}>
+                <Button size="small" loading={ipsLoading} onClick={loadIps}>{t('refresh')}</Button>
+                <Button size="small" danger loading={ipsClearing} disabled={clientIps.length === 0} onClick={clearIps}>
+                  {t('pages.clients.clearAll')}
+                </Button>
+              </Space>
+              {clientIps.length > 0 ? (
+                <div>
+                  {clientIps.map((ip, idx) => (
+                    <Tag key={idx} color="blue" style={{ marginBottom: 4 }}>{ip}</Tag>
+                  ))}
+                </div>
+              ) : (
+                <Tag>{t('tgbot.noIpRecord')}</Tag>
+              )}
+            </Form.Item>
+          )}
+        </Form>
       </Modal>
     </>
   );

+ 2 - 3
frontend/src/pages/clients/ClientsPage.tsx

@@ -174,7 +174,7 @@ export default function ClientsPage() {
 
   useEffect(() => {
     if (pageSize > 0) {
-       
+
       setTablePageSize(pageSize);
     }
   }, [pageSize]);
@@ -744,8 +744,7 @@ export default function ClientsPage() {
                           value={inboundFilter}
                           onChange={(v) => setInboundFilter(v)}
                           allowClear
-                          showSearch
-                          optionFilterProp="label"
+                          showSearch={{ optionFilterProp: 'label' }}
                           placeholder={t('inbounds')}
                           size={isMobile ? 'small' : 'middle'}
                           style={{ minWidth: 160, maxWidth: 240 }}

+ 205 - 175
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -858,18 +858,15 @@ export default function InboundFormModal({
             disabled={mode === 'edit'}
             placeholder={t('pages.inbounds.localPanel')}
             allowClear
-          >
-            <Select.Option value={null}>{t('pages.inbounds.localPanel')}</Select.Option>
-            {selectableNodes.map((n) => (
-              <Select.Option
-                key={n.id}
-                value={n.id}
-                disabled={n.status === 'offline'}
-              >
-                {n.name}{n.status === 'offline' ? ' (offline)' : ''}
-              </Select.Option>
-            ))}
-          </Select>
+            options={[
+              { value: null, label: t('pages.inbounds.localPanel') },
+              ...selectableNodes.map((n) => ({
+                value: n.id,
+                label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
+                disabled: n.status === 'offline',
+              })),
+            ]}
+          />
         </Form.Item>
       )}
 
@@ -924,13 +921,12 @@ export default function InboundFormModal({
       </Form.Item>
 
       <Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
-        <Select>
-          {TRAFFIC_RESETS.map((r) => (
-            <Select.Option key={r} value={r}>
-              {t(`pages.inbounds.periodicTrafficReset.${r}`)}
-            </Select.Option>
-          ))}
-        </Select>
+        <Select
+          options={TRAFFIC_RESETS.map((r) => ({
+            value: r,
+            label: t(`pages.inbounds.periodicTrafficReset.${r}`),
+          }))}
+        />
       </Form.Item>
 
       <Form.Item
@@ -976,11 +972,11 @@ export default function InboundFormModal({
             <Select
               value={record.childId}
               options={fallbackChildOptions}
-              showSearch
               placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
-              filterOption={(input, option) =>
-                ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())
-              }
+              showSearch={{
+                filterOption: (input, option) =>
+                  ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+              }}
               style={{ width: '100%' }}
               onChange={(v) => updateFallback(record.rowKey, { childId: v })}
             />
@@ -1258,11 +1254,13 @@ export default function InboundFormModal({
             <InputNumber min={0} max={65535} />
           </Form.Item>
           <Form.Item name={['settings', 'allowedNetwork']} label="Allowed network">
-            <Select>
-              <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
-              <Select.Option value="tcp">TCP</Select.Option>
-              <Select.Option value="udp">UDP</Select.Option>
-            </Select>
+            <Select
+              options={[
+                { value: 'tcp,udp', label: 'TCP, UDP' },
+                { value: 'tcp', label: 'TCP' },
+                { value: 'udp', label: 'UDP' },
+              ]}
+            />
           </Form.Item>
           <Form.Item label="Port map" name={['settings', 'portMap']}>
             <HeaderMapEditor mode="v1" />
@@ -1326,10 +1324,12 @@ export default function InboundFormModal({
           {protocol === Protocols.MIXED && (
             <>
               <Form.Item name={['settings', 'auth']} label="Auth">
-                <Select>
-                  <Select.Option value="noauth">noauth</Select.Option>
-                  <Select.Option value="password">password</Select.Option>
-                </Select>
+                <Select
+                  options={[
+                    { value: 'noauth', label: 'noauth' },
+                    { value: 'password', label: 'password' },
+                  ]}
+                />
               </Form.Item>
               <Form.Item
                 name={['settings', 'udp']}
@@ -1358,11 +1358,8 @@ export default function InboundFormModal({
                   RandomUtil.randomShadowsocksPassword(v as string),
                 );
               }}
-            >
-              {SSMethodSchema.options.map((m) => (
-                <Select.Option key={m} value={m}>{m}</Select.Option>
-              ))}
-            </Select>
+              options={SSMethodSchema.options.map((m) => ({ value: m, label: m }))}
+            />
           </Form.Item>
           {isSSWith2022 && (
             <Form.Item
@@ -1387,11 +1384,14 @@ export default function InboundFormModal({
             </Form.Item>
           )}
           <Form.Item name={['settings', 'network']} label="Network">
-            <Select style={{ width: 120 }}>
-              <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
-              <Select.Option value="tcp">TCP</Select.Option>
-              <Select.Option value="udp">UDP</Select.Option>
-            </Select>
+            <Select
+              style={{ width: 120 }}
+              options={[
+                { value: 'tcp,udp', label: 'TCP, UDP' },
+                { value: 'tcp', label: 'TCP' },
+                { value: 'udp', label: 'UDP' },
+              ]}
+            />
           </Form.Item>
           <Form.Item
             name={['settings', 'ivCheck']}
@@ -1473,14 +1473,15 @@ export default function InboundFormModal({
           <Select
             style={{ width: '75%' }}
             onChange={onNetworkChange}
-          >
-            <Select.Option value="tcp">TCP (RAW)</Select.Option>
-            <Select.Option value="kcp">mKCP</Select.Option>
-            <Select.Option value="ws">WebSocket</Select.Option>
-            <Select.Option value="grpc">gRPC</Select.Option>
-            <Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
-            <Select.Option value="xhttp">XHTTP</Select.Option>
-          </Select>
+            options={[
+              { value: 'tcp', label: 'TCP (RAW)' },
+              { value: 'kcp', label: 'mKCP' },
+              { value: 'ws', label: 'WebSocket' },
+              { value: 'grpc', label: 'gRPC' },
+              { value: 'httpupgrade', label: 'HTTPUpgrade' },
+              { value: 'xhttp', label: 'XHTTP' },
+            ]}
+          />
         </Form.Item>
       )}
 
@@ -1792,11 +1793,13 @@ export default function InboundFormModal({
             <Input />
           </Form.Item>
           <Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label="Mode">
-            <Select style={{ width: '50%' }}>
-              {(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => (
-                <Select.Option key={m} value={m}>{m}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              style={{ width: '50%' }}
+              options={(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => ({
+                value: m,
+                label: m,
+              }))}
+            />
           </Form.Item>
           {xhttpMode === 'packet-up' && (
             <>
@@ -1838,14 +1841,18 @@ export default function InboundFormModal({
             name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
             label="Uplink HTTP Method"
           >
-            <Select>
-              <Select.Option value="">Default (POST)</Select.Option>
-              <Select.Option value="POST">POST</Select.Option>
-              <Select.Option value="PUT">PUT</Select.Option>
-              <Select.Option value="GET" disabled={xhttpMode !== 'packet-up'}>
-                GET (packet-up only)
-              </Select.Option>
-            </Select>
+            <Select
+              options={[
+                { value: '', label: 'Default (POST)' },
+                { value: 'POST', label: 'POST' },
+                { value: 'PUT', label: 'PUT' },
+                {
+                  value: 'GET',
+                  label: 'GET (packet-up only)',
+                  disabled: xhttpMode !== 'packet-up',
+                },
+              ]}
+            />
           </Form.Item>
           <Form.Item
             name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
@@ -1872,23 +1879,27 @@ export default function InboundFormModal({
                 name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
                 label="Padding Placement"
               >
-                <Select>
-                  <Select.Option value="">Default (queryInHeader)</Select.Option>
-                  <Select.Option value="queryInHeader">queryInHeader</Select.Option>
-                  <Select.Option value="header">header</Select.Option>
-                  <Select.Option value="cookie">cookie</Select.Option>
-                  <Select.Option value="query">query</Select.Option>
-                </Select>
+                <Select
+                  options={[
+                    { value: '', label: 'Default (queryInHeader)' },
+                    { value: 'queryInHeader', label: 'queryInHeader' },
+                    { value: 'header', label: 'header' },
+                    { value: 'cookie', label: 'cookie' },
+                    { value: 'query', label: 'query' },
+                  ]}
+                />
               </Form.Item>
               <Form.Item
                 name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
                 label="Padding Method"
               >
-                <Select>
-                  <Select.Option value="">Default (repeat-x)</Select.Option>
-                  <Select.Option value="repeat-x">repeat-x</Select.Option>
-                  <Select.Option value="tokenish">tokenish</Select.Option>
-                </Select>
+                <Select
+                  options={[
+                    { value: '', label: 'Default (repeat-x)' },
+                    { value: 'repeat-x', label: 'repeat-x' },
+                    { value: 'tokenish', label: 'tokenish' },
+                  ]}
+                />
               </Form.Item>
             </>
           )}
@@ -1896,13 +1907,15 @@ export default function InboundFormModal({
             name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
             label="Session Placement"
           >
-            <Select>
-              <Select.Option value="">Default (path)</Select.Option>
-              <Select.Option value="path">path</Select.Option>
-              <Select.Option value="header">header</Select.Option>
-              <Select.Option value="cookie">cookie</Select.Option>
-              <Select.Option value="query">query</Select.Option>
-            </Select>
+            <Select
+              options={[
+                { value: '', label: 'Default (path)' },
+                { value: 'path', label: 'path' },
+                { value: 'header', label: 'header' },
+                { value: 'cookie', label: 'cookie' },
+                { value: 'query', label: 'query' },
+              ]}
+            />
           </Form.Item>
           {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
             <Form.Item
@@ -1916,13 +1929,15 @@ export default function InboundFormModal({
             name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
             label="Sequence Placement"
           >
-            <Select>
-              <Select.Option value="">Default (path)</Select.Option>
-              <Select.Option value="path">path</Select.Option>
-              <Select.Option value="header">header</Select.Option>
-              <Select.Option value="cookie">cookie</Select.Option>
-              <Select.Option value="query">query</Select.Option>
-            </Select>
+            <Select
+              options={[
+                { value: '', label: 'Default (path)' },
+                { value: 'path', label: 'path' },
+                { value: 'header', label: 'header' },
+                { value: 'cookie', label: 'cookie' },
+                { value: 'query', label: 'query' },
+              ]}
+            />
           </Form.Item>
           {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && (
             <Form.Item
@@ -1938,13 +1953,15 @@ export default function InboundFormModal({
                 name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
                 label="Uplink Data Placement"
               >
-                <Select>
-                  <Select.Option value="">Default (body)</Select.Option>
-                  <Select.Option value="body">body</Select.Option>
-                  <Select.Option value="header">header</Select.Option>
-                  <Select.Option value="cookie">cookie</Select.Option>
-                  <Select.Option value="query">query</Select.Option>
-                </Select>
+                <Select
+                  options={[
+                    { value: '', label: 'Default (body)' },
+                    { value: 'body', label: 'body' },
+                    { value: 'header', label: 'header' },
+                    { value: 'cookie', label: 'cookie' },
+                    { value: 'query', label: 'query' },
+                  ]}
+                />
               </Form.Item>
               {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && (
                 <Form.Item
@@ -2067,11 +2084,14 @@ export default function InboundFormModal({
                           <div key={field.key} style={{ margin: '8px 0' }}>
                             <Space.Compact block>
                               <Form.Item name={[field.name, 'forceTls']} noStyle>
-                                <Select style={{ width: '20%' }}>
-                                  <Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
-                                  <Select.Option value="none">{t('none')}</Select.Option>
-                                  <Select.Option value="tls">TLS</Select.Option>
-                                </Select>
+                                <Select
+                                  style={{ width: '20%' }}
+                                  options={[
+                                    { value: 'same', label: t('pages.inbounds.same') },
+                                    { value: 'none', label: t('none') },
+                                    { value: 'tls', label: 'TLS' },
+                                  ]}
+                                />
                               </Form.Item>
                               <Form.Item name={[field.name, 'dest']} noStyle>
                                 <Input style={{ width: '30%' }} placeholder={t('host')} />
@@ -2104,19 +2124,28 @@ export default function InboundFormModal({
                                       <Input style={{ width: '30%' }} placeholder="SNI (defaults to host)" />
                                     </Form.Item>
                                     <Form.Item name={[field.name, 'fingerprint']} noStyle>
-                                      <Select style={{ width: '30%' }} placeholder="Fingerprint">
-                                        <Select.Option value="">Default</Select.Option>
-                                        {Object.values(UTLS_FINGERPRINT).map((fp) => (
-                                          <Select.Option key={fp} value={fp}>{fp}</Select.Option>
-                                        ))}
-                                      </Select>
+                                      <Select
+                                        style={{ width: '30%' }}
+                                        placeholder="Fingerprint"
+                                        options={[
+                                          { value: '', label: 'Default' },
+                                          ...Object.values(UTLS_FINGERPRINT).map((fp) => ({
+                                            value: fp,
+                                            label: fp,
+                                          })),
+                                        ]}
+                                      />
                                     </Form.Item>
                                     <Form.Item name={[field.name, 'alpn']} noStyle>
-                                      <Select mode="multiple" style={{ width: '40%' }} placeholder="ALPN">
-                                        {Object.values(ALPN_OPTION).map((a) => (
-                                          <Select.Option key={a} value={a}>{a}</Select.Option>
-                                        ))}
-                                      </Select>
+                                      <Select
+                                        mode="multiple"
+                                        style={{ width: '40%' }}
+                                        placeholder="ALPN"
+                                        options={Object.values(ALPN_OPTION).map((a) => ({
+                                          value: a,
+                                          label: a,
+                                        }))}
+                                      />
                                     </Form.Item>
                                   </Space.Compact>
                                 );
@@ -2221,28 +2250,29 @@ export default function InboundFormModal({
             name={['streamSettings', 'sockopt', 'domainStrategy']}
             label="Domain Strategy"
           >
-            <Select style={{ width: '50%' }}>
-              {Object.values(DOMAIN_STRATEGY_OPTION).map((d) => (
-                <Select.Option key={d} value={d}>{d}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              style={{ width: '50%' }}
+              options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
+            />
           </Form.Item>
           <Form.Item
             name={['streamSettings', 'sockopt', 'tcpcongestion']}
             label="TCP Congestion"
           >
-            <Select style={{ width: '50%' }}>
-              {Object.values(TCP_CONGESTION_OPTION).map((c) => (
-                <Select.Option key={c} value={c}>{c}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              style={{ width: '50%' }}
+              options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
+            />
           </Form.Item>
           <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
-            <Select style={{ width: '50%' }}>
-              <Select.Option value="off">Off</Select.Option>
-              <Select.Option value="redirect">Redirect</Select.Option>
-              <Select.Option value="tproxy">TProxy</Select.Option>
-            </Select>
+            <Select
+              style={{ width: '50%' }}
+              options={[
+                { value: 'off', label: 'Off' },
+                { value: 'redirect', label: 'Redirect' },
+                { value: 'tproxy', label: 'TProxy' },
+              ]}
+            />
           </Form.Item>
           <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label="Dialer Proxy">
             <Input />
@@ -2257,22 +2287,26 @@ export default function InboundFormModal({
             name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
             label="Trusted X-Forwarded-For"
           >
-            <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']}>
-              <Select.Option value="CF-Connecting-IP">CF-Connecting-IP</Select.Option>
-              <Select.Option value="X-Real-IP">X-Real-IP</Select.Option>
-              <Select.Option value="True-Client-IP">True-Client-IP</Select.Option>
-              <Select.Option value="X-Client-IP">X-Client-IP</Select.Option>
-            </Select>
+            <Select
+              mode="tags"
+              style={{ width: '100%' }}
+              tokenSeparators={[',']}
+              options={[
+                { value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
+                { value: 'X-Real-IP', label: 'X-Real-IP' },
+                { value: 'True-Client-IP', label: 'True-Client-IP' },
+                { value: 'X-Client-IP', label: 'X-Client-IP' },
+              ]}
+            />
           </Form.Item>
           <Form.Item
             name={['streamSettings', 'sockopt', 'addressPortStrategy']}
             label="Address+port strategy"
           >
-            <Select style={{ width: '50%' }}>
-              {Object.values(Address_Port_Strategy).map((v) => (
-                <Select.Option key={v} value={v}>{v}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              style={{ width: '50%' }}
+              options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
+            />
           </Form.Item>
           <Form.Item shouldUpdate noStyle>
             {({ getFieldValue, setFieldValue }) => {
@@ -2442,28 +2476,26 @@ export default function InboundFormModal({
             <Input placeholder="Server Name Indication" />
           </Form.Item>
           <Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label="Cipher Suites">
-            <Select>
-              <Select.Option value="">Auto</Select.Option>
-              {Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => (
-                <Select.Option key={v} value={v}>{k}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              options={[
+                { value: '', label: 'Auto' },
+                ...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
+              ]}
+            />
           </Form.Item>
           <Form.Item label="Min/Max Version">
             <Space.Compact block>
               <Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
-                <Select style={{ width: '50%' }}>
-                  {Object.values(TLS_VERSION_OPTION).map((v) => (
-                    <Select.Option key={v} value={v}>{v}</Select.Option>
-                  ))}
-                </Select>
+                <Select
+                  style={{ width: '50%' }}
+                  options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
+                />
               </Form.Item>
               <Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
-                <Select style={{ width: '50%' }}>
-                  {Object.values(TLS_VERSION_OPTION).map((v) => (
-                    <Select.Option key={v} value={v}>{v}</Select.Option>
-                  ))}
-                </Select>
+                <Select
+                  style={{ width: '50%' }}
+                  options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
+                />
               </Form.Item>
             </Space.Compact>
           </Form.Item>
@@ -2471,19 +2503,20 @@ export default function InboundFormModal({
             name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
             label="uTLS"
           >
-            <Select>
-              <Select.Option value="">None</Select.Option>
-              {Object.values(UTLS_FINGERPRINT).map((fp) => (
-                <Select.Option key={fp} value={fp}>{fp}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              options={[
+                { value: '', label: 'None' },
+                ...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
+              ]}
+            />
           </Form.Item>
           <Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
-            <Select mode="multiple" tokenSeparators={[',']} style={{ width: '100%' }}>
-              {Object.values(ALPN_OPTION).map((a) => (
-                <Select.Option key={a} value={a}>{a}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              mode="multiple"
+              tokenSeparators={[',']}
+              style={{ width: '100%' }}
+              options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
+            />
           </Form.Item>
           <Form.Item
             name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
@@ -2622,11 +2655,10 @@ export default function InboundFormModal({
                       name={[certField.name, 'usage']}
                       label="Usage Option"
                     >
-                      <Select style={{ width: '50%' }}>
-                        {Object.values(USAGE_OPTION).map((u) => (
-                          <Select.Option key={u} value={u}>{u}</Select.Option>
-                        ))}
-                      </Select>
+                      <Select
+                        style={{ width: '50%' }}
+                        options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
+                      />
                     </Form.Item>
                     <Form.Item
                       noStyle
@@ -2705,11 +2737,9 @@ export default function InboundFormModal({
             name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
             label="uTLS"
           >
-            <Select>
-              {Object.values(UTLS_FINGERPRINT).map((fp) => (
-                <Select.Option key={fp} value={fp}>{fp}</Select.Option>
-              ))}
-            </Select>
+            <Select
+              options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
+            />
           </Form.Item>
           <Form.Item
             name={['streamSettings', 'realitySettings', 'target']}

+ 26 - 14
frontend/src/pages/index/LogModal.tsx

@@ -117,20 +117,32 @@ export default function LogModal({ open, onClose }: LogModalProps) {
       <Form layout="inline" className="log-toolbar">
         <Form.Item>
           <Space.Compact>
-            <Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}>
-              <Select.Option value="10">10</Select.Option>
-              <Select.Option value="20">20</Select.Option>
-              <Select.Option value="50">50</Select.Option>
-              <Select.Option value="100">100</Select.Option>
-              <Select.Option value="500">500</Select.Option>
-            </Select>
-            <Select value={level} size="small" style={{ width: 95 }} onChange={setLevel}>
-              <Select.Option value="debug">Debug</Select.Option>
-              <Select.Option value="info">Info</Select.Option>
-              <Select.Option value="notice">Notice</Select.Option>
-              <Select.Option value="warning">Warning</Select.Option>
-              <Select.Option value="err">Error</Select.Option>
-            </Select>
+            <Select
+              value={rows}
+              size="small"
+              style={{ width: 70 }}
+              onChange={setRows}
+              options={[
+                { value: '10', label: '10' },
+                { value: '20', label: '20' },
+                { value: '50', label: '50' },
+                { value: '100', label: '100' },
+                { value: '500', label: '500' },
+              ]}
+            />
+            <Select
+              value={level}
+              size="small"
+              style={{ width: 95 }}
+              onChange={setLevel}
+              options={[
+                { value: 'debug', label: 'Debug' },
+                { value: 'info', label: 'Info' },
+                { value: 'notice', label: 'Notice' },
+                { value: 'warning', label: 'Warning' },
+                { value: 'err', label: 'Error' },
+              ]}
+            />
           </Space.Compact>
         </Form.Item>
         <Form.Item>

+ 13 - 7
frontend/src/pages/index/XrayLogModal.tsx

@@ -124,13 +124,19 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
     >
       <Form layout="inline" className="log-toolbar">
         <Form.Item>
-          <Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}>
-            <Select.Option value="10">10</Select.Option>
-            <Select.Option value="20">20</Select.Option>
-            <Select.Option value="50">50</Select.Option>
-            <Select.Option value="100">100</Select.Option>
-            <Select.Option value="500">500</Select.Option>
-          </Select>
+          <Select
+            value={rows}
+            size="small"
+            style={{ width: 70 }}
+            onChange={setRows}
+            options={[
+              { value: '10', label: '10' },
+              { value: '20', label: '20' },
+              { value: '50', label: '50' },
+              { value: '100', label: '100' },
+              { value: '500', label: '500' },
+            ]}
+          />
         </Form.Item>
         <Form.Item label={t('filter')} className="filter-item">
           <Input

+ 1 - 2
frontend/src/pages/settings/SettingsPage.tsx

@@ -293,9 +293,8 @@ export default function SettingsPage() {
                     <Alert
                       type="error"
                       showIcon
-                      closable
+                      closable={{ onClose: () => setAlertVisible(false) }}
                       className="conf-alert"
-                      onClose={() => setAlertVisible(false)}
                       title={t('pages.settings.securityWarnings')}
                       description={(
                         <>

+ 3 - 6
frontend/src/pages/xray/NordModal.tsx

@@ -318,8 +318,7 @@ export default function NordModal({
             <Form.Item label="Country">
               <Select
                 value={countryId ?? undefined}
-                showSearch
-                optionFilterProp="label"
+                showSearch={{ optionFilterProp: 'label' }}
                 onChange={(v) => fetchServers(v)}
                 options={countries.map((c) => ({
                   value: c.id,
@@ -332,8 +331,7 @@ export default function NordModal({
               <Form.Item label="City">
                 <Select
                   value={cityId}
-                  showSearch
-                  optionFilterProp="label"
+                  showSearch={{ optionFilterProp: 'label' }}
                   onChange={setCityId}
                   options={[{ value: null, label: 'All cities' }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
                 />
@@ -344,8 +342,7 @@ export default function NordModal({
               <Form.Item label="Server">
                 <Select
                   value={serverId}
-                  showSearch
-                  optionFilterProp="label"
+                  showSearch={{ optionFilterProp: 'label' }}
                   onChange={setServerId}
                   options={filteredServers.map((s) => ({
                     value: s.id,

+ 2 - 2
frontend/src/schemas/_envelope.ts

@@ -1,10 +1,10 @@
 import { z } from 'zod';
 
-export const msgSchema = <T extends z.ZodTypeAny>(obj: T) =>
+export const msgSchema = <T extends z.ZodType>(obj: T) =>
   z.object({
     success: z.boolean(),
     msg: z.string().default(''),
     obj: obj.nullable(),
   });
 
-export type MsgOf<S extends z.ZodTypeAny> = z.infer<ReturnType<typeof msgSchema<S>>>;
+export type MsgOf<S extends z.ZodType> = z.infer<ReturnType<typeof msgSchema<S>>>;

+ 9 - 1
frontend/src/utils/index.ts

@@ -583,7 +583,15 @@ export class ClipboardManager {
       textarea.focus({ preventScroll: true });
       textarea.select();
       textarea.setSelectionRange(0, text.length);
-      ok = document.execCommand('copy');
+      // Routed through a dynamic lookup so the @deprecated tag on
+      // Document.execCommand doesn't surface here. execCommand is the
+      // only copy path that works in insecure contexts (HTTP panels
+      // behind IP/localhost) — reached only after navigator.clipboard
+      // fails or is unavailable.
+      const exec = (document as unknown as Record<string, unknown>)['execCommand'];
+      if (typeof exec === 'function') {
+        ok = (exec as (cmd: string) => boolean).call(document, 'copy');
+      }
     } catch {}
 
     host.removeChild(textarea);

+ 1 - 1
frontend/src/utils/zodForm.ts

@@ -2,7 +2,7 @@ import type { Rule } from 'antd/es/form';
 import type { TFunction } from 'i18next';
 import type { z } from 'zod';
 
-export function antdRule<T extends z.ZodTypeAny>(schema: T, t: TFunction): Rule {
+export function antdRule<T extends z.ZodType>(schema: T, t: TFunction): Rule {
   return {
     validator: async (_rule, value) => {
       const result = schema.safeParse(value);

+ 1 - 1
frontend/src/utils/zodValidate.ts

@@ -1,7 +1,7 @@
 import type { z } from 'zod';
 import { Msg } from '@/utils';
 
-export function parseMsg<T extends z.ZodTypeAny>(
+export function parseMsg<T extends z.ZodType>(
   msg: Msg<unknown>,
   schema: T,
   context: string,