// Holesail UI functions - Create server/client modals and actions // Open create Holesail server modal function openCreateHolesailModal() { const nameEl = document.getElementById('holesail-name'); const portEl = document.getElementById('holesail-port'); const hostEl = document.getElementById('holesail-host'); const keyEl = document.getElementById('holesail-key'); const domainEl = document.getElementById('holesail-domain'); const secureEl = document.getElementById('holesail-secure'); const udpEl = document.getElementById('holesail-udp'); const logEl = document.getElementById('holesail-log'); if (nameEl) nameEl.value = ''; if (portEl) portEl.value = ''; if (hostEl) hostEl.value = '127.0.0.1'; if (keyEl) keyEl.value = ''; if (domainEl) domainEl.value = ''; if (secureEl) secureEl.checked = true; if (udpEl) udpEl.checked = false; if (logEl) logEl.value = '1'; const modal = document.getElementById('createHolesailModal'); if (modal) modal.showModal(); } // Submit create Holesail server async function submitCreateHolesail() { const createBtn = document.getElementById('create-holesail-btn'); if (!createBtn) return; const originalText = createBtn.innerHTML; createBtn.disabled = true; createBtn.innerHTML = 'Creating... '; const opts = { name: document.getElementById('holesail-name')?.value || '', port: parseInt(document.getElementById('holesail-port')?.value || '0'), host: document.getElementById('holesail-host')?.value || '127.0.0.1', key: document.getElementById('holesail-key')?.value || '', domain: document.getElementById('holesail-domain')?.value || '', secure: document.getElementById('holesail-secure')?.checked || false, udp: document.getElementById('holesail-udp')?.checked || false, log: parseInt(document.getElementById('holesail-log')?.value || '1') }; try { const response = await fetch('/api/holesail-create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText); } if (window.showNotification) { window.showNotification('Holesail server created successfully'); } const modal = document.getElementById('createHolesailModal'); if (modal) modal.close(); if (window.genericFetch && window.activeTab === 'host') { window.genericFetch('host-servers', true); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to create Holesail server: ' + err.message, 'error'); } } finally { createBtn.disabled = false; createBtn.innerHTML = originalText; } } // Open create client modal async function openCreateClientModal() { if (!window.domainsData && window.genericFetch) { await window.genericFetch('domains', false); } const select = document.getElementById('client-domain'); if (select && window.domainsData) { // Filter to only show domains where user is owner, excluding internal domains const ownedDomains = window.domainsData.filter(d => d.isOwner === true && d.hash !== 'internal'); if (ownedDomains.length === 0) { select.innerHTML = ''; if (window.showNotification) { window.showNotification('You must own a domain to create a client', 'error'); } } else { select.innerHTML = ownedDomains.map(d => ``).join(''); } } const serviceNameEl = document.getElementById('client-service-name'); const keyEl = document.getElementById('client-key'); const portEl = document.getElementById('client-port'); const protocolEl = document.getElementById('client-protocol'); if (serviceNameEl) serviceNameEl.value = ''; if (keyEl) keyEl.value = ''; if (portEl) portEl.value = ''; if (protocolEl) protocolEl.value = 'tcp'; const modal = document.getElementById('createClientModal'); if (modal) modal.showModal(); } // Submit create client async function submitCreateClient() { const domainEl = document.getElementById('client-domain'); const serviceNameEl = document.getElementById('client-service-name'); const keyEl = document.getElementById('client-key'); const portEl = document.getElementById('client-port'); const protocolEl = document.getElementById('client-protocol'); const domain = domainEl?.value || ''; const serviceName = serviceNameEl?.value || ''; const key = keyEl?.value || ''; const port = parseInt(portEl?.value || '0'); const protocol = protocolEl?.value || 'tcp'; if (!domain || !serviceName || !key || isNaN(port)) { if (window.showNotification) { window.showNotification('All fields are required', 'error'); } return; } // Validate ownership const domainData = window.domainsData?.find(d => d.domain === domain); if (!domainData || !domainData.isOwner) { if (window.showNotification) { window.showNotification('You must own this domain to create a client', 'error'); } return; } const createBtn = document.getElementById('create-client-btn'); if (!createBtn) return; const originalText = createBtn.innerHTML; createBtn.disabled = true; createBtn.innerHTML = 'Creating... '; try { const response = await fetch('/api/holesail-client-create', { 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('Holesail client created successfully'); } const modal = document.getElementById('createClientModal'); if (modal) modal.close(); if (window.genericFetch && window.activeTab === 'host') { window.genericFetch('host-clients', true); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to create Holesail client: ' + err.message, 'error'); } } finally { createBtn.disabled = false; createBtn.innerHTML = originalText; } } // Restart Holesail server async function restartHolesailServer(id) { if (window.showConfirm) { window.showConfirm(`Restart server ${id}?`, async () => { if (window.pendingServerRestarts) { window.pendingServerRestarts.add(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-servers'); } try { const response = await fetch('/api/holesail-restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to restart Holesail server: ' + err.message, 'error'); } if (window.pendingServerRestarts) { window.pendingServerRestarts.delete(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-servers'); } } }); } } // Delete Holesail server function deleteHolesailServer(id) { if (window.showConfirm) { window.showConfirm(`Delete server ${id}?`, async () => { if (window.pendingServerDeletions) { window.pendingServerDeletions.add(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-servers'); } try { const response = await fetch('/api/holesail-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to delete Holesail server: ' + err.message, 'error'); } if (window.pendingServerDeletions) { window.pendingServerDeletions.delete(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-servers'); } } }); } } // Restart Holesail client async function restartHolesailClient(id) { if (window.showConfirm) { window.showConfirm(`Restart client ${id}?`, async () => { if (window.pendingClientRestarts) { window.pendingClientRestarts.add(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-clients'); } try { const response = await fetch('/api/holesail-client-restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to restart Holesail client: ' + err.message, 'error'); } if (window.pendingClientRestarts) { window.pendingClientRestarts.delete(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-clients'); } } }); } } // Delete Holesail client function deleteHolesailClient(id) { if (window.showConfirm) { window.showConfirm(`Delete client ${id}?`, async () => { if (window.pendingClientDeletions) { window.pendingClientDeletions.add(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-clients'); } try { const response = await fetch('/api/holesail-client-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to delete Holesail client: ' + err.message, 'error'); } if (window.pendingClientDeletions) { window.pendingClientDeletions.delete(id); } if (window.activeTab === 'host' && window.genericRenderPaginated) { window.genericRenderPaginated('host-clients'); } } }); } } // Open Holesail log modal function openHolesailLog(id, name) { const titleEl = document.getElementById('holesail-log-title'); if (titleEl) { titleEl.textContent = `Logs for ${name}`; } if (!window.holesailTerm) { if (typeof Terminal !== 'undefined' && typeof FitAddon !== 'undefined') { window.holesailTerm = new Terminal(); window.holesailFitAddon = new FitAddon.FitAddon(); window.holesailTerm.loadAddon(window.holesailFitAddon); } else { console.error('Terminal or FitAddon not loaded'); return; } } const container = document.getElementById('holesail-terminal'); if (container) { container.innerHTML = ''; window.holesailTerm.open(container); window.holesailTerm.reset(); const buffer = window.holesailLogBuffers?.get(id) || []; if (buffer.length === 0) { window.holesailTerm.writeln(''); window.holesailTerm.writeln('No logs available yet.'); window.holesailTerm.writeln('Logs will appear here as they are generated.'); window.holesailTerm.writeln(''); } else { buffer.forEach(line => window.holesailTerm.writeln(line)); } if (window.holesailFitAddon) { window.holesailFitAddon.fit(); } } window.currentOpenHolesailId = id; const modal = document.getElementById('holesailLogModal'); if (modal) { modal.showModal(); modal.addEventListener('close', () => { window.currentOpenHolesailId = null; }, { once: true }); } } // Service Subscription functions let subscriptionDomainsData = []; let subscriptionList = []; let subscribeAllDomains = []; async function openServiceSubscriptionModal() { const modal = document.getElementById('serviceSubscriptionModal'); if (!modal) return; // Load subscriptions try { const subsResponse = await fetch('/api/service-subscriptions'); if (subsResponse.ok) { subscriptionList = await subsResponse.json(); } } catch (err) { console.error('Error loading subscriptions:', err); subscriptionList = []; } // Load subscribe-all domains try { const subscribeAllResponse = await fetch('/api/subscribe-all-domains'); if (subscribeAllResponse.ok) { subscribeAllDomains = await subscribeAllResponse.json(); } } catch (err) { console.error('Error loading subscribe-all domains:', err); subscribeAllDomains = []; } // Load domains with services try { const domainsResponse = await fetch('/api/resolved-domains'); if (domainsResponse.ok) { const allDomains = await domainsResponse.json(); // Fetch services for each domain, but exclude domains the user owns subscriptionDomainsData = []; for (const domain of allDomains) { // Skip domains where the user is the owner (isOwner === true) if (domain.isOwner === true) { continue; } if (domain.hash && domain.hash !== 'none' && domain.hash !== 'internal') { try { const servicesResponse = await fetch(`/api/domain-services?domain=${encodeURIComponent(domain.domain)}`); if (servicesResponse.ok) { const services = await servicesResponse.json(); if (services && services.length > 0) { subscriptionDomainsData.push({ domain: domain.domain, hash: domain.hash, services: services }); } } } catch (err) { console.error(`Error loading services for ${domain.domain}:`, err); } } } } } catch (err) { console.error('Error loading domains:', err); subscriptionDomainsData = []; } renderSubscriptionDomains(); modal.showModal(); } function filterSubscriptionDomains() { renderSubscriptionDomains(); } function renderSubscriptionDomains() { const container = document.getElementById('subscription-domains-list'); if (!container) return; const searchTerm = document.getElementById('subscription-search')?.value.toLowerCase() || ''; const filtered = subscriptionDomainsData.filter(d => d.domain.toLowerCase().includes(searchTerm) || d.services.some(s => s.name.toLowerCase().includes(searchTerm)) ); if (filtered.length === 0) { container.innerHTML = '

