// Config Generator for ntfy (function () { "use strict"; var CONFIG = [ { key: "base-url", env: "NTFY_BASE_URL", section: "basic" }, { key: "listen-http", env: "NTFY_LISTEN_HTTP", section: "basic", def: ":80" }, { key: "behind-proxy", env: "NTFY_BEHIND_PROXY", section: "basic", type: "bool" }, { key: "database-url", env: "NTFY_DATABASE_URL", section: "database" }, { key: "auth-file", env: "NTFY_AUTH_FILE", section: "auth", def: "/var/lib/ntfy/auth.db" }, { key: "auth-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth" }, { key: "enable-login", env: "NTFY_ENABLE_LOGIN", section: "auth", type: "bool" }, { key: "enable-signup", env: "NTFY_ENABLE_SIGNUP", section: "auth", type: "bool" }, { key: "attachment-cache-dir", env: "NTFY_ATTACHMENT_CACHE_DIR", section: "attach", def: "/var/cache/ntfy/attachments" }, { key: "attachment-file-size-limit", env: "NTFY_ATTACHMENT_FILE_SIZE_LIMIT", section: "attach", def: "15M" }, { key: "attachment-total-size-limit", env: "NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT", section: "attach", def: "5G" }, { key: "attachment-expiry-duration", env: "NTFY_ATTACHMENT_EXPIRY_DURATION", section: "attach", def: "3h" }, { key: "cache-file", env: "NTFY_CACHE_FILE", section: "cache", def: "/var/cache/ntfy/cache.db" }, { key: "cache-duration", env: "NTFY_CACHE_DURATION", section: "cache", def: "12h" }, { key: "web-push-public-key", env: "NTFY_WEB_PUSH_PUBLIC_KEY", section: "webpush" }, { key: "web-push-private-key", env: "NTFY_WEB_PUSH_PRIVATE_KEY", section: "webpush" }, { key: "web-push-file", env: "NTFY_WEB_PUSH_FILE", section: "webpush", def: "/var/lib/ntfy/webpush.db" }, { key: "web-push-email-address", env: "NTFY_WEB_PUSH_EMAIL_ADDRESS", section: "webpush" }, { key: "smtp-sender-addr", env: "NTFY_SMTP_SENDER_ADDR", section: "smtp-out" }, { key: "smtp-sender-from", env: "NTFY_SMTP_SENDER_FROM", section: "smtp-out" }, { key: "smtp-sender-user", env: "NTFY_SMTP_SENDER_USER", section: "smtp-out" }, { key: "smtp-sender-pass", env: "NTFY_SMTP_SENDER_PASS", section: "smtp-out" }, { key: "smtp-server-listen", env: "NTFY_SMTP_SERVER_LISTEN", section: "smtp-in", def: ":25" }, { key: "smtp-server-domain", env: "NTFY_SMTP_SERVER_DOMAIN", section: "smtp-in" }, { key: "smtp-server-addr-prefix", env: "NTFY_SMTP_SERVER_ADDR_PREFIX", section: "smtp-in" }, { key: "upstream-base-url", env: "NTFY_UPSTREAM_BASE_URL", section: "upstream" }, ]; var DOCKER_PATH_MAP = { "/var/cache/ntfy/cache.db": "/var/lib/ntfy/cache.db", "/var/cache/ntfy/attachments": "/var/lib/ntfy/attachments", }; // Feature checkbox → nav tab ID var NAV_MAP = { "cg-feat-auth": "cg-nav-auth", "cg-feat-cache": "cg-nav-cache", "cg-feat-attach": "cg-nav-attach", "cg-feat-webpush": "cg-nav-webpush", }; function collectValues() { var values = {}; var modal = document.getElementById("cg-modal"); if (!modal) return values; CONFIG.forEach(function (c) { var el = modal.querySelector('[data-key="' + c.key + '"]'); if (!el) return; // Skip fields in hidden panels (feature not enabled) var panel = el.closest(".cg-panel"); if (panel) { // Panel hidden directly (e.g. PostgreSQL panel when SQLite selected) if (panel.style.display === "none") return; // Panel with a nav tab that is hidden (feature not enabled) if (!panel.classList.contains("active")) { var panelId = panel.id; var navTab = modal.querySelector('[data-panel="' + panelId + '"]'); if (!navTab || navTab.style.display === "none") return; } } // Skip hidden individual fields or sections var ancestor = el.parentElement; while (ancestor && ancestor !== modal) { if (ancestor.style.display === "none") return; ancestor = ancestor.parentElement; } var val; if (c.type === "bool") { if (el.checked) val = "true"; } else { val = el.value.trim(); if (!val) return; } if (val) values[c.key] = val; }); // Provisioned users var userRows = modal.querySelectorAll(".cg-auth-user-row"); var users = []; userRows.forEach(function (row) { var u = row.querySelector('[data-field="username"]'); var p = row.querySelector('[data-field="password"]'); var r = row.querySelector('[data-field="role"]'); if (u && p && u.value.trim() && p.value.trim()) { users.push({ username: u.value.trim(), password: p.value.trim(), role: r ? r.value : "user" }); } }); if (users.length) values["_auth-users"] = users; // Provisioned ACLs var aclRows = modal.querySelectorAll(".cg-auth-acl-row"); var acls = []; aclRows.forEach(function (row) { var u = row.querySelector('[data-field="username"]'); var t = row.querySelector('[data-field="topic"]'); var p = row.querySelector('[data-field="permission"]'); if (u && t && t.value.trim()) { acls.push({ user: u.value.trim(), topic: t.value.trim(), permission: p ? p.value : "read-write" }); } }); if (acls.length) values["_auth-acls"] = acls; // Provisioned tokens var tokenRows = modal.querySelectorAll(".cg-auth-token-row"); var tokens = []; tokenRows.forEach(function (row) { var u = row.querySelector('[data-field="username"]'); var t = row.querySelector('[data-field="token"]'); var l = row.querySelector('[data-field="label"]'); if (u && t && u.value.trim() && t.value.trim()) { tokens.push({ user: u.value.trim(), token: t.value.trim(), label: l ? l.value.trim() : "" }); } }); if (tokens.length) values["_auth-tokens"] = tokens; // UnifiedPush ACL var upYes = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]'); if (upYes && upYes.checked) { if (!values["_auth-acls"]) values["_auth-acls"] = []; values["_auth-acls"].unshift({ user: "*", topic: "up*", permission: "write-only" }); } return values; } function generateServerYml(values) { var lines = []; var sections = { basic: "# Server", database: "# Database", auth: "# Access control", attach: "# Attachments", cache: "# Message cache", webpush: "# Web push", "smtp-out": "# Email notifications (outgoing)", "smtp-in": "# Email publishing (incoming)", upstream: "# Upstream", }; var lastSection = ""; CONFIG.forEach(function (c) { if (!(c.key in values)) return; if (c.section !== lastSection) { if (lines.length) lines.push(""); if (sections[c.section]) lines.push(sections[c.section]); lastSection = c.section; } var val = values[c.key]; if (c.type === "bool") { lines.push(c.key + ": true"); } else { lines.push(c.key + ': "' + val + '"'); } }); if (values["_auth-users"]) { if (lastSection !== "auth") { lines.push(""); lines.push("# Access control"); } lines.push("auth-users:"); values["_auth-users"].forEach(function (u) { lines.push(' - "' + u.username + ":" + u.password + ":" + u.role + '"'); }); } if (values["_auth-acls"]) { if (!values["_auth-users"] && lastSection !== "auth") { lines.push(""); lines.push("# Access control"); } lines.push("auth-access:"); values["_auth-acls"].forEach(function (a) { lines.push(' - "' + (a.user || "*") + ":" + a.topic + ":" + a.permission + '"'); }); } if (values["_auth-tokens"]) { lines.push("auth-tokens:"); values["_auth-tokens"].forEach(function (t) { var entry = t.user + ":" + t.token; if (t.label) entry += ":" + t.label; lines.push(' - "' + entry + '"'); }); } return lines.join("\n"); } function dockerPath(p) { return DOCKER_PATH_MAP[p] || p; } function generateDockerCompose(values) { var lines = []; lines.push("services:"); lines.push(" ntfy:"); lines.push(' image: binwiederhier/ntfy'); lines.push(" command: serve"); lines.push(" environment:"); CONFIG.forEach(function (c) { if (!(c.key in values)) return; var val = values[c.key]; if (c.type === "bool") { val = "true"; } else { val = dockerPath(val); } if (val.indexOf("$") !== -1) { val = val.replace(/\$/g, "$$$$"); lines.push(" # Note: $ is doubled to $$ for docker-compose"); } lines.push(" " + c.env + ": " + val); }); if (values["_auth-users"]) { var usersVal = values["_auth-users"].map(function (u) { return u.username + ":" + u.password + ":" + u.role; }).join(","); usersVal = usersVal.replace(/\$/g, "$$$$"); lines.push(" # Note: $ is doubled to $$ for docker-compose"); lines.push(" NTFY_AUTH_USERS: " + usersVal); } if (values["_auth-acls"]) { var aclsVal = values["_auth-acls"].map(function (a) { return (a.user || "*") + ":" + a.topic + ":" + a.permission; }).join(","); lines.push(" NTFY_AUTH_ACCESS: " + aclsVal); } if (values["_auth-tokens"]) { var tokensVal = values["_auth-tokens"].map(function (t) { var entry = t.user + ":" + t.token; if (t.label) entry += ":" + t.label; return entry; }).join(","); lines.push(" NTFY_AUTH_TOKENS: " + tokensVal); } lines.push(" volumes:"); lines.push(" - ./:/var/lib/ntfy"); lines.push(" ports:"); var listen = values["listen-http"] || ":80"; var port = listen.replace(/.*:/, ""); lines.push(' - "8080:' + port + '"'); return lines.join("\n"); } function generateEnvVars(values) { var lines = []; CONFIG.forEach(function (c) { if (!(c.key in values)) return; var val = values[c.key]; if (c.type === "bool") val = "true"; var q = val.indexOf("$") !== -1 ? "'" : '"'; lines.push(c.env + "=" + q + val + q); }); if (values["_auth-users"]) { var usersStr = values["_auth-users"].map(function (u) { return u.username + ":" + u.password + ":" + u.role; }).join(","); var q = usersStr.indexOf("$") !== -1 ? "'" : '"'; lines.push("NTFY_AUTH_USERS=" + q + usersStr + q); } if (values["_auth-acls"]) { var aclsStr = values["_auth-acls"].map(function (a) { return (a.user || "*") + ":" + a.topic + ":" + a.permission; }).join(","); lines.push('NTFY_AUTH_ACCESS="' + aclsStr + '"'); } if (values["_auth-tokens"]) { var tokensStr = values["_auth-tokens"].map(function (t) { var entry = t.user + ":" + t.token; if (t.label) entry += ":" + t.label; return entry; }).join(","); lines.push('NTFY_AUTH_TOKENS="' + tokensStr + '"'); } return lines.join("\n"); } // Web Push VAPID key generation (P-256 ECDH) function generateVAPIDKeys() { return crypto.subtle.generateKey( { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"] ).then(function (keyPair) { return Promise.all([ crypto.subtle.exportKey("raw", keyPair.publicKey), crypto.subtle.exportKey("pkcs8", keyPair.privateKey) ]); }).then(function (keys) { var pubBytes = new Uint8Array(keys[0]); var privPkcs8 = new Uint8Array(keys[1]); // Extract raw 32-byte private key from PKCS#8 (last 32 bytes of the DER) var privBytes = privPkcs8.slice(privPkcs8.length - 32); return { publicKey: arrayToBase64Url(pubBytes), privateKey: arrayToBase64Url(privBytes) }; }); } function arrayToBase64Url(arr) { var str = ""; for (var i = 0; i < arr.length; i++) { str += String.fromCharCode(arr[i]); } return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function updateOutput() { var modal = document.getElementById("cg-modal"); if (!modal) return; var values = collectValues(); var codeEl = modal.querySelector("#cg-code"); if (!codeEl) return; var activeTab = modal.querySelector(".cg-output-tab.active"); var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml"; var hasValues = false; for (var k in values) { if (values.hasOwnProperty(k)) { hasValues = true; break; } } if (!hasValues) { codeEl.innerHTML = 'Configure options on the left to generate your config...'; return; } var output = ""; if (format === "docker-compose") { output = generateDockerCompose(values); } else if (format === "env-vars") { output = generateEnvVars(values); } else { output = generateServerYml(values); } codeEl.textContent = output; // Validation warnings var warnings = validate(modal, values); var warningsEl = modal.querySelector("#cg-warnings"); if (warningsEl) { if (warnings.length) { warningsEl.innerHTML = warnings.map(function (w) { return '
' + w + '
'; }).join(""); warningsEl.style.display = ""; } else { warningsEl.style.display = "none"; } } } function validate(modal, values) { var warnings = []; var baseUrl = values["base-url"] || ""; // base-url format if (baseUrl) { if (baseUrl.indexOf("http://") !== 0 && baseUrl.indexOf("https://") !== 0) { warnings.push("base-url must start with http:// or https://"); } else { try { var u = new URL(baseUrl); if (u.pathname !== "/" && u.pathname !== "") { warnings.push("base-url must not have a path, ntfy does not support sub-paths"); } } catch (e) { warnings.push("base-url is not a valid URL"); } } } // Web push requires all fields + base-url var wpPublic = values["web-push-public-key"]; var wpPrivate = values["web-push-private-key"]; var wpEmail = values["web-push-email-address"]; var wpFile = values["web-push-file"]; var dbUrl = values["database-url"]; if (wpPublic || wpPrivate || wpEmail) { var missing = []; if (!wpPublic) missing.push("web-push-public-key"); if (!wpPrivate) missing.push("web-push-private-key"); if (!wpFile && !dbUrl) missing.push("web-push-file or database-url"); if (!wpEmail) missing.push("web-push-email-address"); if (!baseUrl) missing.push("base-url"); if (missing.length) { warnings.push("Web push requires: " + missing.join(", ")); } } // SMTP sender requires base-url and smtp-sender-from if (values["smtp-sender-addr"]) { var smtpMissing = []; if (!baseUrl) smtpMissing.push("base-url"); if (!values["smtp-sender-from"]) smtpMissing.push("smtp-sender-from"); if (smtpMissing.length) { warnings.push("Email sending requires: " + smtpMissing.join(", ")); } } // SMTP server requires domain if (values["smtp-server-listen"] && !values["smtp-server-domain"]) { warnings.push("Email publishing requires smtp-server-domain"); } // Attachments require base-url if (values["attachment-cache-dir"] && !baseUrl) { warnings.push("Attachments require base-url to be set"); } // Upstream requires base-url and can't equal it if (values["upstream-base-url"]) { if (!baseUrl) { warnings.push("Upstream server requires base-url to be set"); } else if (baseUrl === values["upstream-base-url"]) { warnings.push("base-url and upstream-base-url cannot be the same"); } } // enable-signup requires enable-login if (values["enable-signup"] && !values["enable-login"]) { warnings.push("Enable signup requires enable-login to also be set"); } return warnings; } function prefill(modal, key, value) { var el = modal.querySelector('[data-key="' + key + '"]'); if (el && !el.value.trim()) el.value = value; } function prefillSelect(modal, key, value) { var el = modal.querySelector('[data-key="' + key + '"]'); if (el) el.value = value; } function updateVisibility() { var modal = document.getElementById("cg-modal"); if (!modal) return; var isPostgres = modal.querySelector('input[name="cg-db-type"][value="postgres"]'); isPostgres = isPostgres && isPostgres.checked; var isPrivate = modal.querySelector('input[name="cg-server-type"][value="private"]'); isPrivate = isPrivate && isPrivate.checked; var isUnifiedPush = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]'); isUnifiedPush = isUnifiedPush && isUnifiedPush.checked; // Auto-check auth when private or UnifiedPush is selected var authCheck = modal.querySelector("#cg-feat-auth"); if (authCheck) { var authForced = isPrivate || isUnifiedPush || isPostgres; if (authForced) authCheck.checked = true; authCheck.disabled = authForced; } var authEnabled = authCheck && authCheck.checked; var cacheEnabled = modal.querySelector("#cg-feat-cache"); cacheEnabled = cacheEnabled && cacheEnabled.checked; var attachEnabled = modal.querySelector("#cg-feat-attach"); attachEnabled = attachEnabled && attachEnabled.checked; var webpushEnabled = modal.querySelector("#cg-feat-webpush"); webpushEnabled = webpushEnabled && webpushEnabled.checked; var smtpOutEnabled = modal.querySelector("#cg-feat-smtp-out"); smtpOutEnabled = smtpOutEnabled && smtpOutEnabled.checked; var smtpInEnabled = modal.querySelector("#cg-feat-smtp-in"); smtpInEnabled = smtpInEnabled && smtpInEnabled.checked; // Show database question only if a DB-dependent feature is selected var needsDb = authEnabled || cacheEnabled || webpushEnabled; var dbStep = modal.querySelector("#cg-wizard-db"); if (dbStep) dbStep.style.display = needsDb ? "" : "none"; // Nav tabs for features for (var featId in NAV_MAP) { var checkbox = modal.querySelector("#" + featId); var navTab = modal.querySelector("#" + NAV_MAP[featId]); if (checkbox && navTab) { navTab.style.display = checkbox.checked ? "" : "none"; } } // Email tab — show if either outgoing or incoming is enabled var navEmail = modal.querySelector("#cg-nav-email"); if (navEmail) navEmail.style.display = (smtpOutEnabled || smtpInEnabled) ? "" : "none"; var emailOutSection = modal.querySelector("#cg-email-out-section"); if (emailOutSection) emailOutSection.style.display = smtpOutEnabled ? "" : "none"; var emailInSection = modal.querySelector("#cg-email-in-section"); if (emailInSection) emailInSection.style.display = smtpInEnabled ? "" : "none"; // If active nav tab got hidden, switch to General var activeNav = modal.querySelector(".cg-nav-tab.active"); if (activeNav && activeNav.style.display === "none") { switchPanel(modal, "cg-panel-general"); } // Hide auth-file and web-push-file if PostgreSQL var authFile = modal.querySelector('[data-key="auth-file"]'); if (authFile) { var authField = authFile.closest(".cg-field"); if (authField) authField.style.display = isPostgres ? "none" : ""; } var wpFile = modal.querySelector('[data-key="web-push-file"]'); if (wpFile) { var wpField = wpFile.closest(".cg-field"); if (wpField) wpField.style.display = isPostgres ? "none" : ""; } // Hide cache-file when PostgreSQL var cacheFileField = modal.querySelector("#cg-cache-file-field"); if (cacheFileField) cacheFileField.style.display = isPostgres ? "none" : ""; // Database tab — show only when PostgreSQL is selected and a DB-dependent feature is on var navDb = modal.querySelector("#cg-nav-database"); if (navDb) navDb.style.display = (needsDb && isPostgres) ? "" : "none"; // iOS question → upstream-base-url var iosYes = modal.querySelector('input[name="cg-ios"][value="yes"]'); var upstreamInput = modal.querySelector('[data-key="upstream-base-url"]'); if (iosYes && upstreamInput) { upstreamInput.value = iosYes.checked ? "https://ntfy.sh" : ""; } // Proxy radio → hidden checkbox var proxyYes = modal.querySelector('input[name="cg-proxy"][value="yes"]'); var proxyCheckbox = modal.querySelector("#cg-behind-proxy"); if (proxyYes && proxyCheckbox) { proxyCheckbox.checked = proxyYes.checked; } // --- Pre-fill defaults --- if (isPostgres) { prefill(modal, "database-url", "postgres://user:pass@host:5432/ntfy"); } if (authEnabled) { if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db"); } if (isPrivate) { prefillSelect(modal, "auth-default-access", "deny-all"); } else { prefillSelect(modal, "auth-default-access", "read-write"); } if (cacheEnabled) { if (!isPostgres) prefill(modal, "cache-file", "/var/cache/ntfy/cache.db"); prefill(modal, "cache-duration", "12h"); } if (attachEnabled) { prefill(modal, "attachment-cache-dir", "/var/cache/ntfy/attachments"); prefill(modal, "attachment-file-size-limit", "15M"); prefill(modal, "attachment-total-size-limit", "5G"); prefill(modal, "attachment-expiry-duration", "3h"); } if (webpushEnabled) { if (!isPostgres) prefill(modal, "web-push-file", "/var/lib/ntfy/webpush.db"); prefill(modal, "web-push-email-address", "admin@example.com"); } if (smtpOutEnabled) { prefill(modal, "smtp-sender-addr", "smtp.example.com:587"); prefill(modal, "smtp-sender-from", "ntfy@example.com"); } if (smtpInEnabled) { prefill(modal, "smtp-server-listen", ":25"); prefill(modal, "smtp-server-domain", "ntfy.example.com"); } } function switchPanel(modal, panelId) { modal.querySelectorAll(".cg-nav-tab").forEach(function (t) { t.classList.remove("active"); }); modal.querySelectorAll(".cg-panel").forEach(function (p) { p.classList.remove("active"); }); var navTab = modal.querySelector('[data-panel="' + panelId + '"]'); var panel = modal.querySelector("#" + panelId); if (navTab) navTab.classList.add("active"); if (panel) panel.classList.add("active"); } function addRepeatableRow(container, type) { var row = document.createElement("div"); row.className = "cg-repeatable-row cg-auth-" + type + "-row"; if (type === "user") { row.innerHTML = '' + '' + '' + ''; } else if (type === "acl") { row.innerHTML = '' + '' + '' + ''; } else if (type === "token") { row.innerHTML = '' + '' + '' + ''; } row.querySelector(".cg-btn-remove").addEventListener("click", function () { row.remove(); updateOutput(); }); row.querySelectorAll("input, select").forEach(function (el) { el.addEventListener("input", updateOutput); }); container.appendChild(row); } function initGenerator() { var modal = document.getElementById("cg-modal"); if (!modal) return; var openBtn = document.getElementById("cg-open-btn"); var closeBtn = document.getElementById("cg-close-btn"); var backdrop = modal.querySelector(".cg-modal-backdrop"); function openModal() { modal.style.display = ""; document.body.style.overflow = "hidden"; updateVisibility(); updateOutput(); } function closeModal() { modal.style.display = "none"; document.body.style.overflow = ""; } if (openBtn) openBtn.addEventListener("click", openModal); if (closeBtn) closeBtn.addEventListener("click", closeModal); if (backdrop) backdrop.addEventListener("click", closeModal); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && modal.style.display !== "none") { closeModal(); } }); // Left nav tab switching modal.querySelectorAll(".cg-nav-tab").forEach(function (tab) { tab.addEventListener("click", function () { var panelId = tab.getAttribute("data-panel"); switchPanel(modal, panelId); }); }); // Output format tab switching modal.querySelectorAll(".cg-output-tab").forEach(function (tab) { tab.addEventListener("click", function () { modal.querySelectorAll(".cg-output-tab").forEach(function (t) { t.classList.remove("active"); }); tab.classList.add("active"); updateOutput(); }); }); // All form inputs trigger update modal.querySelectorAll("input, select").forEach(function (el) { var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input"; el.addEventListener(evt, function () { updateVisibility(); updateOutput(); }); }); // Add buttons for repeatable rows modal.querySelectorAll(".cg-btn-add[data-add-type]").forEach(function (btn) { btn.addEventListener("click", function () { var type = btn.getAttribute("data-add-type"); var container = btn.previousElementSibling; if (!container) container = btn.parentElement.querySelector(".cg-repeatable-container"); addRepeatableRow(container, type); }); }); // Copy button var copyBtn = modal.querySelector("#cg-copy-btn"); if (copyBtn) { var copyIcon = ''; var checkIcon = ''; copyBtn.addEventListener("click", function () { var code = modal.querySelector("#cg-code"); if (code && code.textContent) { navigator.clipboard.writeText(code.textContent).then(function () { copyBtn.innerHTML = checkIcon; copyBtn.style.color = "var(--md-primary-fg-color)"; setTimeout(function () { copyBtn.innerHTML = copyIcon; copyBtn.style.color = ""; }, 2000); }); } }); } // VAPID key generation for web push var vapidKeysGenerated = false; var regenBtn = modal.querySelector("#cg-regen-keys"); function fillVAPIDKeys() { generateVAPIDKeys().then(function (keys) { var pubInput = modal.querySelector('[data-key="web-push-public-key"]'); var privInput = modal.querySelector('[data-key="web-push-private-key"]'); if (pubInput) pubInput.value = keys.publicKey; if (privInput) privInput.value = keys.privateKey; updateOutput(); }); } if (regenBtn) { regenBtn.addEventListener("click", function () { fillVAPIDKeys(); }); } // Auto-generate keys when web push is first enabled var webpushFeat = modal.querySelector("#cg-feat-webpush"); if (webpushFeat) { webpushFeat.addEventListener("change", function () { if (webpushFeat.checked && !vapidKeysGenerated) { vapidKeysGenerated = true; fillVAPIDKeys(); } }); } // Pre-fill base-url if not on ntfy.sh var baseUrlInput = modal.querySelector('[data-key="base-url"]'); if (baseUrlInput && !baseUrlInput.value.trim()) { var host = window.location.hostname; if (host && host.indexOf("ntfy.sh") === -1) { baseUrlInput.value = "https://ntfy.example.com"; } } // Auto-open if URL hash points to config generator if (window.location.hash === "#config-generator") { openModal(); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initGenerator); } else { initGenerator(); } })();