Firewall added & some fixes
This commit is contained in:
166
public/app.js
166
public/app.js
@@ -5,7 +5,8 @@
|
||||
const state = {
|
||||
interfaces: [], // latest data from /api/interfaces
|
||||
pending: [], // interface names with pending config
|
||||
configModal: null, // name of interface being configured
|
||||
configModal: null, // name of interface being configured (null = new VLAN)
|
||||
configModalParent: null, // parent interface when creating a new VLAN
|
||||
nat: null, // {installed, interfaces} from /api/nat
|
||||
};
|
||||
|
||||
@@ -29,6 +30,19 @@ const get = (path) => api('GET', path);
|
||||
const post = (path, body) => api('POST', path, body);
|
||||
const del = (path) => api('DELETE', path);
|
||||
|
||||
// ── VLAN helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function isVLAN(name) {
|
||||
return /\.\d+$/.test(name);
|
||||
}
|
||||
function vlanParent(name) {
|
||||
return name.replace(/\.\d+$/, '');
|
||||
}
|
||||
function vlanId(name) {
|
||||
const m = name.match(/\.(\d+)$/);
|
||||
return m ? parseInt(m[1]) : 0;
|
||||
}
|
||||
|
||||
// ── Format helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function fmtBytes(n) {
|
||||
@@ -56,9 +70,24 @@ function renderAll() {
|
||||
const grid = document.getElementById('ifaceGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
state.interfaces.forEach(iface => {
|
||||
grid.appendChild(buildCard(iface));
|
||||
});
|
||||
// Group VLANs by parent
|
||||
const vlansByParent = {};
|
||||
const physicals = [];
|
||||
|
||||
for (const iface of state.interfaces) {
|
||||
if (isVLAN(iface.name)) {
|
||||
const p = vlanParent(iface.name);
|
||||
if (!vlansByParent[p]) vlansByParent[p] = [];
|
||||
vlansByParent[p].push(iface);
|
||||
} else {
|
||||
physicals.push(iface);
|
||||
}
|
||||
}
|
||||
|
||||
for (const iface of physicals) {
|
||||
const vlans = vlansByParent[iface.name] || [];
|
||||
grid.appendChild(buildCard(iface, vlans));
|
||||
}
|
||||
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
grid.classList.remove('hidden');
|
||||
@@ -66,9 +95,10 @@ function renderAll() {
|
||||
renderPendingBanner();
|
||||
}
|
||||
|
||||
function buildCard(iface) {
|
||||
function buildCard(iface, vlans) {
|
||||
const hasPending = state.pending.includes(iface.name);
|
||||
const sc = stateClass(iface.state);
|
||||
const isLo = iface.name === 'lo' || iface.mode === 'loopback';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'iface-card' + (hasPending ? ' has-pending' : '');
|
||||
@@ -129,11 +159,56 @@ function buildCard(iface) {
|
||||
<button class="btn btn-ghost btn-sm" data-action="restart" data-iface="${iface.name}">RESTART</button>
|
||||
<button class="btn btn-primary btn-sm" data-action="config" data-iface="${iface.name}" style="margin-left:auto">CONFIG</button>
|
||||
</div>
|
||||
|
||||
${!isLo ? buildVLANSection(iface.name, vlans) : ''}
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildVLANSection(parentName, vlans) {
|
||||
const rows = vlans.map(v => {
|
||||
const sc = stateClass(v.state);
|
||||
const hasPending = state.pending.includes(v.name);
|
||||
const ip = v.ipv4
|
||||
? v.ipv4 + (v.ipv4_mask ? ' / ' + v.ipv4_mask : '')
|
||||
: '<span class="none">—</span>';
|
||||
return `
|
||||
<div class="vlan-row" data-name="${v.name}">
|
||||
<div class="vlan-row-left">
|
||||
<span class="state-dot ${sc}" style="width:8px;height:8px"></span>
|
||||
<span class="vlan-iface-name">${v.name}</span>
|
||||
<span class="vlan-id-tag">VLAN ${vlanId(v.name)}</span>
|
||||
<span class="mode-badge ${v.mode || 'unknown'}">${modeLabel(v.mode)}</span>
|
||||
${hasPending ? '<span class="pending-badge">несохранено</span>' : ''}
|
||||
</div>
|
||||
<div class="vlan-row-info">${ip}</div>
|
||||
<div class="vlan-row-actions">
|
||||
<button class="btn btn-success btn-xs" data-action="up" data-iface="${v.name}">ON</button>
|
||||
<button class="btn btn-danger btn-xs" data-action="down" data-iface="${v.name}">OFF</button>
|
||||
<button class="btn btn-primary btn-xs" data-action="config" data-iface="${v.name}">CONFIG</button>
|
||||
<button class="btn btn-danger btn-xs" data-action="delete" data-iface="${v.name}" title="Удалить VLAN">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const empty = vlans.length === 0
|
||||
? `<div class="vlan-empty">Нет тегированных VLAN</div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="vlan-section">
|
||||
<div class="vlan-header">
|
||||
<span class="vlan-title">Теговые VLAN</span>
|
||||
<button class="btn btn-ghost btn-xs" data-action="addvlan" data-iface="${parentName}">+ Добавить</button>
|
||||
</div>
|
||||
<div class="vlan-list">
|
||||
${rows}
|
||||
${empty}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPendingBanner() {
|
||||
const banner = document.getElementById('pendingBanner');
|
||||
const list = document.getElementById('pendingList');
|
||||
@@ -164,6 +239,18 @@ async function loadAll() {
|
||||
// ── Interface actions ─────────────────────────────────────────────────────────
|
||||
|
||||
async function doAction(name, action) {
|
||||
if (action === 'delete') {
|
||||
if (!confirm(`Удалить VLAN ${name}?`)) return;
|
||||
try {
|
||||
await post(`/api/interfaces/${name}/delete`);
|
||||
showToast(`${name}: удалён`, 'success');
|
||||
await loadAll();
|
||||
} catch (e) {
|
||||
showToast(`${name} delete: ${e.message}`, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector(`[data-action="${action}"][data-iface="${name}"]`);
|
||||
if (btn) { btn.disabled = true; btn.textContent = '...'; }
|
||||
|
||||
@@ -182,8 +269,21 @@ async function doAction(name, action) {
|
||||
|
||||
async function openConfig(name) {
|
||||
state.configModal = name;
|
||||
state.configModalParent = null;
|
||||
document.getElementById('modalTitle').textContent = `Настройка: ${name}`;
|
||||
|
||||
// Show/hide VLAN ID field
|
||||
const vlanSection = document.getElementById('vlanIdSection');
|
||||
const vlanInput = document.getElementById('cfgVLANId');
|
||||
if (isVLAN(name)) {
|
||||
vlanSection.classList.remove('hidden');
|
||||
vlanInput.value = vlanId(name);
|
||||
vlanInput.readOnly = true;
|
||||
} else {
|
||||
vlanSection.classList.add('hidden');
|
||||
vlanInput.readOnly = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const [{ config, pending }, natData] = await Promise.all([
|
||||
get(`/api/config/${name}`),
|
||||
@@ -197,6 +297,26 @@ async function openConfig(name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openNewVLAN(parentName) {
|
||||
state.configModal = null;
|
||||
state.configModalParent = parentName;
|
||||
document.getElementById('modalTitle').textContent = `Новый VLAN на ${parentName}`;
|
||||
|
||||
const vlanSection = document.getElementById('vlanIdSection');
|
||||
const vlanInput = document.getElementById('cfgVLANId');
|
||||
vlanSection.classList.remove('hidden');
|
||||
vlanInput.readOnly = false;
|
||||
vlanInput.value = '';
|
||||
|
||||
try {
|
||||
const natData = await get('/api/nat').catch(() => null);
|
||||
if (natData) state.nat = natData;
|
||||
} catch (_) {}
|
||||
|
||||
fillForm({ auto: true, mode: 'static' }, false, '');
|
||||
document.getElementById('modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function fillForm(cfg, pending, name) {
|
||||
document.getElementById('cfgAuto').checked = !!cfg.auto;
|
||||
document.getElementById('cfgAddress').value = cfg.address || '';
|
||||
@@ -208,9 +328,8 @@ function fillForm(cfg, pending, name) {
|
||||
setMode(mode);
|
||||
|
||||
// Mark pending visually
|
||||
const title = document.getElementById('modalTitle');
|
||||
if (pending) {
|
||||
title.textContent = `Настройка: ${state.configModal} (несохранённые изменения)`;
|
||||
if (pending && name) {
|
||||
document.getElementById('modalTitle').textContent = `Настройка: ${name} (несохранённые изменения)`;
|
||||
}
|
||||
|
||||
// NAT section — show for all non-loopback interfaces
|
||||
@@ -244,11 +363,29 @@ function closeModal() {
|
||||
document.getElementById('modal').classList.add('hidden');
|
||||
document.getElementById('configForm').reset();
|
||||
state.configModal = null;
|
||||
state.configModalParent = null;
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const name = state.configModal;
|
||||
if (!name) return;
|
||||
let name = state.configModal;
|
||||
|
||||
if (!name) {
|
||||
// New VLAN — build name from parent + VLAN ID
|
||||
const parent = state.configModalParent;
|
||||
const id = parseInt(document.getElementById('cfgVLANId').value);
|
||||
if (!parent) return;
|
||||
if (!id || id < 1 || id > 4094) {
|
||||
showToast('Укажите корректный VLAN ID (1–4094)', 'error');
|
||||
return;
|
||||
}
|
||||
name = `${parent}.${id}`;
|
||||
|
||||
// Check for duplicate
|
||||
if (state.interfaces.find(i => i.name === name)) {
|
||||
showToast(`VLAN ${name} уже существует`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const mode = currentMode();
|
||||
const cfg = {
|
||||
@@ -262,7 +399,6 @@ async function saveConfig() {
|
||||
extra: {},
|
||||
};
|
||||
|
||||
// Basic validation for static
|
||||
if (mode === 'static' && !cfg.address) {
|
||||
showToast('Укажите IP-адрес', 'error');
|
||||
return;
|
||||
@@ -346,6 +482,8 @@ document.getElementById('ifaceGrid').addEventListener('click', e => {
|
||||
const { action, iface } = btn.dataset;
|
||||
if (action === 'config') {
|
||||
openConfig(iface);
|
||||
} else if (action === 'addvlan') {
|
||||
openNewVLAN(iface);
|
||||
} else {
|
||||
doAction(iface, action);
|
||||
}
|
||||
@@ -378,11 +516,5 @@ setInterval(loadAll, 10000);
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
(async () => {
|
||||
// Try to get hostname
|
||||
try {
|
||||
const res = await fetch('/api/interfaces');
|
||||
// hostname from Location header or just skip
|
||||
} catch (_) {}
|
||||
|
||||
await loadAll();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user