|
@@ -3094,7 +3094,8 @@ function renderRoutesList() {
|
|
|
|
|
|
|
|
routes.forEach(r => {
|
|
routes.forEach(r => {
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
- tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs cursor-pointer ${r.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
+ tr.setAttribute('ondblclick', `openEditRouteModal('${r.id}')`);
|
|
|
|
|
|
|
|
// MikroTik Flags string: D=Dynamic, A=Active, C=Connect, S=Static, X=Disabled
|
|
// MikroTik Flags string: D=Dynamic, A=Active, C=Connect, S=Static, X=Disabled
|
|
|
let flags = '';
|
|
let flags = '';
|
|
@@ -3114,16 +3115,18 @@ function renderRoutesList() {
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 px-3 font-bold ${isDefault ? 'text-cyan-300' : 'text-white'}">${r.dst_address}</td>
|
|
<td class="py-2.5 px-3 font-bold ${isDefault ? 'text-cyan-300' : 'text-white'}">${r.dst_address}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">${r.gateway || '<span class="text-slate-600 font-sans">Direct link</span>'}</td>
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">${r.gateway || '<span class="text-slate-600 font-sans">Direct link</span>'}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400 font-mono">${r.distance || 1}</td>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-mono">${(r.distance !== undefined && r.distance !== null) ? r.distance : 0}</td>
|
|
|
<td class="py-2.5 px-3 font-mono font-semibold text-purple-300">${r.routing_table || 'main'}</td>
|
|
<td class="py-2.5 px-3 font-mono font-semibold text-purple-300">${r.routing_table || 'main'}</td>
|
|
|
<td class="py-2.5 px-3 text-emerald-400 font-mono">${r.interface || '-'}</td>
|
|
<td class="py-2.5 px-3 text-emerald-400 font-mono">${r.interface || '-'}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[140px]">${r.comment || '-'}</td>
|
|
<td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[140px]">${r.comment || '-'}</td>
|
|
|
<td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
|
|
<td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
${r.static ? `
|
|
${r.static ? `
|
|
|
<button onclick="toggleRouteState('${r.id}', ${!r.disabled})" class="text-xs ${r.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${r.disabled ? 'Enable' : 'Disable'}</button>
|
|
<button onclick="toggleRouteState('${r.id}', ${!r.disabled})" class="text-xs ${r.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${r.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
- <button onclick="openEditRouteModal('${r.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
|
|
|
|
+ <button onclick="openEditRouteModal('${r.id}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-semibold">Edit</button>
|
|
|
<button onclick="deleteRoute('${r.id}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
|
|
<button onclick="deleteRoute('${r.id}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
|
|
|
- ` : `<span class="text-slate-600 text-[10px]">Kernel</span>`}
|
|
|
|
|
|
|
+ ` : `
|
|
|
|
|
+ <button onclick="openEditRouteModal('${r.id}')" class="text-xs text-slate-400 hover:text-white">View</button>
|
|
|
|
|
+ `}
|
|
|
</td>
|
|
</td>
|
|
|
`;
|
|
`;
|
|
|
tbody.appendChild(tr);
|
|
tbody.appendChild(tr);
|
|
@@ -3133,23 +3136,45 @@ function renderRoutesList() {
|
|
|
function openAddRouteModal() {
|
|
function openAddRouteModal() {
|
|
|
document.getElementById('route-modal-title').innerText = 'Add Static Route';
|
|
document.getElementById('route-modal-title').innerText = 'Add Static Route';
|
|
|
document.getElementById('route-modal-id').value = '';
|
|
document.getElementById('route-modal-id').value = '';
|
|
|
- document.getElementById('route-modal-dst').value = '0.0.0.0/0';
|
|
|
|
|
- document.getElementById('route-modal-gw').value = '';
|
|
|
|
|
- document.getElementById('route-modal-distance').value = '1';
|
|
|
|
|
- document.getElementById('route-modal-comment').value = '';
|
|
|
|
|
- document.getElementById('route-modal-disabled').checked = false;
|
|
|
|
|
|
|
+ document.getElementById('route-modal-old-dst').value = '';
|
|
|
|
|
+ document.getElementById('route-modal-old-gw').value = '';
|
|
|
|
|
+ document.getElementById('route-modal-old-distance').value = '';
|
|
|
|
|
+ document.getElementById('route-modal-old-table').value = '';
|
|
|
|
|
+ document.getElementById('route-modal-old-iface').value = '';
|
|
|
|
|
+
|
|
|
|
|
+ const dstInput = document.getElementById('route-modal-dst');
|
|
|
|
|
+ const gwInput = document.getElementById('route-modal-gw');
|
|
|
|
|
+ const distInput = document.getElementById('route-modal-distance');
|
|
|
|
|
+ const ifaceSelect = document.getElementById('route-modal-iface');
|
|
|
|
|
+ const tableSelect = document.getElementById('route-modal-table');
|
|
|
|
|
+ const commentInput = document.getElementById('route-modal-comment');
|
|
|
|
|
+ const disCheck = document.getElementById('route-modal-disabled');
|
|
|
|
|
+ const saveBtn = document.querySelector('#route-modal button[type="submit"]');
|
|
|
|
|
+
|
|
|
|
|
+ dstInput.disabled = false;
|
|
|
|
|
+ dstInput.value = '0.0.0.0/0';
|
|
|
|
|
+ gwInput.disabled = false;
|
|
|
|
|
+ gwInput.value = '';
|
|
|
|
|
+ distInput.disabled = false;
|
|
|
|
|
+ distInput.value = '1';
|
|
|
|
|
+ ifaceSelect.disabled = false;
|
|
|
|
|
+ tableSelect.disabled = false;
|
|
|
|
|
+ commentInput.disabled = false;
|
|
|
|
|
+ commentInput.value = '';
|
|
|
|
|
+ disCheck.disabled = false;
|
|
|
|
|
+ disCheck.checked = false;
|
|
|
|
|
+ if (saveBtn) saveBtn.classList.remove('hidden');
|
|
|
|
|
|
|
|
populateInterfaceSelect('route-modal-iface');
|
|
populateInterfaceSelect('route-modal-iface');
|
|
|
|
|
|
|
|
// Populate tables
|
|
// Populate tables
|
|
|
- const sel = document.getElementById('route-modal-table');
|
|
|
|
|
- sel.innerHTML = '';
|
|
|
|
|
|
|
+ tableSelect.innerHTML = '';
|
|
|
(state.routingTables || [{ name: 'main' }]).forEach(t => {
|
|
(state.routingTables || [{ name: 'main' }]).forEach(t => {
|
|
|
const opt = document.createElement('option');
|
|
const opt = document.createElement('option');
|
|
|
opt.value = t.name;
|
|
opt.value = t.name;
|
|
|
opt.innerText = t.name;
|
|
opt.innerText = t.name;
|
|
|
if (t.name === 'main') opt.selected = true;
|
|
if (t.name === 'main') opt.selected = true;
|
|
|
- sel.appendChild(opt);
|
|
|
|
|
|
|
+ tableSelect.appendChild(opt);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
document.getElementById('route-modal').classList.remove('hidden');
|
|
document.getElementById('route-modal').classList.remove('hidden');
|
|
@@ -3159,24 +3184,58 @@ function openEditRouteModal(id) {
|
|
|
const r = (state.routes || []).find(route => route.id === id);
|
|
const r = (state.routes || []).find(route => route.id === id);
|
|
|
if (!r) return;
|
|
if (!r) return;
|
|
|
|
|
|
|
|
- document.getElementById('route-modal-title').innerText = `Edit Route: ${r.dst_address}`;
|
|
|
|
|
|
|
+ const isStatic = !!r.static;
|
|
|
|
|
+ document.getElementById('route-modal-title').innerText = isStatic
|
|
|
|
|
+ ? `Edit Route: ${r.dst_address}`
|
|
|
|
|
+ : `Route Properties (Read Only): ${r.dst_address}`;
|
|
|
|
|
+
|
|
|
document.getElementById('route-modal-id').value = r.id;
|
|
document.getElementById('route-modal-id').value = r.id;
|
|
|
- document.getElementById('route-modal-dst').value = r.dst_address;
|
|
|
|
|
- document.getElementById('route-modal-gw').value = r.gateway || '';
|
|
|
|
|
- document.getElementById('route-modal-distance').value = r.distance || 1;
|
|
|
|
|
- document.getElementById('route-modal-comment').value = r.comment || '';
|
|
|
|
|
- document.getElementById('route-modal-disabled').checked = !!r.disabled;
|
|
|
|
|
|
|
+ document.getElementById('route-modal-old-dst').value = r.dst_address || '';
|
|
|
|
|
+ document.getElementById('route-modal-old-gw').value = r.gateway || '';
|
|
|
|
|
+ document.getElementById('route-modal-old-distance').value = (r.distance !== undefined && r.distance !== null) ? r.distance : '';
|
|
|
|
|
+ document.getElementById('route-modal-old-table').value = r.routing_table || 'main';
|
|
|
|
|
+ document.getElementById('route-modal-old-iface').value = r.interface || '';
|
|
|
|
|
+
|
|
|
|
|
+ const dstInput = document.getElementById('route-modal-dst');
|
|
|
|
|
+ const gwInput = document.getElementById('route-modal-gw');
|
|
|
|
|
+ const distInput = document.getElementById('route-modal-distance');
|
|
|
|
|
+ const ifaceSelect = document.getElementById('route-modal-iface');
|
|
|
|
|
+ const tableSelect = document.getElementById('route-modal-table');
|
|
|
|
|
+ const commentInput = document.getElementById('route-modal-comment');
|
|
|
|
|
+ const disCheck = document.getElementById('route-modal-disabled');
|
|
|
|
|
+ const saveBtn = document.querySelector('#route-modal button[type="submit"]');
|
|
|
|
|
+
|
|
|
|
|
+ dstInput.value = r.dst_address;
|
|
|
|
|
+ gwInput.value = r.gateway || '';
|
|
|
|
|
+ distInput.value = (r.distance !== undefined && r.distance !== null) ? r.distance : 1;
|
|
|
|
|
+ commentInput.value = r.comment || '';
|
|
|
|
|
+ disCheck.checked = !!r.disabled;
|
|
|
|
|
+
|
|
|
|
|
+ dstInput.disabled = !isStatic;
|
|
|
|
|
+ gwInput.disabled = !isStatic;
|
|
|
|
|
+ distInput.disabled = !isStatic;
|
|
|
|
|
+ ifaceSelect.disabled = !isStatic;
|
|
|
|
|
+ tableSelect.disabled = !isStatic;
|
|
|
|
|
+ commentInput.disabled = !isStatic;
|
|
|
|
|
+ disCheck.disabled = !isStatic;
|
|
|
|
|
+
|
|
|
|
|
+ if (saveBtn) {
|
|
|
|
|
+ if (isStatic) {
|
|
|
|
|
+ saveBtn.classList.remove('hidden');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ saveBtn.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
populateInterfaceSelect('route-modal-iface', r.interface);
|
|
populateInterfaceSelect('route-modal-iface', r.interface);
|
|
|
|
|
|
|
|
- const sel = document.getElementById('route-modal-table');
|
|
|
|
|
- sel.innerHTML = '';
|
|
|
|
|
|
|
+ tableSelect.innerHTML = '';
|
|
|
(state.routingTables || [{ name: 'main' }]).forEach(t => {
|
|
(state.routingTables || [{ name: 'main' }]).forEach(t => {
|
|
|
const opt = document.createElement('option');
|
|
const opt = document.createElement('option');
|
|
|
opt.value = t.name;
|
|
opt.value = t.name;
|
|
|
opt.innerText = t.name;
|
|
opt.innerText = t.name;
|
|
|
- if (t.name === r.routing_table) opt.selected = true;
|
|
|
|
|
- sel.appendChild(opt);
|
|
|
|
|
|
|
+ if (t.name === (r.routing_table || 'main')) opt.selected = true;
|
|
|
|
|
+ tableSelect.appendChild(opt);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
document.getElementById('route-modal').classList.remove('hidden');
|
|
document.getElementById('route-modal').classList.remove('hidden');
|
|
@@ -3189,28 +3248,53 @@ function closeRouteModal() {
|
|
|
async function submitSaveRoute(e) {
|
|
async function submitSaveRoute(e) {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
const id = document.getElementById('route-modal-id').value;
|
|
const id = document.getElementById('route-modal-id').value;
|
|
|
|
|
+ const oldDst = document.getElementById('route-modal-old-dst')?.value || '';
|
|
|
|
|
+ const oldGw = document.getElementById('route-modal-old-gw')?.value || '';
|
|
|
|
|
+ const oldDistStr = document.getElementById('route-modal-old-distance')?.value;
|
|
|
|
|
+ const oldTable = document.getElementById('route-modal-old-table')?.value || '';
|
|
|
|
|
+ const oldIface = document.getElementById('route-modal-old-iface')?.value || '';
|
|
|
|
|
+
|
|
|
const dst = document.getElementById('route-modal-dst').value.trim();
|
|
const dst = document.getElementById('route-modal-dst').value.trim();
|
|
|
const gw = document.getElementById('route-modal-gw').value.trim();
|
|
const gw = document.getElementById('route-modal-gw').value.trim();
|
|
|
const table = document.getElementById('route-modal-table').value;
|
|
const table = document.getElementById('route-modal-table').value;
|
|
|
- const distance = parseInt(document.getElementById('route-modal-distance').value, 10) || 1;
|
|
|
|
|
|
|
+ const distance = parseInt(document.getElementById('route-modal-distance').value, 10) || 0;
|
|
|
const iface = document.getElementById('route-modal-iface').value;
|
|
const iface = document.getElementById('route-modal-iface').value;
|
|
|
const comment = document.getElementById('route-modal-comment').value.trim();
|
|
const comment = document.getElementById('route-modal-comment').value.trim();
|
|
|
const disabled = document.getElementById('route-modal-disabled').checked;
|
|
const disabled = document.getElementById('route-modal-disabled').checked;
|
|
|
|
|
|
|
|
|
|
+ if (!dst) {
|
|
|
|
|
+ showToast('Error', 'Destination address is required', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!gw && !iface) {
|
|
|
|
|
+ showToast('Error', 'Either Gateway IP or Interface must be specified', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id,
|
|
|
|
|
+ old_dst_address: oldDst,
|
|
|
|
|
+ old_gateway: oldGw,
|
|
|
|
|
+ old_routing_table: oldTable,
|
|
|
|
|
+ old_interface: oldIface,
|
|
|
|
|
+ dst_address: dst,
|
|
|
|
|
+ gateway: gw || iface,
|
|
|
|
|
+ routing_table: table,
|
|
|
|
|
+ distance,
|
|
|
|
|
+ interface: iface,
|
|
|
|
|
+ comment,
|
|
|
|
|
+ disabled
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (oldDistStr !== undefined && oldDistStr !== '') {
|
|
|
|
|
+ payload.old_distance = parseInt(oldDistStr, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/routes`, {
|
|
const res = await fetch(`${API_BASE}/routes`, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- id,
|
|
|
|
|
- dst_address: dst,
|
|
|
|
|
- gateway: gw,
|
|
|
|
|
- routing_table: table,
|
|
|
|
|
- distance,
|
|
|
|
|
- interface: iface,
|
|
|
|
|
- comment,
|
|
|
|
|
- disabled
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
});
|
|
});
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
showToast('Route Saved', `Route to ${dst} installed`, 'success');
|
|
showToast('Route Saved', `Route to ${dst} installed`, 'success');
|
|
@@ -3218,7 +3302,7 @@ async function submitSaveRoute(e) {
|
|
|
loadRoutes();
|
|
loadRoutes();
|
|
|
} else {
|
|
} else {
|
|
|
const err = await res.json();
|
|
const err = await res.json();
|
|
|
- showToast('Error', err.message, 'error');
|
|
|
|
|
|
|
+ showToast('Error', err.message || 'Failed to save route', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|