feat(MikrotikConfig): enhance applyMikrotikConfig and NetworkConfigManager with detailed request logging and error handling improvements
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s

This commit is contained in:
2026-02-03 20:13:08 +07:00
parent e5f093b65f
commit b581c47c28
2 changed files with 60 additions and 13 deletions
+17 -4
View File
@@ -238,8 +238,11 @@ const APPLY_TIMEOUT_MS = Number(process.env.MIKROTIK_APPLY_TIMEOUT_MS) || 60000;
*/ */
async function applyMikrotikConfig(req, res) { async function applyMikrotikConfig(req, res) {
let conn = null; let conn = null;
const mikrotikRequests = [];
const body = req.body || {};
console.log('[MikroTik apply] Request body:', { serverId: body.serverId, type: body.type, dryRun: body.dryRun });
try { try {
const { serverId, type = 'all', dryRun = true } = req.body || {}; const { serverId, type = 'all', dryRun = true } = body;
if (!serverId) { if (!serverId) {
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
} }
@@ -290,7 +293,15 @@ async function applyMikrotikConfig(req, res) {
port: Number(port) || 8728, port: Number(port) || 8728,
}); });
try { try {
console.log('[MikroTik apply] Connecting to', host + ':' + port, 'user:', user);
await conn.connect(); await conn.connect();
const originalWrite = conn.write.bind(conn);
conn.write = async function (path, args) {
const req = { path, args: args || [] };
mikrotikRequests.push(req);
console.log('[MikroTik API]', path, req.args);
return originalWrite(path, args);
};
const allResults = []; const allResults = [];
for (const block of blocks) { for (const block of blocks) {
const blockResults = await applyBlock(conn, block, !!dryRun); const blockResults = await applyBlock(conn, block, !!dryRun);
@@ -308,7 +319,7 @@ async function applyMikrotikConfig(req, res) {
skipped: allResults.flatMap(b => b.results).filter(r => r.status === 'skip').length, skipped: allResults.flatMap(b => b.results).filter(r => r.status === 'skip').length,
errors: allResults.flatMap(b => b.results).filter(r => r.status === 'error'), errors: allResults.flatMap(b => b.results).filter(r => r.status === 'error'),
}; };
return { ok: true, dryRun: !!dryRun, summary, results: allResults }; return { ok: true, dryRun: !!dryRun, summary, results: allResults, mikrotikRequests };
} catch (err) { } catch (err) {
if (conn) try { conn.close(); } catch (_) {} if (conn) try { conn.close(); } catch (_) {}
conn = null; conn = null;
@@ -326,12 +337,14 @@ async function applyMikrotikConfig(req, res) {
if (conn) try { conn.close(); } catch (_) {} if (conn) try { conn.close(); } catch (_) {}
console.error('Error applying MikroTik config:', error); console.error('Error applying MikroTik config:', error);
const msg = error.message || String(error); const msg = error.message || String(error);
return res.status(500).json({ const errPayload = {
ok: false, ok: false,
error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' : error: msg.includes('ECONNREFUSED') ? 'Соединение отклонено. Проверьте хост и порт.' :
msg.includes('ETIMEDOUT') || msg.includes('Таймаут') ? msg : msg.includes('ETIMEDOUT') || msg.includes('Таймаут') ? msg :
msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg, msg.includes('Authentication') || msg.includes('login') ? 'Неверный логин или пароль' : msg,
}); };
if (mikrotikRequests.length > 0) errPayload.mikrotikRequests = mikrotikRequests;
return res.status(500).json(errPayload);
} }
} }
+43 -9
View File
@@ -2229,37 +2229,49 @@ function NetworkConfigManager() {
setApplyLoading(true); setApplyLoading(true);
if (dryRunOverride !== null) setApplyResult(null); if (dryRunOverride !== null) setApplyResult(null);
const requestBody = { serverId: applyServerId, type: 'all', dryRun: useDryRun };
console.log('[MikroTik apply] Request:', { url: '/api/mikrotik/apply', method: 'POST', body: requestBody });
try { try {
const response = await fetch('/api/mikrotik/apply', { const response = await fetch('/api/mikrotik/apply', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody),
serverId: applyServerId,
type: 'all',
dryRun: useDryRun,
}),
}); });
const data = await response.json().catch(() => ({})); console.log('[MikroTik apply] Response:', { status: response.status, ok: response.ok, url: response.url });
const data = await response.json().catch((parseErr) => {
console.warn('[MikroTik apply] JSON parse error:', parseErr);
return {};
});
if (!response.ok || data.ok === false) { if (!response.ok || data.ok === false) {
const errorMessage = data.error || data.message || `HTTP ${response.status}`; const errorMessage = data.error || data.message || `HTTP ${response.status}`;
setApplyResult({ ok: false, error: errorMessage }); console.error('[MikroTik apply] Error:', { status: response.status, error: errorMessage, data });
setApplyResult({ ok: false, error: errorMessage, mikrotikRequests: data.mikrotikRequests });
notify.error('Не удалось применить конфигурацию по API'); notify.error('Не удалось применить конфигурацию по API');
return; return;
} }
console.log('[MikroTik apply] Success:', data);
if (data.mikrotikRequests?.length) {
console.log('[MikroTik apply] Запросы к MikroTik:', data.mikrotikRequests);
}
setApplyResult(data); setApplyResult(data);
if (data.ok && !useDryRun) { if (data.ok && !useDryRun) {
notify.success(`Применено: создано ${data.summary?.created || 0}, обновлено ${data.summary?.updated || 0}, пропущено ${data.summary?.skipped || 0}`); notify.success(`Применено: создано ${data.summary?.created || 0}, обновлено ${data.summary?.updated || 0}, пропущено ${data.summary?.skipped || 0}`);
} }
} catch (error) { } catch (error) {
console.error('[MikroTik apply] Fetch failed:', error);
const errMsg = error.message || 'Ошибка применения';
const hint = errMsg.includes('fetch') || errMsg.includes('Failed') ? ' Проверьте логи backend — при ERR_EMPTY_RESPONSE сервер мог не ответить.' : '';
setApplyResult({ setApplyResult({
ok: false, ok: false,
error: error.message || 'Ошибка применения', error: errMsg + hint,
}); });
notify.error('Не удалось применить конфигурацию по API'); notify.error('Не удалось применить конфигурацию по API');
} finally { } finally {
@@ -5373,9 +5385,31 @@ function NetworkConfigManager() {
</ul> </ul>
</div> </div>
))} ))}
{applyResult.mikrotikRequests?.length > 0 && (
<details className="mt-3">
<summary className="text-muted small cursor-pointer">Запросы к MikroTik API ({applyResult.mikrotikRequests.length})</summary>
<pre className="bg-dark text-light p-2 rounded small mt-2 mb-0" style={{ maxHeight: '200px', overflow: 'auto', fontSize: '0.75rem' }}>
{applyResult.mikrotikRequests.map((req, i) => (
<div key={i}>{req.path} {Array.isArray(req.args) ? req.args.join(' ') : ''}</div>
))}
</pre>
</details>
)}
</> </>
) : ( ) : (
<div className="alert alert-danger mb-0">{applyResult.error}</div> <>
<div className="alert alert-danger mb-2">{applyResult.error}</div>
{applyResult.mikrotikRequests?.length > 0 && (
<details className="mt-2">
<summary className="text-muted small cursor-pointer">Запросы к MikroTik API до ошибки ({applyResult.mikrotikRequests.length})</summary>
<pre className="bg-dark text-light p-2 rounded small mt-2 mb-0" style={{ maxHeight: '200px', overflow: 'auto', fontSize: '0.75rem' }}>
{applyResult.mikrotikRequests.map((req, i) => (
<div key={i}>{req.path} {Array.isArray(req.args) ? req.args.join(' ') : ''}</div>
))}
</pre>
</details>
)}
</>
)} )}
</> </>
)} )}