feat: IPAM improvements - server/node subnet binding, theme color fixes

- IPAM link-allocations: filter networks by proxmox_server_id and proxmox_node
  to prevent cross-server/cross-node IP assignment
- IPAM link-allocations: add manual server+network selection modal before linking
- IPAM add-network forms: add Proxmox server and node selectors with dynamic
  cluster-info loading (ipam_dashboard.html, ipam_networks.html)
- models.py / schemas.py: add proxmox_node field to IPAMNetwork
- migrations: migration #16 adds proxmox_node column to ipam_networks table
- Theme fixes: replace all hardcoded rgba/color:#fff/color:white with CSS vars
  across base.html, virtual_machines.html, instance_detail.html,
  os_templates.html, proxmox_vms.html, ipam_network_detail.html,
  ipam_allocations.html
- i18n: add proxmox_node, node selector, link-allocations modal keys (ru/en)
- Version bump: 1.0.4 -> 1.0.5
This commit is contained in:
markmorado
2026-03-08 14:17:54 +01:00
parent 4bbc02982a
commit 91dbf0b81c
15 changed files with 542 additions and 119 deletions

View File

@@ -63,10 +63,21 @@
<input type="text" id="networkBridge" placeholder="vmbr0" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
</div>
</div>
<div class="form-group">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('description') }}</label>
<textarea id="networkDescription" rows="2" placeholder="{{ t('description_placeholder') }}" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); resize: vertical;"></textarea>
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('proxmox_server') }}</label>
<select id="networkServer" onchange="onNetworkServerChange()" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
<option value="">{{ t('all_servers') }} ({{ t('global_network') }})</option>
</select>
<small style="color: var(--text-muted);">{{ t('network_server_hint') }}</small>
</div>
<div class="form-group" id="networkNodeRow" style="display: none;">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('proxmox_node') }}</label>
<select id="networkNode" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
<option value="">{{ t('all_nodes') }}</option>
</select>
<small style="color: var(--text-muted);">{{ t('proxmox_node_hint') }}</small>
</div>
</div>
@@ -158,11 +169,52 @@ function getUtilizationColor(percent) {
function showAddNetworkModal() {
document.getElementById('addNetworkModal').style.display = 'flex';
loadServersForIPAM();
}
function closeAddNetworkModal() {
document.getElementById('addNetworkModal').style.display = 'none';
document.getElementById('addNetworkForm').reset();
document.getElementById('networkNodeRow').style.display = 'none';
}
let ipamServers = [];
async function loadServersForIPAM() {
try {
const response = await fetchWithAuth('/proxmox/api/servers');
if (response.ok) {
ipamServers = await response.json();
const select = document.getElementById('networkServer');
select.innerHTML = `<option value="">{{ t('all_servers') }} ({{ t('global_network') }})</option>`;
ipamServers.forEach(s => {
select.innerHTML += `<option value="${s.id}">${s.name} (${s.ip_address})</option>`;
});
}
} catch(e) { console.error('Error loading servers:', e); }
}
async function onNetworkServerChange() {
const serverId = document.getElementById('networkServer').value;
const nodeRow = document.getElementById('networkNodeRow');
const nodeSelect = document.getElementById('networkNode');
if (!serverId) {
nodeRow.style.display = 'none';
nodeSelect.innerHTML = `<option value="">{{ t('all_nodes') }}</option>`;
return;
}
nodeRow.style.display = 'block';
nodeSelect.innerHTML = `<option value="">{{ t('loading_nodes') }}</option>`;
try {
const resp = await fetchWithAuth(`/proxmox/api/servers/${serverId}/cluster-info`);
const info = resp.ok ? await resp.json() : {};
const nodes = info.nodes || [];
nodeSelect.innerHTML = `<option value="">{{ t('all_nodes') }}</option>`;
nodes.forEach(n => { nodeSelect.innerHTML += `<option value="${n}">${n}</option>`; });
if (nodes.length === 1) nodeSelect.value = nodes[0];
} catch(e) {
nodeSelect.innerHTML = `<option value="">{{ t('all_nodes') }}</option>`;
}
}
async function createNetwork(event) {
@@ -174,6 +226,8 @@ async function createNetwork(event) {
gateway: document.getElementById('networkGateway').value || null,
vlan_id: document.getElementById('networkVLAN').value ? parseInt(document.getElementById('networkVLAN').value) : null,
proxmox_bridge: document.getElementById('networkBridge').value || null,
proxmox_server_id: document.getElementById('networkServer').value ? parseInt(document.getElementById('networkServer').value) : null,
proxmox_node: document.getElementById('networkNode').value || null,
description: document.getElementById('networkDescription').value || null
};