fix: add full i18n support for RemnaWave section and improve sync UI

This commit is contained in:
c0mrade
2026-02-02 11:04:25 +03:00
parent 6c795539f2
commit ed86dfa8bd
6 changed files with 326 additions and 121 deletions

View File

@@ -863,6 +863,11 @@
"memory": "Memory", "memory": "Memory",
"uptime": "Uptime", "uptime": "Uptime",
"traffic": "Traffic Statistics", "traffic": "Traffic Statistics",
"traffic2days": "2 days",
"traffic7days": "7 days",
"traffic30days": "30 days",
"trafficMonth": "Month",
"trafficYear": "Year",
"usersByStatus": "Users by Status" "usersByStatus": "Users by Status"
}, },
"nodes": { "nodes": {
@@ -874,29 +879,62 @@
"enable": "Enable", "enable": "Enable",
"disable": "Disable", "disable": "Disable",
"restartAll": "Restart All", "restartAll": "Restart All",
"confirmRestartAll": "Are you sure you want to restart all nodes?" "confirmRestartAll": "Are you sure you want to restart all nodes?",
"usersOnline": "online",
"trafficUsed": "used",
"uptimeLabel": "Uptime",
"stats": {
"total": "Total",
"online": "Online",
"offline": "Offline",
"disabled": "Disabled",
"users": "Users"
}
}, },
"squads": { "squads": {
"synced": "Synced", "synced": "Synced",
"notSynced": "Not synced", "notSynced": "Not synced",
"noSquads": "No squads found", "noSquads": "No squads found",
"syncServers": "Sync Servers" "syncServers": "Sync Servers",
"members": "members",
"inbounds": "inbounds",
"available": "Available",
"unavailable": "Unavailable",
"stats": {
"total": "Total",
"synced": "Synced",
"available": "Available",
"members": "Members"
},
"detail": "Squad Details",
"loadError": "Failed to load squad",
"info": "Information",
"originalName": "Original Name",
"countryCode": "Country",
"statsTitle": "Statistics",
"inboundsList": "Inbounds",
"localSettings": "Local Settings",
"trialEligible": "Trial Eligible",
"price": "Price",
"users": "Users"
}, },
"sync": { "sync": {
"autoSync": "Auto Sync", "autoSync": "Auto Sync",
"runNow": "Run Now", "runNow": "Run",
"manual": "Manual Sync", "run": "Run",
"fromPanel": "Sync from Panel", "running": "Running...",
"fromPanelDesc": "Import users from RemnaWave panel to bot database", "schedule": "Schedule",
"toPanel": "Sync to Panel", "status": "Status",
"toPanelDesc": "Export users from bot database to RemnaWave panel", "lastRun": "Last Run",
"subscriptions": "Subscriptions", "nextRun": "Next Run",
"validate": "Validate", "enabled": "Enabled",
"validateDesc": "Check and fix subscription inconsistencies", "disabled": "Disabled",
"cleanup": "Cleanup", "success": "Success",
"cleanupDesc": "Remove orphaned subscriptions without users", "runAutoSyncNow": "Run Auto Sync Now",
"statuses": "Sync Statuses", "fromPanel": "From Panel",
"statusesDesc": "Synchronize subscription statuses with panel" "fromPanelDesc": "Import users from RemnaWave to bot",
"toPanel": "To Panel",
"toPanelDesc": "Export users from bot to RemnaWave"
} }
}, },
"wheel": { "wheel": {

View File

@@ -1702,7 +1702,17 @@
"offline": "آفلاین", "offline": "آفلاین",
"online": "آنلاین", "online": "آنلاین",
"restart": "راه‌اندازی مجدد", "restart": "راه‌اندازی مجدد",
"restartAll": "راه‌اندازی مجدد همه" "restartAll": "راه‌اندازی مجدد همه",
"usersOnline": "آنلاین",
"trafficUsed": "استفاده شده",
"uptimeLabel": "زمان فعالیت",
"stats": {
"total": "کل",
"online": "آنلاین",
"offline": "آفلاین",
"disabled": "غیرفعال",
"users": "کاربران"
}
}, },
"overview": { "overview": {
"bandwidth": "پهنای باند لحظه‌ای", "bandwidth": "پهنای باند لحظه‌ای",
@@ -1716,6 +1726,11 @@
"total": "کل", "total": "کل",
"totalUsers": "کل کاربران", "totalUsers": "کل کاربران",
"traffic": "آمار ترافیک", "traffic": "آمار ترافیک",
"traffic2days": "۲ روز",
"traffic7days": "۷ روز",
"traffic30days": "۳۰ روز",
"trafficMonth": "ماه",
"trafficYear": "سال",
"upload": "آپلود", "upload": "آپلود",
"uptime": "زمان فعالیت", "uptime": "زمان فعالیت",
"usersByStatus": "کاربران بر اساس وضعیت", "usersByStatus": "کاربران بر اساس وضعیت",
@@ -1725,23 +1740,46 @@
"noSquads": "گروهی یافت نشد", "noSquads": "گروهی یافت نشد",
"notSynced": "همگام نشده", "notSynced": "همگام نشده",
"syncServers": "همگام‌سازی سرورها", "syncServers": "همگام‌سازی سرورها",
"synced": "همگام شده" "synced": "همگام شده",
"members": "اعضا",
"inbounds": "ورودی‌ها",
"available": "در دسترس",
"unavailable": "در دسترس نیست",
"stats": {
"total": "کل",
"synced": "همگام",
"available": "در دسترس",
"members": "اعضا"
},
"detail": "جزئیات گروه",
"loadError": "بارگذاری گروه ناموفق بود",
"info": "اطلاعات",
"originalName": "نام اصلی",
"countryCode": "کشور",
"statsTitle": "آمار",
"inboundsList": "لیست ورودی‌ها",
"localSettings": "تنظیمات محلی",
"trialEligible": "واجد شرایط آزمایشی",
"price": "قیمت",
"users": "کاربران"
}, },
"sync": { "sync": {
"autoSync": "همگام‌سازی خودکار", "autoSync": "همگام‌سازی خودکار",
"cleanup": "پاکسازی", "runNow": "اجرا",
"cleanupDesc": "حذف اشتراک‌های بدون کاربر", "run": "اجرا",
"fromPanel": "همگام‌سازی از پنل", "running": "در حال اجرا...",
"fromPanelDesc": "وارد کردن کاربران از پنل RemnaWave به پایگاه داده ربات", "schedule": "برنامه",
"manual": "همگام‌سازی دستی", "status": "وضعیت",
"runNow": "اجرای فوری", "lastRun": "آخرین اجرا",
"statuses": "همگام‌سازی وضعیت‌ها", "nextRun": "اجرای بعدی",
"statusesDesc": "همگام‌سازی وضعیت اشتراک‌ها با پنل", "enabled": "فعال",
"subscriptions": "اشتراک‌ها", "disabled": "غیرفعال",
"toPanel": "همگام‌سازی به پنل", "success": "موفق",
"toPanelDesc": "صادر کردن کاربران از پایگاه داده ربات به پنل RemnaWave", "runAutoSyncNow": "اجرای همگام‌سازی خودکار",
"validate": "اعتبارسنجی", "fromPanel": "از پنل",
"validateDesc": "بررسی و رفع ناسازگاری‌های اشتراک" "fromPanelDesc": "وارد کردن کاربران از RemnaWave به ربات",
"toPanel": "به پنل",
"toPanelDesc": "صادر کردن کاربران از ربات به RemnaWave"
} }
}, },
"theme": { "theme": {

View File

@@ -879,6 +879,11 @@
"memory": "Память", "memory": "Память",
"uptime": "Аптайм", "uptime": "Аптайм",
"traffic": "Статистика трафика", "traffic": "Статистика трафика",
"traffic2days": "2 дня",
"traffic7days": "7 дней",
"traffic30days": "30 дней",
"trafficMonth": "Месяц",
"trafficYear": "Год",
"usersByStatus": "Пользователи по статусу" "usersByStatus": "Пользователи по статусу"
}, },
"nodes": { "nodes": {
@@ -890,29 +895,62 @@
"enable": "Включить", "enable": "Включить",
"disable": "Отключить", "disable": "Отключить",
"restartAll": "Перезапустить все", "restartAll": "Перезапустить все",
"confirmRestartAll": "Вы уверены, что хотите перезапустить все ноды?" "confirmRestartAll": "Вы уверены, что хотите перезапустить все ноды?",
"usersOnline": "онлайн",
"trafficUsed": "использовано",
"uptimeLabel": "Аптайм",
"stats": {
"total": "Всего",
"online": "Онлайн",
"offline": "Офлайн",
"disabled": "Отключены",
"users": "Пользователей"
}
}, },
"squads": { "squads": {
"synced": "Синхронизирован", "synced": "Синхронизирован",
"notSynced": "Не синхронизирован", "notSynced": "Не синхронизирован",
"noSquads": "Сквады не найдены", "noSquads": "Сквады не найдены",
"syncServers": "Синхронизировать серверы" "syncServers": "Синхронизировать серверы",
"members": "участников",
"inbounds": "inbounds",
"available": "Доступен",
"unavailable": "Недоступен",
"stats": {
"total": "Всего",
"synced": "Синхр.",
"available": "Доступно",
"members": "Участников"
},
"detail": "Детали сквада",
"loadError": "Не удалось загрузить сквад",
"info": "Информация",
"originalName": "Оригинальное имя",
"countryCode": "Страна",
"statsTitle": "Статистика",
"inboundsList": "Inbounds",
"localSettings": "Локальные настройки",
"trialEligible": "Доступен триал",
"price": "Цена",
"users": "Пользователей"
}, },
"sync": { "sync": {
"autoSync": "Автосинхронизация", "autoSync": "Автосинхронизация",
"runNow": "Запустить сейчас", "runNow": "Запустить",
"manual": "Ручная синхронизация", "run": "Запустить",
"fromPanel": "Синхронизация из панели", "running": "Выполняется...",
"fromPanelDesc": "Импорт пользователей из RemnaWave панели в базу бота", "schedule": "Расписание",
"toPanel": "Синхронизация в панель", "status": "Статус",
"toPanelDesc": "Экспорт пользователей из базы бота в RemnaWave панель", "lastRun": "Посл. запуск",
"subscriptions": "Подписки", "nextRun": "След. запуск",
"validate": "Валидация", "enabled": "Включено",
"validateDesc": "Проверка и исправление несоответствий подписок", "disabled": "Выключено",
"cleanup": "Очистка", "success": "Успешно",
"cleanupDesc": "Удаление orphaned подписок без пользователей", "runAutoSyncNow": "Запустить автосинхронизацию",
"statuses": "Синхронизация статусов", "fromPanel": "Из панели",
"statusesDesc": "Синхронизация статусов подписок с панелью" "fromPanelDesc": "Импорт пользователей из RemnaWave в бота",
"toPanel": "В панель",
"toPanelDesc": "Экспорт пользователей из бота в RemnaWave"
} }
}, },
"wheel": { "wheel": {

View File

@@ -1710,7 +1710,17 @@
"offline": "离线", "offline": "离线",
"online": "在线", "online": "在线",
"restart": "重启", "restart": "重启",
"restartAll": "全部重启" "restartAll": "全部重启",
"usersOnline": "在线",
"trafficUsed": "已使用",
"uptimeLabel": "运行时间",
"stats": {
"total": "总计",
"online": "在线",
"offline": "离线",
"disabled": "已禁用",
"users": "用户"
}
}, },
"overview": { "overview": {
"bandwidth": "实时带宽", "bandwidth": "实时带宽",
@@ -1724,6 +1734,11 @@
"total": "总计", "total": "总计",
"totalUsers": "总用户数", "totalUsers": "总用户数",
"traffic": "流量统计", "traffic": "流量统计",
"traffic2days": "2天",
"traffic7days": "7天",
"traffic30days": "30天",
"trafficMonth": "本月",
"trafficYear": "本年",
"upload": "上传", "upload": "上传",
"uptime": "运行时间", "uptime": "运行时间",
"usersByStatus": "按状态分类的用户", "usersByStatus": "按状态分类的用户",
@@ -1733,23 +1748,46 @@
"noSquads": "未找到小队", "noSquads": "未找到小队",
"notSynced": "未同步", "notSynced": "未同步",
"syncServers": "同步服务器", "syncServers": "同步服务器",
"synced": "已同步" "synced": "已同步",
"members": "成员",
"inbounds": "入站",
"available": "可用",
"unavailable": "不可用",
"stats": {
"total": "总计",
"synced": "已同步",
"available": "可用",
"members": "成员"
},
"detail": "小队详情",
"loadError": "加载小队失败",
"info": "信息",
"originalName": "原始名称",
"countryCode": "国家",
"statsTitle": "统计",
"inboundsList": "入站列表",
"localSettings": "本地设置",
"trialEligible": "可试用",
"price": "价格",
"users": "用户"
}, },
"sync": { "sync": {
"autoSync": "自动同步", "autoSync": "自动同步",
"cleanup": "清理", "runNow": "运行",
"cleanupDesc": "删除无用户的孤立订阅", "run": "运行",
"fromPanel": "从面板同步", "running": "运行中...",
"fromPanelDesc": "从RemnaWave面板导入用户到机器人数据库", "schedule": "计划",
"manual": "手动同步", "status": "状态",
"runNow": "立即运行", "lastRun": "上次运行",
"statuses": "同步状态", "nextRun": "下次运行",
"statusesDesc": "与面板同步订阅状态", "enabled": "已启用",
"subscriptions": "订阅", "disabled": "已禁用",
"toPanel": "同步到面板", "success": "成功",
"toPanelDesc": "从机器人数据库导出用户到RemnaWave面板", "runAutoSyncNow": "立即运行自动同步",
"validate": "验证", "fromPanel": "从面板",
"validateDesc": "检查并修复订阅不一致" "fromPanelDesc": "从RemnaWave导入用户到机器人",
"toPanel": "到面板",
"toPanelDesc": "从机器人导出用户到RemnaWave"
} }
} }
}, },

