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

@@ -1083,16 +1083,16 @@ body.update-in-progress .layout {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed; top: 20px; right: 20px; min-width: 300px; max-width: 400px;
padding: 16px 20px; background: rgba(30, 30, 40, 0.95); border: 1px solid rgba(255,255,255,0.1);
padding: 16px 20px; background: var(--bg-surface); border: 1px solid var(--border-default);
border-left: 4px solid ${borderColor}; border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
z-index: 99999; animation: toastSlide 0.3s ease-out;
`;
toast.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<span style="font-weight: 600; color: #fff;">${title}</span>
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: #888; cursor: pointer; font-size: 18px;">&times;</button>
<span style="font-weight: 600; color: var(--text-primary);">${title}</span>
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 18px;">&times;</button>
</div>
<div style="color: #aaa; font-size: 14px;">${msg || ''}</div>
<div style="color: var(--text-secondary); font-size: 14px;">${msg || ''}</div>
`;
document.body.appendChild(toast);

View File

@@ -30,7 +30,7 @@
<div>
<h2 id="instance-name" style="margin: 0; color: var(--text-primary);">{{ t('loading') }}</h2>
<div style="display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem;">
<span id="instance-type-badge" style="padding: 0.25rem 0.75rem; border-radius: 6px; background: var(--primary); color: white; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.5px;">QEMU</span>
<span id="instance-type-badge" style="padding: 0.25rem 0.75rem; border-radius: 6px; background: var(--primary); color: var(--text-on-primary); font-size: 0.8rem; font-weight: 700; letter-spacing: 0.5px;">QEMU</span>
</div>
<div style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.35rem;">ID: {{ vmid }} | Node: {{ node }} | {{ server.name }}</div>
</div>
@@ -79,7 +79,7 @@
<i class="fa-solid fa-compact-disc"></i> {{ t('reinstall') }}
</button>
<!-- HA Button -->
<button id="btn-ha" onclick="toggleHA()" class="btn" style="background: var(--purple); color: white; display: none;" title="{{ t('high_availability') or 'High Availability' }}" data-permission="vms.ha">
<button id="btn-ha" onclick="toggleHA()" class="btn" style="background: var(--purple); color: var(--text-on-primary); display: none;" title="{{ t('high_availability') or 'High Availability' }}" data-permission="vms.ha">
<span id="btn-ha-icon"><i class="fa-solid fa-shield"></i></span> <span id="btn-ha-text">HA</span>
</button>
<button onclick="deleteInstance()" class="btn btn-danger" data-permission="vms.delete">
@@ -1218,7 +1218,7 @@ function showNotification(message, type = 'info') {
bottom: 2rem;
right: 2rem;
background: ${colors[type]};
color: white;
color: var(--text-on-primary);
padding: 1rem 1.5rem;
border-radius: 8px;
font-weight: 500;

View File

@@ -239,8 +239,8 @@ function renderAllocations(allocations) {
</td>
<td style="padding: 0.75rem;">
${a.status === 'reserved' ?
`<span class="badge badge-info" style="color: #fff;"><i class="fa-solid fa-lock"></i> ${t('reserved')}</span>` :
`<span class="badge badge-success" style="color: #fff;">✓ ${t('allocated')}</span>`}
`<span class="badge badge-info"><i class="fa-solid fa-lock"></i> ${t('reserved')}</span>` :
`<span class="badge badge-success">✓ ${t('allocated')}</span>`}
</td>
<td style="padding: 0.75rem; color: var(--text-muted); font-size: 0.875rem;">${a.created_at ? new Date(a.created_at).toLocaleDateString('ru-RU') : '-'}</td>
<td style="padding: 0.75rem; text-align: right;">

View File

@@ -121,11 +121,19 @@
<div class="form-group">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('proxmox_server') }}</label>
<select id="networkServer" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
<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 style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
@@ -304,6 +312,37 @@ async function loadServersForIPAM() {
}
}
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 single-node server, pre-select it
if (nodes.length === 1) {
nodeSelect.value = nodes[0];
}
} catch(e) {
nodeSelect.innerHTML = `<option value="">{{ t('all_nodes') }}</option>`;
}
}
function showAddNetworkModal() {
document.getElementById('addNetworkModal').style.display = 'flex';
loadServersForIPAM();
@@ -312,12 +351,14 @@ function showAddNetworkModal() {
function closeAddNetworkModal() {
document.getElementById('addNetworkModal').style.display = 'none';
document.getElementById('addNetworkForm').reset();
document.getElementById('networkNodeRow').style.display = 'none';
}
async function createNetwork(event) {
event.preventDefault();
const serverId = document.getElementById('networkServer').value;
const nodeVal = document.getElementById('networkNode').value;
const data = {
name: document.getElementById('networkName').value,
@@ -326,6 +367,7 @@ async function createNetwork(event) {
vlan_id: document.getElementById('networkVLAN').value ? parseInt(document.getElementById('networkVLAN').value) : null,
proxmox_bridge: document.getElementById('networkBridge').value || null,
proxmox_server_id: serverId ? parseInt(serverId) : null,
proxmox_node: nodeVal || null,
dns_primary: document.getElementById('networkDNS1').value || null,
dns_secondary: document.getElementById('networkDNS2').value || null,
dns_domain: document.getElementById('networkDomain').value || null,
@@ -502,23 +544,93 @@ async function cleanupOrphans() {
}
async function linkAllocations() {
const confirmed = await showDeleteConfirm(
'{{ t("link_to_vms") }}',
'{{ t("confirm_link_allocations") }}'
);
if (!confirmed) return;
const btn = document.querySelector('#maintenanceModal .btn-primary');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> {{ t("linking") }}...';
await showLinkModal();
}
// ---- Link Allocations Modal ----
let _linkServers = [];
async function showLinkModal() {
document.getElementById('linkAllocationsModal').style.display = 'flex';
const serverSel = document.getElementById('linkServerSelect');
const networkSel = document.getElementById('linkNetworkSelect');
serverSel.innerHTML = `<option value="">{{ t('loading_ellipsis') }}</option>`;
networkSel.innerHTML = `<option value="">{{ t('select_network_first_server') }}</option>`;
networkSel.disabled = true;
document.getElementById('submitLinkBtn').disabled = true;
try {
const response = await fetchWithAuth('/ipam/api/link-allocations', { method: 'POST' });
const resp = await fetchWithAuth('/proxmox/api/servers');
_linkServers = resp.ok ? await resp.json() : [];
} catch(e) { _linkServers = []; }
serverSel.innerHTML = `<option value="">{{ t('select_server_for_link') }}</option>`;
_linkServers.forEach(s => {
serverSel.innerHTML += `<option value="${s.id}">${s.name} (${s.ip_address})</option>`;
});
}
function closeLinkModal() {
document.getElementById('linkAllocationsModal').style.display = 'none';
}
async function onLinkServerChange() {
const serverId = document.getElementById('linkServerSelect').value;
const networkSel = document.getElementById('linkNetworkSelect');
const submitBtn = document.getElementById('submitLinkBtn');
networkSel.disabled = true;
submitBtn.disabled = true;
if (!serverId) {
networkSel.innerHTML = `<option value="">{{ t('select_network_first_server') }}</option>`;
return;
}
networkSel.innerHTML = `<option value="">{{ t('loading_ellipsis') }}</option>`;
try {
const resp = await fetchWithAuth(`/ipam/api/networks?proxmox_server_id=${serverId}`);
const data = resp.ok ? await resp.json() : [];
if (!data.length) {
networkSel.innerHTML = `<option value="">{{ t('no_networks_for_server') }}</option>`;
return;
}
networkSel.innerHTML = `<option value="">{{ t('select_network') }}</option>`;
data.forEach(n => {
networkSel.innerHTML += `<option value="${n.id}">${n.name} (${n.network})</option>`;
});
networkSel.disabled = false;
} catch(e) {
networkSel.innerHTML = `<option value="">{{ t('error') }}</option>`;
}
}
function onLinkNetworkChange() {
const networkSel = document.getElementById('linkNetworkSelect');
document.getElementById('submitLinkBtn').disabled = !networkSel.value;
}
async function submitLinkAllocations() {
const serverId = document.getElementById('linkServerSelect').value;
const networkId = document.getElementById('linkNetworkSelect').value;
if (!serverId || !networkId) return;
const submitBtn = document.getElementById('submitLinkBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> {{ t("linking") }}...';
try {
const response = await fetchWithAuth('/ipam/api/link-allocations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server_id: parseInt(serverId), network_id: parseInt(networkId) })
});
if (response.ok) {
const data = await response.json();
closeLinkModal();
showToast('success', '{{ t("success") }}', `{{ t("linked") }}: ${data.linked_count}, {{ t("not_found") }}: ${data.not_found_count}`);
loadMaintenanceData();
loadData(); // Refresh main data
loadData();
} else {
const error = await response.json();
showToast('error', '{{ t("error") }}', error.detail || '{{ t("linking_failed") }}');
@@ -527,8 +639,8 @@ async function linkAllocations() {
console.error('Error linking allocations:', e);
showToast('error', '{{ t("error") }}', '{{ t("linking_failed") }}');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-link"></i> {{ t("link_to_vms") }}';
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fa-solid fa-link"></i> {{ t("link_to_vms") }}';
}
}
@@ -538,6 +650,37 @@ window.addEventListener('DOMContentLoaded', () => {
});
</script>
<!-- Link Allocations Modal -->
<div id="linkAllocationsModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); z-index: 3000; align-items: center; justify-content: center; padding: 1rem;">
<div class="card" style="max-width: 480px; width: 95%;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0;"><i class="fa-solid fa-link"></i> {{ t('link_allocations_title') }}</h3>
<button onclick="closeLinkModal()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer;">&times;</button>
</div>
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1.25rem;">{{ t('link_scope_hint') }}</p>
<div style="display: grid; gap: 1rem; margin-bottom: 1.5rem;">
<div>
<label style="display: block; margin-bottom: 0.4rem; font-size: 0.85rem; color: var(--text-secondary);">{{ t('proxmox_server') }} *</label>
<select id="linkServerSelect" onchange="onLinkServerChange()" style="width: 100%; padding: 0.65rem 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
<option value="">{{ t('select_server_for_link') }}</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 0.4rem; font-size: 0.85rem; color: var(--text-secondary);">{{ t('network') }} *</label>
<select id="linkNetworkSelect" onchange="onLinkNetworkChange()" disabled style="width: 100%; padding: 0.65rem 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
<option value="">{{ t('select_network_first_server') }}</option>
</select>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.75rem;">
<button onclick="closeLinkModal()" class="btn btn-secondary">{{ t('cancel') }}</button>
<button onclick="submitLinkAllocations()" id="submitLinkBtn" disabled class="btn btn-primary">
<i class="fa-solid fa-link"></i> {{ t('link_to_vms') }}
</button>
</div>
</div>
</div>
<!-- Maintenance Modal -->
<div id="maintenanceModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); z-index: 2000; align-items: center; justify-content: center; padding: 1rem;">
<div class="card" style="max-width: 800px; width: 95%; max-height: 90vh; overflow-y: auto;">

View File

@@ -337,11 +337,11 @@ async function loadIPMap() {
function getIPColor(status) {
switch(status) {
case 'available': return 'rgba(34, 197, 94, 0.2)';
case 'allocated': return 'rgba(59, 130, 246, 0.3)';
case 'reserved': return 'rgba(245, 158, 11, 0.3)';
case 'gateway': return 'rgba(139, 92, 246, 0.3)';
case 'conflict': return 'rgba(239, 68, 68, 0.3)';
case 'available': return 'var(--success-light)';
case 'allocated': return 'var(--info-light)';
case 'reserved': return 'var(--warning-light)';
case 'gateway': return 'var(--purple-light)';
case 'conflict': return 'var(--danger-light)';
default: return 'var(--bg-tertiary)';
}
}
@@ -392,7 +392,7 @@ async function loadAllocations() {
<td style="padding: 0.5rem; color: var(--text-secondary);">${alloc.resource_type || '-'}</td>
<td style="padding: 0.5rem; color: var(--text-secondary);">${alloc.hostname || '-'}</td>
<td style="padding: 0.5rem;">
<span style="padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; background: ${getStatusColor(alloc.status)}; color: #fff;">
<span style="padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; background: ${getStatusColor(alloc.status)}; color: ${getStatusTextColor(alloc.status)}; font-weight: 600;">
${alloc.status}
</span>
</td>
@@ -412,12 +412,20 @@ async function loadAllocations() {
function getStatusColor(status) {
switch(status) {
case 'allocated': return 'rgba(59, 130, 246, 0.3)';
case 'reserved': return 'rgba(245, 158, 11, 0.3)';
case 'conflict': return 'rgba(239, 68, 68, 0.3)';
case 'allocated': return 'var(--info-light)';
case 'reserved': return 'var(--warning-light)';
case 'conflict': return 'var(--danger-light)';
default: return 'var(--bg-tertiary)';
}
}
function getStatusTextColor(status) {
switch(status) {
case 'allocated': return 'var(--info)';
case 'reserved': return 'var(--warning)';
case 'conflict': return 'var(--danger)';
default: return 'var(--text-secondary)';
}
}
// Modal functions
function showAllocateModal() {
@@ -591,7 +599,7 @@ async function showIPDetail(ip) {
<div style="font-family: monospace; color: var(--text-primary);">${alloc.ip_address}</div>
<div style="color: var(--text-muted);">${t('status')}:</div>
<div><span style="padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.875rem; background: ${getStatusColor(alloc.status)}; color: #fff;">${alloc.status}</span></div>
<div><span style="padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.875rem; background: ${getStatusColor(alloc.status)}; color: ${getStatusTextColor(alloc.status)}; font-weight: 600;">${alloc.status}</span></div>
<div style="color: var(--text-muted);">${t('resource_type')}:</div>
<div style="color: var(--text-primary);">${alloc.resource_type || '-'}</div>

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
};

View File

@@ -150,7 +150,7 @@
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button type="button" onclick="closeModal('autoImportModal')" class="btn" style="background: var(--bg-tertiary); color: var(--text-primary);">{{ t('cancel') }}</button>
<button type="button" onclick="previewAutoImport()" id="previewImportBtn" class="btn" style="background: var(--info); color: white;"><i class="fa-solid fa-eye"></i> {{ t('preview') }}</button>
<button type="button" onclick="previewAutoImport()" id="previewImportBtn" class="btn" style="background: var(--info); color: var(--text-on-info);"><i class="fa-solid fa-eye"></i> {{ t('preview') }}</button>
<button type="button" onclick="executeAutoImport()" id="executeImportBtn" class="btn btn-primary" style="display: none;"><i class="fa-solid fa-rocket"></i> {{ t('import') }}</button>
</div>
</div>
@@ -702,7 +702,7 @@ function renderTemplates(templates) {
<span style="color: var(--text-muted); margin-left: 0.5rem;">→</span>
<span style="color: var(--info); font-weight: 500; margin-left: 0.5rem;"><i class="fa-solid fa-cube"></i> ${group.node}</span>
</div>
<span class="badge" style="background: var(--primary); color: white; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem;">
<span class="badge" style="background: var(--primary); color: var(--text-on-primary); padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem;">
${group.templates.length} ${i18n.templates_count}
</span>
</div>
@@ -942,7 +942,7 @@ async function executeAutoImport() {
html += `<div style="margin-bottom: 0.5rem;">
<span style="font-weight: bold;">${getGroupIcon(groupName)} ${groupName}:</span>`;
templates.forEach(tpl => {
html += ` <span style="background: var(--success); color: white; padding: 0.125rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; margin-left: 0.25rem;">${tpl.name} (#${tpl.sort_order})</span>`;
html += ` <span style="background: var(--success); color: var(--text-on-success); padding: 0.125rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; margin-left: 0.25rem;">${tpl.name} (#${tpl.sort_order})</span>`;
});
html += '</div>';
}

View File

@@ -67,13 +67,13 @@
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button onclick="testConnection({{ server.id }})" class="btn btn-sm" style="background: var(--success); color: white;" title="{{ t('test') }}">
<button onclick="testConnection({{ server.id }})" class="btn btn-sm" style="background: var(--success); color: var(--text-on-success);" title="{{ t('test') }}">
<i class="fa-solid fa-plug"></i> {{ t('test') }}
</button>
<button onclick="window.location.href='/proxmox/server/{{ server.id }}'" class="btn btn-sm" style="background: var(--info); color: white;" title="{{ t('details') }}">
<button onclick="window.location.href='/proxmox/server/{{ server.id }}'" class="btn btn-sm" style="background: var(--info); color: var(--text-on-info);" title="{{ t('details') }}">
<i class="fa-solid fa-chart-simple"></i> {{ t('details') }}
</button>
<button onclick="editServer({{ server.id }})" class="btn btn-sm" style="background: var(--warning); color: white;" title="{{ t('edit') }}" data-permission="proxmox.servers.edit">
<button onclick="editServer({{ server.id }}, '{{ server.name }}')" class="btn btn-sm" style="background: var(--warning); color: var(--text-on-warning);" title="{{ t('edit') }}" data-permission="proxmox.servers.edit">
<i class="fa-solid fa-pen"></i>
</button>
<button onclick="deleteServer({{ server.id }}, '{{ server.name }}')" class="btn btn-sm btn-delete" title="{{ t('delete') }}" data-permission="proxmox.servers.delete">
@@ -160,17 +160,17 @@
}
.status-badge.status-online {
background: rgba(34, 197, 94, 0.15);
background: var(--success-light);
color: var(--success);
}
.status-badge.status-offline {
background: rgba(239, 68, 68, 0.15);
background: var(--danger-light);
color: var(--danger);
}
.status-badge.status-loading {
background: rgba(156, 163, 175, 0.15);
background: var(--bg-surface-hover);
color: var(--text-muted);
}
</style>

View File

@@ -259,12 +259,12 @@
.vm-type-badge.qemu {
background: var(--info);
color: white;
color: var(--text-on-info);
}
.vm-type-badge.lxc {
background: var(--purple);
color: white;
color: var(--text-on-primary);
}
.vm-cluster-link {
@@ -438,7 +438,7 @@
gap: 1rem;
padding: 0.75rem 1.5rem;
background: var(--primary);
color: white;
color: var(--text-on-primary);
animation: slideDown 0.2s ease;
}
@@ -463,7 +463,7 @@
border: 1px solid rgba(255,255,255,0.3);
border-radius: 6px;
background: rgba(255,255,255,0.1);
color: white;
color: var(--text-on-primary);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.15s ease;
@@ -486,7 +486,7 @@
.bulk-actions-bar .cancel-btn {
background: none;
border: none;
color: white;
color: var(--text-on-primary);
cursor: pointer;
padding: 0.25rem;
opacity: 0.8;
@@ -2830,7 +2830,7 @@ async function loadCreateLXCForm() {
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color);">
<button type="button" onclick="closeCreateLXCModal()" class="btn btn-secondary">{{ t('cancel') }}</button>
<button type="submit" id="createLXCBtn" class="btn" style="background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); color: white;"><i class="fa-solid fa-cube"></i> {{ t('create') }}</button>
<button type="submit" id="createLXCBtn" class="btn" style="background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); color: var(--text-on-primary);"><i class="fa-solid fa-cube"></i> {{ t('create') }}</button>
</div>
</form>
`;
@@ -3635,7 +3635,7 @@ window.addEventListener('beforeunload', () => {
.badge-info {
background: var(--info);
color: white;
color: var(--text-on-info);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
@@ -3782,7 +3782,7 @@ window.addEventListener('beforeunload', () => {
.snapshot-queue-indicator .badge {
background: var(--primary);
color: white;
color: var(--text-on-primary);
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.75rem;