diff --git a/database/db.go b/database/db.go
index c92e315f..fba55a3c 100644
--- a/database/db.go
+++ b/database/db.go
@@ -346,7 +346,15 @@ func isTableEmpty(tableName string) (bool, error) {
func InitDB(dbPath string) error {
var gormLogger logger.Interface
if config.IsDebug() {
- gormLogger = logger.Default
+ gormLogger = logger.New(
+ log.New(os.Stdout, "\r\n", log.LstdFlags),
+ logger.Config{
+ SlowThreshold: time.Second,
+ LogLevel: logger.Info,
+ IgnoreRecordNotFoundError: true,
+ Colorful: true,
+ },
+ )
} else {
gormLogger = logger.Discard
}
diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts
index ea3799db..a2089ad9 100644
--- a/frontend/src/hooks/useClients.ts
+++ b/frontend/src/hooks/useClients.ts
@@ -146,6 +146,17 @@ export function useClients() {
return results;
}, [refresh]);
+ const bulkAdjust = useCallback(async (emails: string[], addDays: number, addBytes: number) => {
+ if (!Array.isArray(emails) || emails.length === 0) return null;
+ const msg = await HttpUtil.post(
+ '/panel/api/clients/bulkAdjust',
+ { emails, addDays, addBytes },
+ JSON_HEADERS,
+ ) as ApiMsg<{ adjusted: number; skipped?: { email: string; reason: string }[] }>;
+ if (msg?.success) await refresh();
+ return msg;
+ }, [refresh]);
+
const attach = useCallback(async (email: string, inboundIds: number[]) => {
if (!email) return null;
const encoded = encodeURIComponent(email);
@@ -269,6 +280,7 @@ export function useClients() {
update,
remove,
removeMany,
+ bulkAdjust,
attach,
detach,
resetTraffic,
diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js
index 431e1e08..1fca7994 100644
--- a/frontend/src/pages/api-docs/endpoints.js
+++ b/frontend/src/pages/api-docs/endpoints.js
@@ -461,6 +461,13 @@ export const sections = [
summary: 'Delete every client whose traffic quota is exhausted (used >= total, when reset is disabled) or whose expiry has passed. Returns the deleted count and triggers an Xray restart when any client was on a running inbound.',
response: '{\n "success": true,\n "obj": {\n "deleted": 0\n }\n}',
},
+ {
+ method: 'POST',
+ path: '/panel/api/clients/bulkAdjust',
+ summary: 'Shift expiry and/or traffic quota for many clients in one call. addDays/addBytes may be negative. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field — bulk extend never converts unlimited to limited. Returns the adjusted count and per-email skip reasons.',
+ body: '{\n "emails": ["alice", "bob"],\n "addDays": 30,\n "addBytes": 53687091200\n}',
+ response: '{\n "success": true,\n "obj": {\n "adjusted": 2,\n "skipped": [\n { "email": "carol", "reason": "unlimited expiry" }\n ]\n }\n}',
+ },
{
method: 'POST',
path: '/panel/api/clients/resetTraffic/:email',
diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx
index 43358b78..1b15267f 100644
--- a/frontend/src/pages/clients/ClientBulkAddModal.tsx
+++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx
@@ -308,7 +308,7 @@ export default function ClientBulkAddModal({
)}
- update('totalGB', Number(v) || 0)} />
+ update('totalGB', Number(v) || 0)} />
diff --git a/frontend/src/pages/clients/ClientBulkAdjustModal.tsx b/frontend/src/pages/clients/ClientBulkAdjustModal.tsx
new file mode 100644
index 00000000..b13dcdea
--- /dev/null
+++ b/frontend/src/pages/clients/ClientBulkAdjustModal.tsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert, Form, InputNumber, Modal, message } from 'antd';
+
+const GB = 1024 * 1024 * 1024;
+
+interface ClientBulkAdjustModalProps {
+ open: boolean;
+ count: number;
+ onOpenChange: (open: boolean) => void;
+ onSubmit: (addDays: number, addBytes: number) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>;
+}
+
+export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSubmit }: ClientBulkAdjustModalProps) {
+ const { t } = useTranslation();
+ const [messageApi, messageContextHolder] = message.useMessage();
+ const [addDays, setAddDays] = useState(0);
+ const [addGB, setAddGB] = useState(0);
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (open) {
+ setAddDays(0);
+ setAddGB(0);
+ }
+ }, [open]);
+
+ async function handleOk() {
+ const days = Math.trunc(Number(addDays) || 0);
+ const gb = Number(addGB) || 0;
+ if (days === 0 && gb === 0) {
+ messageApi.warning(t('pages.clients.bulkAdjustNothing'));
+ return;
+ }
+ setSubmitting(true);
+ try {
+ const bytes = Math.trunc(gb * GB);
+ const result = await onSubmit(days, bytes);
+ if (!result) return;
+ const ok = result.adjusted ?? 0;
+ const skipped = result.skipped?.length ?? 0;
+ if (skipped === 0) {
+ messageApi.success(t('pages.clients.toasts.bulkAdjusted', { count: ok }));
+ } else {
+ const firstReason = result.skipped?.[0]?.reason ?? '';
+ messageApi.warning(firstReason
+ ? `${t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped })} — ${firstReason}`
+ : t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped }));
+ }
+ onOpenChange(false);
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+ <>
+ {messageContextHolder}
+ onOpenChange(false)}
+ destroyOnHidden
+ >
+
+
+ setAddDays(Number(v) || 0)}
+ style={{ width: '100%' }}
+ step={1}
+ precision={0}
+ />
+
+
+ setAddGB(Number(v) || 0)}
+ style={{ width: '100%' }}
+ step={1}
+ />
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx
index 67b07647..616e24ab 100644
--- a/frontend/src/pages/clients/ClientFormModal.tsx
+++ b/frontend/src/pages/clients/ClientFormModal.tsx
@@ -393,7 +393,7 @@ export default function ClientFormModal({
- update('totalGB', Number(v) || 0)} />
diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx
index 3018fadb..788df288 100644
--- a/frontend/src/pages/clients/ClientsPage.tsx
+++ b/frontend/src/pages/clients/ClientsPage.tsx
@@ -25,6 +25,7 @@ import {
} from 'antd';
import type { ColumnsType, TableProps } from 'antd/es/table';
import {
+ ClockCircleOutlined,
DeleteOutlined,
EditOutlined,
FilterOutlined,
@@ -54,6 +55,7 @@ import ClientFormModal from './ClientFormModal';
import ClientInfoModal from './ClientInfoModal';
import ClientQrModal from './ClientQrModal';
import ClientBulkAddModal from './ClientBulkAddModal';
+import ClientBulkAdjustModal from './ClientBulkAdjustModal';
import '@/styles/page-cards.css';
import './ClientsPage.css';
@@ -96,7 +98,7 @@ export default function ClientsPage() {
const {
clients, inbounds, onlines, loading, fetched, subSettings,
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
- create, update, remove, removeMany, attach, detach,
+ create, update, remove, removeMany, bulkAdjust, attach, detach,
resetTraffic, resetAllTraffics, delDepleted, setEnable,
applyTrafficEvent, applyClientStatsEvent, applyInvalidate,
} = useClients();
@@ -117,6 +119,7 @@ export default function ClientsPage() {
const [qrOpen, setQrOpen] = useState(false);
const [qrClient, setQrClient] = useState(null);
const [bulkAddOpen, setBulkAddOpen] = useState(false);
+ const [bulkAdjustOpen, setBulkAdjustOpen] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const initial = readFilterState();
@@ -587,7 +590,7 @@ export default function ClientsPage() {
}, 'expiryTime'),
];
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [t, togglingEmail, sortColumn, sortOrder, clientBucket, isOnline]);
+ }, [t, togglingEmail, sortColumn, sortOrder, clientBucket, isOnline, inboundsById]);
const tablePagination = {
current: currentPage,
@@ -700,9 +703,14 @@ export default function ClientsPage() {
{!isMobile && t('pages.clients.bulk')}
{selectedRowKeys.length > 0 && (
- } onClick={onBulkDelete}>
- {t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
-
+ <>
+ } onClick={() => setBulkAdjustOpen(true)}>
+ {t('pages.clients.adjustSelected', { count: selectedRowKeys.length })}
+
+ } onClick={onBulkDelete}>
+ {t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
+
+ >
)}
} onClick={onResetAllTraffics}>
{!isMobile && t('pages.clients.resetAllTraffics')}
@@ -902,6 +910,19 @@ export default function ClientsPage() {
onOpenChange={setBulkAddOpen}
onSaved={() => setBulkAddOpen(false)}
/>
+ {
+ const msg = await bulkAdjust([...selectedRowKeys], addDays, addBytes);
+ if (msg?.success) {
+ setSelectedRowKeys([]);
+ return msg.obj ?? { adjusted: 0 };
+ }
+ return null;
+ }}
+ />
);
diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx
index b75c0413..2eaaa852 100644
--- a/frontend/src/pages/inbounds/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/InboundFormModal.tsx
@@ -966,7 +966,7 @@ export default function InboundFormModal({
{
form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0);
refresh();
diff --git a/sub/subService.go b/sub/subService.go
index 077ab9a5..364e280b 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -174,20 +174,6 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{}
}
-func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
- db := database.GetDB()
- var inbound *model.Inbound
- err := db.Model(model.Inbound{}).
- Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
- Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
- Find(&inbound).Error
- if err != nil {
- return "", 0, "", err
- }
-
- return inbound.Listen, inbound.Port, mergeStreamFromMaster(streamSettings, inbound.StreamSettings), nil
-}
-
// projectThroughFallbackMaster mutates the inbound in place so its
// Listen/Port/StreamSettings reflect the externally reachable master
// when applicable. Covers both fallback mechanisms:
diff --git a/web/controller/client.go b/web/controller/client.go
index 7a4f0d36..fe567488 100644
--- a/web/controller/client.go
+++ b/web/controller/client.go
@@ -42,6 +42,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
g.POST("/:email/detach", a.detach)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/delDepleted", a.delDepleted)
+ g.POST("/bulkAdjust", a.bulkAdjust)
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
g.POST("/ips/:email", a.getIps)
@@ -162,6 +163,30 @@ func (a *ClientController) resetAllTraffics(c *gin.Context) {
notifyClientsChanged()
}
+type bulkAdjustRequest struct {
+ Emails []string `json:"emails"`
+ AddDays int `json:"addDays"`
+ AddBytes int64 `json:"addBytes"`
+}
+
+func (a *ClientController) bulkAdjust(c *gin.Context) {
+ var req bulkAdjustRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+ return
+ }
+ result, needRestart, err := a.clientService.BulkAdjust(&a.inboundService, req.Emails, req.AddDays, req.AddBytes)
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+ return
+ }
+ jsonObj(c, result, nil)
+ if needRestart {
+ a.xrayService.SetToNeedRestart()
+ }
+ notifyClientsChanged()
+}
+
func (a *ClientController) delDepleted(c *gin.Context) {
deleted, needRestart, err := a.clientService.DelDepleted(&a.inboundService)
if err != nil {
diff --git a/web/service/client.go b/web/service/client.go
index 862ec695..d9d726e4 100644
--- a/web/service/client.go
+++ b/web/service/client.go
@@ -803,6 +803,99 @@ func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email st
return needRestart, nil
}
+// BulkAdjustResult is returned by BulkAdjust to report how many clients were
+// successfully updated and which were skipped (typically because the field
+// being adjusted was unlimited for that client) or failed.
+type BulkAdjustResult struct {
+ Adjusted int `json:"adjusted"`
+ Skipped []BulkAdjustReport `json:"skipped,omitempty"`
+}
+
+type BulkAdjustReport struct {
+ Email string `json:"email"`
+ Reason string `json:"reason"`
+}
+
+// BulkAdjust shifts ExpiryTime by addDays (days) and TotalGB by addBytes
+// for every email in the list. Clients whose corresponding field is
+// unlimited (0) are skipped — bulk extend should not accidentally
+// limit an unlimited client. addDays and addBytes may be negative.
+func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, addDays int, addBytes int64) (BulkAdjustResult, bool, error) {
+ result := BulkAdjustResult{}
+ needRestart := false
+ if len(emails) == 0 {
+ return result, needRestart, nil
+ }
+ if addDays == 0 && addBytes == 0 {
+ return result, needRestart, common.NewError("no adjustment specified")
+ }
+
+ addExpiryMs := int64(addDays) * 24 * 60 * 60 * 1000
+
+ for _, email := range emails {
+ email = strings.TrimSpace(email)
+ if email == "" {
+ continue
+ }
+ rec, err := s.GetRecordByEmail(nil, email)
+ if err != nil {
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: err.Error()})
+ continue
+ }
+ client := rec.ToClient()
+
+ applied := false
+ if addDays != 0 {
+ switch {
+ case rec.ExpiryTime == 0:
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "unlimited expiry"})
+ case rec.ExpiryTime > 0:
+ next := rec.ExpiryTime + addExpiryMs
+ if next <= 0 {
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "reduction exceeds remaining time"})
+ } else {
+ client.ExpiryTime = next
+ applied = true
+ }
+ default:
+ next := rec.ExpiryTime - addExpiryMs
+ if next >= 0 {
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "reduction exceeds delay window"})
+ } else {
+ client.ExpiryTime = next
+ applied = true
+ }
+ }
+ }
+ if addBytes != 0 {
+ if rec.TotalGB == 0 {
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "unlimited traffic"})
+ } else {
+ next := rec.TotalGB + addBytes
+ if next < 0 {
+ next = 0
+ }
+ client.TotalGB = next
+ applied = true
+ }
+ }
+ if !applied {
+ continue
+ }
+
+ nr, err := s.Update(inboundSvc, rec.Id, *client)
+ if err != nil {
+ result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: err.Error()})
+ continue
+ }
+ if nr {
+ needRestart = true
+ }
+ result.Adjusted++
+ }
+ return result, needRestart, nil
+}
+
func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) {
db := database.GetDB()
now := time.Now().UnixMilli()
diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json
index 7a2b288c..fd452c1a 100644
--- a/web/translation/ar-EG.json
+++ b/web/translation/ar-EG.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "حذف العميل {email}؟",
"deleteConfirmContent": "سيؤدي هذا إلى إزالة العميل من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
"deleteSelected": "حذف ({count})",
+ "adjustSelected": "تعديل ({count})",
"bulkDeleteConfirmTitle": "حذف {count} عميل؟",
"bulkDeleteConfirmContent": "سيتم إزالة كل عميل محدد من جميع الاتصالات الواردة المرتبطة وحذف سجل حركة مروره. لا يمكن التراجع.",
+ "bulkAdjustTitle": "تعديل {count} عميل",
+ "bulkAdjustHint": "القيم الموجبة تزيد، السالبة تنقص. العملاء بصلاحية أو ترافيك غير محدود يُتخطّون لذلك الحقل.",
+ "bulkAdjustNothing": "حدد الأيام أو الترافيك قبل التطبيق.",
+ "addDays": "إضافة أيام",
+ "addTrafficGB": "إضافة ترافيك (GB)",
"delDepleted": "حذف المنتهية",
"delDepletedConfirmTitle": "حذف العملاء المنتهية حصصهم؟",
"delDepletedConfirmContent": "يُحذف كل عميل استُنفِدت حصة حركة مروره أو انتهت صلاحيته. لا يمكن التراجع.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "تم حذف {ok}, وفشل {failed}",
"bulkCreated": "تم إنشاء {count} عميل",
"bulkCreatedMixed": "تم إنشاء {ok}, وفشل {failed}",
+ "bulkAdjusted": "تم تعديل {count} عميل",
+ "bulkAdjustedMixed": "{ok} تم تعديلهم، {skipped} تم تخطيهم",
"delDepleted": "تم حذف {count} عميل منتهٍ"
}
},
diff --git a/web/translation/en-US.json b/web/translation/en-US.json
index 667ac0bc..afbd9c26 100644
--- a/web/translation/en-US.json
+++ b/web/translation/en-US.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Delete client {email}?",
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
"deleteSelected": "Delete ({count})",
+ "adjustSelected": "Adjust ({count})",
"bulkDeleteConfirmTitle": "Delete {count} clients?",
"bulkDeleteConfirmContent": "Each selected client is removed from every attached inbound and its traffic record is dropped. This cannot be undone.",
+ "bulkAdjustTitle": "Adjust {count} clients",
+ "bulkAdjustHint": "Positive values extend, negative values reduce. Clients with unlimited expiry or traffic are skipped for that field.",
+ "bulkAdjustNothing": "Set days or traffic before applying.",
+ "addDays": "Add days",
+ "addTrafficGB": "Add traffic (GB)",
"delDepleted": "Delete depleted",
"delDepletedConfirmTitle": "Delete depleted clients?",
"delDepletedConfirmContent": "Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} deleted, {failed} failed",
"bulkCreated": "{count} clients created",
"bulkCreatedMixed": "{ok} created, {failed} failed",
+ "bulkAdjusted": "{count} clients adjusted",
+ "bulkAdjustedMixed": "{ok} adjusted, {skipped} skipped",
"delDepleted": "{count} depleted clients deleted"
}
},
diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json
index 1cf78b39..a9371371 100644
--- a/web/translation/es-ES.json
+++ b/web/translation/es-ES.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "¿Eliminar al cliente {email}?",
"deleteConfirmContent": "Esto elimina al cliente de cada inbound asociado y descarta su registro de tráfico. No se puede deshacer.",
"deleteSelected": "Eliminar ({count})",
+ "adjustSelected": "Ajustar ({count})",
"bulkDeleteConfirmTitle": "¿Eliminar {count} clientes?",
"bulkDeleteConfirmContent": "Cada cliente seleccionado se elimina de los inbounds asociados y se descarta su registro de tráfico. No se puede deshacer.",
+ "bulkAdjustTitle": "Ajustar {count} clientes",
+ "bulkAdjustHint": "Los valores positivos extienden, los negativos reducen. Los clientes con expiración o tráfico ilimitado se omiten para ese campo.",
+ "bulkAdjustNothing": "Establece días o tráfico antes de aplicar.",
+ "addDays": "Añadir días",
+ "addTrafficGB": "Añadir tráfico (GB)",
"delDepleted": "Eliminar agotados",
"delDepletedConfirmTitle": "¿Eliminar clientes agotados?",
"delDepletedConfirmContent": "Elimina todos los clientes con cuota agotada o expirados. No se puede deshacer.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} eliminados, {failed} fallidos",
"bulkCreated": "{count} clientes creados",
"bulkCreatedMixed": "{ok} creados, {failed} fallidos",
+ "bulkAdjusted": "{count} clientes ajustados",
+ "bulkAdjustedMixed": "{ok} ajustados, {skipped} omitidos",
"delDepleted": "{count} clientes agotados eliminados"
}
},
diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json
index fe863164..69d8048a 100644
--- a/web/translation/fa-IR.json
+++ b/web/translation/fa-IR.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "حذف کلاینت {email}؟",
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
"deleteSelected": "حذف ({count})",
+ "adjustSelected": "تنظیم ({count})",
"bulkDeleteConfirmTitle": "حذف {count} کلاینت؟",
"bulkDeleteConfirmContent": "هر کلاینت انتخابشده از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
+ "bulkAdjustTitle": "تنظیم {count} کلاینت",
+ "bulkAdjustHint": "مقادیر مثبت اضافه و منفی کم میکنند. کلاینتهایی که زمان یا ترافیک نامحدود دارند برای همان فیلد رد میشوند.",
+ "bulkAdjustNothing": "قبل از اعمال، روز یا ترافیک را تنظیم کنید.",
+ "addDays": "افزودن روز",
+ "addTrafficGB": "افزودن ترافیک (گیگابایت)",
"delDepleted": "حذف اتمامیافتهها",
"delDepletedConfirmTitle": "حذف کلاینتهای اتمامیافته؟",
"delDepletedConfirmContent": "هر کلاینتی که سهمیه ترافیکاش تمام شده یا تاریخ انقضایش گذشته است حذف میشود. این عمل غیرقابل بازگشت است.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} حذف، {failed} ناموفق",
"bulkCreated": "{count} کلاینت ساخته شد",
"bulkCreatedMixed": "{ok} ساخته شد، {failed} ناموفق",
+ "bulkAdjusted": "{count} کلاینت تنظیم شد",
+ "bulkAdjustedMixed": "{ok} تنظیم، {skipped} رد شد",
"delDepleted": "{count} کلاینت اتمامیافته حذف شد"
}
},
diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json
index 452f6897..5dc70580 100644
--- a/web/translation/id-ID.json
+++ b/web/translation/id-ID.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Hapus klien {email}?",
"deleteConfirmContent": "Tindakan ini menghapus klien dari setiap inbound terlampir dan menghapus catatan lalu lintasnya. Tidak dapat dibatalkan.",
"deleteSelected": "Hapus ({count})",
+ "adjustSelected": "Sesuaikan ({count})",
"bulkDeleteConfirmTitle": "Hapus {count} klien?",
"bulkDeleteConfirmContent": "Setiap klien yang dipilih dihapus dari semua inbound terlampir dan catatan lalu lintasnya dihapus. Tidak dapat dibatalkan.",
+ "bulkAdjustTitle": "Sesuaikan {count} klien",
+ "bulkAdjustHint": "Nilai positif menambah, negatif mengurangi. Klien dengan masa berlaku atau trafik tak terbatas dilewati untuk bidang tersebut.",
+ "bulkAdjustNothing": "Setel hari atau trafik sebelum menerapkan.",
+ "addDays": "Tambah hari",
+ "addTrafficGB": "Tambah trafik (GB)",
"delDepleted": "Hapus yang habis",
"delDepletedConfirmTitle": "Hapus klien yang habis?",
"delDepletedConfirmContent": "Hapus setiap klien yang kuota lalu lintasnya habis atau yang masa berlakunya telah berakhir. Tidak dapat dibatalkan.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} dihapus, {failed} gagal",
"bulkCreated": "{count} klien dibuat",
"bulkCreatedMixed": "{ok} dibuat, {failed} gagal",
+ "bulkAdjusted": "{count} klien disesuaikan",
+ "bulkAdjustedMixed": "{ok} disesuaikan, {skipped} dilewati",
"delDepleted": "{count} klien habis dihapus"
}
},
diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json
index 7d1970b1..73928cee 100644
--- a/web/translation/ja-JP.json
+++ b/web/translation/ja-JP.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "クライアント {email} を削除しますか?",
"deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
"deleteSelected": "削除 ({count})",
+ "adjustSelected": "調整 ({count})",
"bulkDeleteConfirmTitle": "{count} 件のクライアントを削除しますか?",
"bulkDeleteConfirmContent": "選択された各クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
+ "bulkAdjustTitle": "{count} 件のクライアントを調整",
+ "bulkAdjustHint": "正の値は延長、負の値は短縮します。無期限の有効期限または無制限のトラフィックを持つクライアントは、その項目についてスキップされます。",
+ "bulkAdjustNothing": "適用する前に日数またはトラフィックを設定してください。",
+ "addDays": "日数を追加",
+ "addTrafficGB": "トラフィックを追加 (GB)",
"delDepleted": "使い切ったクライアントを削除",
"delDepletedConfirmTitle": "使い切ったクライアントを削除しますか?",
"delDepletedConfirmContent": "トラフィック上限に達したか有効期限が切れたクライアントをすべて削除します。元に戻せません。",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} 件削除、{failed} 件失敗",
"bulkCreated": "{count} 件のクライアントを作成しました",
"bulkCreatedMixed": "{ok} 件作成、{failed} 件失敗",
+ "bulkAdjusted": "{count} 件のクライアントを調整しました",
+ "bulkAdjustedMixed": "{ok} 件調整、{skipped} 件スキップ",
"delDepleted": "使い切った {count} 件のクライアントを削除しました"
}
},
diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json
index c977db3d..f84fd4eb 100644
--- a/web/translation/pt-BR.json
+++ b/web/translation/pt-BR.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Excluir o cliente {email}?",
"deleteConfirmContent": "Isto remove o cliente de cada inbound associado e descarta o registro de tráfego. Não é possível desfazer.",
"deleteSelected": "Excluir ({count})",
+ "adjustSelected": "Ajustar ({count})",
"bulkDeleteConfirmTitle": "Excluir {count} clientes?",
"bulkDeleteConfirmContent": "Cada cliente selecionado é removido dos inbounds associados e o registro de tráfego é descartado. Não é possível desfazer.",
+ "bulkAdjustTitle": "Ajustar {count} clientes",
+ "bulkAdjustHint": "Valores positivos estendem, negativos reduzem. Clientes com expiração ou tráfego ilimitado são ignorados para esse campo.",
+ "bulkAdjustNothing": "Defina dias ou tráfego antes de aplicar.",
+ "addDays": "Adicionar dias",
+ "addTrafficGB": "Adicionar tráfego (GB)",
"delDepleted": "Excluir esgotados",
"delDepletedConfirmTitle": "Excluir clientes esgotados?",
"delDepletedConfirmContent": "Remove todos os clientes cuja cota de tráfego foi esgotada ou cuja expiração já passou. Não é possível desfazer.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} excluídos, {failed} com falha",
"bulkCreated": "{count} clientes criados",
"bulkCreatedMixed": "{ok} criados, {failed} com falha",
+ "bulkAdjusted": "{count} clientes ajustados",
+ "bulkAdjustedMixed": "{ok} ajustados, {skipped} ignorados",
"delDepleted": "{count} clientes esgotados excluídos"
}
},
diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json
index 4208b59b..9fbb32de 100644
--- a/web/translation/ru-RU.json
+++ b/web/translation/ru-RU.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Удалить клиента {email}?",
"deleteConfirmContent": "Клиент будет удалён из всех привязанных входящих, а его запись трафика будет уничтожена. Это действие нельзя отменить.",
"deleteSelected": "Удалить ({count})",
+ "adjustSelected": "Изменить ({count})",
"bulkDeleteConfirmTitle": "Удалить {count} клиентов?",
"bulkDeleteConfirmContent": "Каждый выбранный клиент удаляется из всех привязанных входящих, его запись трафика уничтожается. Это действие нельзя отменить.",
+ "bulkAdjustTitle": "Изменить {count} клиентов",
+ "bulkAdjustHint": "Положительные значения добавляют, отрицательные — уменьшают. Клиенты с неограниченным сроком или трафиком пропускаются для соответствующего поля.",
+ "bulkAdjustNothing": "Укажите дни или трафик перед применением.",
+ "addDays": "Добавить дни",
+ "addTrafficGB": "Добавить трафик (ГБ)",
"delDepleted": "Удалить исчерпанных",
"delDepletedConfirmTitle": "Удалить исчерпанных клиентов?",
"delDepletedConfirmContent": "Удаляются все клиенты, у которых исчерпана квота трафика или истёк срок. Это действие нельзя отменить.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "Удалено: {ok}, не удалось: {failed}",
"bulkCreated": "Создано клиентов: {count}",
"bulkCreatedMixed": "Создано: {ok}, не удалось: {failed}",
+ "bulkAdjusted": "Изменено клиентов: {count}",
+ "bulkAdjustedMixed": "Изменено: {ok}, пропущено: {skipped}",
"delDepleted": "Удалено исчерпанных клиентов: {count}"
}
},
diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json
index a2ffa111..7a5113d6 100644
--- a/web/translation/tr-TR.json
+++ b/web/translation/tr-TR.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "{email} istemcisi silinsin mi?",
"deleteConfirmContent": "Bu işlem istemciyi bağlı tüm inbound'lardan kaldırır ve trafik kaydını siler. Geri alınamaz.",
"deleteSelected": "Sil ({count})",
+ "adjustSelected": "Ayarla ({count})",
"bulkDeleteConfirmTitle": "{count} istemci silinsin mi?",
"bulkDeleteConfirmContent": "Seçili her istemci bağlı tüm inbound'lardan kaldırılır ve trafik kaydı silinir. Geri alınamaz.",
+ "bulkAdjustTitle": "{count} istemciyi ayarla",
+ "bulkAdjustHint": "Pozitif değerler ekler, negatif değerler azaltır. Sınırsız süreli veya trafikli istemciler ilgili alan için atlanır.",
+ "bulkAdjustNothing": "Uygulamadan önce gün veya trafik belirleyin.",
+ "addDays": "Gün ekle",
+ "addTrafficGB": "Trafik ekle (GB)",
"delDepleted": "Tükenmişleri sil",
"delDepletedConfirmTitle": "Tükenmiş istemciler silinsin mi?",
"delDepletedConfirmContent": "Trafik kotası dolan veya süresi geçen tüm istemciler silinir. Geri alınamaz.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "{ok} silindi, {failed} başarısız",
"bulkCreated": "{count} istemci oluşturuldu",
"bulkCreatedMixed": "{ok} oluşturuldu, {failed} başarısız",
+ "bulkAdjusted": "{count} istemci ayarlandı",
+ "bulkAdjustedMixed": "{ok} ayarlandı, {skipped} atlandı",
"delDepleted": "{count} tükenmiş istemci silindi"
}
},
diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json
index e015bafd..4cc60f43 100644
--- a/web/translation/uk-UA.json
+++ b/web/translation/uk-UA.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Видалити клієнта {email}?",
"deleteConfirmContent": "Клієнт буде вилучений з усіх прив'язаних вхідних, його запис трафіку буде знищено. Цю дію неможливо скасувати.",
"deleteSelected": "Видалити ({count})",
+ "adjustSelected": "Змінити ({count})",
"bulkDeleteConfirmTitle": "Видалити {count} клієнтів?",
"bulkDeleteConfirmContent": "Кожен вибраний клієнт вилучається з усіх прив'язаних вхідних, його запис трафіку знищується. Цю дію неможливо скасувати.",
+ "bulkAdjustTitle": "Змінити {count} клієнтів",
+ "bulkAdjustHint": "Додатні значення подовжують, від'ємні зменшують. Клієнти з необмеженим терміном або трафіком пропускаються для відповідного поля.",
+ "bulkAdjustNothing": "Вкажіть дні або трафік перед застосуванням.",
+ "addDays": "Додати дні",
+ "addTrafficGB": "Додати трафік (ГБ)",
"delDepleted": "Видалити вичерпаних",
"delDepletedConfirmTitle": "Видалити вичерпаних клієнтів?",
"delDepletedConfirmContent": "Видаляються всі клієнти, у яких вичерпана квота трафіку або сплив термін. Цю дію неможливо скасувати.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "Видалено: {ok}, не вдалось: {failed}",
"bulkCreated": "Створено клієнтів: {count}",
"bulkCreatedMixed": "Створено: {ok}, не вдалось: {failed}",
+ "bulkAdjusted": "Змінено клієнтів: {count}",
+ "bulkAdjustedMixed": "Змінено: {ok}, пропущено: {skipped}",
"delDepleted": "Видалено вичерпаних клієнтів: {count}"
}
},
diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json
index be74b9ec..d5428561 100644
--- a/web/translation/vi-VN.json
+++ b/web/translation/vi-VN.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "Xóa khách hàng {email}?",
"deleteConfirmContent": "Hành động này gỡ khách hàng khỏi mọi inbound đã gắn và xóa bản ghi lưu lượng. Không thể hoàn tác.",
"deleteSelected": "Xóa ({count})",
+ "adjustSelected": "Điều chỉnh ({count})",
"bulkDeleteConfirmTitle": "Xóa {count} khách hàng?",
"bulkDeleteConfirmContent": "Mỗi khách hàng được chọn sẽ bị gỡ khỏi tất cả inbound đã gắn và bản ghi lưu lượng cũng bị xóa. Không thể hoàn tác.",
+ "bulkAdjustTitle": "Điều chỉnh {count} khách hàng",
+ "bulkAdjustHint": "Giá trị dương kéo dài, giá trị âm rút ngắn. Khách hàng có hạn hoặc lưu lượng không giới hạn sẽ bị bỏ qua cho trường đó.",
+ "bulkAdjustNothing": "Đặt số ngày hoặc lưu lượng trước khi áp dụng.",
+ "addDays": "Thêm ngày",
+ "addTrafficGB": "Thêm lưu lượng (GB)",
"delDepleted": "Xóa hết hạn mức",
"delDepletedConfirmTitle": "Xóa khách hàng hết hạn mức?",
"delDepletedConfirmContent": "Gỡ tất cả khách hàng đã dùng hết hạn mức lưu lượng hoặc đã quá hạn. Không thể hoàn tác.",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "Đã xóa {ok}, thất bại {failed}",
"bulkCreated": "Đã tạo {count} khách hàng",
"bulkCreatedMixed": "Đã tạo {ok}, thất bại {failed}",
+ "bulkAdjusted": "Đã điều chỉnh {count} khách hàng",
+ "bulkAdjustedMixed": "Đã điều chỉnh {ok}, bỏ qua {skipped}",
"delDepleted": "Đã xóa {count} khách hàng hết hạn mức"
}
},
diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json
index 8e800ac7..6180579f 100644
--- a/web/translation/zh-CN.json
+++ b/web/translation/zh-CN.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "删除客户端 {email}?",
"deleteConfirmContent": "将从所有关联入站中移除该客户端并删除其流量记录。该操作不可撤销。",
"deleteSelected": "删除 ({count})",
+ "adjustSelected": "调整 ({count})",
"bulkDeleteConfirmTitle": "删除 {count} 个客户端?",
"bulkDeleteConfirmContent": "每个所选客户端都会从关联的入站中被移除,其流量记录也会被删除。该操作不可撤销。",
+ "bulkAdjustTitle": "调整 {count} 个客户端",
+ "bulkAdjustHint": "正值延长,负值减少。具有无限期限或流量的客户端将跳过该字段。",
+ "bulkAdjustNothing": "应用前请设置天数或流量。",
+ "addDays": "添加天数",
+ "addTrafficGB": "添加流量 (GB)",
"delDepleted": "删除已耗尽",
"delDepletedConfirmTitle": "删除已耗尽的客户端?",
"delDepletedConfirmContent": "删除所有流量配额已用尽或已过期的客户端。该操作不可撤销。",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "已删除 {ok} 个,失败 {failed} 个",
"bulkCreated": "已创建 {count} 个客户端",
"bulkCreatedMixed": "已创建 {ok} 个,失败 {failed} 个",
+ "bulkAdjusted": "已调整 {count} 个客户端",
+ "bulkAdjustedMixed": "已调整 {ok} 个,跳过 {skipped} 个",
"delDepleted": "已删除 {count} 个已耗尽的客户端"
}
},
diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json
index 2e335d09..8b839885 100644
--- a/web/translation/zh-TW.json
+++ b/web/translation/zh-TW.json
@@ -479,8 +479,14 @@
"deleteConfirmTitle": "刪除客戶端 {email}?",
"deleteConfirmContent": "將從所有關聯入站中移除該客戶端並刪除其流量紀錄。此操作無法復原。",
"deleteSelected": "刪除 ({count})",
+ "adjustSelected": "調整 ({count})",
"bulkDeleteConfirmTitle": "刪除 {count} 個客戶端?",
"bulkDeleteConfirmContent": "每個所選客戶端都會從關聯的入站中被移除,其流量紀錄也會被刪除。此操作無法復原。",
+ "bulkAdjustTitle": "調整 {count} 個客戶端",
+ "bulkAdjustHint": "正值延長,負值減少。具有無限期限或流量的客戶端將跳過該欄位。",
+ "bulkAdjustNothing": "套用前請設定天數或流量。",
+ "addDays": "新增天數",
+ "addTrafficGB": "新增流量 (GB)",
"delDepleted": "刪除已耗盡",
"delDepletedConfirmTitle": "刪除已耗盡的客戶端?",
"delDepletedConfirmContent": "刪除所有流量配額已用盡或已過期的客戶端。此操作無法復原。",
@@ -503,6 +509,8 @@
"bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個",
"bulkCreated": "已建立 {count} 個客戶端",
"bulkCreatedMixed": "已建立 {ok} 個,失敗 {failed} 個",
+ "bulkAdjusted": "已調整 {count} 個客戶端",
+ "bulkAdjustedMixed": "已調整 {ok} 個,跳過 {skipped} 個",
"delDepleted": "已刪除 {count} 個已耗盡的客戶端"
}
},