View File

@@ -157,12 +157,19 @@ function NodeCard({ node, onAction, isLoading }: NodeCardProps) {
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-dark-400"> <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-dark-400">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<UsersIcon className="h-3.5 w-3.5" /> <UsersIcon className="h-3.5 w-3.5" />
{node.users_online ?? 0} online {node.users_online ?? 0} {t('admin.remnawave.nodes.usersOnline', 'online')}
</span> </span>
{node.traffic_used_bytes !== undefined && ( {node.traffic_used_bytes !== undefined && (
<span>{formatBytes(node.traffic_used_bytes)} used</span> <span>
{formatBytes(node.traffic_used_bytes)}{' '}
{t('admin.remnawave.nodes.trafficUsed', 'used')}
</span>
)}
{node.xray_uptime && (
<span>
{t('admin.remnawave.nodes.uptimeLabel', 'Uptime')}: {node.xray_uptime}
</span>
)} )}
{node.xray_uptime && <span>Uptime: {node.xray_uptime}</span>}
</div> </div>
</div> </div>
@@ -232,17 +239,21 @@ function SquadCard({ squad, onClick }: SquadCardProps) {
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-dark-400"> <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-dark-400">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<UsersIcon className="h-3.5 w-3.5" /> <UsersIcon className="h-3.5 w-3.5" />
{squad.members_count} members {squad.members_count} {t('admin.remnawave.squads.members', 'members')}
</span> </span>
{squad.current_users !== undefined && ( {squad.current_users !== undefined && (
<span> <span>
{squad.current_users} / {squad.max_users ?? '∞'} {squad.current_users} / {squad.max_users ?? '∞'}
</span> </span>
)} )}
<span>{squad.inbounds_count} inbounds</span> <span>
{squad.inbounds_count} {t('admin.remnawave.squads.inbounds', 'inbounds')}
</span>
{squad.is_available !== undefined && ( {squad.is_available !== undefined && (
<span className={squad.is_available ? 'text-success-400' : 'text-error-400'}> <span className={squad.is_available ? 'text-success-400' : 'text-error-400'}>
{squad.is_available ? '✓ Available' : '✗ Unavailable'} {squad.is_available
? `${t('admin.remnawave.squads.available', 'Available')}`
: `${t('admin.remnawave.squads.unavailable', 'Unavailable')}`}
</span> </span>
)} )}
</div> </div>
@@ -271,6 +282,8 @@ interface SyncCardProps {
} }
function SyncCard({ title, description, onAction, isLoading, lastResult }: SyncCardProps) { function SyncCard({ title, description, onAction, isLoading, lastResult }: SyncCardProps) {
const { t } = useTranslation();
return ( return (
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-4"> <div className="rounded-xl border border-dark-700 bg-dark-800/50 p-4">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@@ -291,7 +304,9 @@ function SyncCard({ title, description, onAction, isLoading, lastResult }: SyncC
className="flex shrink-0 items-center gap-2 rounded-lg bg-accent-500/20 px-3 py-1.5 text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50" className="flex shrink-0 items-center gap-2 rounded-lg bg-accent-500/20 px-3 py-1.5 text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50"
> >
<RefreshIcon spinning={isLoading} /> <RefreshIcon spinning={isLoading} />
{isLoading ? 'Running...' : 'Run'} {isLoading
? t('admin.remnawave.sync.running', 'Running...')
: t('admin.remnawave.sync.run', 'Run')}
</button> </button>
</div> </div>
</div> </div>
@@ -431,31 +446,31 @@ function OverviewTab({ stats, isLoading, onRefresh }: OverviewTabProps) {
</h3> </h3>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5"> <div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
<StatCard <StatCard
label="2 days" label={t('admin.remnawave.overview.traffic2days', '2 days')}
value={formatBytes(stats.traffic_periods.last_2_days.current)} value={formatBytes(stats.traffic_periods.last_2_days.current)}
icon={<span className="text-xs">📊</span>} icon={<span className="text-xs">📊</span>}
color="accent" color="accent"
/> />
<StatCard <StatCard
label="7 days" label={t('admin.remnawave.overview.traffic7days', '7 days')}
value={formatBytes(stats.traffic_periods.last_7_days.current)} value={formatBytes(stats.traffic_periods.last_7_days.current)}
icon={<span className="text-xs">📊</span>} icon={<span className="text-xs">📊</span>}
color="blue" color="blue"
/> />
<StatCard <StatCard
label="30 days" label={t('admin.remnawave.overview.traffic30days', '30 days')}
value={formatBytes(stats.traffic_periods.last_30_days.current)} value={formatBytes(stats.traffic_periods.last_30_days.current)}
icon={<span className="text-xs">📊</span>} icon={<span className="text-xs">📊</span>}
color="green" color="green"
/> />
<StatCard <StatCard
label="Month" label={t('admin.remnawave.overview.trafficMonth', 'Month')}
value={formatBytes(stats.traffic_periods.current_month.current)} value={formatBytes(stats.traffic_periods.current_month.current)}
icon={<span className="text-xs">📊</span>} icon={<span className="text-xs">📊</span>}
color="purple" color="purple"
/> />
<StatCard <StatCard
label="Year" label={t('admin.remnawave.overview.trafficYear', 'Year')}
value={formatBytes(stats.traffic_periods.current_year.current)} value={formatBytes(stats.traffic_periods.current_year.current)}
icon={<span className="text-xs">📊</span>} icon={<span className="text-xs">📊</span>}
color="orange" color="orange"
@@ -526,11 +541,36 @@ function NodesTab({
<div className="space-y-4"> <div className="space-y-4">
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5"> <div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
<StatCard label="Total" value={stats.total} icon={<GlobeIcon />} color="accent" /> <StatCard
<StatCard label="Online" value={stats.online} icon={<GlobeIcon />} color="green" /> label={t('admin.remnawave.nodes.stats.total', 'Total')}
<StatCard label="Offline" value={stats.offline} icon={<GlobeIcon />} color="red" /> value={stats.total}
<StatCard label="Disabled" value={stats.disabled} icon={<GlobeIcon />} color="accent" /> icon={<GlobeIcon />}
<StatCard label="Users" value={stats.totalUsers} icon={<UsersIcon />} color="blue" /> color="accent"
/>
<StatCard
label={t('admin.remnawave.nodes.stats.online', 'Online')}
value={stats.online}
icon={<GlobeIcon />}
color="green"
/>
<StatCard
label={t('admin.remnawave.nodes.stats.offline', 'Offline')}
value={stats.offline}
icon={<GlobeIcon />}
color="red"
/>
<StatCard
label={t('admin.remnawave.nodes.stats.disabled', 'Disabled')}
value={stats.disabled}
icon={<GlobeIcon />}
color="accent"
/>
<StatCard
label={t('admin.remnawave.nodes.stats.users', 'Users')}
value={stats.totalUsers}
icon={<UsersIcon />}
color="blue"
/>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -608,19 +648,29 @@ function SquadsTab({
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard <StatCard
label="Total" label={t('admin.remnawave.squads.stats.total', 'Total')}
value={stats.total} value={stats.total}
icon={<ServerIcon className="h-5 w-5" />} icon={<ServerIcon className="h-5 w-5" />}
color="accent" color="accent"
/> />
<StatCard label="Synced" value={stats.synced} icon={<SyncIcon />} color="green" />
<StatCard <StatCard
label="Available" label={t('admin.remnawave.squads.stats.synced', 'Synced')}
value={stats.synced}
icon={<SyncIcon />}
color="green"
/>
<StatCard
label={t('admin.remnawave.squads.stats.available', 'Available')}
value={stats.available} value={stats.available}
icon={<ServerIcon className="h-5 w-5" />} icon={<ServerIcon className="h-5 w-5" />}
color="blue" color="blue"
/> />
<StatCard label="Members" value={stats.totalMembers} icon={<UsersIcon />} color="purple" /> <StatCard
label={t('admin.remnawave.squads.stats.members', 'Members')}
value={stats.totalMembers}
icon={<UsersIcon />}
color="purple"
/>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -692,33 +742,25 @@ function SyncTab({
{/* Auto Sync Status */} {/* Auto Sync Status */}
{autoSyncStatus && ( {autoSyncStatus && (
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-4"> <div className="rounded-xl border border-dark-700 bg-dark-800/50 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<h3 className="flex items-center gap-2 font-medium text-dark-100"> <h3 className="flex items-center gap-2 font-medium text-dark-100">
<SyncIcon /> <SyncIcon />
{t('admin.remnawave.sync.autoSync', 'Auto Sync')} {t('admin.remnawave.sync.autoSync', 'Auto Sync')}
</h3> </h3>
<div className="flex items-center gap-2"> <span
<span className={`rounded-full px-2 py-0.5 text-xs ${
className={`rounded-full px-2 py-0.5 text-xs ${ autoSyncStatus.enabled
autoSyncStatus.enabled ? 'bg-success-500/20 text-success-400'
? 'bg-success-500/20 text-success-400' : 'bg-dark-600 text-dark-400'
: 'bg-dark-600 text-dark-400' }`}
}`} >
> {autoSyncStatus.enabled
{autoSyncStatus.enabled ? 'Enabled' : 'Disabled'} ? t('admin.remnawave.sync.enabled', 'Enabled')
</span> : t('admin.remnawave.sync.disabled', 'Disabled')}
<button </span>
onClick={onRunAutoSync}
disabled={loadingStates.autoSync || autoSyncStatus.is_running}
className="flex items-center gap-1.5 rounded-lg bg-accent-500/20 px-3 py-1.5 text-sm text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50"
>
<RefreshIcon spinning={loadingStates.autoSync || autoSyncStatus.is_running} />
{t('admin.remnawave.sync.runNow', 'Run Now')}
</button>
</div>
</div> </div>
<div className="grid grid-cols-2 gap-3 text-sm lg:grid-cols-4"> <div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-lg bg-dark-700/50 p-3"> <div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500"> <p className="text-xs text-dark-500">
{t('admin.remnawave.sync.schedule', 'Schedule')} {t('admin.remnawave.sync.schedule', 'Schedule')}
@@ -727,24 +769,6 @@ function SyncTab({
{autoSyncStatus.times.length > 0 ? autoSyncStatus.times.join(', ') : '—'} {autoSyncStatus.times.length > 0 ? autoSyncStatus.times.join(', ') : '—'}
</p> </p>
</div> </div>
<div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500">
{t('admin.remnawave.sync.nextRun', 'Next Run')}
</p>
<p className="mt-1 text-dark-200">
{autoSyncStatus.next_run ? new Date(autoSyncStatus.next_run).toLocaleString() : '—'}
</p>
</div>
<div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500">
{t('admin.remnawave.sync.lastRun', 'Last Run')}
</p>
<p className="mt-1 text-dark-200">
{autoSyncStatus.last_run_finished_at
? new Date(autoSyncStatus.last_run_finished_at).toLocaleString()
: '—'}
</p>
</div>
<div className="rounded-lg bg-dark-700/50 p-3"> <div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500">{t('admin.remnawave.sync.status', 'Status')}</p> <p className="text-xs text-dark-500">{t('admin.remnawave.sync.status', 'Status')}</p>
<p <p
@@ -763,7 +787,36 @@ function SyncTab({
: autoSyncStatus.last_run_error || '—'} : autoSyncStatus.last_run_error || '—'}
</p> </p>
</div> </div>
<div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500">
{t('admin.remnawave.sync.lastRun', 'Last Run')}
</p>
<p className="mt-1 text-dark-200">
{autoSyncStatus.last_run_finished_at
? new Date(autoSyncStatus.last_run_finished_at).toLocaleString()
: '—'}
</p>
</div>
<div className="rounded-lg bg-dark-700/50 p-3">
<p className="text-xs text-dark-500">
{t('admin.remnawave.sync.nextRun', 'Next Run')}
</p>
<p className="mt-1 text-dark-200">
{autoSyncStatus.next_run ? new Date(autoSyncStatus.next_run).toLocaleString() : '—'}
</p>
</div>
</div> </div>
<button
onClick={onRunAutoSync}
disabled={loadingStates.autoSync || autoSyncStatus.is_running}
className="mt-4 flex w-full items-center justify-center gap-2 rounded-lg bg-accent-500/20 px-4 py-2.5 text-sm font-medium text-accent-400 transition-colors hover:bg-accent-500/30 disabled:opacity-50"
>
<RefreshIcon spinning={loadingStates.autoSync || autoSyncStatus.is_running} />
{autoSyncStatus.is_running
? t('admin.remnawave.sync.running', 'Running...')
: t('admin.remnawave.sync.runAutoSyncNow', 'Run Auto Sync Now')}
</button>
</div> </div>
)} )}

View File

@@ -158,7 +158,7 @@ export default function AdminRemnawaveSquadDetail() {
{/* Stats */} {/* Stats */}
<div className="card"> <div className="card">
<h3 className="mb-4 text-lg font-semibold text-dark-100"> <h3 className="mb-4 text-lg font-semibold text-dark-100">
{t('admin.remnawave.squads.stats', 'Statistics')} {t('admin.remnawave.squads.statsTitle', 'Statistics')}
</h3> </h3>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div className="rounded-lg bg-dark-700/50 p-4"> <div className="rounded-lg bg-dark-700/50 p-4">