No remote domains with services found.

'; return; } container.innerHTML = filtered.map(domainData => { const servicesHtml = domainData.services.map(service => { const isSubscribed = subscriptionList.some(sub => sub.domain === domainData.domain && sub.serviceName === service.name ); return `
${service.name}
Port: ${service.port} | Protocol: ${service.protocol}
`; }).join(''); // Check if subscribeAll is enabled for this domain const isSubscribeAll = subscribeAllDomains.includes(domainData.domain); return `

${domainData.domain}

${isSubscribeAll ? 'Auto-Subscribe Enabled' : ''}
${servicesHtml}
`; }).join(''); } async function subscribeToService(domain, serviceName, key, port, protocol, buttonElement) { if (!buttonElement) return; const originalText = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = 'Subscribing... '; 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); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to subscribe: ' + err.message, 'error'); } } finally { buttonElement.disabled = false; buttonElement.innerHTML = originalText; } } async function unsubscribeFromService(domain, serviceName, buttonElement) { if (!buttonElement) return; const originalText = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = 'Unsubscribing... '; 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); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to unsubscribe: ' + err.message, 'error'); } } finally { buttonElement.disabled = false; buttonElement.innerHTML = originalText; } } async function subscribeAllToDomain(domain, buttonElement) { if (!buttonElement) return; const originalText = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = 'Enabling... '; 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) { for (const service of domainData.services) { if (!subscriptionList.some(sub => sub.domain === domain && sub.serviceName === service.name)) { await subscribeToService(domain, service.name, service.key, service.port, service.protocol); } } } // 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); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to enable auto-subscribe: ' + err.message, 'error'); } } finally { buttonElement.disabled = false; buttonElement.innerHTML = originalText; } } async function unsubscribeAllFromDomain(domain, buttonElement) { if (!buttonElement) return; const originalText = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = 'Disabling... '; 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); } } catch (err) { if (window.showNotification) { window.showNotification('Failed to disable auto-subscribe: ' + err.message, 'error'); } } finally { buttonElement.disabled = false; buttonElement.innerHTML = originalText; } } // Make functions globally accessible window.openCreateHolesailModal = openCreateHolesailModal; window.submitCreateHolesail = submitCreateHolesail; window.openCreateClientModal = openCreateClientModal; window.submitCreateClient = submitCreateClient; window.restartHolesailServer = restartHolesailServer; window.deleteHolesailServer = deleteHolesailServer; window.restartHolesailClient = restartHolesailClient; window.deleteHolesailClient = deleteHolesailClient; window.openHolesailLog = openHolesailLog; window.openServiceSubscriptionModal = openServiceSubscriptionModal; window.filterSubscriptionDomains = filterSubscriptionDomains; window.subscribeToService = subscribeToService; window.unsubscribeFromService = unsubscribeFromService; window.subscribeAllToDomain = subscribeAllToDomain; window.unsubscribeAllFromDomain = unsubscribeAllFromDomain;