Refine routing defaults for global and device fallbacks
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s

This commit is contained in:
2026-05-09 09:53:12 +03:00
parent 62b39cdf58
commit aab7533438
14 changed files with 695 additions and 160 deletions

View File

@@ -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