add: rotation status

This commit is contained in:
Den Piligrim
2026-02-03 18:42:53 +03:00
parent 653896ae56
commit 98ff14d464
5 changed files with 324 additions and 101 deletions
+103 -7
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip } from '@mui/material';
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton } from '@mui/material';
import api from '../api';
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Schedule, Update } from '@mui/icons-material';
const ROTATION_PRESETS = [
{ label: 'Сутки', value: 1440 },
@@ -14,6 +15,8 @@ export default function SettingsPage() {
xui_login: '',
xui_password: '',
rotation_interval: '30',
rotation_status: 'active',
last_rotation_timestamp: '',
});
const [adminProfile, setAdminProfile] = useState({
@@ -125,20 +128,20 @@ export default function SettingsPage() {
}
};
const handleForceRotate = async () => {
const handleForceRotate = async () => {
if (confirm('ВНИМАНИЕ: Это немедленно обновит конфиги в подписках.\n\nИнтервал автоматической ротации НЕ будет сброшен.\n\nПродолжить?')) {
try {
setLoadingRotate(true);
const res = await api.post('/rotation/rotate-all');
setLoadingRotate(false);
if (res.data && res.data.success) {
setMsg({ open: true, type: 'success', text: res.data.message || 'Ротация успешно выполнена!' });
} else {
setMsg({
open: true,
type: 'error',
text: res.data?.message || 'Ошибка выполнения ротации'
setMsg({
open: true,
type: 'error',
text: res.data?.message || 'Ошибка выполнения ротации'
});
}
} catch (e) {
@@ -148,12 +151,105 @@ const handleForceRotate = async () => {
}
};
const togglePause = async () => {
const newStatus = settings.rotation_status === 'active' ? 'stopped' : 'active';
const updatedSettings = { ...settings, rotation_status: newStatus };
setSettings(updatedSettings);
try {
await api.post('/settings', updatedSettings);
} catch (e) {
setSettings((prev: any) => ({ ...prev, rotation_status: settings.rotation_status }));
setMsg({ open: true, type: 'error', text: 'Не удалось изменить статус' });
}
};
const formatDate = (isoString: string) => {
if (!isoString) return 'Нет данных';
return new Date(+isoString).toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
};
const getNextRotationDate = () => {
if (settings.rotation_status === 'stopped') return 'Пауза';
if (!settings.last_rotation_timestamp) return 'Ожидание...';
const last = new Date(+settings.last_rotation_timestamp);
const intervalMinutes = parseInt(settings.rotation_interval) || 60;
const next = new Date(last.getTime() + intervalMinutes * 60000);
return next.toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
};
const isPaused = settings.rotation_status === 'stopped';
return (
<Box>
<Typography variant="h4" gutterBottom>Настройки утилиты</Typography>
<Grid container spacing={3}>
<Grid size={{ xs: 12 }}>
<Grid container spacing={1}>
<Grid size={{ xs: 12, md: 4 }}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
Статус сервиса
</Typography>
{isPaused ?
<Chip icon={<PauseCircleFilled />} label="Остановлен" color="warning" size="small" variant="outlined" /> :
<Chip icon={<CheckCircle />} label="Активен" color="success" size="small" variant="outlined" />
}
<Tooltip title={isPaused ? "Возобновить ротацию" : "Поставить на паузу"}>
<IconButton
onClick={togglePause}
size="small"
sx={{
bgcolor: 'background.paper',
boxShadow: 2,
'&:hover': { bgcolor: 'background.paper' },
ml: 1
}}
>
{isPaused ? <PlayCircleFilled fontSize="large" /> : <PauseCircleFilled fontSize="large" />}
</IconButton>
</Tooltip>
</Grid>
{/* Последняя генерация */}
<Grid size={{ xs: 12, md: 4 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Box>
<Typography variant="subtitle2" color="textSecondary">
Последняя генерация
</Typography>
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
{formatDate(settings.last_rotation_timestamp)}
</Typography>
</Box>
</Stack>
</Grid>
{/* Следующая генерация */}
<Grid size={{ xs: 12, md: 4 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Box>
<Typography variant="subtitle2" color="textSecondary">
Следующая генерация
</Typography>
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
{getNextRotationDate()}
</Typography>
</Box>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Paper sx={{ p: 3, height: '100%' }}>
<Typography variant="h6" gutterBottom>Панель 3x-ui</Typography>
+8 -2
View File
@@ -191,12 +191,18 @@ export default function SubscriptionsPage() {
</Table>
</Paper>
<Dialog open={open} onClose={() => setOpen(false)}>
<Dialog open={open} onClose={() => setOpen(false)} disableRestoreFocus>
<DialogTitle>Новая подписка</DialogTitle>
<DialogContent>
<TextField
autoFocus margin="dense" label="Имя пользователя" fullWidth
value={name} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCreate();
}
}}
/>
</DialogContent>
<DialogActions>
@@ -215,7 +221,7 @@ export default function SubscriptionsPage() {
/>
</DialogContent>
<DialogActions>
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать всё</Button>
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать все</Button>
<Button onClick={() => setLinksOpen(false)}>Закрыть</Button>
</DialogActions>
</Dialog>