mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 17:37:19 +00:00
feat(hosts): bulk-add multiple hosts to multiple inbounds (#5677)
* feat(hosts): bulk-add multiple hosts to multiple inbounds Allow users to select multiple inbound IDs and enter multiple host addresses (with optional per-host port override) in a single form submission. - Add BulkAddHostReq entity and POST /panel/api/hosts/bulk/add endpoint - Add AddHostsBulk service with GORM transaction safety - Add parseHostAndPort helper (IPv4, bracketed/bracketless IPv6, port) - Update HostFormModal to multi-select inbounds and tag-input hosts - Wire bulkCreate mutation in HostsPage with existing-host suggestions - Register endpoint in api-docs/endpoints.ts and regenerate OpenAPI/Zod * feat(hosts): group override records by group_id and support group editing * fix: import Popover in HostList * fix: use messageApi in HostFormModal * fix(hosts): resolve 4 bugs found in host-group code review - fix(schema): allow empty hosts array in BulkAddHostSchema so users can save a host without an address (inherits inbound endpoint). The old .min(1) was never enforced at runtime since the schema is only used for type inference, but the type was incorrect. - fix(service): validate new inbound IDs in UpdateHostGroup before deleting old rows, matching the same check already present in AddHostGroup. Prevents orphaned host rows when an invalid inbound ID is supplied on edit. - fix(service): replace full-table scan in GetHostsByInbound with two targeted queries (DISTINCT group_id WHERE inbound_id=?, then WHERE group_id IN ?) to avoid loading every host in the DB. - fix(mutations): remove unused createMut / create export from useHostMutations. The /hosts/add endpoint is identical to /hosts/bulk/add; only bulkCreate is used by the UI. * fix(hosts): address code review feedback (optimize bulk inserts, add validation tests, and remove comments) * fix(fmt): apply gofumpt formatting to model.go and db.go The previous merge commit incorrectly applied gofmt (tab-aligned) to these files. The repository's golangci config requires gofumpt+goimports which produces space-aligned struct fields. This commit restores the correct gofumpt formatting that matches upstream/main. * chore(frontend): regenerate API schemas and update lockfile * fix * refactor(hosts): dedupe host-group service and tidy frontend AddHostGroup and UpdateHostGroup shared an identical ~35-field model.Host construction and hand-rolled transaction boilerplate (tx.Begin plus a committed flag plus a deferred recover/rollback). Extract buildHostRows, validateInboundsExist and formatHostAddr, and run every mutation through db.Transaction. groupHosts collapses its duplicated address/port formatting and create/append fork into one path using slices.Contains. Behavior-preserving: host.go drops ~90 lines with the existing service/controller tests green. Frontend: drop the Partial union and two as-casts in HostsPage.onSave (the modal always passes a full BulkAddHostValues), and remove the movable index map in HostList in favor of the table render index arg. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "ملاحظة",
|
||||
"serverDescription": "الوصف",
|
||||
"inbound": "الوارد",
|
||||
"inbound": "الواردات",
|
||||
"address": "العنوان",
|
||||
"port": "المنفذ",
|
||||
"endpoint": "النهاية",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"tags": "وسوم",
|
||||
"nodeGuids": "النودز",
|
||||
"excludeFromSubTypes": "استبعاد من الصيغ",
|
||||
"verifyPeerCertByName": "التحقق من شهادة النظير بالاسم"
|
||||
"verifyPeerCertByName": "التحقق من شهادة النظير بالاسم",
|
||||
"inheritAddress": "يرث العنوان"
|
||||
},
|
||||
"hints": {
|
||||
"address": "اتركه فارغاً ليرث عنوان الوارد نفسه.",
|
||||
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"fields": {
|
||||
"remark": "Remark",
|
||||
"serverDescription": "Description",
|
||||
"inbound": "Inbound",
|
||||
"inbound": "Inbounds",
|
||||
"address": "Address",
|
||||
"port": "Port",
|
||||
"endpoint": "Endpoint",
|
||||
@@ -1043,7 +1043,8 @@
|
||||
"shuffleHost": "Shuffle host",
|
||||
"tags": "Tags",
|
||||
"nodeGuids": "Nodes",
|
||||
"excludeFromSubTypes": "Exclude from formats"
|
||||
"excludeFromSubTypes": "Exclude from formats",
|
||||
"inheritAddress": "Inherits"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Leave blank to inherit the inbound's own address.",
|
||||
@@ -1098,9 +1099,9 @@
|
||||
"toasts": {
|
||||
"list": "Failed to load hosts",
|
||||
"obtain": "Failed to load host",
|
||||
"add": "Add host",
|
||||
"update": "Update host",
|
||||
"delete": "Delete host",
|
||||
"add": "Host added successfully",
|
||||
"update": "Host updated successfully",
|
||||
"delete": "Host deleted successfully",
|
||||
"badTag": "Invalid tag",
|
||||
"badVlessRoute": "Enter a single number between 0 and 65535"
|
||||
}
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Notas",
|
||||
"serverDescription": "Descripción",
|
||||
"inbound": "Inbound",
|
||||
"inbound": "Inbounds",
|
||||
"address": "Dirección",
|
||||
"port": "Puerto",
|
||||
"endpoint": "Punto final",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "Barajar host",
|
||||
"tags": "Etiquetas",
|
||||
"nodeGuids": "Nodos",
|
||||
"excludeFromSubTypes": "Excluir de formatos"
|
||||
"excludeFromSubTypes": "Excluir de formatos",
|
||||
"inheritAddress": "Hereda dirección"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Déjalo en blanco para heredar la dirección propia del inbound.",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "نام",
|
||||
"serverDescription": "توضیحات",
|
||||
"inbound": "اینباند",
|
||||
"inbound": "اینباندها",
|
||||
"address": "آدرس",
|
||||
"port": "پورت",
|
||||
"endpoint": "نقطه پایانی",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "درهمسازی میزبان",
|
||||
"tags": "برچسبها",
|
||||
"nodeGuids": "نودها",
|
||||
"excludeFromSubTypes": "حذف از فرمتها"
|
||||
"excludeFromSubTypes": "حذف از فرمتها",
|
||||
"inheritAddress": "ارثبری آدرس"
|
||||
},
|
||||
"hints": {
|
||||
"address": "برای ارثبری آدرس خودِ اینباند خالی بگذارید.",
|
||||
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"tags": "Tag",
|
||||
"nodeGuids": "Node",
|
||||
"excludeFromSubTypes": "Kecualikan dari format",
|
||||
"verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama"
|
||||
"verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama",
|
||||
"inheritAddress": "Warisi alamat"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Biarkan kosong untuk mewarisi alamat inbound itu sendiri.",
|
||||
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "ホストをシャッフル",
|
||||
"tags": "タグ",
|
||||
"nodeGuids": "ノード",
|
||||
"excludeFromSubTypes": "形式から除外"
|
||||
"excludeFromSubTypes": "形式から除外",
|
||||
"inheritAddress": "アドレス継承"
|
||||
},
|
||||
"hints": {
|
||||
"address": "空欄にするとインバウンド自身のアドレスを継承します。",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Observação",
|
||||
"serverDescription": "Descrição",
|
||||
"inbound": "Entrada",
|
||||
"inbound": "Entradas",
|
||||
"address": "Endereço",
|
||||
"port": "Porta",
|
||||
"endpoint": "Endpoint",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "Embaralhar host",
|
||||
"tags": "Tags",
|
||||
"nodeGuids": "Nós",
|
||||
"excludeFromSubTypes": "Excluir dos formatos"
|
||||
"excludeFromSubTypes": "Excluir dos formatos",
|
||||
"inheritAddress": "Herda endereço"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Deixe em branco para herdar o próprio endereço da entrada.",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Примечание",
|
||||
"serverDescription": "Описание",
|
||||
"inbound": "Входящее",
|
||||
"inbound": "Входящие",
|
||||
"address": "Адрес",
|
||||
"port": "Порт",
|
||||
"endpoint": "Конечная точка",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "Перемешивать хост",
|
||||
"tags": "Теги",
|
||||
"nodeGuids": "Узлы",
|
||||
"excludeFromSubTypes": "Исключить из форматов"
|
||||
"excludeFromSubTypes": "Исключить из форматов",
|
||||
"inheritAddress": "Наследует адрес"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Оставьте пустым, чтобы унаследовать собственный адрес входящего.",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Açıklama",
|
||||
"serverDescription": "Tanım",
|
||||
"inbound": "Gelen Bağlantı",
|
||||
"inbound": "Gelen Bağlantılar",
|
||||
"address": "Adres",
|
||||
"port": "Port",
|
||||
"endpoint": "Uç Nokta",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"tags": "Etiketler",
|
||||
"nodeGuids": "Düğümler",
|
||||
"excludeFromSubTypes": "Formatlardan hariç tut",
|
||||
"verifyPeerCertByName": "Peer sertifikasını ada göre doğrula"
|
||||
"verifyPeerCertByName": "Peer sertifikasını ada göre doğrula",
|
||||
"inheritAddress": "Adresi devralır"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Gelen bağlantının kendi adresini devralmak için boş bırakın.",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Примітка",
|
||||
"serverDescription": "Опис",
|
||||
"inbound": "Вхідний",
|
||||
"inbound": "Вхідні",
|
||||
"address": "Адреса",
|
||||
"port": "Порт",
|
||||
"endpoint": "Кінцева точка",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "Перемішувати host",
|
||||
"tags": "Теги",
|
||||
"nodeGuids": "Вузли",
|
||||
"excludeFromSubTypes": "Виключити з форматів"
|
||||
"excludeFromSubTypes": "Виключити з форматів",
|
||||
"inheritAddress": "Успадковує адресу"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Залиште порожнім, щоб успадкувати власну адресу вхідного.",
|
||||
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"fields": {
|
||||
"remark": "Ghi chú",
|
||||
"serverDescription": "Mô tả",
|
||||
"inbound": "Inbound",
|
||||
"inbound": "Inbounds",
|
||||
"address": "Địa chỉ",
|
||||
"port": "Cổng",
|
||||
"endpoint": "Endpoint",
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "Xáo trộn host",
|
||||
"tags": "Tag",
|
||||
"nodeGuids": "Nút",
|
||||
"excludeFromSubTypes": "Loại trừ khỏi định dạng"
|
||||
"excludeFromSubTypes": "Loại trừ khỏi định dạng",
|
||||
"inheritAddress": "Kế thừa địa chỉ"
|
||||
},
|
||||
"hints": {
|
||||
"address": "Để trống để kế thừa địa chỉ của chính inbound.",
|
||||
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "随机打乱 Host",
|
||||
"tags": "标签",
|
||||
"nodeGuids": "节点",
|
||||
"excludeFromSubTypes": "从格式中排除"
|
||||
"excludeFromSubTypes": "从格式中排除",
|
||||
"inheritAddress": "继承地址"
|
||||
},
|
||||
"hints": {
|
||||
"address": "留空则继承入站自身的地址。",
|
||||
|
||||
@@ -1921,7 +1921,8 @@
|
||||
"shuffleHost": "隨機排序 Host",
|
||||
"tags": "標籤",
|
||||
"nodeGuids": "節點",
|
||||
"excludeFromSubTypes": "從格式中排除"
|
||||
"excludeFromSubTypes": "從格式中排除",
|
||||
"inheritAddress": "繼承地址"
|
||||
},
|
||||
"hints": {
|
||||
"address": "留空以繼承入站本身的地址。",
|
||||
|
||||
Reference in New Issue
Block a user