diff --git a/app/db/crud/user.py b/app/db/crud/user.py index b4c3f20c9..65efa3b73 100644 --- a/app/db/crud/user.py +++ b/app/db/crud/user.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta from typing import Literal -from sqlalchemy import and_, bindparam, case, delete, desc, func, literal, not_, or_, select, update +from sqlalchemy import Row, and_, bindparam, case, delete, desc, func, literal, not_, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload, load_only, selectinload, with_expression from sqlalchemy.sql import Select @@ -666,7 +666,7 @@ def _cleanup_target_user_conditions( expired_after: datetime | None = None, expired_before: datetime | None = None, admin_id: int | None = None, - target: Literal["expired", "limited", "on_hold", "disabled"] = "expired", + target: Literal["expired", "limited", "on_hold", "disabled", "inactive"] = "expired", ): if target == "expired": # Time-expired users: date range filters on the expiration date. @@ -675,6 +675,15 @@ def _cleanup_target_user_conditions( conditions.append(User.expire >= expired_after) if expired_before: conditions.append(User.expire <= expired_before) + elif target == "inactive": + # Fall back to account creation for users that have never connected, + # so newly created accounts are not treated as long-term inactive. + last_activity = func.coalesce(User.online_at, User.created_at) + conditions = [] + if expired_after: + conditions.append(last_activity >= expired_after) + if expired_before: + conditions.append(last_activity <= expired_before) else: # For limited / on_hold / disabled: date range filters apply to # last_status_change (i.e. when the user entered that status). @@ -700,21 +709,27 @@ async def remove_expired_users( expired_after: datetime | None = None, expired_before: datetime | None = None, admin_id: int | None = None, - target: Literal["expired", "limited", "on_hold", "disabled"] = "expired", + target: Literal["expired", "limited", "on_hold", "disabled", "inactive"] = "expired", dry_run: bool = False, -) -> list[str]: +) -> tuple[list[str], list[Row]]: + """ + Delete the users matching a cleanup target. + + Returns the usernames, plus the deleted users that nodes still serve (active and on_hold, + as `id`/`proxy_settings` rows) so the caller can remove them from the nodes. + """ conditions = _cleanup_target_user_conditions(expired_after, expired_before, admin_id, target) - rows = (await db.execute(select(User.id, User.username).where(*conditions))).all() + rows = (await db.execute(select(User.id, User.username, User.status, User.proxy_settings).where(*conditions))).all() if not rows: - return [] + return [], [] - usernames = [username for _, username in rows] + usernames = [row.username for row in rows] if dry_run: - return usernames + return usernames, [] - user_ids = [user_id for user_id, _ in rows] + user_ids = [row.id for row in rows] for start in range(0, len(user_ids), 1000): chunk = user_ids[start : start + 1000] @@ -723,7 +738,7 @@ async def remove_expired_users( await db.execute(delete(User).where(User.id.in_(chunk))) await db.commit() - return usernames + return usernames, [row for row in rows if row.status in (UserStatus.active, UserStatus.on_hold)] async def get_active_to_expire_users(db: AsyncSession) -> list[User]: diff --git a/app/models/user.py b/app/models/user.py index e067ba57a..1b7318441 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -313,7 +313,7 @@ class UsersUsageBreakdownQuery(UsersUsageQuery): class ExpiredUsersQuery(BaseModel): admin_username: str | None = Field(default=None) - target: Literal["expired", "limited", "on_hold", "disabled"] = Field(default="expired") + target: Literal["expired", "limited", "on_hold", "disabled", "inactive"] = Field(default="expired") expired_after: OptionalAwareDatetime = Field(default=None, examples=["2024-01-01T00:00:00+03:30"]) expired_before: OptionalAwareDatetime = Field(default=None, examples=["2024-01-31T23:59:59+03:30"]) dry_run: bool = Field( @@ -321,6 +321,12 @@ class ExpiredUsersQuery(BaseModel): description="If true, returns users that would be deleted without actually deleting them.", ) + @model_validator(mode="after") + def validate_inactive_cutoff(self): + if self.target == "inactive" and self.expired_before is None: + raise ValueError("expired_before is required when target is inactive") + return self + class UserSubscriptionUpdateSchema(BaseModel): created_at: AwareDatetime diff --git a/app/operation/user.py b/app/operation/user.py index 218d6fe6e..21d243801 100644 --- a/app/operation/user.py +++ b/app/operation/user.py @@ -97,7 +97,7 @@ UsersUsageQuery, UserUsageQuery, ) -from app.node.sync import remove_user as sync_remove_user, sync_user, sync_users +from app.node.sync import remove_user as sync_remove_user, remove_users as sync_remove_users, sync_user, sync_users from app.node.user import node_payload_signature from app.operation import BaseOperation, OperatorType from app.operation.permissions import ( @@ -1603,10 +1603,11 @@ async def get_expired_users( """ Get users who match the cleanup target within the specified date range. - - **target**: `expired` | `limited` | `on_hold` | `disabled` + - **target**: `expired` | `limited` | `on_hold` | `disabled` | `inactive` - **expired_after** / **expired_before** UTC datetime (optional) - For `expired`: filters by expiration date. - For `limited` / `on_hold` / `disabled`: filters by last_status_change. + - For `inactive`: filters by last activity (online_at, or created_at if never online). - If both dates are omitted, returns all users matching target. """ @@ -1631,10 +1632,11 @@ async def delete_expired_users( """ Delete users who match the cleanup target within the specified date range. - - **target**: `expired` | `limited` | `on_hold` | `disabled` + - **target**: `expired` | `limited` | `on_hold` | `disabled` | `inactive` - **expired_after** / **expired_before** UTC datetime (optional) - For `expired`: filters by expiration date. - For `limited` / `on_hold` / `disabled`: filters by last_status_change. + - For `inactive`: filters by last activity (online_at, or created_at if never online). - **dry_run**: if true, returns the list of users that would be deleted without deleting them. """ @@ -1644,7 +1646,7 @@ async def delete_expired_users( admin_id = (await self.get_validated_admin(db, query.admin_username)).id else: admin_id = None - username_list = await remove_expired_users( + username_list, node_users = await remove_expired_users( db, expired_after, expired_before, @@ -1653,6 +1655,8 @@ async def delete_expired_users( dry_run=query.dry_run, ) if not query.dry_run: + # Active and on_hold users are still on the nodes (the inactive target includes them). + await sync_remove_users(node_users) await self.remove_users_logger(users=username_list, by=admin.username) return RemoveUsersResponse(users=username_list, count=len(username_list)) diff --git a/app/routers/user.py b/app/routers/user.py index a9ffff916..f65da8b2c 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -607,10 +607,11 @@ async def get_expired_users( """ Get cleanup-target users in the specified scope. - - **target**: `expired` | `limited` | `on_hold` | `disabled` + - **target**: `expired` | `limited` | `on_hold` | `disabled` | `inactive` - **expired_after** / **expired_before** UTC datetime (optional) - For `expired`: filters by expiration date. - For `limited` / `on_hold` / `disabled`: filters by last_status_change (when they entered that status). + - For `inactive`: filters by last activity (online_at, or created_at if never online); expired_before is required. """ return await user_operator.get_expired_users(db, query=query) @@ -625,10 +626,11 @@ async def delete_expired_users( """ Delete cleanup-target users in the specified scope. - - **target**: `expired` | `limited` | `on_hold` | `disabled` + - **target**: `expired` | `limited` | `on_hold` | `disabled` | `inactive` - **expired_after** / **expired_before** UTC datetime (optional) - For `expired`: filters by expiration date. - For `limited` / `on_hold` / `disabled`: filters by last_status_change (when they entered that status). + - For `inactive`: filters by last activity (online_at, or created_at if never online); expired_before is required. - **dry_run**: if true, returns users that would be deleted without deleting them. """ return await user_operator.delete_expired_users(db, admin, query=query) diff --git a/dashboard/public/statics/locales/en.json b/dashboard/public/statics/locales/en.json index ccd382274..9bd429633 100644 --- a/dashboard/public/statics/locales/en.json +++ b/dashboard/public/statics/locales/en.json @@ -554,10 +554,12 @@ "expired": "Time Expired", "limited": "Usage Limited", "on_hold": "On Hold", - "disabled": "Disabled" + "disabled": "Disabled", + "inactive": "Inactive Users" }, "statusChangedAfter": "Status Changed After", "statusChangedBefore": "Status Changed Before", + "lastActivityBefore": "Last Activity Before", "expiredAfterPlaceholder": "Select start date", "expiredBeforePlaceholder": "Select end date", "dateRange": "Date Range", @@ -568,27 +570,33 @@ "deleteLimited": "Delete Limited", "deleteOnHold": "Delete On Hold", "deleteDisabled": "Delete Disabled", + "deleteInactive": "Delete Inactive", "deleting": "Deleting...", "deleteSuccess": "{{count}} expired accounts deleted successfully", "deleteLimitedSuccess": "{{count}} limited accounts deleted successfully", "deleteOnHoldSuccess": "{{count}} on-hold accounts deleted successfully", "deleteDisabledSuccess": "{{count}} disabled accounts deleted successfully", + "deleteInactiveSuccess": "{{count}} inactive accounts deleted successfully", "dryRunSuccess": "Dry run: {{count}} accounts would be deleted", "deleteFailed": "Failed to delete expired accounts", "deleteLimitedFailed": "Failed to delete limited accounts", "deleteOnHoldFailed": "Failed to delete on-hold accounts", "deleteDisabledFailed": "Failed to delete disabled accounts", + "deleteInactiveFailed": "Failed to delete inactive accounts", "confirmDelete": "Delete Expired Accounts", "confirmDeleteLimited": "Delete Limited Accounts", "confirmDeleteOnHold": "Delete On Hold Accounts", "confirmDeleteDisabled": "Delete Disabled Accounts", + "confirmDeleteInactive": "Delete Inactive Accounts", "confirmDeleteMessage": "Are you sure you want to delete all expired accounts in the selected date range? This action cannot be undone.", "confirmDeleteLimitedMessage": "Are you sure you want to delete all usage-limited accounts in the selected date range? This action cannot be undone.", "confirmDeleteOnHoldMessage": "Are you sure you want to delete all on-hold accounts in the selected date range? This action cannot be undone.", "confirmDeleteDisabledMessage": "Are you sure you want to delete all disabled accounts in the selected date range? This action cannot be undone.", + "confirmDeleteInactiveMessage": "Are you sure you want to delete accounts whose last activity was before the selected date? This action cannot be undone.", "noDateSelected": "Please select at least one date", "selectDateRange": "Filter by expiration date. Leave both empty to delete all matching accounts.", "selectStatusChangedDateRange": "Filter by the date the user entered this status. Leave both empty to delete all matching accounts.", + "selectInactiveDate": "Select a cutoff date. Accounts last online before it will match; for accounts never online, their creation date is used.", "selectLimitedInfo": "Delete all usage-limited accounts for the selected admin scope." }, "resetUsage": { diff --git a/dashboard/public/statics/locales/fa.json b/dashboard/public/statics/locales/fa.json index cea6a6c79..9e0ed3027 100644 --- a/dashboard/public/statics/locales/fa.json +++ b/dashboard/public/statics/locales/fa.json @@ -406,10 +406,12 @@ "expired": "انقضای زمانی", "limited": "محدودیت حجم", "on_hold": "در انتظار", - "disabled": "غیرفعال" + "disabled": "غیرفعال", + "inactive": "کاربران بدون فعالیت" }, "statusChangedAfter": "تغییر وضعیت بعد از", "statusChangedBefore": "تغییر وضعیت قبل از", + "lastActivityBefore": "آخرین فعالیت قبل از", "expiredAfterPlaceholder": "تاریخ شروع را انتخاب کنید", "expiredBeforePlaceholder": "تاریخ پایان را انتخاب کنید", "dateRange": "بازه زمانی", @@ -420,27 +422,33 @@ "deleteLimited": "حذف محدودها", "deleteOnHold": "حذف در انتظارها", "deleteDisabled": "حذف غیرفعال‌ها", + "deleteInactive": "حذف کاربران بدون فعالیت", "deleting": "در حال حذف...", "deleteSuccess": "{{count}} حساب منقضی شده با موفقیت حذف شد", "deleteLimitedSuccess": "{{count}} حساب محدود با موفقیت حذف شد", "deleteOnHoldSuccess": "{{count}} حساب در انتظار با موفقیت حذف شد", "deleteDisabledSuccess": "{{count}} حساب غیرفعال با موفقیت حذف شد", + "deleteInactiveSuccess": "{{count}} حساب بدون فعالیت با موفقیت حذف شد", "dryRunSuccess": "آزمایشی: {{count}} حساب حذف می‌شد", "deleteFailed": "حذف حساب‌های منقضی شده ناموفق بود", "deleteLimitedFailed": "حذف حساب‌های محدود ناموفق بود", "deleteOnHoldFailed": "حذف حساب‌های در انتظار ناموفق بود", "deleteDisabledFailed": "حذف حساب‌های غیرفعال ناموفق بود", + "deleteInactiveFailed": "حذف حساب‌های بدون فعالیت ناموفق بود", "confirmDelete": "حذف حساب‌های منقضی شده", "confirmDeleteLimited": "حذف حساب‌های محدود", "confirmDeleteOnHold": "حذف حساب‌های در انتظار", "confirmDeleteDisabled": "حذف حساب‌های غیرفعال", + "confirmDeleteInactive": "حذف حساب‌های بدون فعالیت", "confirmDeleteMessage": "آیا مطمئنید که می‌خواهید تمام حساب‌های منقضی شده در بازه زمانی انتخابی را حذف کنید؟ این عمل قابل بازگشت نیست.", "confirmDeleteLimitedMessage": "آیا مطمئنید که می‌خواهید تمام حساب‌های محدود در بازه زمانی انتخابی را حذف کنید؟ این عمل قابل بازگشت نیست.", "confirmDeleteOnHoldMessage": "آیا مطمئنید که می‌خواهید تمام حساب‌های در انتظار در بازه زمانی انتخابی را حذف کنید؟ این عمل قابل بازگشت نیست.", "confirmDeleteDisabledMessage": "آیا مطمئنید که می‌خواهید تمام حساب‌های غیرفعال در بازه زمانی انتخابی را حذف کنید؟ این عمل قابل بازگشت نیست.", + "confirmDeleteInactiveMessage": "آیا مطمئنید که می‌خواهید حساب‌هایی را که آخرین فعالیتشان قبل از تاریخ انتخابی بوده حذف کنید؟ این عمل قابل بازگشت نیست.", "noDateSelected": "لطفاً حداقل یک تاریخ را انتخاب کنید", "selectDateRange": "بر اساس تاریخ انقضا فیلتر کنید. اگر هر دو تاریخ خالی باشند، تمام حساب‌های مطابق حذف می‌شوند.", "selectStatusChangedDateRange": "بر اساس تاریخ ورود کاربر به این وضعیت فیلتر کنید. اگر هر دو تاریخ خالی باشند، تمام حساب‌های مطابق حذف می‌شوند.", + "selectInactiveDate": "یک تاریخ مرزی انتخاب کنید. حساب‌هایی که آخرین اتصالشان قبل از آن بوده انتخاب می‌شوند؛ برای حساب‌هایی که هرگز آنلاین نشده‌اند، تاریخ ساخت حساب ملاک است.", "selectLimitedInfo": "حذف تمام حساب‌های محدود در محدوده مدیر انتخابی." }, "resetUsage": { diff --git a/dashboard/public/statics/locales/ru.json b/dashboard/public/statics/locales/ru.json index cde02d81d..f09956051 100644 --- a/dashboard/public/statics/locales/ru.json +++ b/dashboard/public/statics/locales/ru.json @@ -564,10 +564,12 @@ "expired": "По сроку", "limited": "По трафику", "on_hold": "На удержании", - "disabled": "Отключённые" + "disabled": "Отключённые", + "inactive": "Неактивные пользователи" }, "statusChangedAfter": "Статус изменён после", "statusChangedBefore": "Статус изменён до", + "lastActivityBefore": "Последняя активность до", "expiredAfterPlaceholder": "Выберите дату начала", "expiredBeforePlaceholder": "Выберите дату окончания", "dateRange": "Диапазон дат", @@ -578,27 +580,33 @@ "deleteLimited": "Удалить ограниченные", "deleteOnHold": "Удалить на удержании", "deleteDisabled": "Удалить отключённые", + "deleteInactive": "Удалить неактивные", "deleting": "Удаление...", "deleteSuccess": "{{count}} просроченных аккаунтов успешно удалено", "deleteLimitedSuccess": "Успешно удалено {{count}} ограниченных аккаунтов", "deleteOnHoldSuccess": "Успешно удалено {{count}} аккаунтов на удержании", "deleteDisabledSuccess": "Успешно удалено {{count}} отключённых аккаунтов", + "deleteInactiveSuccess": "Успешно удалено {{count}} неактивных аккаунтов", "dryRunSuccess": "Тестовый запуск: {{count}} аккаунтов будет удалено", "deleteFailed": "Не удалось удалить просроченные аккаунты", "deleteLimitedFailed": "Не удалось удалить ограниченные аккаунты", "deleteOnHoldFailed": "Не удалось удалить аккаунты на удержании", "deleteDisabledFailed": "Не удалось удалить отключённые аккаунты", + "deleteInactiveFailed": "Не удалось удалить неактивные аккаунты", "confirmDelete": "Удалить просроченные аккаунты", "confirmDeleteLimited": "Удалить ограниченные аккаунты", "confirmDeleteOnHold": "Удалить аккаунты на удержании", "confirmDeleteDisabled": "Удалить отключённые аккаунты", + "confirmDeleteInactive": "Удалить неактивные аккаунты", "confirmDeleteMessage": "Вы уверены, что хотите удалить все просроченные аккаунты в выбранном диапазоне дат? Это действие нельзя отменить.", "confirmDeleteLimitedMessage": "Вы уверены, что хотите удалить все ограниченные аккаунты в выбранном диапазоне дат? Это действие нельзя отменить.", "confirmDeleteOnHoldMessage": "Вы уверены, что хотите удалить все аккаунты на удержании в выбранном диапазоне дат? Это действие нельзя отменить.", "confirmDeleteDisabledMessage": "Вы уверены, что хотите удалить все отключённые аккаунты в выбранном диапазоне дат? Это действие нельзя отменить.", + "confirmDeleteInactiveMessage": "Удалить аккаунты, последняя активность которых была до выбранной даты? Это действие нельзя отменить.", "noDateSelected": "Пожалуйста, выберите хотя бы одну дату", "selectDateRange": "Фильтровать по дате истечения срока. Если обе даты пусты, будут удалены все подходящие аккаунты.", "selectStatusChangedDateRange": "Фильтровать по дате, когда пользователь получил этот статус. Если обе даты пусты, будут удалены все подходящие аккаунты.", + "selectInactiveDate": "Выберите дату отсечения. Будут выбраны аккаунты, которые последний раз были онлайн до неё; для аккаунтов, которые ещё не были онлайн, используется дата создания.", "selectLimitedInfo": "Удаляются все аккаунты с ограничением по трафику в выбранной области администратора." }, "resetUsage": { diff --git a/dashboard/public/statics/locales/zh.json b/dashboard/public/statics/locales/zh.json index 6892333a1..a9331d17a 100644 --- a/dashboard/public/statics/locales/zh.json +++ b/dashboard/public/statics/locales/zh.json @@ -525,10 +525,12 @@ "expired": "按时间过期", "limited": "按流量受限", "on_hold": "暂停中", - "disabled": "已禁用" + "disabled": "已禁用", + "inactive": "不活跃用户" }, "statusChangedAfter": "状态变更时间晚于", "statusChangedBefore": "状态变更时间早于", + "lastActivityBefore": "最后活动时间早于", "expiredAfterPlaceholder": "选择开始日期", "expiredBeforePlaceholder": "选择结束日期", "dateRange": "日期范围", @@ -539,27 +541,33 @@ "deleteLimited": "删除受限账户", "deleteOnHold": "删除暂停账户", "deleteDisabled": "删除禁用账户", + "deleteInactive": "删除不活跃账户", "deleting": "删除中...", "deleteSuccess": "成功删除 {{count}} 个过期账户", "deleteLimitedSuccess": "成功删除 {{count}} 个受限账户", "deleteOnHoldSuccess": "成功删除 {{count}} 个暂停账户", "deleteDisabledSuccess": "成功删除 {{count}} 个禁用账户", + "deleteInactiveSuccess": "成功删除 {{count}} 个不活跃账户", "dryRunSuccess": "模拟运行:将删除 {{count}} 个账户", "deleteFailed": "删除过期账户失败", "deleteLimitedFailed": "删除受限账户失败", "deleteOnHoldFailed": "删除暂停账户失败", "deleteDisabledFailed": "删除禁用账户失败", + "deleteInactiveFailed": "删除不活跃账户失败", "confirmDelete": "删除过期账户", "confirmDeleteLimited": "删除受限账户", "confirmDeleteOnHold": "删除暂停账户", "confirmDeleteDisabled": "删除禁用账户", + "confirmDeleteInactive": "删除不活跃账户", "confirmDeleteMessage": "您确定要删除所选日期范围内的所有过期账户吗?此操作无法撤销。", "confirmDeleteLimitedMessage": "您确定要删除所选日期范围内的所有受限账户吗?此操作无法撤销。", "confirmDeleteOnHoldMessage": "您确定要删除所选日期范围内的所有暂停账户吗?此操作无法撤销。", "confirmDeleteDisabledMessage": "您确定要删除所选日期范围内的所有禁用账户吗?此操作无法撤销。", + "confirmDeleteInactiveMessage": "您确定要删除最后活动时间早于所选日期的账户吗?此操作无法撤销。", "noDateSelected": "请至少选择一个日期", "selectDateRange": "按到期日期过滤。如果开始和结束日期都为空,将删除所有匹配账户。", "selectStatusChangedDateRange": "按用户进入此状态的日期过滤。如果开始和结束日期都为空,将删除所有匹配账户。", + "selectInactiveDate": "请选择截止日期。最后在线时间早于该日期的账户将被选中;从未上线的账户使用创建日期。", "selectLimitedInfo": "将删除所选管理员范围内的所有按流量受限账户。" }, "resetUsage": { diff --git a/dashboard/src/pages/_dashboard.settings.cleanup.tsx b/dashboard/src/pages/_dashboard.settings.cleanup.tsx index aebcfd999..26fef1d41 100644 --- a/dashboard/src/pages/_dashboard.settings.cleanup.tsx +++ b/dashboard/src/pages/_dashboard.settings.cleanup.tsx @@ -30,7 +30,7 @@ import { toast } from 'sonner' import { hasScopeAll, isOwner, roleLabel } from '@/utils/rbac' const PAGE_SIZE = 20 -type CleanupDeleteTarget = 'expired' | 'limited' | 'on_hold' | 'disabled' +type CleanupDeleteTarget = 'expired' | 'limited' | 'on_hold' | 'disabled' | 'inactive' export default function CleanupSettings() { const { t, i18n } = useTranslation() @@ -110,7 +110,7 @@ export default function CleanupSettings() { const deleteExpiredUsersMutation = useDeleteExpiredUsers() const resetUsersDataUsageMutation = useResetUsersDataUsage() const clearUsageDataMutation = useClearUsageData() - + // Track which operation is running const [isPreviewRunning, setIsPreviewRunning] = useState(false) @@ -123,7 +123,7 @@ export default function CleanupSettings() { const target = deleteTarget const params: any = { target, dry_run: isDryRun } - if (statusChangedAfter) params.expired_after = startOfDay(statusChangedAfter).toISOString() + if (statusChangedAfter && target !== 'inactive') params.expired_after = startOfDay(statusChangedAfter).toISOString() if (statusChangedBefore) params.expired_before = endOfDay(statusChangedBefore).toISOString() if (selectedAdmin) params.admin_username = selectedAdmin.username @@ -132,12 +132,14 @@ export default function CleanupSettings() { limited: 'settings.cleanup.expiredUsers.deleteLimitedSuccess', on_hold: 'settings.cleanup.expiredUsers.deleteOnHoldSuccess', disabled: 'settings.cleanup.expiredUsers.deleteDisabledSuccess', + inactive: 'settings.cleanup.expiredUsers.deleteInactiveSuccess', } const failKeyMap: Record = { expired: 'settings.cleanup.expiredUsers.deleteFailed', limited: 'settings.cleanup.expiredUsers.deleteLimitedFailed', on_hold: 'settings.cleanup.expiredUsers.deleteOnHoldFailed', disabled: 'settings.cleanup.expiredUsers.deleteDisabledFailed', + inactive: 'settings.cleanup.expiredUsers.deleteInactiveFailed', } // Set preview state if this is a dry run @@ -184,7 +186,7 @@ export default function CleanupSettings() { } toast.error(t(failureMessageKey), { description: errorMessage }) - + // Reset preview state on error if (isDryRun) { setIsPreviewRunning(false) @@ -405,33 +407,44 @@ export default function CleanupSettings() { {t('settings.cleanup.expiredUsers.targets.disabled')} + + {t('settings.cleanup.expiredUsers.targets.inactive')} +
-
- -
+ {deleteTarget !== 'inactive' && ( +
+ +
+ )}
- {deleteTarget === 'expired' ? t('settings.cleanup.expiredUsers.selectDateRange') : t('settings.cleanup.expiredUsers.selectStatusChangedDateRange')} + {deleteTarget === 'expired' + ? t('settings.cleanup.expiredUsers.selectDateRange') + : deleteTarget === 'inactive' + ? t('settings.cleanup.expiredUsers.selectInactiveDate') + : t('settings.cleanup.expiredUsers.selectStatusChangedDateRange')}
{/* Preview Button */} - {/* Delete Button with confirmation */} -