Refine routing defaults for global and device fallbacks
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s
This commit is contained in:
@@ -27,7 +27,11 @@ function App() {
|
||||
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
||||
const [servers, setServers] = useState([]);
|
||||
const [customRules, setCustomRules] = useState([]);
|
||||
const [deviceRules, setDeviceRules] = useState([]);
|
||||
const [devicesConfig, setDevicesConfig] = useState({
|
||||
defaultTransparentMode: 'direct',
|
||||
proxyDefaultMode: 'vpn',
|
||||
devices: [],
|
||||
});
|
||||
const [selectedTag, setSelectedTag] = useState('');
|
||||
const [pendingTag, setPendingTag] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -68,7 +72,11 @@ function App() {
|
||||
setState(data);
|
||||
setServers(data.servers || []);
|
||||
if (!rulesDirtyRef.current) setCustomRules(data.customRules || []);
|
||||
setDeviceRules(data.deviceRules || []);
|
||||
setDevicesConfig(data.devicesConfig || {
|
||||
defaultTransparentMode: 'direct',
|
||||
proxyDefaultMode: 'vpn',
|
||||
devices: data.devices || [],
|
||||
});
|
||||
setSelectedTag((prev) => prev || data.selectedTag || '');
|
||||
setPendingTag((prev) => prev || data.selectedTag || '');
|
||||
}
|
||||
@@ -195,35 +203,55 @@ function App() {
|
||||
});
|
||||
}
|
||||
|
||||
// === Device Rules ===
|
||||
async function saveDeviceRules(rules) {
|
||||
// === Devices ===
|
||||
async function saveDevicesConfig(nextConfig) {
|
||||
try {
|
||||
const data = await api.deviceRules.save(rules);
|
||||
setDeviceRules(data.deviceRules || rules);
|
||||
const data = await api.devices.save(nextConfig);
|
||||
setDevicesConfig({
|
||||
defaultTransparentMode: data.defaultTransparentMode || data.defaultMode || 'direct',
|
||||
proxyDefaultMode: data.proxyDefaultMode || 'vpn',
|
||||
devices: data.devices || [],
|
||||
});
|
||||
setState((prev) => prev ? { ...prev, devicesUpdatedAt: data.devicesUpdatedAt } : prev);
|
||||
} catch (err) {
|
||||
pushToast({ kind: 'danger', title: 'Не удалось сохранить устройства', message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
function addDevice() {
|
||||
const next = [
|
||||
...deviceRules,
|
||||
{ id: `dev-${Date.now()}`, name: 'Новое устройство', enabled: true, sourceIps: [], outbound: 'direct' },
|
||||
];
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
const nextConfig = {
|
||||
...devicesConfig,
|
||||
devices: [
|
||||
...devicesConfig.devices,
|
||||
{ id: `dev-${Date.now()}`, name: 'Новое устройство', enabled: true, ip: '', mac: '', mode: 'direct', lastSeen: null },
|
||||
],
|
||||
};
|
||||
setDevicesConfig(nextConfig);
|
||||
saveDevicesConfig(nextConfig);
|
||||
}
|
||||
|
||||
function updateDevice(id, patch) {
|
||||
const next = deviceRules.map((d) => (d.id === id ? { ...d, ...patch } : d));
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
const nextConfig = {
|
||||
...devicesConfig,
|
||||
devices: devicesConfig.devices.map((d) => (d.id === id ? { ...d, ...patch } : d)),
|
||||
};
|
||||
setDevicesConfig(nextConfig);
|
||||
saveDevicesConfig(nextConfig);
|
||||
}
|
||||
|
||||
function removeDevice(id) {
|
||||
const next = deviceRules.filter((d) => d.id !== id);
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
const nextConfig = {
|
||||
...devicesConfig,
|
||||
devices: devicesConfig.devices.filter((d) => d.id !== id),
|
||||
};
|
||||
setDevicesConfig(nextConfig);
|
||||
saveDevicesConfig(nextConfig);
|
||||
}
|
||||
|
||||
function updateDeviceDefaults(patch) {
|
||||
const nextConfig = { ...devicesConfig, ...patch };
|
||||
setDevicesConfig(nextConfig);
|
||||
saveDevicesConfig(nextConfig);
|
||||
}
|
||||
|
||||
// === Rules CRUD ===
|
||||
@@ -326,11 +354,16 @@ function App() {
|
||||
);
|
||||
|
||||
const dirtyRules = rulesSaveStatus === 'pending' || rulesSaveStatus === 'saving';
|
||||
const dirtyDevices = Boolean(
|
||||
state?.devicesUpdatedAt &&
|
||||
(!state?.rulesAppliedAt || state.devicesUpdatedAt > state.rulesAppliedAt),
|
||||
);
|
||||
const dirtyServer = pendingTag && pendingTag !== state?.selectedTag;
|
||||
const dirty = dirtyRules || dirtyServer;
|
||||
const dirtyRouting = dirtyRules || dirtyDevices;
|
||||
const dirty = dirtyRouting || dirtyServer;
|
||||
|
||||
const sidebarBadges = {
|
||||
routing: dirtyRules ? { kind: 'warn', text: '●' } : null,
|
||||
routing: dirtyRouting ? { kind: 'warn', text: '●' } : null,
|
||||
servers: dirtyServer ? { kind: 'warn', text: '●' } : null,
|
||||
settings: !state?.hasSubscription ? { kind: 'danger', text: '!' } : null,
|
||||
};
|
||||
@@ -391,13 +424,14 @@ function App() {
|
||||
onRemove={removeRule}
|
||||
onSaveNow={saveRulesNow}
|
||||
onReorder={reorderRules}
|
||||
deviceRules={deviceRules}
|
||||
devicesConfig={devicesConfig}
|
||||
onUpdateDeviceDefaults={updateDeviceDefaults}
|
||||
onAddDevice={addDevice}
|
||||
onUpdateDevice={updateDevice}
|
||||
onRemoveDevice={removeDevice}
|
||||
/>
|
||||
)}
|
||||
{page === 'logs' && <LogsPage deviceRules={deviceRules} />}
|
||||
{page === 'logs' && <LogsPage devices={devicesConfig.devices} />}
|
||||
{page === 'settings' && (
|
||||
<SettingsPage
|
||||
state={state}
|
||||
@@ -413,19 +447,22 @@ function App() {
|
||||
)}
|
||||
|
||||
{/* Sticky bar — для routing/servers */}
|
||||
{(page === 'routing' && rulesSaveStatus !== 'saved') && (
|
||||
{(page === 'routing' && dirtyRouting) && (
|
||||
<div className="sticky-bar">
|
||||
<div className="flex">
|
||||
<span className={`dot ${rulesSaveStatus === 'error' ? 'danger' : 'warning'}`} />
|
||||
<strong>
|
||||
{rulesSaveStatus === 'saving' && 'Сохраняем…'}
|
||||
{rulesSaveStatus === 'pending' && 'Есть несохранённые изменения'}
|
||||
{rulesSaveStatus === 'saved' && dirtyDevices && 'Изменения устройств сохранены'}
|
||||
{rulesSaveStatus === 'error' && 'Ошибка сохранения'}
|
||||
</strong>
|
||||
<small className="muted">Изменения сохранены, но конфиг не пересобран. Применить — на странице «Серверы».</small>
|
||||
<small className="muted">Конфиг sing-box нужно пересобрать и применить.</small>
|
||||
</div>
|
||||
<div className="btn-group">
|
||||
<button className="btn btn-secondary sm" onClick={saveRulesNow}>Сохранить сейчас</button>
|
||||
{rulesSaveStatus !== 'saved' && (
|
||||
<button className="btn btn-secondary sm" onClick={saveRulesNow}>Сохранить сейчас</button>
|
||||
)}
|
||||
{state?.selectedTag && (
|
||||
<button className="btn btn-primary sm" onClick={() => applyServer(state.selectedTag)} disabled={busy}>
|
||||
Применить config
|
||||
|
||||
Reference in New Issue
Block a user