manual auto-rotation

This commit is contained in:
iqubik
2026-03-28 23:46:42 +03:00
parent 9fdbde3cab
commit e7a63dfff0
13 changed files with 391 additions and 36 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import axios from 'axios';
import { Logger } from './utils/logger';
const api = axios.create({
baseURL: `${location.protocol}//${location.hostname}:${location.port}/api`,
baseURL: '/api',
});
// Interceptor для добавления токена к каждому запросу
+157 -3
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton, useTheme, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton, useTheme, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, FormControlLabel, Checkbox } from '@mui/material';
import api from '../api';
import { CheckCircle, PauseCircleFilled, PlayCircleFilled } from '@mui/icons-material';
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Refresh } from '@mui/icons-material';
import { Logger } from '../utils/logger';
const ROTATION_PRESETS = [
@@ -10,6 +10,13 @@ const ROTATION_PRESETS = [
{ label: 'Неделя', value: 10080 },
];
interface Subscription {
id: string;
name: string;
uuid: string;
isAutoRotationEnabled?: boolean;
}
export default function SettingsPage() {
const [settings, setSettings] = useState({
xui_url: '',
@@ -25,6 +32,8 @@ export default function SettingsPage() {
password: '',
});
const [subs, setSubs] = useState<Subscription[]>([]);
const [msg, setMsg] = useState({ open: false, type: 'success' as 'success' | 'error', text: '' });
const [loadingRotate, setLoadingRotate] = useState<boolean>(false);
const [confirmDialog, setConfirmDialog] = useState({ open: false, title: '', onConfirm: () => {} });
@@ -51,9 +60,21 @@ export default function SettingsPage() {
}
}, []);
const loadSubscriptions = useCallback(async () => {
try {
Logger.debug('Loading subscriptions...', 'Settings');
const { data } = await api.get('/subscriptions');
setSubs(data);
Logger.debug(`Loaded ${data.length} subscriptions`, 'Settings');
} catch (error) {
Logger.error('Failed to load subscriptions', 'Settings', error);
}
}, []);
useEffect(() => {
loadSettings();
}, [loadSettings]);
loadSubscriptions();
}, [loadSettings, loadSubscriptions]);
const getIntervalError = () => {
const val = parseInt(settings.rotation_interval, 10);
@@ -221,6 +242,63 @@ export default function SettingsPage() {
});
};
const handleToggleAutoRotation = async (subscriptionId: string, enabled: boolean) => {
try {
await api.put('/subscriptions/bulk-auto-rotation', {
subscriptionIds: [subscriptionId],
enabled
});
setSubs(prev => prev.map(s =>
s.id === subscriptionId ? { ...s, isAutoRotationEnabled: enabled } : s
));
setMsg({
open: true,
type: 'success',
text: enabled ? 'Авторотация включена' : 'Авторотация выключена'
});
} catch (error: unknown) {
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
Logger.error(`Toggle auto-rotation error: ${message}`, 'Settings');
setMsg({ open: true, type: 'error', text: message });
loadSubscriptions();
}
};
const handleManualRotate = async (sub: Subscription) => {
setConfirmDialog({
open: true,
title: `Обновить подписку "${sub.name}" сейчас?`,
onConfirm: async () => {
try {
Logger.debug(`Starting manual rotation for subscription: ${sub.id}`, 'Settings');
const res = await api.post(`/rotation/rotate-one/${sub.id}`);
Logger.debug('Manual rotation completed', 'Settings');
setMsg({ open: true, type: 'success', text: res.data.message || 'Ротация выполнена' });
loadSubscriptions();
} catch (error: unknown) {
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка ротации';
Logger.error(`Manual rotation error: ${message}`, 'Settings');
setMsg({ open: true, type: 'error', text: message });
}
}
});
};
const handleBulkUpdate = async (enabled: boolean) => {
try {
const { data } = await api.put('/subscriptions/bulk-auto-rotation', {
subscriptionIds: subs.map(s => s.id),
enabled
});
setMsg({ open: true, type: 'success', text: data.message || 'Настройки обновлены' });
loadSubscriptions();
} catch (error: unknown) {
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
Logger.error(`Bulk update error: ${message}`, 'Settings');
setMsg({ open: true, type: 'error', text: message });
}
};
const togglePause = async () => {
const newStatus = settings.rotation_status === 'active' ? 'stopped' : 'active';
const updatedSettings = { ...settings, rotation_status: newStatus };
@@ -398,6 +476,82 @@ export default function SettingsPage() {
>
Сгенерировать сейчас
</Button>
<Divider sx={{ my: 3 }} />
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600 }}>
Управление авторотацией подписок
</Typography>
<Typography variant="body2" color="textSecondary" paragraph>
Выберите подписки для автоматической ротации:
</Typography>
{subs.length === 0 ? (
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Нет активных подписок
</Typography>
) : (
<List sx={{ maxHeight: 400, overflow: 'auto', bgcolor: 'background.default', borderRadius: 1 }}>
{subs.map(sub => (
<ListItem
key={sub.id}
sx={{
py: 1,
borderBottom: '1px solid',
borderColor: 'divider',
'&:last-child': { borderBottom: 'none' }
}}
>
<FormControlLabel
control={
<Checkbox
checked={sub.isAutoRotationEnabled ?? true}
onChange={(e) => handleToggleAutoRotation(sub.id, e.target.checked)}
color="primary"
/>
}
label={
<Box>
<Typography variant="body2" sx={{ fontWeight: 500 }}>{sub.name}</Typography>
<Typography variant="caption" color="textSecondary">
{sub.uuid.substring(0, 8)}...
</Typography>
</Box>
}
sx={{ flexGrow: 1 }}
/>
<Tooltip title="Обновить подписку вручную">
<IconButton
size="small"
onClick={() => handleManualRotate(sub)}
color="primary"
>
<Refresh />
</IconButton>
</Tooltip>
</ListItem>
))}
</List>
)}
{subs.length > 0 && (
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={() => handleBulkUpdate(true)}
>
Включить для всех
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handleBulkUpdate(false)}
>
Выключить для всех
</Button>
</Box>
)}
</Paper>
<Paper sx={{ p: 3 }}>
+60 -2
View File
@@ -8,9 +8,10 @@ import {
useMediaQuery,
Menu,
ListItemIcon,
ListItemText
ListItemText,
Checkbox
} from '@mui/material';
import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router, Edit, MoreVert, Remove } from '@mui/icons-material';
import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router, Edit, MoreVert, Remove, Refresh } from '@mui/icons-material';
import api from '../api';
import { Logger } from '../utils/logger';
@@ -20,6 +21,7 @@ interface Subscription {
uuid: string;
inbounds: unknown[];
inboundsConfig?: unknown[];
isAutoRotationEnabled?: boolean;
}
interface Tunnel {
@@ -279,6 +281,48 @@ export default function SubscriptionsPage() {
});
};
const handleToggleAutoRotation = async (subscriptionId: string, enabled: boolean) => {
try {
await api.put('/subscriptions/bulk-auto-rotation', {
subscriptionIds: [subscriptionId],
enabled
});
setSubs(prev => prev.map(s =>
s.id === subscriptionId ? { ...s, isAutoRotationEnabled: enabled } : s
));
setSnackbar({
open: true,
type: 'success',
message: enabled ? 'Авторотация включена' : 'Авторотация выключена'
});
} catch (error: unknown) {
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
Logger.error(`Toggle auto-rotation error: ${message}`, 'Subs');
setSnackbar({ open: true, type: 'error', message });
loadSubs();
}
};
const handleManualRotate = async (sub: Subscription) => {
setConfirmDialog({
open: true,
title: `Обновить подписку "${sub.name}" сейчас?`,
onConfirm: async () => {
try {
Logger.debug(`Starting manual rotation for subscription: ${sub.id}`, 'Subs');
const res = await api.post(`/rotation/rotate-one/${sub.id}`);
Logger.debug('Manual rotation completed', 'Subs');
setSnackbar({ open: true, type: 'success', message: res.data.message || 'Ротация выполнена' });
loadSubs();
} catch (error: unknown) {
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка ротации';
Logger.error(`Manual rotation error: ${message}`, 'Subs');
setSnackbar({ open: true, type: 'error', message });
}
}
});
};
const showLinks = (sub: Subscription) => {
let links: string[] = [];
if (selectedServer === 'main') {
@@ -334,6 +378,7 @@ export default function SubscriptionsPage() {
<TableCell>Имя</TableCell>
<TableCell>UUID</TableCell>
<TableCell>Инбаунды</TableCell>
<TableCell>Авторотация</TableCell>
<TableCell align="right">Действия</TableCell>
</TableRow>
</TableHead>
@@ -343,6 +388,13 @@ export default function SubscriptionsPage() {
<TableCell sx={{ fontWeight: 700 }}>{sub.name}</TableCell>
<TableCell sx={{ fontFamily: 'monospace' }}>{sub.uuid}</TableCell>
<TableCell>{sub.inbounds?.length || 0}</TableCell>
<TableCell>
<Checkbox
checked={sub.isAutoRotationEnabled ?? true}
onChange={(e) => handleToggleAutoRotation(sub.id, e.target.checked)}
color="primary"
/>
</TableCell>
<TableCell align="right">
{!isMobile && (
<>
@@ -401,6 +453,12 @@ export default function SubscriptionsPage() {
<ListItemText>Показать конфиги</ListItemText>
</MenuItem>
)}
{activeSub && (
<MenuItem onClick={() => handleManualRotate(activeSub)}>
<ListItemIcon><Refresh fontSize="small" color="primary" /></ListItemIcon>
<ListItemText>Обновить сейчас</ListItemText>
</MenuItem>
)}
{activeSub && (
<MenuItem onClick={() => handleOpenEdit(activeSub)}>
<ListItemIcon><Edit fontSize="small" /></ListItemIcon>