feat(mikrotikBackupScheduler, MikrotikBackupsManager): enhance backup server management by integrating UI settings and adding server selection functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m58s

This commit is contained in:
2026-02-09 17:06:49 +07:00
parent 0d9542d7e9
commit 5eecb8d070
3 changed files with 181 additions and 29 deletions
+124 -4
View File
@@ -45,6 +45,9 @@ function MikrotikBackupsManager() {
const [servers, setServers] = useState([]);
const [selectedServer, setSelectedServer] = useState(null);
const [uiSettings, setUiSettings] = useState({});
const [uiSettingsEtag, setUiSettingsEtag] = useState('');
const [backupServerIds, setBackupServerIds] = useState(new Set());
const [backups, setBackups] = useState([]);
const [loadingServers, setLoadingServers] = useState(false);
const [loadingBackups, setLoadingBackups] = useState(false);
@@ -57,6 +60,7 @@ function MikrotikBackupsManager() {
useEffect(() => {
fetchServers();
fetchUiSettings();
}, []);
useEffect(() => {
@@ -73,6 +77,8 @@ function MikrotikBackupsManager() {
[servers],
);
const backupServerIdsList = useMemo(() => Array.from(backupServerIds), [backupServerIds]);
const fetchServers = async () => {
try {
setLoadingServers(true);
@@ -91,6 +97,21 @@ function MikrotikBackupsManager() {
}
};
const fetchUiSettings = async () => {
try {
const res = await api.get('/ui-settings');
const data = res?.data || {};
setUiSettings(data);
const list = Array.isArray(data.mikrotikBackupServers) ? data.mikrotikBackupServers : [];
setBackupServerIds(new Set(list.map((v) => String(v))));
const e = res?.headers?.etag || res?.headers?.ETag || '';
setUiSettingsEtag(e ? String(e) : '');
} catch (err) {
console.error('Error fetching UI settings for backups:', err);
// мягко игнорируем, просто будут дефолты
}
};
const fetchBackups = async (serverId) => {
if (!serverId) return;
try {
@@ -196,6 +217,36 @@ function MikrotikBackupsManager() {
}
};
const toggleBackupServer = (serverId) => {
setBackupServerIds((prev) => {
const next = new Set(prev);
if (next.has(serverId)) next.delete(serverId);
else next.add(serverId);
return next;
});
};
const saveBackupServers = async () => {
try {
const settingsPayload = {
...uiSettings,
mikrotikBackupServers: backupServerIdsList,
};
const payload = {
settings: settingsPayload,
etag: uiSettingsEtag,
};
const res = await api.post('/ui-settings', payload);
const meta = res?.data || {};
setUiSettings(settingsPayload);
setUiSettingsEtag(String(meta?.etag || ''));
notify.success('Список серверов для автобэкапа сохранён');
} catch (err) {
console.error('Error saving backup servers to ui-settings:', err);
notify.error(err?.response?.data?.message || 'Не удалось сохранить список серверов для автобэкапа');
}
};
const actions = (
<div className="d-flex flex-wrap gap-2 align-items-center">
<div style={{ minWidth: 260 }}>
@@ -229,10 +280,10 @@ function MikrotikBackupsManager() {
</button>
</Tooltip>
<div className="text-muted small ms-2">
Автобэкапы настраиваются через переменные окружения:
<code className="ms-1">MIKROTIK_BACKUP_ENABLED</code>,
<code className="ms-1">MIKROTIK_BACKUP_INTERVAL_MINUTES</code>,
<code className="ms-1">MIKROTIK_BACKUP_SERVERS</code>.
Автобэкапы включаются для выбранных ниже Jumphost-серверов. Интервал и включение/выключение
планировщика настраиваются через переменные окружения
<code className="ms-1">MIKROTIK_BACKUP_ENABLED</code> и
<code className="ms-1">MIKROTIK_BACKUP_INTERVAL_MINUTES</code>.
</div>
</div>
);
@@ -249,6 +300,75 @@ function MikrotikBackupsManager() {
actions={actions}
/>
{/* Блок выбора серверов для автобэкапа */}
<div className="card mb-4">
<div className="card-header d-flex justify-content-between align-items-center">
<div>
<div className="card-title mb-0">Сервера для автоматических бэкапов</div>
<div className="text-muted small">
Отмеченные здесь Jumphost-серверы будут периодически бэкапиться планировщиком на backend.
</div>
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm d-inline-flex align-items-center"
onClick={saveBackupServers}
disabled={jumphostServers.length === 0}
>
Сохранить список
</button>
</div>
<div className="card-body">
{jumphostServers.length === 0 ? (
<div className="text-muted small">
Нет серверов типа <code>jumphost</code>. Добавьте их в разделе «Серверы».
</div>
) : (
<div className="table-responsive">
<table className="table table-sm table-hover">
<thead>
<tr>
<th style={{ width: 40 }}></th>
<th>Сервер</th>
<th>IP / DNS</th>
<th>Страна</th>
<th>Провайдер</th>
</tr>
</thead>
<tbody>
{jumphostServers.map((s) => {
const id = s.id || s.dns || s.ip;
const checked = id && backupServerIds.has(String(id));
return (
<tr key={id || `srv-${s.ip}-${s.dns}`}>
<td>
<input
type="checkbox"
className="form-check-input"
checked={!!checked}
onChange={() => id && toggleBackupServer(String(id))}
/>
</td>
<td>{s.dns || s.ip || id}</td>
<td>
<div className="small text-muted">
{s.ip}
{s.dns && s.ip ? ' · ' : ''}
{s.dns}
</div>
</td>
<td className="small">{s.country || '-'}</td>
<td className="small">{s.provider || '-'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
{loadingServers ? (
<TableSkeleton rows={5} columns={4} />
) : !selectedServer ? (
+12 -7
View File
@@ -14,6 +14,7 @@ export default function SettingsModal({ open, onClose }) {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [etag, setEtag] = useState('');
const [rawSettings, setRawSettings] = useState({});
const [dohServer, setDohServer] = useState('');
const [wsUrl, setWsUrl] = useState('');
const [baseAS, setBaseAS] = useState('65001');
@@ -30,6 +31,7 @@ export default function SettingsModal({ open, onClose }) {
try {
const res = await api.get('/ui-settings');
const data = res?.data || {};
setRawSettings(data);
setDohServer(String(data?.dohServer || ''));
setWsUrl(String(data?.wsUpdateUrl || ''));
setBaseAS(String(data?.baseAS || '65001'));
@@ -90,20 +92,23 @@ export default function SettingsModal({ open, onClose }) {
setSaving(true);
try {
const mergedSettings = {
...rawSettings,
dohServer: String(dohServer || '').trim(),
wsUpdateUrl: String(wsUrl || '').trim(),
baseAS: String(baseAS || '65001').trim(),
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
};
const payload = {
settings: {
dohServer: String(dohServer || '').trim(),
wsUpdateUrl: String(wsUrl || '').trim(),
baseAS: String(baseAS || '65001').trim(),
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim()
},
settings: mergedSettings,
etag
};
const res = await api.post('/ui-settings', payload);
const meta = res?.data || {};
setSuccess('Настройки успешно сохранены');
setEtag(String(meta?.etag || ''));
setRawSettings(mergedSettings);
setTimeout(() => setSuccess(''), 3000);
} catch (e) {
setError(e?.response?.data?.message || 'Ошибка при сохранении настроек');