IPAM: duplikaatti-IP-tarkistus, vapaat lohkot, asiakas-kentän poisto + varattu oletus
- Duplikaatti-IP/verkko -tarkistus: estää saman verkko-osoitteen lisäämisen kahdesti - Vapaan tilan näyttö: kun subnet avataan, näytetään vapaat osoitelohkot lasten välissä (vihreä "Vapaa"-rivi) - Asiakas-kenttä poistettu IPAM-näkymästä (taulukot, lomake, haku) - Varattu oletustilaksi verkkoa/VLANia lisättäessä Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
136
script.js
136
script.js
@@ -3022,6 +3022,80 @@ function isSubnetOf(childNet, childPrefix, childBits, parentNet, parentPrefix, p
|
||||
const shift = BigInt(parentBits - parentPrefix);
|
||||
return (childNet >> shift) === (parentNet >> shift);
|
||||
}
|
||||
// BigInt -> IP-osoite merkkijono
|
||||
function biToIpv4(bi) {
|
||||
return [Number((bi >> 24n) & 0xFFn), Number((bi >> 16n) & 0xFFn), Number((bi >> 8n) & 0xFFn), Number(bi & 0xFFn)].join('.');
|
||||
}
|
||||
function biToIpv6(bi) {
|
||||
const parts = [];
|
||||
for (let i = 7; i >= 0; i--) parts.push(Number((bi >> BigInt(i * 16)) & 0xFFFFn).toString(16));
|
||||
// Yksinkertaistettu: ei :: kompressointia
|
||||
return parts.join(':');
|
||||
}
|
||||
function biToIp(bi, v6) { return v6 ? biToIpv6(bi) : biToIpv4(bi); }
|
||||
|
||||
// Laske vapaat lohkot parent-subnetin sisällä (aukot lasten välissä)
|
||||
function findFreeSpaces(parentNode, maxEntries = 30) {
|
||||
if (!parentNode || parentNode.bits === 0 || parentNode.entry.tyyppi !== 'subnet') return [];
|
||||
const pNet = parentNode.net;
|
||||
const pPrefix = parentNode.prefix;
|
||||
const pBits = parentNode.bits;
|
||||
const hostBits = BigInt(pBits - pPrefix);
|
||||
const parentStart = (pNet >> hostBits) << hostBits;
|
||||
const parentSize = 1n << hostBits;
|
||||
const parentEnd = parentStart + parentSize;
|
||||
|
||||
// Kerää lapset samasta osoiteperheestä, järjestä osoitteen mukaan
|
||||
const children = parentNode.children
|
||||
.filter(c => c.bits === pBits)
|
||||
.sort((a, b) => a.net < b.net ? -1 : a.net > b.net ? 1 : 0);
|
||||
|
||||
const result = [];
|
||||
let pos = parentStart;
|
||||
|
||||
for (const child of children) {
|
||||
const cHostBits = BigInt(pBits - child.prefix);
|
||||
const childStart = (child.net >> cHostBits) << cHostBits;
|
||||
const childEnd = childStart + (1n << cHostBits);
|
||||
if (pos < childStart) {
|
||||
addAlignedBlocks(result, pos, childStart, pBits, pBits === 128, maxEntries - result.length);
|
||||
}
|
||||
if (childEnd > pos) pos = childEnd;
|
||||
}
|
||||
if (pos < parentEnd) {
|
||||
addAlignedBlocks(result, pos, parentEnd, pBits, pBits === 128, maxEntries - result.length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addAlignedBlocks(result, start, end, totalBits, v6, maxAdd) {
|
||||
let pos = start;
|
||||
const tb = BigInt(totalBits);
|
||||
while (pos < end && maxAdd > 0) {
|
||||
const space = end - pos;
|
||||
// Alignment: kuinka monta trailing nollaa pos:ssa
|
||||
let alignBits = 0n;
|
||||
if (pos === 0n) {
|
||||
alignBits = tb;
|
||||
} else {
|
||||
let tmp = pos;
|
||||
while ((tmp & 1n) === 0n && alignBits < tb) { alignBits++; tmp >>= 1n; }
|
||||
}
|
||||
// Suurin 2^n joka mahtuu tilaan
|
||||
let spaceBits = 0n;
|
||||
let tmp = space >> 1n;
|
||||
while (tmp > 0n) { spaceBits++; tmp >>= 1n; }
|
||||
// Tarkista ettei ylitä
|
||||
if ((1n << spaceBits) > space) spaceBits--;
|
||||
const blockBits = alignBits < spaceBits ? alignBits : spaceBits;
|
||||
if (blockBits < 0n) break;
|
||||
const prefix = totalBits - Number(blockBits);
|
||||
result.push({ net: pos, prefix, bits: totalBits, v6, verkko: biToIp(pos, v6) + '/' + prefix });
|
||||
pos += (1n << blockBits);
|
||||
maxAdd--;
|
||||
}
|
||||
}
|
||||
|
||||
// Laske subnetin käyttöaste: kuinka monta lasta (direct children) vs kapasiteetti
|
||||
function subnetUsageHtml(node) {
|
||||
if (node.entry.tyyppi !== 'subnet' || node.children.length === 0) return '';
|
||||
@@ -3107,9 +3181,30 @@ function flattenTree(nodes, depth, drillId) {
|
||||
for (const node of list) {
|
||||
const hasChildren = node.children.length > 0;
|
||||
const expanded = ipamExpandedIds.has(node.entry.id);
|
||||
rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node });
|
||||
rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node, isFree: false });
|
||||
if (hasChildren && expanded) {
|
||||
render(node.children, d + 1);
|
||||
// Laske vapaat lohkot ja sekoita lasten sekaan osoitejärjestyksessä
|
||||
const freeSpaces = findFreeSpaces(node);
|
||||
if (freeSpaces.length > 0) {
|
||||
// Yhdistä lapset + vapaat, järjestä osoitteen mukaan
|
||||
const allItems = [
|
||||
...node.children.map(c => ({ type: 'node', item: c, sortKey: c.net })),
|
||||
...freeSpaces.map(f => ({ type: 'free', item: f, sortKey: f.net }))
|
||||
];
|
||||
allItems.sort((a, b) => a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0);
|
||||
for (const ai of allItems) {
|
||||
if (ai.type === 'node') {
|
||||
render([ai.item], d + 1);
|
||||
} else {
|
||||
rows.push({
|
||||
entry: { verkko: ai.item.verkko, tyyppi: 'free', nimi: '', tila: 'vapaa' },
|
||||
depth: d + 1, hasChildren: false, expanded: false, node: ai.item, isFree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
render(node.children, d + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -3135,7 +3230,6 @@ function renderIpam() {
|
||||
(e.verkko || '').toLowerCase().includes(query) ||
|
||||
(e.nimi || '').toLowerCase().includes(query) ||
|
||||
(e.site_name || '').toLowerCase().includes(query) ||
|
||||
(e.asiakas || '').toLowerCase().includes(query) ||
|
||||
(e.lisatiedot || '').toLowerCase().includes(query) ||
|
||||
String(e.vlan_id || '').includes(query)
|
||||
);
|
||||
@@ -3177,6 +3271,18 @@ function renderIpam() {
|
||||
tbody.innerHTML = rows.map(r => {
|
||||
const e = r.entry;
|
||||
const indent = r.depth * 1.5;
|
||||
|
||||
// Vapaa-lohko (ei oikea entry, vaan laskettu vapaa tila)
|
||||
if (r.isFree) {
|
||||
return `<tr class="ipam-tree-row ipam-free-row">
|
||||
<td style="padding-left:${indent}rem;white-space:nowrap;">
|
||||
<span class="ipam-toggle-placeholder"></span> <span class="ipam-type-free">Vapaa</span>
|
||||
</td>
|
||||
<td><code class="ipam-network ipam-network-free">${esc(e.verkko)}</code></td>
|
||||
<td colspan="4" style="color:#999;font-style:italic;">Käytettävissä</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const toggleIcon = r.hasChildren
|
||||
? `<span class="ipam-toggle" onclick="event.stopPropagation();ipamToggle('${e.id}')">${r.expanded ? '▼' : '▶'}</span> `
|
||||
: '<span class="ipam-toggle-placeholder"></span> ';
|
||||
@@ -3195,7 +3301,6 @@ function renderIpam() {
|
||||
<td>${vlanRefHtml(e.vlan_id)}</td>
|
||||
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
||||
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
||||
<td>${esc(e.asiakas || '-')}</td>
|
||||
<td class="actions-cell" onclick="event.stopPropagation()">
|
||||
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
||||
@@ -3218,8 +3323,7 @@ function renderIpamVlans(query) {
|
||||
String(e.vlan_id || '').includes(query) ||
|
||||
(e.nimi || '').toLowerCase().includes(query) ||
|
||||
(e.verkko || '').toLowerCase().includes(query) ||
|
||||
(e.site_name || '').toLowerCase().includes(query) ||
|
||||
(e.asiakas || '').toLowerCase().includes(query)
|
||||
(e.site_name || '').toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
|
||||
@@ -3233,7 +3337,7 @@ function renderIpamVlans(query) {
|
||||
|
||||
if (section) section.style.display = '';
|
||||
if (vlans.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#aaa;padding:1rem;">Ei VLANeja vielä.</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#aaa;padding:1rem;">Ei VLANeja vielä.</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = vlans.map(e => `<tr>
|
||||
<td><strong>${e.vlan_id || '-'}</strong></td>
|
||||
@@ -3241,7 +3345,6 @@ function renderIpamVlans(query) {
|
||||
<td>${esc(e.nimi || '-')}</td>
|
||||
<td>${e.site_name ? esc(e.site_name) : '<span style="color:#ccc;">—</span>'}</td>
|
||||
<td><span class="ipam-tila ${tilaClass[e.tila] || ''}">${tilaLabel[e.tila] || e.tila}</span></td>
|
||||
<td>${esc(e.asiakas || '-')}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn-link" onclick="editIpam('${e.id}')">✎</button>
|
||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteIpam('${e.id}','${esc(e.nimi || e.verkko)}')">🗑</button>
|
||||
@@ -3259,17 +3362,6 @@ function vlanRefHtml(vlanId) {
|
||||
return `<strong>${vlanId}</strong>${label ? ` <small style="color:#888;">${label}</small>` : ''}`;
|
||||
}
|
||||
|
||||
// --- Asiakas-dropdown populointi ---
|
||||
async function populateIpamCustomerDropdown(selectedName) {
|
||||
if (!customers || customers.length === 0) {
|
||||
try { customers = await apiCall('customers'); } catch(e) {}
|
||||
}
|
||||
const sel = document.getElementById('ipam-form-asiakas');
|
||||
const sorted = [...customers].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || ''));
|
||||
sel.innerHTML = '<option value="">— Ei asiakasta —</option>' +
|
||||
sorted.map(c => `<option value="${esc(c.nimi)}" ${c.nimi === selectedName ? 'selected' : ''}>${esc(c.nimi)}</option>`).join('');
|
||||
}
|
||||
|
||||
// --- Toggle & Drill ---
|
||||
function ipamToggle(id) {
|
||||
if (ipamExpandedIds.has(id)) ipamExpandedIds.delete(id);
|
||||
@@ -3310,7 +3402,6 @@ async function editIpam(id) {
|
||||
document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
|
||||
document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
|
||||
document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
|
||||
await populateIpamCustomerDropdown(e.asiakas || '');
|
||||
await loadIpamSitesDropdown();
|
||||
document.getElementById('ipam-form-site').value = e.site_id || '';
|
||||
document.getElementById('ipam-modal-title').textContent = e.tyyppi === 'vlan' ? 'Muokkaa VLANia' : 'Muokkaa verkkoa / IP:tä';
|
||||
@@ -3329,7 +3420,7 @@ document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
|
||||
document.getElementById('ipam-form-id').value = '';
|
||||
document.getElementById('ipam-form').reset();
|
||||
document.getElementById('ipam-form-tyyppi').value = 'subnet';
|
||||
await populateIpamCustomerDropdown('');
|
||||
document.getElementById('ipam-form-tila').value = 'varattu';
|
||||
await loadIpamSitesDropdown();
|
||||
document.getElementById('ipam-modal-title').textContent = 'Lisää verkko / IP';
|
||||
document.getElementById('ipam-modal').style.display = 'flex';
|
||||
@@ -3339,7 +3430,7 @@ document.getElementById('btn-add-vlan')?.addEventListener('click', async () => {
|
||||
document.getElementById('ipam-form-id').value = '';
|
||||
document.getElementById('ipam-form').reset();
|
||||
document.getElementById('ipam-form-tyyppi').value = 'vlan';
|
||||
await populateIpamCustomerDropdown('');
|
||||
document.getElementById('ipam-form-tila').value = 'varattu';
|
||||
await loadIpamSitesDropdown();
|
||||
document.getElementById('ipam-modal-title').textContent = 'Lisää VLAN';
|
||||
document.getElementById('ipam-modal').style.display = 'flex';
|
||||
@@ -3362,7 +3453,6 @@ document.getElementById('ipam-form')?.addEventListener('submit', async (e) => {
|
||||
nimi: document.getElementById('ipam-form-nimi').value.trim(),
|
||||
site_id: document.getElementById('ipam-form-site').value || null,
|
||||
tila: document.getElementById('ipam-form-tila').value,
|
||||
asiakas: document.getElementById('ipam-form-asiakas').value.trim(),
|
||||
lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
|
||||
};
|
||||
if (id) data.id = id;
|
||||
|
||||
Reference in New Issue
Block a user