manual auto-rotation
This commit is contained in:
+1
-1
@@ -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 для добавления токена к каждому запросу
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user