+
-
+
Incoming (publishing)
Listen address
@@ -378,7 +378,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
Configure options on the left to generate your config...
-
+
diff --git a/docs/static/css/config-generator.css b/docs/static/css/config-generator.css
index 594134e1..dffbd6ed 100644
--- a/docs/static/css/config-generator.css
+++ b/docs/static/css/config-generator.css
@@ -1,5 +1,10 @@
/* Config Generator */
+/* Hidden utility */
+.cg-hidden {
+ display: none !important;
+}
+
/* Open button */
.cg-open-btn {
display: inline-block;
@@ -417,6 +422,10 @@
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
}
+#cg-email-in-section {
+ margin-top: 20px;
+}
+
.cg-pg-label {
font-size: 0.75rem;
color: #888;
diff --git a/docs/static/js/config-generator.js b/docs/static/js/config-generator.js
index c0494f3e..e74b4d65 100644
--- a/docs/static/js/config-generator.js
+++ b/docs/static/js/config-generator.js
@@ -1,8 +1,95 @@
// Config Generator for ntfy
+//
+// How it works
+// ------------
+// The generator is a modal with a left panel (form inputs) and a right panel (live output).
+// On every input change, the update cycle runs: updateVisibility() syncs the UI state, then
+// updateOutput() collects values from the form and renders them as server.yml, docker-compose.yml,
+// or env vars.
+//
+// The CONFIG array is the source of truth for which config keys exist, their env var names,
+// which section they belong to, and optional defaults. collectValues() walks CONFIG, reads each
+// matching DOM element, skips anything in a hidden panel or section, and returns a plain
+// {key: value} object. The three generators (generateServerYml, generateDockerCompose,
+// generateEnvVars) each iterate CONFIG in order and format the collected values. Provisioned
+// users, ACLs, and tokens are collected separately from repeatable rows and stored as arrays
+// under "_auth-users", "_auth-acls", "_auth-tokens". The formatAuthUsers/Acls/Tokens() helpers
+// turn those arrays into "user:pass:role" strings shared by all three generators.
+//
+// Visibility is managed by updateVisibility(), which delegates to five helpers:
+// syncRadiosToHiddenInputs() copies user-facing radios and selects to hidden inputs that CONFIG
+// knows about (e.g. login mode radio → enable-login + require-login checkboxes).
+// updateFeatureVisibility() shows/hides nav tabs, configure buttons, and email sections based
+// on which feature checkboxes are checked. updatePostgresFields() swaps file-path inputs for
+// "Using PostgreSQL" labels when PostgreSQL is selected. prefillDefaults() sets sensible values
+// (file paths, addresses) when a feature is first enabled, tracked via a data-cleared attribute
+// so user edits are respected. autoDetectServerType() flips the server-type radio to "custom"
+// if the user's access/login settings no longer match "open" or "private".
+//
+// Event listeners are grouped into setup functions (setupModalEvents, setupAuthEvents,
+// setupServerTypeEvents, setupFormListeners, setupWebPushEvents) called from initGenerator().
+// A general listener on all inputs calls the update cycle. Specific listeners handle cleanup
+// logic, e.g. unchecking auth resets all auth-related fields and provisioned rows.
+//
+// Frequently-used DOM elements are queried once in cacheElements() and passed around as an
+// `els` object, avoiding repeated querySelector calls.
+//
+// Field inter-dependencies
+// ------------------------
+// Several UI fields don't map 1:1 to config keys. Instead, user-friendly controls drive
+// hidden inputs that CONFIG knows about. The sync happens in syncRadiosToHiddenInputs(),
+// called on every change via updateVisibility().
+//
+// Server type (Open / Private / Custom)
+// "Open" → unchecks auth, sets default-access to read-write, login to disabled
+// "Private" → checks auth, sets default-access to deny-all, login to required
+// "Custom" → no automatic changes; also auto-selected when the user manually
+// changes access/login to values that don't match Open or Private
+//
+// Auth checkbox (#cg-feat-auth)
+// When unchecked → resets: default-access to read-write, login to disabled,
+// signup to no, UnifiedPush to no, removes all provisioned users/ACLs/tokens,
+// clears auth-file, switches server type back to Open.
+// Also explicitly unchecks hidden enable-login, require-login, enable-signup.
+// When checked by PostgreSQL auto-enable → no reset, just enables the tab.
+//
+// Login mode (Disabled / Enabled / Required) — three-way radio
+// Maps to two hidden checkboxes:
+// enable-login = checked when Enabled OR Required
+// require-login = checked when Required only
+//
+// Signup (Yes / No) — radio pair
+// Maps to hidden enable-signup checkbox.
+//
+// Proxy (Yes / No) — radio pair
+// Maps to hidden behind-proxy checkbox.
+//
+// iOS support (Yes / No) — radio pair
+// Sets upstream-base-url to "https://ntfy.sh" when Yes, clears when No.
+//
+// UnifiedPush (Yes / No) — radio pair
+// When Yes, prepends a "*:up*:write-only" ACL entry at collection time.
+//
+// Database type (SQLite / PostgreSQL)
+// When PostgreSQL is selected:
+// - Auto-enables auth if not already on
+// - Hides file-path fields (auth-file, cache-file, web-push-file) and shows
+// "Using PostgreSQL" labels instead
+// - Shows the Database nav tab for the database-url field
+// - Prefills database-url with a postgres:// template
+// The database question itself only appears when a DB-dependent feature
+// (auth, cache, or web push) is enabled.
+//
+// Feature checkboxes (auth, cache, attachments, web push, email out, email in)
+// Each shows/hides its nav tab and "Configure" button.
+// When first enabled, prefillDefaults() fills in sensible paths/values.
+// The prefill is skipped if the user has already typed (or cleared) the field
+// (tracked via data-cleared attribute).
+//
(function () {
"use strict";
- var CONFIG = [
+ const CONFIG = [
{ key: "base-url", env: "NTFY_BASE_URL", section: "basic" },
{ key: "behind-proxy", env: "NTFY_BEHIND_PROXY", section: "basic", type: "bool" },
{ key: "database-url", env: "NTFY_DATABASE_URL", section: "database" },
@@ -32,43 +119,89 @@
];
// Feature checkbox → nav tab ID
- var NAV_MAP = {
+ const 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;
+ const SECTION_COMMENTS = {
+ 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",
+ };
- CONFIG.forEach(function (c) {
- var el = modal.querySelector('[data-key="' + c.key + '"]');
+ const durationRegex = /^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$/i;
+ const sizeRegex = /^(\d+)([tgmkb])?$/i;
+
+ // --- DOM cache ---
+
+ function cacheElements(modal) {
+ return {
+ modal,
+ authCheckbox: modal.querySelector("#cg-feat-auth"),
+ cacheCheckbox: modal.querySelector("#cg-feat-cache"),
+ attachCheckbox: modal.querySelector("#cg-feat-attach"),
+ webpushCheckbox: modal.querySelector("#cg-feat-webpush"),
+ smtpOutCheckbox: modal.querySelector("#cg-feat-smtp-out"),
+ smtpInCheckbox: modal.querySelector("#cg-feat-smtp-in"),
+ accessSelect: modal.querySelector("#cg-default-access-select"),
+ accessHidden: modal.querySelector('input[type="hidden"][data-key="auth-default-access"]'),
+ loginHidden: modal.querySelector("#cg-enable-login-hidden"),
+ requireLoginHidden: modal.querySelector("#cg-require-login-hidden"),
+ signupHidden: modal.querySelector("#cg-enable-signup-hidden"),
+ proxyCheckbox: modal.querySelector("#cg-behind-proxy"),
+ dbStep: modal.querySelector("#cg-wizard-db"),
+ navDb: modal.querySelector("#cg-nav-database"),
+ navEmail: modal.querySelector("#cg-nav-email"),
+ emailOutSection: modal.querySelector("#cg-email-out-section"),
+ emailInSection: modal.querySelector("#cg-email-in-section"),
+ codeEl: modal.querySelector("#cg-code"),
+ warningsEl: modal.querySelector("#cg-warnings"),
+ };
+ }
+
+ // --- Collect values ---
+
+ function collectValues(els) {
+ const { modal } = els;
+ const values = {};
+
+ CONFIG.forEach((c) => {
+ const el = modal.querySelector(`[data-key="${c.key}"]`);
if (!el) return;
// Skip fields in hidden panels (feature not enabled)
- var panel = el.closest(".cg-panel");
+ const panel = el.closest(".cg-panel");
if (panel) {
- // Panel hidden directly (e.g. PostgreSQL panel when SQLite selected)
- if (panel.style.display === "none") return;
+ // Panel hidden directly
+ if (panel.style.display === "none" || panel.classList.contains("cg-hidden")) 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;
+ const panelId = panel.id;
+ const navTab = modal.querySelector(`[data-panel="${panelId}"]`);
+ if (!navTab || navTab.classList.contains("cg-hidden")) return;
}
}
+ // Skip file inputs replaced by PostgreSQL
+ if (el.dataset.pgDisabled) return;
+
// Skip hidden individual fields or sections
- var ancestor = el.parentElement;
+ let ancestor = el.parentElement;
while (ancestor && ancestor !== modal) {
- if (ancestor.style.display === "none") return;
+ if (ancestor.style.display === "none" || ancestor.classList.contains("cg-hidden")) return;
ancestor = ancestor.parentElement;
}
- var val;
+ let val;
if (c.type === "bool") {
if (el.checked) val = "true";
} else {
@@ -80,46 +213,43 @@
});
// 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"]');
+ const users = collectRepeatableRows(modal, ".cg-auth-user-row", (row) => {
+ const u = row.querySelector('[data-field="username"]');
+ const p = row.querySelector('[data-field="password"]');
+ const 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" });
+ return { username: u.value.trim(), password: p.value.trim(), role: r ? r.value : "user" };
}
+ return null;
});
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"]');
+ const acls = collectRepeatableRows(modal, ".cg-auth-acl-row", (row) => {
+ const u = row.querySelector('[data-field="username"]');
+ const t = row.querySelector('[data-field="topic"]');
+ const 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" });
+ return { user: u.value.trim(), topic: t.value.trim(), permission: p ? p.value : "read-write" };
}
+ return null;
});
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"]');
+ const tokens = collectRepeatableRows(modal, ".cg-auth-token-row", (row) => {
+ const u = row.querySelector('[data-field="username"]');
+ const t = row.querySelector('[data-field="token"]');
+ const 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() : "" });
+ return { user: u.value.trim(), token: t.value.trim(), label: l ? l.value.trim() : "" };
}
+ return null;
});
if (tokens.length) values["_auth-tokens"] = tokens;
// UnifiedPush ACL
- var upYes = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]');
+ const 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" });
@@ -128,46 +258,59 @@
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 = "";
- var hadAuth = false;
+ function collectRepeatableRows(modal, selector, extractor) {
+ const results = [];
+ modal.querySelectorAll(selector).forEach((row) => {
+ const item = extractor(row);
+ if (item) results.push(item);
+ });
+ return results;
+ }
- CONFIG.forEach(function (c) {
+ // --- Shared auth formatting ---
+
+ function formatAuthUsers(values) {
+ if (!values["_auth-users"]) return null;
+ return values["_auth-users"].map((u) => `${u.username}:${u.password}:${u.role}`);
+ }
+
+ function formatAuthAcls(values) {
+ if (!values["_auth-acls"]) return null;
+ return values["_auth-acls"].map((a) => `${a.user || "*"}:${a.topic}:${a.permission}`);
+ }
+
+ function formatAuthTokens(values) {
+ if (!values["_auth-tokens"]) return null;
+ return values["_auth-tokens"].map((t) => t.label ? `${t.user}:${t.token}:${t.label}` : `${t.user}:${t.token}`);
+ }
+
+ // --- Output generators ---
+
+ function generateServerYml(values) {
+ const lines = [];
+ let lastSection = "";
+ let hadAuth = false;
+
+ CONFIG.forEach((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]);
+ if (SECTION_COMMENTS[c.section]) lines.push(SECTION_COMMENTS[c.section]);
lastSection = c.section;
}
if (c.section === "auth") hadAuth = true;
- var val = values[c.key];
- if (c.type === "bool") {
- lines.push(c.key + ": true");
- } else {
- lines.push(c.key + ': "' + val + '"');
- }
+ const val = values[c.key];
+ lines.push(c.type === "bool" ? `${c.key}: true` : `${c.key}: "${val}"`);
});
// Find where auth section ends to insert users/acls/tokens there
- var authInsertIdx = lines.length;
+ let authInsertIdx = lines.length;
if (hadAuth) {
- for (var i = 0; i < lines.length; i++) {
+ for (let i = 0; i < lines.length; i++) {
if (lines[i] === "# Access control") {
// Find the end of this section (next section comment or end)
- for (var j = i + 1; j < lines.length; j++) {
- if (lines[j].indexOf("# ") === 0) { authInsertIdx = j - 1; break; }
+ for (let j = i + 1; j < lines.length; j++) {
+ if (lines[j].startsWith("# ")) { authInsertIdx = j - 1; break; }
authInsertIdx = j + 1;
}
break;
@@ -175,151 +318,132 @@
}
}
- var authExtra = [];
- if (values["_auth-users"]) {
+ const authExtra = [];
+ const users = formatAuthUsers(values);
+ if (users) {
if (!hadAuth) { authExtra.push(""); authExtra.push("# Access control"); hadAuth = true; }
authExtra.push("auth-users:");
- values["_auth-users"].forEach(function (u) {
- authExtra.push(' - "' + u.username + ":" + u.password + ":" + u.role + '"');
- });
+ users.forEach((entry) => authExtra.push(` - "${entry}"`));
}
- if (values["_auth-acls"]) {
+ const acls = formatAuthAcls(values);
+ if (acls) {
if (!hadAuth) { authExtra.push(""); authExtra.push("# Access control"); hadAuth = true; }
authExtra.push("auth-access:");
- values["_auth-acls"].forEach(function (a) {
- authExtra.push(' - "' + (a.user || "*") + ":" + a.topic + ":" + a.permission + '"');
- });
+ acls.forEach((entry) => authExtra.push(` - "${entry}"`));
}
- if (values["_auth-tokens"]) {
+ const tokens = formatAuthTokens(values);
+ if (tokens) {
if (!hadAuth) { authExtra.push(""); authExtra.push("# Access control"); hadAuth = true; }
authExtra.push("auth-tokens:");
- values["_auth-tokens"].forEach(function (t) {
- var entry = t.user + ":" + t.token;
- if (t.label) entry += ":" + t.label;
- authExtra.push(' - "' + entry + '"');
- });
+ tokens.forEach((entry) => authExtra.push(` - "${entry}"`));
}
// Splice auth extras into the right position
if (authExtra.length) {
- lines.splice.apply(lines, [authInsertIdx, 0].concat(authExtra));
+ lines.splice(authInsertIdx, 0, ...authExtra);
}
return lines.join("\n");
}
function generateDockerCompose(values) {
- var lines = [];
- lines.push("services:");
- lines.push(" ntfy:");
- lines.push(' image: binwiederhier/ntfy');
- lines.push(" command: serve");
- lines.push(" environment:");
+ const lines = [
+ "services:",
+ " ntfy:",
+ " image: binwiederhier/ntfy",
+ " command: serve",
+ " environment:",
+ ];
- CONFIG.forEach(function (c) {
+ CONFIG.forEach((c) => {
if (!(c.key in values)) return;
- var val = values[c.key];
- if (c.type === "bool") {
- val = "true";
- }
- if (val.indexOf("$") !== -1) {
+ let val = c.type === "bool" ? "true" : values[c.key];
+ if (val.includes("$")) {
val = val.replace(/\$/g, "$$$$");
lines.push(" # Note: $ is doubled to $$ for docker-compose");
}
- lines.push(" " + c.env + ": " + val);
+ 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(",");
+ const users = formatAuthUsers(values);
+ if (users) {
+ let usersVal = users.join(",");
usersVal = usersVal.replace(/\$/g, "$$$$");
lines.push(" # Note: $ is doubled to $$ for docker-compose");
- lines.push(" NTFY_AUTH_USERS: " + usersVal);
+ 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);
+ const acls = formatAuthAcls(values);
+ if (acls) {
+ lines.push(` NTFY_AUTH_ACCESS: ${acls.join(",")}`);
}
- 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);
+ const tokens = formatAuthTokens(values);
+ if (tokens) {
+ lines.push(` NTFY_AUTH_TOKENS: ${tokens.join(",")}`);
}
- lines.push(" volumes:");
- lines.push(" - /var/cache/ntfy:/var/cache/ntfy");
- lines.push(" - /etc/ntfy:/etc/ntfy");
- lines.push(" ports:");
- lines.push(' - "80:80"');
- lines.push(" restart: unless-stopped");
+ lines.push(
+ " volumes:",
+ " - /var/cache/ntfy:/var/cache/ntfy",
+ " - /etc/ntfy:/etc/ntfy",
+ " ports:",
+ ' - "80:80"',
+ " restart: unless-stopped",
+ );
return lines.join("\n");
}
function generateEnvVars(values) {
- var lines = [];
+ const lines = [];
- CONFIG.forEach(function (c) {
+ CONFIG.forEach((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);
+ const val = c.type === "bool" ? "true" : values[c.key];
+ const q = val.includes("$") ? "'" : '"';
+ 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);
+ const users = formatAuthUsers(values);
+ if (users) {
+ const usersStr = users.join(",");
+ const q = usersStr.includes("$") ? "'" : '"';
+ 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 + '"');
+ const acls = formatAuthAcls(values);
+ if (acls) {
+ lines.push(`NTFY_AUTH_ACCESS="${acls.join(",")}"`);
}
- 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 + '"');
+ const tokens = formatAuthTokens(values);
+ if (tokens) {
+ lines.push(`NTFY_AUTH_TOKENS="${tokens.join(",")}"`);
}
return lines.join("\n");
}
- // Web Push VAPID key generation (P-256 ECDH)
+ // --- Web Push VAPID key generation (P-256 ECDH) ---
+
function generateVAPIDKeys() {
return crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveBits"]
- ).then(function (keyPair) {
+ ).then((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]);
+ }).then((keys) => {
+ const pubBytes = new Uint8Array(keys[0]);
+ const 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);
+ const privBytes = privPkcs8.slice(privPkcs8.length - 32);
return {
publicKey: arrayToBase64Url(pubBytes),
privateKey: arrayToBase64Url(privBytes)
@@ -328,35 +452,31 @@
}
function arrayToBase64Url(arr) {
- var str = "";
- for (var i = 0; i < arr.length; i++) {
+ let str = "";
+ for (let 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;
+ // --- Output + validation ---
- var values = collectValues();
- var codeEl = modal.querySelector("#cg-code");
+ function updateOutput(els) {
+ const { modal, codeEl, warningsEl } = els;
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; }
- }
+ const values = collectValues(els);
+ const activeTab = modal.querySelector(".cg-output-tab.active");
+ const format = activeTab ? activeTab.getAttribute("data-format") : "server-yml";
+ const hasValues = Object.keys(values).length > 0;
if (!hasValues) {
codeEl.innerHTML = '
Configure options on the left to generate your config... ';
+ setHidden(warningsEl, true);
return;
}
- var output = "";
+ let output;
if (format === "docker-compose") {
output = generateDockerCompose(values);
} else if (format === "env-vars") {
@@ -368,42 +488,26 @@
codeEl.textContent = output;
// Validation warnings
- var warnings = validate(modal, values);
- var warningsEl = modal.querySelector("#cg-warnings");
+ const warnings = validate(values);
if (warningsEl) {
if (warnings.length) {
- warningsEl.innerHTML = warnings.map(function (w) {
- return '
' + w + '
';
- }).join("");
- warningsEl.style.display = "";
- } else {
- warningsEl.style.display = "none";
+ warningsEl.innerHTML = warnings.map((w) => `
${w}
`).join("");
}
+ setHidden(warningsEl, !warnings.length);
}
}
- var durationRegex = /^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$/i;
- var sizeRegex = /^(\d+)([tgmkb])?$/i;
-
- function isValidDuration(s) {
- return durationRegex.test(s);
- }
-
- function isValidSize(s) {
- return sizeRegex.test(s);
- }
-
- function validate(modal, values) {
- var warnings = [];
- var baseUrl = values["base-url"] || "";
+ function validate(values) {
+ const warnings = [];
+ const baseUrl = values["base-url"] || "";
// base-url format
if (baseUrl) {
- if (baseUrl.indexOf("http://") !== 0 && baseUrl.indexOf("https://") !== 0) {
+ if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
warnings.push("base-url must start with http:// or https://");
} else {
try {
- var u = new URL(baseUrl);
+ const u = new URL(baseUrl);
if (u.pathname !== "/" && u.pathname !== "") {
warnings.push("base-url must not have a path, ntfy does not support sub-paths");
}
@@ -414,35 +518,35 @@
}
// database-url must start with postgres://
- if (values["database-url"] && values["database-url"].indexOf("postgres://") !== 0) {
+ if (values["database-url"] && !values["database-url"].startsWith("postgres://")) {
warnings.push("database-url must start with postgres://");
}
// 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"];
+ const wpPublic = values["web-push-public-key"];
+ const wpPrivate = values["web-push-private-key"];
+ const wpEmail = values["web-push-email-address"];
+ const wpFile = values["web-push-file"];
+ const dbUrl = values["database-url"];
if (wpPublic || wpPrivate || wpEmail) {
- var missing = [];
+ const 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(", "));
+ warnings.push(`Web push requires: ${missing.join(", ")}`);
}
}
// SMTP sender requires base-url and smtp-sender-from
if (values["smtp-sender-addr"]) {
- var smtpMissing = [];
+ const 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(", "));
+ warnings.push(`Email sending requires: ${smtpMissing.join(", ")}`);
}
}
@@ -471,129 +575,144 @@
}
// Duration field validation
- var durationFields = [
+ [
{ key: "cache-duration", label: "Cache duration" },
{ key: "attachment-expiry-duration", label: "Attachment expiry duration" },
- ];
- durationFields.forEach(function (f) {
- if (values[f.key] && !isValidDuration(values[f.key])) {
- warnings.push(f.label + " must be a valid duration (e.g. 12h, 3d, 30m, 60s)");
+ ].forEach((f) => {
+ if (values[f.key] && !durationRegex.test(values[f.key])) {
+ warnings.push(`${f.label} must be a valid duration (e.g. 12h, 3d, 30m, 60s)`);
}
});
// Size field validation
- var sizeFields = [
+ [
{ key: "attachment-file-size-limit", label: "Attachment file size limit" },
{ key: "attachment-total-size-limit", label: "Attachment total size limit" },
- ];
- sizeFields.forEach(function (f) {
- if (values[f.key] && !isValidSize(values[f.key])) {
- warnings.push(f.label + " must be a valid size (e.g. 15M, 5G, 100K)");
+ ].forEach((f) => {
+ if (values[f.key] && !sizeRegex.test(values[f.key])) {
+ warnings.push(`${f.label} must be a valid size (e.g. 15M, 5G, 100K)`);
}
});
return warnings;
}
+ // --- Helpers ---
+
function generateToken() {
- var chars = "abcdefghijklmnopqrstuvwxyz0123456789";
- var token = "tk_";
- for (var i = 0; i < 29; i++) {
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ let token = "tk_";
+ for (let i = 0; i < 29; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
}
function prefill(modal, key, value) {
- var el = modal.querySelector('[data-key="' + key + '"]');
+ const el = modal.querySelector(`[data-key="${key}"]`);
if (el && !el.value.trim() && !el.dataset.cleared) el.value = value;
}
+ function switchPanel(modal, panelId) {
+ modal.querySelectorAll(".cg-nav-tab").forEach((t) => t.classList.remove("active"));
+ modal.querySelectorAll(".cg-panel").forEach((p) => p.classList.remove("active"));
- function updateVisibility() {
- var modal = document.getElementById("cg-modal");
- if (!modal) return;
+ const navTab = modal.querySelector(`[data-panel="${panelId}"]`);
+ const panel = modal.querySelector(`#${panelId}`);
+ if (navTab) navTab.classList.add("active");
+ if (panel) panel.classList.add("active");
+ }
- var isPostgres = modal.querySelector('input[name="cg-db-type"][value="postgres"]');
- isPostgres = isPostgres && isPostgres.checked;
+ function setHidden(el, hidden) {
+ if (!el) return;
+ if (hidden) {
+ el.classList.add("cg-hidden");
+ } else {
+ el.classList.remove("cg-hidden");
+ }
+ }
- // Auto-enable auth when PostgreSQL is selected
- if (isPostgres) {
- var authCb = modal.querySelector("#cg-feat-auth");
- if (authCb && !authCb.checked) {
- authCb.checked = true;
- }
+ // --- Visibility: broken into focused helpers ---
+
+ function syncRadiosToHiddenInputs(els) {
+ const { modal, accessSelect, accessHidden, loginHidden, requireLoginHidden, signupHidden, proxyCheckbox } = els;
+
+ // Proxy radio → hidden checkbox
+ const proxyYes = modal.querySelector('input[name="cg-proxy"][value="yes"]');
+ if (proxyYes && proxyCheckbox) {
+ proxyCheckbox.checked = proxyYes.checked;
}
- var serverTypeRadio = modal.querySelector('input[name="cg-server-type"]:checked');
- var serverType = serverTypeRadio ? serverTypeRadio.value : "open";
- var isPrivate = serverType === "private";
+ // Default access select → hidden input
+ if (accessSelect && accessHidden) {
+ accessHidden.value = accessSelect.value;
+ }
- var isUnifiedPush = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]');
- isUnifiedPush = isUnifiedPush && isUnifiedPush.checked;
+ // Login mode three-way toggle → hidden checkboxes
+ const loginMode = modal.querySelector('input[name="cg-login-mode"]:checked');
+ const loginModeVal = loginMode ? loginMode.value : "disabled";
+ if (loginHidden) loginHidden.checked = (loginModeVal === "enabled" || loginModeVal === "required");
+ if (requireLoginHidden) requireLoginHidden.checked = (loginModeVal === "required");
- var authCheck = modal.querySelector("#cg-feat-auth");
- var authEnabled = authCheck && authCheck.checked;
+ const signupYes = modal.querySelector('input[name="cg-enable-signup"][value="yes"]');
+ if (signupYes && signupHidden) signupHidden.checked = signupYes.checked;
- var cacheEnabled = modal.querySelector("#cg-feat-cache");
- cacheEnabled = cacheEnabled && cacheEnabled.checked;
+ return loginModeVal;
+ }
- 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;
+ function updateFeatureVisibility(els, flags) {
+ const { modal, dbStep, navDb, navEmail, emailOutSection, emailInSection } = els;
+ const { authEnabled, cacheEnabled, webpushEnabled, smtpOutEnabled, smtpInEnabled, needsDb, isPostgres } = flags;
// 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";
+ setHidden(dbStep, !needsDb);
// Nav tabs for features
- for (var featId in NAV_MAP) {
- var checkbox = modal.querySelector("#" + featId);
- var navTab = modal.querySelector("#" + NAV_MAP[featId]);
+ for (const featId in NAV_MAP) {
+ const checkbox = modal.querySelector(`#${featId}`);
+ const navTab = modal.querySelector(`#${NAV_MAP[featId]}`);
if (checkbox && navTab) {
- navTab.style.display = checkbox.checked ? "" : "none";
+ setHidden(navTab, !checkbox.checked);
}
}
// 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";
+ setHidden(navEmail, !smtpOutEnabled && !smtpInEnabled);
+ setHidden(emailOutSection, !smtpOutEnabled);
+ setHidden(emailInSection, !smtpInEnabled);
// Show/hide configure buttons next to feature checkboxes
- modal.querySelectorAll(".cg-btn-configure").forEach(function (btn) {
- var row = btn.closest(".cg-feature-row");
+ modal.querySelectorAll(".cg-btn-configure").forEach((btn) => {
+ const row = btn.closest(".cg-feature-row");
if (!row) return;
- var cb = row.querySelector('input[type="checkbox"]');
- btn.style.display = (cb && cb.checked) ? "" : "none";
+ const cb = row.querySelector('input[type="checkbox"]');
+ setHidden(btn, !(cb && cb.checked));
});
// If active nav tab got hidden, switch to General
- var activeNav = modal.querySelector(".cg-nav-tab.active");
- if (activeNav && activeNav.style.display === "none") {
+ const activeNav = modal.querySelector(".cg-nav-tab.active");
+ if (activeNav && activeNav.classList.contains("cg-hidden")) {
switchPanel(modal, "cg-panel-general");
}
+ // Database tab — show only when PostgreSQL is selected and a DB-dependent feature is on
+ setHidden(navDb, !(needsDb && isPostgres));
+ }
+
+ function updatePostgresFields(modal, isPostgres) {
// Show "Using PostgreSQL" instead of file inputs when PostgreSQL is selected
- ["auth-file", "web-push-file", "cache-file"].forEach(function (key) {
- var input = modal.querySelector('[data-key="' + key + '"]');
+ ["auth-file", "web-push-file", "cache-file"].forEach((key) => {
+ const input = modal.querySelector(`[data-key="${key}"]`);
if (!input) return;
- var field = input.closest(".cg-field");
+ const field = input.closest(".cg-field");
if (!field) return;
input.style.display = isPostgres ? "none" : "";
- var pgLabel = field.querySelector(".cg-pg-label");
+ if (isPostgres) {
+ input.dataset.pgDisabled = "1";
+ } else {
+ delete input.dataset.pgDisabled;
+ }
+ let pgLabel = field.querySelector(".cg-pg-label");
if (isPostgres) {
if (!pgLabel) {
pgLabel = document.createElement("span");
@@ -607,44 +726,17 @@
}
});
- // 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"]');
+ const iosYes = modal.querySelector('input[name="cg-ios"][value="yes"]');
+ const 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;
- }
+ function prefillDefaults(modal, flags) {
+ const { isPostgres, authEnabled, cacheEnabled, attachEnabled, webpushEnabled, smtpOutEnabled, smtpInEnabled } = flags;
- // Default access select → hidden input
- var accessSelect = modal.querySelector("#cg-default-access-select");
- var accessHidden = modal.querySelector('input[type="hidden"][data-key="auth-default-access"]');
- if (accessSelect && accessHidden) {
- accessHidden.value = accessSelect.value;
- }
-
- // Login mode three-way toggle → hidden checkboxes
- var loginMode = modal.querySelector('input[name="cg-login-mode"]:checked');
- var loginModeVal = loginMode ? loginMode.value : "disabled";
- var loginHidden = modal.querySelector("#cg-enable-login-hidden");
- var requireLoginHidden = modal.querySelector("#cg-require-login-hidden");
- if (loginHidden) loginHidden.checked = (loginModeVal === "enabled" || loginModeVal === "required");
- if (requireLoginHidden) requireLoginHidden.checked = (loginModeVal === "required");
-
- var signupYes = modal.querySelector('input[name="cg-enable-signup"][value="yes"]');
- var signupHidden = modal.querySelector("#cg-enable-signup-hidden");
- if (signupYes && signupHidden) signupHidden.checked = signupYes.checked;
-
- // --- Pre-fill defaults ---
if (isPostgres) {
prefill(modal, "database-url", "postgres://user:pass@host:5432/ntfy");
}
@@ -653,18 +745,6 @@
if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db");
}
- // Auto-detect server type based on current auth settings
- if (serverType !== "custom") {
- var currentAccess = accessSelect ? accessSelect.value : "read-write";
- var currentLoginEnabled = loginModeVal !== "disabled";
- var matchesOpen = currentAccess === "read-write" && !currentLoginEnabled;
- var matchesPrivate = currentAccess === "deny-all" && currentLoginEnabled;
- if (!matchesOpen && !matchesPrivate) {
- var customRadio = modal.querySelector('input[name="cg-server-type"][value="custom"]');
- if (customRadio) customRadio.checked = true;
- }
- }
-
if (cacheEnabled) {
if (!isPostgres) prefill(modal, "cache-file", "/var/cache/ntfy/cache.db");
}
@@ -681,6 +761,8 @@
if (smtpOutEnabled) {
prefill(modal, "smtp-sender-addr", "smtp.example.com:587");
prefill(modal, "smtp-sender-from", "ntfy@example.com");
+ prefill(modal, "smtp-sender-user", "yoursmtpuser");
+ prefill(modal, "smtp-sender-pass", "yoursmtppass");
}
if (smtpInEnabled) {
@@ -689,19 +771,56 @@
}
}
- 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"); });
+ function autoDetectServerType(els, loginModeVal) {
+ const { modal, accessSelect } = els;
+ const serverTypeRadio = modal.querySelector('input[name="cg-server-type"]:checked');
+ const serverType = serverTypeRadio ? serverTypeRadio.value : "open";
- var navTab = modal.querySelector('[data-panel="' + panelId + '"]');
- var panel = modal.querySelector("#" + panelId);
- if (navTab) navTab.classList.add("active");
- if (panel) panel.classList.add("active");
+ if (serverType !== "custom") {
+ const currentAccess = accessSelect ? accessSelect.value : "read-write";
+ const currentLoginEnabled = loginModeVal !== "disabled";
+ const matchesOpen = currentAccess === "read-write" && !currentLoginEnabled;
+ const matchesPrivate = currentAccess === "deny-all" && currentLoginEnabled;
+ if (!matchesOpen && !matchesPrivate) {
+ const customRadio = modal.querySelector('input[name="cg-server-type"][value="custom"]');
+ if (customRadio) customRadio.checked = true;
+ }
+ }
}
- function addRepeatableRow(container, type) {
- var row = document.createElement("div");
- row.className = "cg-repeatable-row cg-auth-" + type + "-row";
+ function updateVisibility(els) {
+ const { modal, authCheckbox, cacheCheckbox, attachCheckbox, webpushCheckbox, smtpOutCheckbox, smtpInCheckbox } = els;
+
+ const isPostgresRadio = modal.querySelector('input[name="cg-db-type"][value="postgres"]');
+ const isPostgres = isPostgresRadio && isPostgresRadio.checked;
+
+ // Auto-enable auth when PostgreSQL is selected
+ if (isPostgres && authCheckbox && !authCheckbox.checked) {
+ authCheckbox.checked = true;
+ }
+
+ const authEnabled = authCheckbox && authCheckbox.checked;
+ const cacheEnabled = cacheCheckbox && cacheCheckbox.checked;
+ const attachEnabled = attachCheckbox && attachCheckbox.checked;
+ const webpushEnabled = webpushCheckbox && webpushCheckbox.checked;
+ const smtpOutEnabled = smtpOutCheckbox && smtpOutCheckbox.checked;
+ const smtpInEnabled = smtpInCheckbox && smtpInCheckbox.checked;
+ const needsDb = authEnabled || cacheEnabled || webpushEnabled;
+
+ const flags = { isPostgres, authEnabled, cacheEnabled, attachEnabled, webpushEnabled, smtpOutEnabled, smtpInEnabled, needsDb };
+
+ const loginModeVal = syncRadiosToHiddenInputs(els);
+ updateFeatureVisibility(els, flags);
+ updatePostgresFields(modal, isPostgres);
+ prefillDefaults(modal, flags);
+ autoDetectServerType(els, loginModeVal);
+ }
+
+ // --- Repeatable rows ---
+
+ function addRepeatableRow(container, type, onUpdate) {
+ const row = document.createElement("div");
+ row.className = `cg-repeatable-row cg-auth-${type}-row`;
if (type === "user") {
row.innerHTML =
@@ -718,215 +837,242 @@
} else if (type === "token") {
row.innerHTML =
'
' +
- '
' +
+ `
` +
'
' +
'
× ';
}
- row.querySelector(".cg-btn-remove").addEventListener("click", function () {
+ row.querySelector(".cg-btn-remove").addEventListener("click", () => {
row.remove();
- updateOutput();
+ onUpdate();
});
- row.querySelectorAll("input, select").forEach(function (el) {
- el.addEventListener("input", updateOutput);
+ row.querySelectorAll("input, select").forEach((el) => {
+ el.addEventListener("input", onUpdate);
});
container.appendChild(row);
}
- function initGenerator() {
- var modal = document.getElementById("cg-modal");
- if (!modal) return;
+ // --- Modal functions (module-level) ---
- var openBtn = document.getElementById("cg-open-btn");
- var closeBtn = document.getElementById("cg-close-btn");
- var backdrop = modal.querySelector(".cg-modal-backdrop");
+ function openModal(els) {
+ els.modal.style.display = "";
+ document.body.style.overflow = "hidden";
+ updateVisibility(els);
+ updateOutput(els);
+ }
- function openModal() {
- modal.style.display = "";
- document.body.style.overflow = "hidden";
- updateVisibility();
- updateOutput();
- }
+ function closeModal(els) {
+ els.modal.style.display = "none";
+ document.body.style.overflow = "";
+ }
- function closeModal() {
- modal.style.display = "none";
- document.body.style.overflow = "";
- }
+ function resetAll(els) {
+ const { modal } = els;
- var resetBtn = document.getElementById("cg-reset-btn");
-
- function resetAll() {
- // Reset all text/password inputs and clear flags
- modal.querySelectorAll('input[type="text"], input[type="password"]').forEach(function (el) {
- el.value = "";
- delete el.dataset.cleared;
- });
- // Uncheck all checkboxes
- modal.querySelectorAll('input[type="checkbox"]').forEach(function (el) {
+ // Reset all text/password inputs and clear flags
+ modal.querySelectorAll('input[type="text"], input[type="password"]').forEach((el) => {
+ el.value = "";
+ delete el.dataset.cleared;
+ });
+ // Uncheck all checkboxes
+ modal.querySelectorAll('input[type="checkbox"]').forEach((el) => {
+ el.checked = false;
+ el.disabled = false;
+ });
+ // Reset radio buttons to first option
+ const radioGroups = {};
+ modal.querySelectorAll('input[type="radio"]').forEach((el) => {
+ if (!radioGroups[el.name]) {
+ radioGroups[el.name] = true;
+ const first = modal.querySelector(`input[type="radio"][name="${el.name}"]`);
+ if (first) first.checked = true;
+ } else {
el.checked = false;
- el.disabled = false;
- });
- // Reset radio buttons to first option
- var radioGroups = {};
- modal.querySelectorAll('input[type="radio"]').forEach(function (el) {
- if (!radioGroups[el.name]) {
- radioGroups[el.name] = true;
- var first = modal.querySelector('input[type="radio"][name="' + el.name + '"]');
- if (first) first.checked = true;
- } else {
- el.checked = false;
- }
- });
- // Reset selects to first option
- modal.querySelectorAll("select").forEach(function (el) {
- el.selectedIndex = 0;
- });
- // Remove all repeatable rows
- modal.querySelectorAll(".cg-auth-user-row, .cg-auth-acl-row, .cg-auth-token-row").forEach(function (row) {
- row.remove();
- });
- // Re-prefill base-url
- var baseUrlInput = modal.querySelector('[data-key="base-url"]');
- if (baseUrlInput) {
- var host = window.location.hostname;
- if (host && host.indexOf("ntfy.sh") === -1) {
- baseUrlInput.value = "https://ntfy.example.com";
- }
- }
- // Reset to General tab
- switchPanel(modal, "cg-panel-general");
- updateVisibility();
- updateOutput();
- }
-
- if (openBtn) openBtn.addEventListener("click", openModal);
- if (closeBtn) closeBtn.addEventListener("click", closeModal);
- if (resetBtn) resetBtn.addEventListener("click", resetAll);
- if (backdrop) backdrop.addEventListener("click", closeModal);
-
- document.addEventListener("keydown", function (e) {
- if (e.key === "Escape" && modal.style.display !== "none") {
- closeModal();
}
});
+ // Reset selects to first option
+ modal.querySelectorAll("select").forEach((el) => {
+ el.selectedIndex = 0;
+ });
+ // Remove all repeatable rows
+ modal.querySelectorAll(".cg-auth-user-row, .cg-auth-acl-row, .cg-auth-token-row").forEach((row) => {
+ row.remove();
+ });
+ // Re-prefill base-url
+ const baseUrlInput = modal.querySelector('[data-key="base-url"]');
+ if (baseUrlInput) {
+ const host = window.location.hostname;
+ if (host && !host.includes("ntfy.sh")) {
+ baseUrlInput.value = "https://ntfy.example.com";
+ }
+ }
+ // Reset to General tab
+ switchPanel(modal, "cg-panel-general");
+ updateVisibility(els);
+ updateOutput(els);
+ }
+
+ function fillVAPIDKeys(els) {
+ const { modal } = els;
+ generateVAPIDKeys().then((keys) => {
+ const pubInput = modal.querySelector('[data-key="web-push-public-key"]');
+ const privInput = modal.querySelector('[data-key="web-push-private-key"]');
+ if (pubInput) pubInput.value = keys.publicKey;
+ if (privInput) privInput.value = keys.privateKey;
+ updateOutput(els);
+ });
+ }
+
+ // --- Event setup (grouped) ---
+
+ function setupModalEvents(els) {
+ const { modal } = els;
+ const openBtn = document.getElementById("cg-open-btn");
+ const closeBtn = document.getElementById("cg-close-btn");
+ const backdrop = modal.querySelector(".cg-modal-backdrop");
+ const resetBtn = document.getElementById("cg-reset-btn");
+
+ if (openBtn) openBtn.addEventListener("click", () => openModal(els));
+ if (closeBtn) closeBtn.addEventListener("click", () => closeModal(els));
+ if (resetBtn) resetBtn.addEventListener("click", () => resetAll(els));
+ if (backdrop) backdrop.addEventListener("click", () => closeModal(els));
+
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape" && modal.style.display !== "none") {
+ closeModal(els);
+ }
+ });
+ }
+
+ function setupAuthEvents(els) {
+ const { modal, authCheckbox, accessSelect } = els;
+ if (!authCheckbox) return;
+
+ // Auth checkbox: clean up when unchecked
+ authCheckbox.addEventListener("change", () => {
+ if (!authCheckbox.checked) {
+ // Clear auth-file
+ const authFile = modal.querySelector('[data-key="auth-file"]');
+ if (authFile) { authFile.value = ""; delete authFile.dataset.cleared; }
+ // Reset default access
+ if (accessSelect) accessSelect.value = "read-write";
+ // Reset login mode to Disabled and unset hidden checkboxes
+ const loginDisabled = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
+ if (loginDisabled) loginDisabled.checked = true;
+ if (els.loginHidden) els.loginHidden.checked = false;
+ if (els.requireLoginHidden) els.requireLoginHidden.checked = false;
+ const signupNo = modal.querySelector('input[name="cg-enable-signup"][value="no"]');
+ if (signupNo) signupNo.checked = true;
+ if (els.signupHidden) els.signupHidden.checked = false;
+ // Reset UnifiedPush to No
+ const upNo = modal.querySelector('input[name="cg-unifiedpush"][value="no"]');
+ if (upNo) upNo.checked = true;
+ // Remove provisioned users/ACLs/tokens
+ modal.querySelectorAll(".cg-auth-user-row, .cg-auth-acl-row, .cg-auth-token-row").forEach((row) => {
+ row.remove();
+ });
+ // Switch server type to Open
+ const openRadio = modal.querySelector('input[name="cg-server-type"][value="open"]');
+ if (openRadio) openRadio.checked = true;
+ }
+ });
+ }
+
+ function setupServerTypeEvents(els) {
+ const { modal, authCheckbox, accessSelect } = els;
+
+ modal.querySelectorAll('input[name="cg-server-type"]').forEach((radio) => {
+ radio.addEventListener("change", () => {
+ const loginDisabledRadio = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
+ const loginRequiredRadio = modal.querySelector('input[name="cg-login-mode"][value="required"]');
+ if (radio.value === "open") {
+ if (accessSelect) accessSelect.value = "read-write";
+ if (loginDisabledRadio) loginDisabledRadio.checked = true;
+ if (authCheckbox) authCheckbox.checked = false;
+ // Trigger the auth cleanup
+ authCheckbox.dispatchEvent(new Event("change"));
+ } else if (radio.value === "private") {
+ // Enable auth with required login
+ if (authCheckbox) authCheckbox.checked = true;
+ if (accessSelect) accessSelect.value = "deny-all";
+ if (loginRequiredRadio) loginRequiredRadio.checked = true;
+ if (els.loginHidden) els.loginHidden.checked = true;
+ if (els.requireLoginHidden) els.requireLoginHidden.checked = true;
+ }
+ // "custom" doesn't change anything
+ });
+ });
+ }
+
+ function setupFormListeners(els) {
+ const { modal } = els;
+ const onUpdate = () => {
+ updateVisibility(els);
+ updateOutput(els);
+ };
// Left nav tab switching
- modal.querySelectorAll(".cg-nav-tab").forEach(function (tab) {
- tab.addEventListener("click", function () {
- var panelId = tab.getAttribute("data-panel");
+ modal.querySelectorAll(".cg-nav-tab").forEach((tab) => {
+ tab.addEventListener("click", () => {
+ const panelId = tab.getAttribute("data-panel");
switchPanel(modal, panelId);
});
});
// Configure buttons in feature grid
- modal.querySelectorAll(".cg-btn-configure").forEach(function (btn) {
- btn.addEventListener("click", function () {
- var panelId = btn.getAttribute("data-panel");
+ modal.querySelectorAll(".cg-btn-configure").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const panelId = btn.getAttribute("data-panel");
if (panelId) 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"); });
+ modal.querySelectorAll(".cg-output-tab").forEach((tab) => {
+ tab.addEventListener("click", () => {
+ modal.querySelectorAll(".cg-output-tab").forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
- updateOutput();
- });
- });
-
- // Auth checkbox: clean up when unchecked
- var authCheckbox = modal.querySelector("#cg-feat-auth");
- if (authCheckbox) {
- authCheckbox.addEventListener("change", function () {
- if (!authCheckbox.checked) {
- // Clear auth-file
- var authFile = modal.querySelector('[data-key="auth-file"]');
- if (authFile) { authFile.value = ""; delete authFile.dataset.cleared; }
- // Reset default access
- var accessSelect = modal.querySelector("#cg-default-access-select");
- if (accessSelect) accessSelect.value = "read-write";
- // Reset login mode to Disabled
- var loginDisabled = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
- if (loginDisabled) loginDisabled.checked = true;
- var signupNo = modal.querySelector('input[name="cg-enable-signup"][value="no"]');
- if (signupNo) signupNo.checked = true;
- // Reset UnifiedPush to No
- var upNo = modal.querySelector('input[name="cg-unifiedpush"][value="no"]');
- if (upNo) upNo.checked = true;
- // Remove provisioned users/ACLs/tokens
- modal.querySelectorAll(".cg-auth-user-row, .cg-auth-acl-row, .cg-auth-token-row").forEach(function (row) {
- row.remove();
- });
- // Switch server type to Open
- var openRadio = modal.querySelector('input[name="cg-server-type"][value="open"]');
- if (openRadio) openRadio.checked = true;
- }
- });
- }
-
- // Server type radio: apply mode settings when clicked
- modal.querySelectorAll('input[name="cg-server-type"]').forEach(function (radio) {
- radio.addEventListener("change", function () {
- var accessSelect = modal.querySelector("#cg-default-access-select");
- var loginDisabledRadio = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
- var loginRequiredRadio = modal.querySelector('input[name="cg-login-mode"][value="required"]');
- if (radio.value === "open") {
- if (accessSelect) accessSelect.value = "read-write";
- if (loginDisabledRadio) loginDisabledRadio.checked = true;
- var authCheck = modal.querySelector("#cg-feat-auth");
- if (authCheck) authCheck.checked = false;
- // Trigger the auth cleanup
- authCheck.dispatchEvent(new Event("change"));
- } else if (radio.value === "private") {
- // Enable auth
- var authCheck = modal.querySelector("#cg-feat-auth");
- if (authCheck) authCheck.checked = true;
- if (accessSelect) accessSelect.value = "deny-all";
- if (loginRequiredRadio) loginRequiredRadio.checked = true;
- }
- // "custom" doesn't change anything
+ updateOutput(els);
});
});
// 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 () {
+ modal.querySelectorAll("input, select").forEach((el) => {
+ const evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
+ el.addEventListener(evt, () => {
// Mark text fields as cleared when user empties them
- if (el.type === "text" && el.dataset.key && !el.value.trim()) {
+ if ((el.type === "text" || el.type === "password") && el.dataset.key && !el.value.trim()) {
el.dataset.cleared = "1";
- } else if (el.type === "text" && el.dataset.key && el.value.trim()) {
+ } else if ((el.type === "text" || el.type === "password") && el.dataset.key && el.value.trim()) {
delete el.dataset.cleared;
}
- updateVisibility();
- updateOutput();
+ onUpdate();
});
});
// 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;
+ modal.querySelectorAll(".cg-btn-add[data-add-type]").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const type = btn.getAttribute("data-add-type");
+ let container = btn.previousElementSibling;
if (!container) container = btn.parentElement.querySelector(".cg-repeatable-container");
- addRepeatableRow(container, type);
+ addRepeatableRow(container, type, onUpdate);
});
});
// Copy button
- var copyBtn = modal.querySelector("#cg-copy-btn");
+ const copyBtn = modal.querySelector("#cg-copy-btn");
if (copyBtn) {
- var copyIcon = '
';
- var checkIcon = '
';
- copyBtn.addEventListener("click", function () {
- var code = modal.querySelector("#cg-code");
+ const copyIcon = '
';
+ const checkIcon = '
';
+ copyBtn.addEventListener("click", () => {
+ const code = modal.querySelector("#cg-code");
if (code && code.textContent) {
- navigator.clipboard.writeText(code.textContent).then(function () {
+ navigator.clipboard.writeText(code.textContent).then(() => {
copyBtn.innerHTML = checkIcon;
copyBtn.style.color = "var(--md-primary-fg-color)";
- setTimeout(function () {
+ setTimeout(() => {
copyBtn.innerHTML = copyIcon;
copyBtn.style.color = "";
}, 2000);
@@ -934,50 +1080,55 @@
}
});
}
+ }
- // 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();
- });
- }
+ function setupWebPushEvents(els) {
+ const { modal } = els;
+ let vapidKeysGenerated = false;
+ const regenBtn = modal.querySelector("#cg-regen-keys");
if (regenBtn) {
- regenBtn.addEventListener("click", function () {
- fillVAPIDKeys();
- });
+ regenBtn.addEventListener("click", () => fillVAPIDKeys(els));
}
// Auto-generate keys when web push is first enabled
- var webpushFeat = modal.querySelector("#cg-feat-webpush");
+ const webpushFeat = modal.querySelector("#cg-feat-webpush");
if (webpushFeat) {
- webpushFeat.addEventListener("change", function () {
+ webpushFeat.addEventListener("change", () => {
if (webpushFeat.checked && !vapidKeysGenerated) {
vapidKeysGenerated = true;
- fillVAPIDKeys();
+ fillVAPIDKeys(els);
}
});
}
+ }
+
+ // --- Init ---
+
+ function initGenerator() {
+ const modal = document.getElementById("cg-modal");
+ if (!modal) return;
+
+ const els = cacheElements(modal);
+
+ setupModalEvents(els);
+ setupAuthEvents(els);
+ setupServerTypeEvents(els);
+ setupFormListeners(els);
+ setupWebPushEvents(els);
// Pre-fill base-url if not on ntfy.sh
- var baseUrlInput = modal.querySelector('[data-key="base-url"]');
+ const baseUrlInput = modal.querySelector('[data-key="base-url"]');
if (baseUrlInput && !baseUrlInput.value.trim()) {
- var host = window.location.hostname;
- if (host && host.indexOf("ntfy.sh") === -1) {
+ const host = window.location.hostname;
+ if (host && !host.includes("ntfy.sh")) {
baseUrlInput.value = "https://ntfy.example.com";
}
}
// Auto-open if URL hash points to config generator
if (window.location.hash === "#config-generator") {
- openModal();
+ openModal(els);
}
}