show loading indicators when actions are pending

This commit is contained in:
Raven Scott
2025-12-17 20:42:00 -05:00
parent b5eaf9993d
commit a977320bfc
7 changed files with 337 additions and 222 deletions
@@ -56,8 +56,11 @@ async function handleDomainsRoutes(req, res) {
} catch (err) {
// Fallback if plugin system not available
}
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true });
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain))));
+7 -7
View File
@@ -337,7 +337,7 @@
Settings
<button onclick="openInfoModal('settings')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</h2>
<button onclick="saveSettings()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Save Settings</button>
<button id="save-settings-btn" onclick="saveSettings(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Save Settings</button>
</div>
<div class="mb-6">
<input id="search-settings" type="text" placeholder="Search settings..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterSettings()">
@@ -380,7 +380,7 @@
</table>
</div>
<div id="backupsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
<button onclick="createBackup()" class="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover flex-shrink-0">Create Backup</button>
<button id="create-backup-btn" onclick="createBackup(this)" class="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover flex-shrink-0">Create Backup</button>
</div>
<div id="stats" class="tab-content hidden">
@@ -916,7 +916,7 @@
<input id="local-ttl" type="number" placeholder="TTL" value="3600" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('localDnsModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="local-submit" onclick="submitLocalDns()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="local-submit" onclick="submitLocalDns(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
@@ -1061,9 +1061,9 @@
>
Cancel
</button>
<button
<button
id="reset-identity-submit"
onclick="resetIdentity()"
onclick="resetIdentity(this)"
disabled
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
@@ -1101,7 +1101,7 @@
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('subnetModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="subnet-submit" onclick="submitSubnet()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="subnet-submit" onclick="submitSubnet(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
@@ -1110,7 +1110,7 @@
<input id="dns-server-ip" placeholder="DNS Server IP (e.g., 1.1.1.1)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('dnsServerModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="dns-server-submit" onclick="submitDnsServer()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="dns-server-submit" onclick="submitDnsServer(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
</div>
+55 -32
View File
@@ -133,8 +133,8 @@ function loadBackupsBatch() {
<td class="p-3">${backup.version || 'unknown'}</td>
<td class="p-3">
<button onclick="viewBackupDetails('${backup.name}')" class="px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 mr-2">Details</button>
<button onclick="restoreBackup('${backup.name}')" class="px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 mr-2">Restore</button>
<button onclick="deleteBackup('${backup.name}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
<button id="restore-btn-${backup.name.replace(/[^a-zA-Z0-9]/g, '-')}" onclick="restoreBackup('${backup.name}', this)" class="px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 mr-2">Restore</button>
<button id="delete-btn-${backup.name.replace(/[^a-zA-Z0-9]/g, '-')}" onclick="deleteBackup('${backup.name}', this)" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
</td>
`;
container.appendChild(tr);
@@ -201,9 +201,15 @@ function setupBackupsObserver(container) {
// Pagination function removed - using infinite scroll instead
// Create backup
async function createBackup() {
async function createBackup(buttonElement) {
if (!buttonElement) return;
if (window.showConfirm) {
window.showConfirm('Create a new backup? This may take a moment.', async () => {
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Creating... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/backups/create', {
method: 'POST'
@@ -217,47 +223,61 @@ async function createBackup() {
} catch (err) {
console.error('Failed to create backup:', err);
if (window.showNotification) window.showNotification('Failed to create backup: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
});
}
}
// Restore backup
async function restoreBackup(backupName) {
if (window.showConfirm) {
window.showConfirm(`Restore from backup "${backupName}"? This will create a backup of current state first, then restore.`, async () => {
try {
const response = await fetch('/api/backups/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ backupName })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to restore backup');
}
if (window.showNotification) window.showNotification('Backup restored successfully. System may need to refresh.', 'success');
await renderBackups();
// Optionally reload after restore
setTimeout(() => {
if (window.showConfirm) {
window.showConfirm('Reload page to see restored data?', () => {
location.reload();
});
}
}, 2000);
} catch (err) {
console.error('Failed to restore backup:', err);
if (window.showNotification) window.showNotification('Failed to restore backup: ' + err.message, 'error');
}
async function restoreBackup(backupName, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Restoring... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/backups/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ backupName })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to restore backup');
}
if (window.showNotification) window.showNotification('Backup restored successfully. System may need to refresh.', 'success');
await renderBackups();
// Optionally reload after restore
setTimeout(() => {
if (window.showConfirm) {
window.showConfirm('Reload page to see restored data?', () => {
location.reload();
});
}
}, 2000);
} catch (err) {
console.error('Failed to restore backup:', err);
if (window.showNotification) window.showNotification('Failed to restore backup: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
// Delete backup
async function deleteBackup(backupName) {
async function deleteBackup(backupName, buttonElement) {
if (!buttonElement) return;
if (window.showConfirm) {
window.showConfirm(`Delete backup "${backupName}"? This action cannot be undone.`, async () => {
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Deleting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch(`/api/backups/${encodeURIComponent(backupName)}`, {
method: 'DELETE'
@@ -267,12 +287,15 @@ async function deleteBackup(backupName) {
throw new Error(error.error || 'Failed to delete backup');
}
if (window.showNotification) window.showNotification('Backup deleted successfully');
// Re-fetch and re-render backups
await renderBackups();
} catch (err) {
console.error('Failed to delete backup:', err);
if (window.showNotification) window.showNotification('Failed to delete backup: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
});
}
+66 -28
View File
@@ -482,8 +482,9 @@ function renderSubscriptionDomains() {
<div class="font-semibold">${service.name}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Port: ${service.port} | Protocol: ${service.protocol}</div>
</div>
<button
onclick="${isSubscribed ? `unsubscribeFromService('${domainData.domain}', '${service.name}')` : `subscribeToService('${domainData.domain}', '${service.name}', '${service.key}', ${service.port}, '${service.protocol}')`}"
<button
id="btn-${domainData.domain.replace(/[^a-zA-Z0-9]/g, '-')}-${service.name.replace(/[^a-zA-Z0-9]/g, '-')}"
onclick="${isSubscribed ? `unsubscribeFromService('${domainData.domain}', '${service.name}', this)` : `subscribeToService('${domainData.domain}', '${service.name}', '${service.key}', ${service.port}, '${service.protocol}', this)`}"
class="px-4 py-2 ${isSubscribed ? 'bg-red-500 hover:bg-red-600' : 'bg-primary hover:bg-primary-hover'} text-white rounded"
>
${isSubscribed ? 'Unsubscribe' : 'Subscribe'}
@@ -502,8 +503,9 @@ function renderSubscriptionDomains() {
<h4 class="text-lg font-semibold">${domainData.domain}</h4>
${isSubscribeAll ? '<span class="px-2 py-1 text-xs bg-green-500 text-white rounded">Auto-Subscribe Enabled</span>' : ''}
</div>
<button
onclick="${isSubscribeAll ? `unsubscribeAllFromDomain('${domainData.domain}')` : `subscribeAllToDomain('${domainData.domain}')`}"
<button
id="btn-subscribe-all-${domainData.domain.replace(/[^a-zA-Z0-9]/g, '-')}"
onclick="${isSubscribeAll ? `unsubscribeAllFromDomain('${domainData.domain}', this)` : `subscribeAllToDomain('${domainData.domain}', this)`}"
class="px-3 py-1 text-sm ${isSubscribeAll ? 'bg-red-500 hover:bg-red-600' : 'bg-green-600 hover:bg-green-700'} text-white rounded"
>
${isSubscribeAll ? 'Disable Auto-Subscribe' : 'Enable Auto-Subscribe'}
@@ -517,30 +519,36 @@ function renderSubscriptionDomains() {
}).join('');
}
async function subscribeToService(domain, serviceName, key, port, protocol) {
async function subscribeToService(domain, serviceName, key, port, protocol, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Subscribing... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/service-subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain, serviceName, key, port, protocol })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
if (window.showNotification) {
window.showNotification(`Subscribed to ${domain}/${serviceName}`);
}
// Reload subscriptions and re-render
const subsResponse = await fetch('/api/service-subscriptions');
if (subsResponse.ok) {
subscriptionList = await subsResponse.json();
}
renderSubscriptionDomains();
if (window.genericFetch && window.activeTab === 'host') {
window.genericFetch('host-clients', true);
}
@@ -548,33 +556,42 @@ async function subscribeToService(domain, serviceName, key, port, protocol) {
if (window.showNotification) {
window.showNotification('Failed to subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function unsubscribeFromService(domain, serviceName) {
async function unsubscribeFromService(domain, serviceName, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Unsubscribing... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/service-unsubscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain, serviceName })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
if (window.showNotification) {
window.showNotification(`Unsubscribed from ${domain}/${serviceName}`);
}
// Reload subscriptions and re-render
const subsResponse = await fetch('/api/service-subscriptions');
if (subsResponse.ok) {
subscriptionList = await subsResponse.json();
}
renderSubscriptionDomains();
if (window.genericFetch && window.activeTab === 'host') {
window.genericFetch('host-clients', true);
}
@@ -582,32 +599,41 @@ async function unsubscribeFromService(domain, serviceName) {
if (window.showNotification) {
window.showNotification('Failed to unsubscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function subscribeAllToDomain(domain) {
async function subscribeAllToDomain(domain, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Enabling... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/subscribe-all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
if (window.showNotification) {
window.showNotification(`Enabled auto-subscribe for ${domain}`);
}
// Reload subscribe-all domains and re-render
const subscribeAllResponse = await fetch('/api/subscribe-all-domains');
if (subscribeAllResponse.ok) {
subscribeAllDomains = await subscribeAllResponse.json();
}
// Also subscribe to existing services
const domainData = subscriptionDomainsData.find(d => d.domain === domain);
if (domainData && domainData.services) {
@@ -617,15 +643,15 @@ async function subscribeAllToDomain(domain) {
}
}
}
// Reload subscriptions
const subsResponse = await fetch('/api/service-subscriptions');
if (subsResponse.ok) {
subscriptionList = await subsResponse.json();
}
renderSubscriptionDomains();
if (window.genericFetch && window.activeTab === 'host') {
window.genericFetch('host-clients', true);
}
@@ -633,34 +659,43 @@ async function subscribeAllToDomain(domain) {
if (window.showNotification) {
window.showNotification('Failed to enable auto-subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function unsubscribeAllFromDomain(domain) {
async function unsubscribeAllFromDomain(domain, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Disabling... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/unsubscribe-all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
if (window.showNotification) {
window.showNotification(`Disabled auto-subscribe for ${domain}`);
}
// Reload subscribe-all domains and re-render
const subscribeAllResponse = await fetch('/api/subscribe-all-domains');
if (subscribeAllResponse.ok) {
subscribeAllDomains = await subscribeAllResponse.json();
}
renderSubscriptionDomains();
if (window.genericFetch && window.activeTab === 'host') {
window.genericFetch('host-clients', true);
}
@@ -668,6 +703,9 @@ async function unsubscribeAllFromDomain(domain) {
if (window.showNotification) {
window.showNotification('Failed to disable auto-subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
+53 -44
View File
@@ -122,53 +122,59 @@ function updateLocalForm(rec = null) {
}
}
async function submitLocalDns() {
const nameEl = document.getElementById('local-name');
const typeEl = document.getElementById('local-type');
const ttlEl = document.getElementById('local-ttl');
if (!nameEl || !typeEl || !ttlEl) return;
const name = nameEl.value;
const type = typeEl.value;
const ttl = parseInt(ttlEl.value) || 3600;
let record = { name, type, ttl, class: 'IN' };
async function submitLocalDns(buttonElement) {
if (!buttonElement) return;
switch (type) {
case 'MX':
const prefEl = document.getElementById('local-preference');
const exchEl = document.getElementById('local-exchange');
record.preference = parseInt(prefEl?.value) || 10;
record.exchange = exchEl?.value || '';
break;
case 'SRV':
record.priority = parseInt(document.getElementById('local-priority')?.value) || 0;
record.weight = parseInt(document.getElementById('local-weight')?.value) || 0;
record.port = parseInt(document.getElementById('local-port')?.value) || 0;
record.target = document.getElementById('local-target')?.value || '';
break;
case 'SOA':
record.mname = document.getElementById('local-mname')?.value || '';
record.rname = document.getElementById('local-rname')?.value || '';
record.serial = parseInt(document.getElementById('local-serial')?.value) || 0;
record.refresh = parseInt(document.getElementById('local-refresh')?.value) || 0;
record.retry = parseInt(document.getElementById('local-retry')?.value) || 0;
record.expire = parseInt(document.getElementById('local-expire')?.value) || 0;
record.minimum = parseInt(document.getElementById('local-minimum')?.value) || 0;
break;
case 'CAA':
record.flags = parseInt(document.getElementById('local-flags')?.value) || 0;
record.tag = document.getElementById('local-tag')?.value || '';
record.value = document.getElementById('local-value')?.value || '';
break;
default:
record.data = document.getElementById('local-data')?.value || '';
break;
}
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
const isEdit = window.editLocalIndex >= 0;
const url = isEdit ? '/api/update-local-dns' : '/api/add-local-dns';
const body = isEdit ? JSON.stringify({ index: window.editLocalIndex, record }) : JSON.stringify(record);
try {
const nameEl = document.getElementById('local-name');
const typeEl = document.getElementById('local-type');
const ttlEl = document.getElementById('local-ttl');
if (!nameEl || !typeEl || !ttlEl) return;
const name = nameEl.value;
const type = typeEl.value;
const ttl = parseInt(ttlEl.value) || 3600;
let record = { name, type, ttl, class: 'IN' };
switch (type) {
case 'MX':
const prefEl = document.getElementById('local-preference');
const exchEl = document.getElementById('local-exchange');
record.preference = parseInt(prefEl?.value) || 10;
record.exchange = exchEl?.value || '';
break;
case 'SRV':
record.priority = parseInt(document.getElementById('local-priority')?.value) || 0;
record.weight = parseInt(document.getElementById('local-weight')?.value) || 0;
record.port = parseInt(document.getElementById('local-port')?.value) || 0;
record.target = document.getElementById('local-target')?.value || '';
break;
case 'SOA':
record.mname = document.getElementById('local-mname')?.value || '';
record.rname = document.getElementById('local-rname')?.value || '';
record.serial = parseInt(document.getElementById('local-serial')?.value) || 0;
record.refresh = parseInt(document.getElementById('local-refresh')?.value) || 0;
record.retry = parseInt(document.getElementById('local-retry')?.value) || 0;
record.expire = parseInt(document.getElementById('local-expire')?.value) || 0;
record.minimum = parseInt(document.getElementById('local-minimum')?.value) || 0;
break;
case 'CAA':
record.flags = parseInt(document.getElementById('local-flags')?.value) || 0;
record.tag = document.getElementById('local-tag')?.value || '';
record.value = document.getElementById('local-value')?.value || '';
break;
default:
record.data = document.getElementById('local-data')?.value || '';
break;
}
const isEdit = window.editLocalIndex >= 0;
const url = isEdit ? '/api/update-local-dns' : '/api/add-local-dns';
const body = isEdit ? JSON.stringify({ index: window.editLocalIndex, record }) : JSON.stringify(record);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -184,6 +190,9 @@ async function submitLocalDns() {
} catch (err) {
console.error('Failed to submit local DNS record:', err);
if (window.showNotification) window.showNotification('Failed to submit record: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
+148 -109
View File
@@ -583,24 +583,29 @@ async function recheckPeers() {
window.recheckPeers = recheckPeers;
// Save settings
async function saveSettings() {
const container = document.getElementById('settingsContainer');
if (!container) return;
const settings = {};
// Get all inputs, selects, and checkboxes
const inputs = container.querySelectorAll('input[data-key], select[data-key]');
inputs.forEach(input => {
const key = input.dataset.key;
if (input.type === 'checkbox') {
settings[key] = input.checked ? 'true' : 'false';
} else {
settings[key] = input.value;
}
});
async function saveSettings(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const container = document.getElementById('settingsContainer');
if (!container) return;
const settings = {};
// Get all inputs, selects, and checkboxes
const inputs = container.querySelectorAll('input[data-key], select[data-key]');
inputs.forEach(input => {
const key = input.dataset.key;
if (input.type === 'checkbox') {
settings[key] = input.checked ? 'true' : 'false';
} else {
settings[key] = input.value;
}
});
const response = await fetch('/api/update-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -638,6 +643,9 @@ async function saveSettings() {
if (window.showNotification) {
window.showNotification('Failed to save settings: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
@@ -807,59 +815,70 @@ function deleteSubnet(index) {
}
// Submit subnet (add or update)
async function submitSubnet() {
const nameEl = document.getElementById('subnet-name');
const baseEl = document.getElementById('subnet-base');
const cidrEl = document.getElementById('subnet-cidr');
const startIndexEl = document.getElementById('subnet-startIndex');
const name = nameEl?.value.trim() || '';
const base = baseEl?.value.trim() || '';
const cidr = parseInt(cidrEl?.value || '0', 10);
const startIndex = parseInt(startIndexEl?.value || '0', 10);
// Validation
if (!name) {
if (window.showNotification) {
window.showNotification('Subnet name is required', 'error');
async function submitSubnet(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const nameEl = document.getElementById('subnet-name');
const baseEl = document.getElementById('subnet-base');
const cidrEl = document.getElementById('subnet-cidr');
const startIndexEl = document.getElementById('subnet-startIndex');
const name = nameEl?.value.trim() || '';
const base = baseEl?.value.trim() || '';
const cidr = parseInt(cidrEl?.value || '0', 10);
const startIndex = parseInt(startIndexEl?.value || '0', 10);
// Validation
if (!name) {
if (window.showNotification) {
window.showNotification('Subnet name is required', 'error');
}
return;
}
return;
}
if (!base || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(base)) {
if (window.showNotification) {
window.showNotification('Valid base IP address is required (e.g., 192.168.3.0)', 'error');
if (!base || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(base)) {
if (window.showNotification) {
window.showNotification('Valid base IP address is required (e.g., 192.168.3.0)', 'error');
}
return;
}
return;
}
if (isNaN(cidr) || cidr < 1 || cidr > 32) {
if (window.showNotification) {
window.showNotification('CIDR must be between 1 and 32', 'error');
if (isNaN(cidr) || cidr < 1 || cidr > 32) {
if (window.showNotification) {
window.showNotification('CIDR must be between 1 and 32', 'error');
}
return;
}
return;
}
if (isNaN(startIndex) || startIndex < 1 || startIndex > 254) {
if (window.showNotification) {
window.showNotification('Start index must be between 1 and 254', 'error');
if (isNaN(startIndex) || startIndex < 1 || startIndex > 254) {
if (window.showNotification) {
window.showNotification('Start index must be between 1 and 254', 'error');
}
return;
}
return;
const subnet = { name, base, cidr, startIndex };
let newSubnets;
if (window.editingSubnetIndex !== null) {
// Update existing
newSubnets = [...window.currentSubnets];
newSubnets[window.editingSubnetIndex] = subnet;
} else {
// Add new
newSubnets = [...window.currentSubnets, subnet];
}
await saveSubnets(newSubnets);
const modal = document.getElementById('subnetModal');
if (modal) modal.close();
window.editingSubnetIndex = null;
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
const subnet = { name, base, cidr, startIndex };
let newSubnets;
if (window.editingSubnetIndex !== null) {
// Update existing
newSubnets = [...window.currentSubnets];
newSubnets[window.editingSubnetIndex] = subnet;
} else {
// Add new
newSubnets = [...window.currentSubnets, subnet];
}
await saveSubnets(newSubnets);
const modal = document.getElementById('subnetModal');
if (modal) modal.close();
window.editingSubnetIndex = null;
}
// Make functions globally accessible
@@ -904,7 +923,13 @@ function onResetIdentityConfirmChange() {
}
// Reset identity (delete keypair)
async function resetIdentity() {
async function resetIdentity(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Resetting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/reset-identity', {
method: 'POST',
@@ -947,6 +972,9 @@ async function resetIdentity() {
if (window.showNotification) {
window.showNotification('Failed to reset identity: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
@@ -1109,58 +1137,69 @@ function deleteDnsServer(index) {
}
// Submit DNS server (add or update)
async function submitDnsServer() {
const ipEl = document.getElementById('dns-server-ip');
const ip = ipEl?.value.trim() || '';
// Validation
if (!ip) {
if (window.showNotification) {
window.showNotification('DNS server IP address is required', 'error');
}
return;
}
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
if (window.showNotification) {
window.showNotification('Invalid IP address format. Please use IPv4 format (e.g., 1.1.1.1)', 'error');
}
return;
}
// Check for duplicates
if (window.editingDnsServerIndex === null) {
if (currentDnsServers.includes(ip)) {
async function submitDnsServer(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const ipEl = document.getElementById('dns-server-ip');
const ip = ipEl?.value.trim() || '';
// Validation
if (!ip) {
if (window.showNotification) {
window.showNotification('This DNS server is already in the list', 'error');
window.showNotification('DNS server IP address is required', 'error');
}
return;
}
} else {
// When editing, check if the new IP conflicts with other servers
const otherServers = currentDnsServers.filter((_, i) => i !== window.editingDnsServerIndex);
if (otherServers.includes(ip)) {
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
if (window.showNotification) {
window.showNotification('This DNS server is already in the list', 'error');
window.showNotification('Invalid IP address format. Please use IPv4 format (e.g., 1.1.1.1)', 'error');
}
return;
}
// Check for duplicates
if (window.editingDnsServerIndex === null) {
if (currentDnsServers.includes(ip)) {
if (window.showNotification) {
window.showNotification('This DNS server is already in the list', 'error');
}
return;
}
} else {
// When editing, check if the new IP conflicts with other servers
const otherServers = currentDnsServers.filter((_, i) => i !== window.editingDnsServerIndex);
if (otherServers.includes(ip)) {
if (window.showNotification) {
window.showNotification('This DNS server is already in the list', 'error');
}
return;
}
}
let newServers;
if (window.editingDnsServerIndex !== null) {
// Update existing
newServers = [...currentDnsServers];
newServers[window.editingDnsServerIndex] = ip;
} else {
// Add new
newServers = [...currentDnsServers, ip];
}
await saveDnsServers(newServers);
const modal = document.getElementById('dnsServerModal');
if (modal) modal.close();
window.editingDnsServerIndex = null;
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
let newServers;
if (window.editingDnsServerIndex !== null) {
// Update existing
newServers = [...currentDnsServers];
newServers[window.editingDnsServerIndex] = ip;
} else {
// Add new
newServers = [...currentDnsServers, ip];
}
await saveDnsServers(newServers);
const modal = document.getElementById('dnsServerModal');
if (modal) modal.close();
window.editingDnsServerIndex = null;
}
window.onResetIdentityConfirmChange = onResetIdentityConfirmChange;
+4 -1
View File
@@ -48,8 +48,11 @@ async function handleDomainsRoutes(req, res) {
} catch (err) {
// Fallback if plugin system not available
}
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
resolved.push({ domain: d, hash: 'internal', isLocal: true });
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain))));