// 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: "cache-file", env: "NTFY_CACHE_FILE", section: "database", def: "/var/cache/ntfy/cache.db" }, { 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-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" }, { key: "enable-metrics", env: "NTFY_ENABLE_METRICS", section: "metrics", type: "bool" }, ]; var DOCKER_PATH_MAP = { "/var/cache/ntfy/cache.db": "/var/lib/ntfy/cache.db", "/var/cache/ntfy/attachments": "/var/lib/ntfy/attachments", }; function collectValues() { var values = {}; var gen = document.getElementById("config-generator-app"); if (!gen) return values; var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]'); isPostgres = isPostgres && isPostgres.checked; CONFIG.forEach(function (c) { var el = gen.querySelector('[data-key="' + c.key + '"]'); if (!el) return; // Skip hidden fields var container = el.closest(".cg-conditional"); if (container && !container.classList.contains("visible")) return; var field = el.closest(".cg-field"); if (field && field.style.display === "none") return; 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 = gen.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 = gen.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 = gen.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; 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", metrics: "# Monitoring", }; 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 + '"'); } }); // Auth users 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(" - username: " + u.username); lines.push(" password: " + u.password); lines.push(" role: " + u.role); }); } // Auth ACLs if (values["_auth-acls"]) { lines.push("auth-default-access:"); lines.push(" everyone:"); values["_auth-acls"].forEach(function (a) { // This uses the topic-level provisioning format }); // Actually use provisioned format lines.pop(); lines.pop(); lines.push("auth-access:"); values["_auth-acls"].forEach(function (a) { lines.push(" - user: " + (a.user || "*")); lines.push(" topic: " + a.topic); lines.push(" permission: " + a.permission); }); } // Auth tokens if (values["_auth-tokens"]) { lines.push("auth-tokens:"); values["_auth-tokens"].forEach(function (t) { lines.push(" - user: " + t.user); lines.push(" token: " + t.token); if (t.label) lines.push(" label: " + t.label); }); } 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 { // Adjust paths for Docker val = dockerPath(val); } // Double $ in bcrypt hashes if (val.indexOf("$") !== -1) { val = val.replace(/\$/g, "$$$$"); lines.push(" # Note: $ is doubled to $$ for docker-compose"); } lines.push(" " + c.env + ": " + val); }); // Auth users in Docker if (values["_auth-users"]) { lines.push(" # Note: $ is doubled to $$ for docker-compose"); values["_auth-users"].forEach(function (u, i) { var pw = u.password.replace(/\$/g, "$$$$"); lines.push(" NTFY_AUTH_USERS_" + i + "_USERNAME: " + u.username); lines.push(" NTFY_AUTH_USERS_" + i + "_PASSWORD: " + pw); lines.push(" NTFY_AUTH_USERS_" + i + "_ROLE: " + u.role); }); } // Auth ACLs in Docker if (values["_auth-acls"]) { values["_auth-acls"].forEach(function (a, i) { lines.push(" NTFY_AUTH_ACCESS_" + i + "_USER: " + (a.user || "*")); lines.push(" NTFY_AUTH_ACCESS_" + i + "_TOPIC: " + a.topic); lines.push(" NTFY_AUTH_ACCESS_" + i + "_PERMISSION: " + a.permission); }); } // Auth tokens in Docker if (values["_auth-tokens"]) { values["_auth-tokens"].forEach(function (t, i) { var tok = t.token.replace(/\$/g, "$$$$"); lines.push(" NTFY_AUTH_TOKENS_" + i + "_USER: " + t.user); lines.push(" NTFY_AUTH_TOKENS_" + i + "_TOKEN: " + tok); if (t.label) lines.push(" NTFY_AUTH_TOKENS_" + i + "_LABEL: " + t.label); }); } 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"; // Use single quotes if value contains $ var q = val.indexOf("$") !== -1 ? "'" : '"'; lines.push("export " + c.env + "=" + q + val + q); }); if (values["_auth-users"]) { values["_auth-users"].forEach(function (u, i) { var q = u.password.indexOf("$") !== -1 ? "'" : '"'; lines.push("export NTFY_AUTH_USERS_" + i + '_USERNAME="' + u.username + '"'); lines.push("export NTFY_AUTH_USERS_" + i + "_PASSWORD=" + q + u.password + q); lines.push("export NTFY_AUTH_USERS_" + i + '_ROLE="' + u.role + '"'); }); } if (values["_auth-acls"]) { values["_auth-acls"].forEach(function (a, i) { lines.push("export NTFY_AUTH_ACCESS_" + i + '_USER="' + (a.user || "*") + '"'); lines.push("export NTFY_AUTH_ACCESS_" + i + '_TOPIC="' + a.topic + '"'); lines.push("export NTFY_AUTH_ACCESS_" + i + '_PERMISSION="' + a.permission + '"'); }); } if (values["_auth-tokens"]) { values["_auth-tokens"].forEach(function (t, i) { var q = t.token.indexOf("$") !== -1 ? "'" : '"'; lines.push("export NTFY_AUTH_TOKENS_" + i + '_USER="' + t.user + '"'); lines.push("export NTFY_AUTH_TOKENS_" + i + "_TOKEN=" + q + t.token + q); if (t.label) lines.push("export NTFY_AUTH_TOKENS_" + i + '_LABEL="' + t.label + '"'); }); } return lines.join("\n"); } function updateOutput() { var gen = document.getElementById("config-generator-app"); if (!gen) return; var values = collectValues(); var codeEl = gen.querySelector("#cg-code"); if (!codeEl) return; var activeTab = gen.querySelector(".cg-tab.active"); var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml"; var output = ""; 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; } if (format === "server-yml") { output = generateServerYml(values); } else if (format === "docker-compose") { output = generateDockerCompose(values); } else { output = generateEnvVars(values); } codeEl.textContent = output; } function updateConditionalVisibility() { var gen = document.getElementById("config-generator-app"); if (!gen) return; var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]'); isPostgres = isPostgres && isPostgres.checked; // Database fields var sqliteFields = gen.querySelector("#cg-sqlite-fields"); var pgFields = gen.querySelector("#cg-postgres-fields"); if (sqliteFields) sqliteFields.style.display = isPostgres ? "none" : "block"; if (pgFields) pgFields.style.display = isPostgres ? "block" : "none"; // Hide auth-file and web-push-file if PostgreSQL var authFile = gen.querySelector('[data-key="auth-file"]'); if (authFile) { var authField = authFile.closest(".cg-field"); if (authField) authField.style.display = isPostgres ? "none" : ""; } var wpFile = gen.querySelector('[data-key="web-push-file"]'); if (wpFile) { var wpField = wpFile.closest(".cg-field"); if (wpField) wpField.style.display = isPostgres ? "none" : ""; } // Conditional sections (checkboxes that show/hide detail fields) var toggles = gen.querySelectorAll("[data-toggle]"); toggles.forEach(function (toggle) { var target = gen.querySelector("#" + toggle.getAttribute("data-toggle")); if (target) { if (toggle.checked) { target.classList.add("visible"); } else { target.classList.remove("visible"); } } }); } 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 gen = document.getElementById("config-generator-app"); if (!gen) return; // Accordion toggle gen.querySelectorAll(".cg-section-header").forEach(function (header) { header.addEventListener("click", function () { header.parentElement.classList.toggle("open"); }); }); // Open first section by default var first = gen.querySelector(".cg-section"); if (first) first.classList.add("open"); // Tab switching gen.querySelectorAll(".cg-tab").forEach(function (tab) { tab.addEventListener("click", function () { gen.querySelectorAll(".cg-tab").forEach(function (t) { t.classList.remove("active"); }); tab.classList.add("active"); updateOutput(); }); }); // All form inputs trigger update gen.querySelectorAll("input, select").forEach(function (el) { var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input"; el.addEventListener(evt, function () { updateConditionalVisibility(); updateOutput(); }); }); // Conditional toggles gen.querySelectorAll("[data-toggle]").forEach(function (toggle) { toggle.addEventListener("change", function () { updateConditionalVisibility(); updateOutput(); }); }); // Database radio gen.querySelectorAll('input[name="cg-db-type"]').forEach(function (r) { r.addEventListener("change", function () { updateConditionalVisibility(); updateOutput(); }); }); // Add buttons for repeatable rows gen.querySelectorAll(".cg-btn-add").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 = gen.querySelector("#cg-copy-btn"); if (copyBtn) { copyBtn.addEventListener("click", function () { var code = gen.querySelector("#cg-code"); if (code && code.textContent) { navigator.clipboard.writeText(code.textContent).then(function () { copyBtn.textContent = "Copied!"; setTimeout(function () { copyBtn.textContent = "Copy"; }, 2000); }); } }); } // Upstream checkbox special handling var upstreamCheck = gen.querySelector("#cg-upstream-check"); if (upstreamCheck) { upstreamCheck.addEventListener("change", function () { var input = gen.querySelector('[data-key="upstream-base-url"]'); if (input) input.value = upstreamCheck.checked ? "https://ntfy.sh" : ""; updateOutput(); }); } // Metrics checkbox special handling var metricsCheck = gen.querySelector("#cg-metrics-check"); if (metricsCheck) { metricsCheck.addEventListener("change", function () { var input = gen.querySelector('[data-key="enable-metrics"]'); if (input) input.checked = metricsCheck.checked; updateOutput(); }); } updateConditionalVisibility(); updateOutput(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initGenerator); } else { initGenerator(); } })();