show loading indicators when actions are pending
This commit is contained in:
@@ -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))));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))));
|
||||
|
||||
Reference in New Issue
Block a user