Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
649 lines
25 KiB
JavaScript
649 lines
25 KiB
JavaScript
// ========== SERVICE CREATION ==========
|
|
// Add service modal, local/external service creation flows, and event wiring.
|
|
(function () {
|
|
|
|
// ===== SUBDOMAIN AUTO-DERIVE =====
|
|
|
|
function deriveSubdomain(name) {
|
|
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
}
|
|
|
|
function getSmartSslDefault() {
|
|
return SITE.defaults?.sslType || (SITE.configurationType === 'public' ? 'letsencrypt' : 'caddy-managed');
|
|
}
|
|
|
|
// ===== SERVICE PREVIEW =====
|
|
|
|
function updateServicePreview() {
|
|
const subdomain = document.getElementById('service-subdomain-input').value || 'subdomain';
|
|
const ip = document.getElementById('service-ip-input').value || QUICK_IPS.lan || 'localhost';
|
|
const port = document.getElementById('service-port-input').value || DC.DEFAULTS.SERVICE_PORT;
|
|
const sslType = document.getElementById('ssl-type-select').value;
|
|
const caName = document.getElementById('ca-name-input').value || 'sami-ca';
|
|
const existingCa = document.getElementById('existing-ca-select').value;
|
|
const enableAuth = document.getElementById('enable-auth').checked;
|
|
const enableCors = document.getElementById('enable-cors').checked;
|
|
const customHeaders = document.getElementById('custom-headers-input').value;
|
|
const upstreamPath = document.getElementById('upstream-path-input').value || '/';
|
|
const healthCheck = document.getElementById('health-check-input').value;
|
|
const timeout = document.getElementById('timeout-input').value || 30;
|
|
|
|
const dnsPreview = document.getElementById('dns-preview');
|
|
if (dnsPreview) dnsPreview.textContent = `${buildDomain(subdomain)} \u2192 ${ip}`;
|
|
|
|
const urlPreview = document.getElementById('url-preview');
|
|
if (urlPreview) urlPreview.textContent = `https://${buildDomain(subdomain)}`;
|
|
|
|
const config = {
|
|
subdomain, port, ip, sslType, caName, existingCa,
|
|
enableAuth, enableCors, customHeaders, upstreamPath, healthCheck, timeout
|
|
};
|
|
|
|
const caddyConfig = window.generateCaddyConfig(config);
|
|
const configPreview = document.getElementById('caddy-config-preview');
|
|
if (configPreview) configPreview.value = caddyConfig;
|
|
}
|
|
|
|
// ===== QUICK IP CONFIGURATION =====
|
|
|
|
const QUICK_IPS = {
|
|
localhost: '127.0.0.1',
|
|
lan: '',
|
|
tailscale: ''
|
|
};
|
|
|
|
async function detectNetworkIPs() {
|
|
try {
|
|
const response = await fetch('/api/v1/network/ips', {
|
|
signal: AbortSignal.timeout(2000)
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.lan) QUICK_IPS.lan = data.lan;
|
|
if (data.tailscale) QUICK_IPS.tailscale = data.tailscale;
|
|
}
|
|
} catch (e) {
|
|
// API not available
|
|
}
|
|
|
|
const lanBtn = document.getElementById('quick-ip-lan');
|
|
const tsBtn = document.getElementById('quick-ip-tailscale');
|
|
if (lanBtn) {
|
|
if (QUICK_IPS.lan) {
|
|
lanBtn.dataset.ip = QUICK_IPS.lan;
|
|
lanBtn.textContent = `LAN (${QUICK_IPS.lan})`;
|
|
lanBtn.title = `LAN IP: ${QUICK_IPS.lan}`;
|
|
} else {
|
|
lanBtn.style.display = 'none';
|
|
}
|
|
}
|
|
if (tsBtn) {
|
|
if (QUICK_IPS.tailscale) {
|
|
tsBtn.dataset.ip = QUICK_IPS.tailscale;
|
|
tsBtn.textContent = `Tailscale (${QUICK_IPS.tailscale})`;
|
|
tsBtn.title = `Tailscale IP: ${QUICK_IPS.tailscale}`;
|
|
} else {
|
|
tsBtn.style.display = 'none';
|
|
}
|
|
}
|
|
const ipInput = document.getElementById('service-ip-input');
|
|
if (ipInput && !ipInput.value && QUICK_IPS.lan) ipInput.value = QUICK_IPS.lan;
|
|
}
|
|
|
|
function initQuickIPButtons() {
|
|
document.querySelectorAll('.quick-ip-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const ip = btn.dataset.ip;
|
|
if (ip) {
|
|
document.getElementById('service-ip-input').value = ip;
|
|
document.querySelectorAll('.quick-ip-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
updateServicePreview();
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('service-ip-input')?.addEventListener('input', (e) => {
|
|
const currentIP = e.target.value;
|
|
document.querySelectorAll('.quick-ip-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.ip === currentIP);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== ADD SERVICE MODAL =====
|
|
|
|
async function openAddServiceModal() {
|
|
const modal = document.getElementById('add-service-modal');
|
|
modal.classList.add('show');
|
|
|
|
const modalContent = modal.querySelector('.weather-modal-content');
|
|
if (modalContent) modalContent.scrollTop = 0;
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Set smart SSL default
|
|
const sslSelect = document.getElementById('ssl-type-select');
|
|
if (sslSelect) sslSelect.value = getSmartSslDefault();
|
|
|
|
await detectNetworkIPs();
|
|
|
|
const caddyfilePath = document.getElementById('caddyfile-path-input').value || DC.DEFAULTS.CADDYFILE;
|
|
await window.loadExistingCAs(caddyfilePath);
|
|
|
|
// Check Tailscale status
|
|
const tailscaleStatus = document.getElementById('manual-tailscale-status');
|
|
const tailscaleCheckbox = document.getElementById('manual-tailscale-only');
|
|
try {
|
|
const response = await fetch('/api/v1/tailscale/status');
|
|
const data = await response.json();
|
|
if (data.success && data.installed && data.connected) {
|
|
tailscaleStatus.innerHTML = `
|
|
<span style="color: #4caf50;">\u2713 Connected</span>
|
|
<span style="color: var(--muted); margin-left: 6px;">${data.self?.hostname} (${data.self?.ip})</span>
|
|
`;
|
|
tailscaleCheckbox.disabled = false;
|
|
} else if (data.installed) {
|
|
tailscaleStatus.innerHTML = `<span style="color: #ff9800;">\u26A0 Not connected</span>`;
|
|
tailscaleCheckbox.disabled = true;
|
|
} else {
|
|
tailscaleStatus.innerHTML = `<span style="color: var(--muted);">Not available</span>`;
|
|
tailscaleCheckbox.disabled = true;
|
|
}
|
|
} catch (e) {
|
|
tailscaleStatus.innerHTML = `<span style="color: var(--muted);">Could not check</span>`;
|
|
tailscaleCheckbox.disabled = true;
|
|
}
|
|
tailscaleCheckbox.checked = false;
|
|
|
|
updateServicePreview();
|
|
}
|
|
|
|
// ===== SERVICE TYPE SWITCHING (TAB STYLE) =====
|
|
|
|
function setupServiceTypeSwitching() {
|
|
const localRadio = document.getElementById('service-type-local');
|
|
const externalRadio = document.getElementById('service-type-external');
|
|
const localConfig = document.getElementById('local-service-config');
|
|
const externalConfig = document.getElementById('external-service-config');
|
|
const tabLocal = document.getElementById('tab-local');
|
|
const tabExternal = document.getElementById('tab-external');
|
|
|
|
function switchServiceType() {
|
|
if (localRadio.checked) {
|
|
localConfig.style.display = 'grid';
|
|
externalConfig.style.display = 'none';
|
|
if (tabLocal) { tabLocal.style.background = 'var(--accent)'; tabLocal.style.color = 'var(--bg)'; }
|
|
if (tabExternal) { tabExternal.style.background = 'transparent'; tabExternal.style.color = 'var(--muted)'; }
|
|
} else {
|
|
localConfig.style.display = 'none';
|
|
externalConfig.style.display = 'block';
|
|
if (tabExternal) { tabExternal.style.background = 'var(--accent)'; tabExternal.style.color = 'var(--bg)'; }
|
|
if (tabLocal) { tabLocal.style.background = 'transparent'; tabLocal.style.color = 'var(--muted)'; }
|
|
}
|
|
}
|
|
|
|
localRadio?.addEventListener('change', switchServiceType);
|
|
externalRadio?.addEventListener('change', switchServiceType);
|
|
}
|
|
|
|
// ===== AUTO-DERIVE SUBDOMAIN FROM NAME =====
|
|
|
|
function setupAutoSubdomain() {
|
|
// Local service: name → subdomain + preview
|
|
const nameInput = document.getElementById('service-name-input');
|
|
const subdomainInput = document.getElementById('service-subdomain-input');
|
|
const subdomainPreview = document.getElementById('subdomain-preview');
|
|
let userEditedSubdomain = false;
|
|
|
|
nameInput?.addEventListener('input', () => {
|
|
const derived = deriveSubdomain(nameInput.value);
|
|
if (!userEditedSubdomain && subdomainInput) {
|
|
subdomainInput.value = derived;
|
|
}
|
|
if (subdomainPreview) {
|
|
subdomainPreview.textContent = derived ? `\u2192 ${buildDomain(derived)}` : '';
|
|
}
|
|
updateServicePreview();
|
|
});
|
|
|
|
subdomainInput?.addEventListener('input', () => {
|
|
userEditedSubdomain = subdomainInput.value !== deriveSubdomain(nameInput?.value || '');
|
|
const sub = subdomainInput.value.trim() || deriveSubdomain(nameInput?.value || '');
|
|
if (subdomainPreview) {
|
|
subdomainPreview.textContent = sub ? `\u2192 ${buildDomain(sub)}` : '';
|
|
}
|
|
updateServicePreview();
|
|
});
|
|
|
|
// External service: name → subdomain + preview
|
|
const extNameInput = document.getElementById('external-service-name');
|
|
const extSubdomainInput = document.getElementById('external-service-subdomain');
|
|
const extSubdomainPreview = document.getElementById('external-subdomain-preview');
|
|
const extDomainPreview = document.getElementById('external-domain-preview');
|
|
let userEditedExtSubdomain = false;
|
|
|
|
extNameInput?.addEventListener('input', () => {
|
|
const derived = deriveSubdomain(extNameInput.value);
|
|
if (!userEditedExtSubdomain && extSubdomainInput) {
|
|
extSubdomainInput.value = derived;
|
|
}
|
|
const sub = extSubdomainInput?.value || derived;
|
|
if (extSubdomainPreview) {
|
|
extSubdomainPreview.textContent = sub ? `\u2192 ${buildDomain(sub)}` : '';
|
|
}
|
|
if (extDomainPreview) {
|
|
extDomainPreview.textContent = sub ? buildDomain(sub) : '';
|
|
}
|
|
});
|
|
|
|
extSubdomainInput?.addEventListener('input', () => {
|
|
userEditedExtSubdomain = extSubdomainInput.value !== deriveSubdomain(extNameInput?.value || '');
|
|
const sub = extSubdomainInput.value.trim() || deriveSubdomain(extNameInput?.value || '');
|
|
if (extSubdomainPreview) {
|
|
extSubdomainPreview.textContent = sub ? `\u2192 ${buildDomain(sub)}` : '';
|
|
}
|
|
if (extDomainPreview) {
|
|
extDomainPreview.textContent = sub ? buildDomain(sub) : '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== CREATE EXTERNAL SERVICE =====
|
|
|
|
async function createExternalService() {
|
|
const name = document.getElementById('external-service-name').value.trim();
|
|
const externalUrl = document.getElementById('external-service-url').value.trim();
|
|
const subdomain = (document.getElementById('external-service-subdomain').value.trim() || deriveSubdomain(name)).toLowerCase();
|
|
const logo = document.getElementById('external-service-logo').value.trim();
|
|
const icon = document.getElementById('external-service-icon').value.trim();
|
|
const createDns = document.getElementById('external-create-dns').checked;
|
|
const createCaddy = document.getElementById('external-create-caddy').checked;
|
|
const proxyIp = document.getElementById('external-proxy-ip').value.trim() || SITE.dnsIp || 'localhost';
|
|
const preserveHost = document.getElementById('external-preserve-host').checked;
|
|
const followRedirects = document.getElementById('external-follow-redirects').checked;
|
|
|
|
if (!name || !externalUrl) {
|
|
showNotification('Please fill in Name and External URL', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (!subdomain) {
|
|
showNotification('Could not derive subdomain from name. Please set one in Options.', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (!externalUrl.startsWith('http://') && !externalUrl.startsWith('https://')) {
|
|
showNotification('External URL must start with http:// or https://', 'warning');
|
|
return;
|
|
}
|
|
|
|
const domain = buildDomain(subdomain);
|
|
|
|
try {
|
|
const results = { dns: null, caddy: null, dashboard: false };
|
|
|
|
if (createDns) {
|
|
const adminToken = window.getToken('dns2', 'admin');
|
|
if (adminToken) {
|
|
try {
|
|
const dnsResponse = await secureFetch('/api/v1/dns/record', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
domain: domain,
|
|
ip: proxyIp,
|
|
ttl: DC.DEFAULTS.TTL,
|
|
server: SITE.dnsIp
|
|
})
|
|
});
|
|
const dnsResult = await dnsResponse.json();
|
|
results.dns = dnsResult.success ? 'created' : (dnsResult.error || 'failed');
|
|
} catch (e) {
|
|
results.dns = e.message;
|
|
}
|
|
} else {
|
|
results.dns = 'no admin token (configure in \uD83D\uDD11 Tokens)';
|
|
}
|
|
}
|
|
|
|
if (createCaddy) {
|
|
try {
|
|
const caddyConfig = {
|
|
subdomain: subdomain,
|
|
externalUrl: externalUrl,
|
|
preserveHost: preserveHost,
|
|
followRedirects: followRedirects,
|
|
sslType: 'caddy-managed',
|
|
caddyfilePath: DC.DEFAULTS.CADDYFILE,
|
|
reloadCaddy: true
|
|
};
|
|
|
|
const caddyResponse = await secureFetch('/api/v1/site/external', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(caddyConfig)
|
|
});
|
|
|
|
const caddyResult = await caddyResponse.json();
|
|
results.caddy = caddyResult.success ? 'created' : (caddyResult.error || 'failed');
|
|
} catch (e) {
|
|
results.caddy = e.message;
|
|
}
|
|
}
|
|
|
|
const newService = {
|
|
id: subdomain,
|
|
name: name,
|
|
url: `https://${domain}`,
|
|
externalUrl: externalUrl,
|
|
logo: logo || icon || '\uD83C\uDF10',
|
|
isExternal: true,
|
|
isCustom: true
|
|
};
|
|
|
|
window.APPS.push(newService);
|
|
results.dashboard = true;
|
|
|
|
const defaultServices = ['plex', 'router', 'chat', 'sync', 'torrent', 'radarr', 'sonarr', 'prowlarr', 'portainer', 'requests', 'jellyfin', 'emby'];
|
|
const customServices = window.APPS.filter(app => !defaultServices.includes(app.id));
|
|
safeSet('custom-services', JSON.stringify(customServices));
|
|
|
|
try {
|
|
await secureFetch('/api/v1/services', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(window.APPS)
|
|
});
|
|
} catch (e) {
|
|
console.warn('Failed to save to services.json:', e);
|
|
}
|
|
|
|
window.buildGrid();
|
|
window.refreshAll();
|
|
|
|
closeAddServiceModal();
|
|
|
|
const parts = [`External service "${name}" added!`];
|
|
if (createDns) parts.push(`DNS: ${results.dns === 'created' ? '\u2713' : '\u26A0 ' + results.dns}`);
|
|
if (createCaddy) parts.push(`Caddy: ${results.caddy === 'created' ? '\u2713' : '\u26A0 ' + results.caddy}`);
|
|
parts.push(`Access at: https://${domain}`);
|
|
showNotification(parts.join(' | '), 'success', 6000);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create external service:', error);
|
|
showNotification(`Failed to create external service: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// ===== CLOSE ADD SERVICE MODAL =====
|
|
|
|
function closeAddServiceModal() {
|
|
closeModal('add-service-modal');
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
document.getElementById('service-name-input').value = '';
|
|
document.getElementById('service-subdomain-input').value = '';
|
|
document.getElementById('service-port-input').value = '';
|
|
document.getElementById('service-ip-input').value = QUICK_IPS.lan || '';
|
|
document.getElementById('service-logo-input').value = '';
|
|
document.getElementById('dns-ttl-input').value = DC.DEFAULTS.TTL;
|
|
document.getElementById('ssl-type-select').value = getSmartSslDefault();
|
|
document.getElementById('ca-name-input').value = '';
|
|
document.getElementById('enable-auth').checked = false;
|
|
document.getElementById('enable-cors').checked = false;
|
|
document.getElementById('custom-headers-input').value = '';
|
|
document.getElementById('upstream-path-input').value = '/';
|
|
document.getElementById('health-check-input').value = '';
|
|
document.getElementById('timeout-input').value = '30';
|
|
|
|
// Clear subdomain previews
|
|
const subPrev = document.getElementById('subdomain-preview');
|
|
if (subPrev) subPrev.textContent = '';
|
|
const extSubPrev = document.getElementById('external-subdomain-preview');
|
|
if (extSubPrev) extSubPrev.textContent = '';
|
|
|
|
// Clear external fields
|
|
const extName = document.getElementById('external-service-name');
|
|
if (extName) extName.value = '';
|
|
const extSub = document.getElementById('external-service-subdomain');
|
|
if (extSub) extSub.value = '';
|
|
const extUrl = document.getElementById('external-service-url');
|
|
if (extUrl) extUrl.value = '';
|
|
const extLogo = document.getElementById('external-service-logo');
|
|
if (extLogo) extLogo.value = '';
|
|
const extIcon = document.getElementById('external-service-icon');
|
|
if (extIcon) extIcon.value = '';
|
|
|
|
// Collapse options
|
|
const localOpts = document.getElementById('local-advanced-options');
|
|
if (localOpts) localOpts.removeAttribute('open');
|
|
const extOpts = document.getElementById('external-advanced-options');
|
|
if (extOpts) extOpts.removeAttribute('open');
|
|
|
|
// Reset to local tab
|
|
const localRadio = document.getElementById('service-type-local');
|
|
if (localRadio) localRadio.checked = true;
|
|
const localConfig = document.getElementById('local-service-config');
|
|
const externalConfig = document.getElementById('external-service-config');
|
|
if (localConfig) localConfig.style.display = 'grid';
|
|
if (externalConfig) externalConfig.style.display = 'none';
|
|
const tabLocal = document.getElementById('tab-local');
|
|
const tabExternal = document.getElementById('tab-external');
|
|
if (tabLocal) { tabLocal.style.background = 'var(--accent)'; tabLocal.style.color = 'var(--bg)'; }
|
|
if (tabExternal) { tabExternal.style.background = 'transparent'; tabExternal.style.color = 'var(--muted)'; }
|
|
}
|
|
|
|
// ===== CREATE NEW SERVICE =====
|
|
|
|
async function createNewService() {
|
|
const name = document.getElementById('service-name-input').value.trim();
|
|
const subdomain = (document.getElementById('service-subdomain-input').value.trim() || deriveSubdomain(name)).toLowerCase();
|
|
const port = document.getElementById('service-port-input').value.trim();
|
|
const ip = document.getElementById('service-ip-input').value.trim();
|
|
const logo = document.getElementById('service-logo-input').value.trim();
|
|
const createDns = document.getElementById('create-dns-record').checked;
|
|
const ttl = parseInt(document.getElementById('dns-ttl-input').value) || DC.DEFAULTS.TTL;
|
|
const tailscaleOnly = document.getElementById('manual-tailscale-only')?.checked || false;
|
|
|
|
const sslType = document.getElementById('ssl-type-select')?.value || 'caddy-managed';
|
|
const caName = document.getElementById('ca-name-input')?.value || '';
|
|
const existingCa = document.getElementById('existing-ca-select')?.value || '';
|
|
const enableAuth = document.getElementById('enable-auth')?.checked || false;
|
|
const enableCors = document.getElementById('enable-cors')?.checked || false;
|
|
const customHeaders = document.getElementById('custom-headers-input')?.value || '';
|
|
const upstreamPath = document.getElementById('upstream-path-input')?.value || '/';
|
|
const healthCheck = document.getElementById('health-check-input')?.value || '';
|
|
const timeout = document.getElementById('timeout-input')?.value || 30;
|
|
|
|
const dnsToken = window.getToken('dns2', 'admin');
|
|
|
|
if (!name || !port || !ip) {
|
|
showNotification('Please fill in Name, Port, and IP Address', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (!subdomain) {
|
|
showNotification('Could not derive subdomain from name. Please set one in Options.', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (createDns && !dnsToken) {
|
|
showNotification('DNS Admin token required. Configure it in the Tokens menu first.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const results = { dns: null, caddy: null, dashboard: false };
|
|
|
|
try {
|
|
if (createDns) {
|
|
try {
|
|
await window.createDnsRecord(subdomain, ip, ttl);
|
|
results.dns = 'created';
|
|
} catch (error) {
|
|
console.error('DNS creation failed:', error);
|
|
results.dns = error.message;
|
|
throw new Error(`DNS creation failed: ${error.message}`);
|
|
}
|
|
} else {
|
|
results.dns = 'skipped';
|
|
}
|
|
|
|
const caddyConfig = window.generateCaddyConfig({
|
|
subdomain, port, ip, sslType, caName, existingCa,
|
|
enableAuth, enableCors, customHeaders, upstreamPath, healthCheck, timeout, tailscaleOnly
|
|
});
|
|
|
|
try {
|
|
const caddyResponse = await secureFetch('/api/v1/site', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
domain: buildDomain(subdomain),
|
|
upstream: `${ip}:${port}`,
|
|
config: caddyConfig
|
|
})
|
|
});
|
|
|
|
const caddyResult = await caddyResponse.json();
|
|
if (caddyResult.success) {
|
|
results.caddy = 'added & reloaded';
|
|
} else {
|
|
console.error('Caddy configuration failed:', caddyResult.error);
|
|
results.caddy = caddyResult.error || 'failed';
|
|
throw new Error(`Caddy configuration failed: ${caddyResult.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Caddy API error:', error);
|
|
results.caddy = error.message;
|
|
throw new Error(`Caddy API error: ${error.message}`);
|
|
}
|
|
|
|
const serviceConfig = {
|
|
name, subdomain, port, ip,
|
|
logo: logo || `/assets/${subdomain}.png`,
|
|
tailscaleOnly: tailscaleOnly || false
|
|
};
|
|
|
|
await window.addServiceToConfig(serviceConfig);
|
|
results.dashboard = true;
|
|
|
|
const statusParts = [
|
|
`DNS: ${results.dns === 'created' ? '\u2713' : results.dns === 'skipped' ? '\u25CB' : '\u2717'}`,
|
|
`Caddy: ${results.caddy === 'added & reloaded' ? '\u2713' : '\u2717'}`,
|
|
`Dashboard: ${results.dashboard ? '\u2713' : '\u2717'}`
|
|
];
|
|
showNotification(`Service "${name}" created! ${statusParts.join(' | ')} \u2014 https://${buildDomain(subdomain)}${tailscaleOnly ? ' (Tailscale)' : ''}`, 'success', 6000);
|
|
|
|
closeAddServiceModal();
|
|
|
|
window.buildGrid();
|
|
window.refreshAll();
|
|
|
|
} catch (error) {
|
|
console.error('Error creating service:', error);
|
|
showNotification(`Error creating "${name}": ${error.message}`, 'error', 6000);
|
|
}
|
|
}
|
|
|
|
// ===== EVENT LISTENERS =====
|
|
|
|
document.getElementById('add-service')?.addEventListener('click', openAddServiceModal);
|
|
document.getElementById('add-service-cancel')?.addEventListener('click', closeAddServiceModal);
|
|
document.getElementById('add-service-create')?.addEventListener('click', () => {
|
|
const serviceType = document.querySelector('input[name="service-type"]:checked')?.value;
|
|
if (serviceType === 'external') {
|
|
createExternalService();
|
|
} else {
|
|
createNewService();
|
|
}
|
|
});
|
|
|
|
setupServiceTypeSwitching();
|
|
setupAutoSubdomain();
|
|
initQuickIPButtons();
|
|
|
|
// SSL type change handler
|
|
document.getElementById('ssl-type-select')?.addEventListener('change', (e) => {
|
|
const existingCaConfig = document.getElementById('existing-ca-config');
|
|
const customCaConfig = document.getElementById('custom-ca-config');
|
|
|
|
existingCaConfig.style.display = 'none';
|
|
customCaConfig.style.display = 'none';
|
|
|
|
if (e.target.value === 'existing-ca') {
|
|
existingCaConfig.style.display = 'block';
|
|
} else if (e.target.value === 'custom-ca') {
|
|
customCaConfig.style.display = 'block';
|
|
}
|
|
|
|
updateServicePreview();
|
|
});
|
|
|
|
// Refresh CAs button
|
|
document.getElementById('refresh-cas')?.addEventListener('click', async () => {
|
|
const button = document.getElementById('refresh-cas');
|
|
const originalText = button.textContent;
|
|
button.textContent = '\u231B Loading...';
|
|
button.disabled = true;
|
|
|
|
try {
|
|
const caddyfilePath = document.getElementById('caddyfile-path-input').value || DC.DEFAULTS.CADDYFILE;
|
|
await window.loadExistingCAs(caddyfilePath);
|
|
button.textContent = '\u2705 Refreshed';
|
|
} catch (error) {
|
|
button.textContent = '\u274C Failed';
|
|
console.error('Failed to refresh CAs:', error);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
button.textContent = originalText;
|
|
button.disabled = false;
|
|
}, 2000);
|
|
});
|
|
|
|
// DNS record checkbox handler
|
|
document.getElementById('create-dns-record')?.addEventListener('change', (e) => {
|
|
const dnsConfig = document.getElementById('dns-config');
|
|
dnsConfig.style.display = e.target.checked ? 'block' : 'none';
|
|
});
|
|
|
|
// Real-time preview updates
|
|
['service-subdomain-input', 'service-ip-input', 'service-port-input', 'ca-name-input',
|
|
'existing-ca-select', 'enable-auth', 'enable-cors', 'custom-headers-input', 'upstream-path-input',
|
|
'health-check-input', 'timeout-input'].forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.addEventListener('input', updateServicePreview);
|
|
element.addEventListener('change', updateServicePreview);
|
|
}
|
|
});
|
|
|
|
// ===== CUSTOM SERVICES FROM LOCALSTORAGE =====
|
|
|
|
function loadCustomServices() {
|
|
const customServices = safeGet('custom-services');
|
|
if (customServices) {
|
|
try {
|
|
const services = JSON.parse(customServices);
|
|
services.forEach(service => {
|
|
if (!window.APPS.find(app => app.id === service.id)) {
|
|
window.APPS.push(service);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.warn('Failed to load custom services:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
loadCustomServices();
|
|
|
|
// ===== WINDOW EXPORTS =====
|
|
|
|
window.openAddServiceModal = openAddServiceModal;
|
|
window.closeAddServiceModal = closeAddServiceModal;
|
|
|
|
})();
|