Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
648
status/js/core/service-create.js
Normal file
648
status/js/core/service-create.js
Normal file
@@ -0,0 +1,648 @@
|
||||
// ========== 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;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user