+
Configure the PostgreSQL connection. See
PostgreSQL for details.
Database URL
diff --git a/docs/static/css/config-generator.css b/docs/static/css/config-generator.css
index cc23936b..f3377a25 100644
--- a/docs/static/css/config-generator.css
+++ b/docs/static/css/config-generator.css
@@ -53,9 +53,45 @@
flex-shrink: 0;
}
+.cg-modal-header-left {
+ display: flex;
+ align-items: baseline;
+ gap: 12px;
+ min-width: 0;
+}
+
.cg-modal-title {
font-weight: 600;
font-size: 0.95rem;
+ white-space: nowrap;
+}
+
+.cg-modal-desc {
+ font-size: 0.75rem;
+ color: #888;
+}
+
+.cg-modal-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.cg-modal-reset {
+ background: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ color: #777;
+ cursor: pointer;
+ padding: 4px 12px;
+ font-family: inherit;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.cg-modal-reset:hover {
+ color: #333;
+ border-color: #999;
}
.cg-modal-close {
@@ -134,6 +170,30 @@
display: block;
}
+.cg-panel-desc {
+ font-size: 0.75rem;
+ color: #888;
+ margin-bottom: 12px;
+ line-height: 1.5;
+}
+
+.cg-panel-desc a {
+ color: var(--md-primary-fg-color);
+}
+
+.cg-help {
+ color: var(--md-primary-fg-color);
+ text-decoration: none;
+ margin-left: 4px;
+ vertical-align: middle;
+ flex-shrink: 0;
+ transition: color 0.15s;
+}
+
+.cg-help:hover {
+ color: var(--md-primary-fg-color--dark, #2a6e5f);
+}
+
/* Right panel */
#cg-right {
flex: 1;
@@ -195,6 +255,8 @@
flex: 1;
overflow: auto;
padding: 16px 20px;
+ display: flex;
+ flex-direction: column;
}
.cg-output-wrap pre {
@@ -207,7 +269,7 @@
overflow-x: auto;
font-size: 0.76rem;
line-height: 1.5;
- min-height: 100px;
+ flex: 1;
white-space: pre;
}
@@ -228,11 +290,16 @@
/* Form fields */
.cg-field {
- margin-bottom: 12px;
+ margin-bottom: 0;
+ padding: 8px 12px;
}
-.cg-field:last-child {
- margin-bottom: 0;
+.cg-field:nth-child(odd) {
+ background: #f8f8f8;
+}
+
+.cg-field:nth-child(even) {
+ background: #fff;
}
.cg-field > label {
@@ -301,12 +368,98 @@
accent-color: var(--md-primary-fg-color);
}
+/* Inline field: label + control side by side */
+.cg-inline-field {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.cg-inline-field > label {
+ margin-bottom: 0;
+ width: 60%;
+ flex-shrink: 0;
+}
+
+.cg-inline-field > input[type="text"],
+.cg-inline-field > select {
+ padding: 4px 10px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-family: inherit;
+ box-sizing: border-box;
+ line-height: 1.4;
+ background: #fff;
+}
+
+.cg-inline-field > input[type="text"]:focus,
+.cg-inline-field > select:focus {
+ border-color: var(--md-primary-fg-color);
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
+}
+
+/* Button group toggle */
+.cg-btn-group {
+ display: flex;
+ flex-shrink: 0;
+}
+
+.cg-btn-group label {
+ cursor: pointer;
+ margin: 0;
+}
+
+.cg-btn-group input[type="radio"] {
+ display: none;
+}
+
+.cg-btn-group span {
+ display: block;
+ padding: 4px 14px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ line-height: 1.4;
+ border: 1px solid #ccc;
+ color: #555;
+ background: #fff;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
+ user-select: none;
+}
+
+.cg-btn-group label:first-child span {
+ border-radius: 4px 0 0 4px;
+}
+
+.cg-btn-group label:last-child span {
+ border-radius: 0 4px 4px 0;
+}
+
+.cg-btn-group label + label span {
+ margin-left: -1px;
+}
+
+.cg-btn-group input[type="radio"]:checked + span {
+ background: var(--md-primary-fg-color);
+ color: #fff;
+ border-color: var(--md-primary-fg-color);
+ position: relative;
+ z-index: 1;
+}
+
.cg-feature-grid {
display: flex;
flex-direction: column;
gap: 5px;
}
+.cg-feature-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
.cg-feature-grid label {
display: flex;
align-items: center;
@@ -319,6 +472,24 @@
accent-color: var(--md-primary-fg-color);
}
+.cg-btn-configure {
+ background: none;
+ border: 1px solid var(--md-primary-fg-color);
+ border-radius: 10px;
+ color: var(--md-primary-fg-color);
+ font-size: 0.68rem;
+ font-family: inherit;
+ padding: 1px 10px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s, color 0.15s;
+}
+
+.cg-btn-configure:hover {
+ background: var(--md-primary-fg-color);
+ color: #fff;
+}
+
/* Repeatable rows */
.cg-repeatable-row {
display: flex;
@@ -392,6 +563,20 @@ body[data-md-color-scheme="slate"] .cg-modal-title {
color: #ddd;
}
+body[data-md-color-scheme="slate"] .cg-modal-desc {
+ color: #777;
+}
+
+body[data-md-color-scheme="slate"] .cg-modal-reset {
+ border-color: #555;
+ color: #888;
+}
+
+body[data-md-color-scheme="slate"] .cg-modal-reset:hover {
+ border-color: #888;
+ color: #ddd;
+}
+
body[data-md-color-scheme="slate"] .cg-modal-close {
color: #777;
}
@@ -442,6 +627,18 @@ body[data-md-color-scheme="slate"] .cg-output-wrap pre {
border-color: #444;
}
+body[data-md-color-scheme="slate"] .cg-field:nth-child(odd) {
+ background: #232334;
+}
+
+body[data-md-color-scheme="slate"] .cg-field:nth-child(even) {
+ background: #1e1e2e;
+}
+
+body[data-md-color-scheme="slate"] .cg-panel-desc {
+ color: #777;
+}
+
body[data-md-color-scheme="slate"] .cg-field > label {
color: #aaa;
}
@@ -454,6 +651,18 @@ body[data-md-color-scheme="slate"] .cg-field select {
color: #ddd;
}
+body[data-md-color-scheme="slate"] .cg-btn-group span {
+ background: #2a2a3a;
+ border-color: #555;
+ color: #aaa;
+}
+
+body[data-md-color-scheme="slate"] .cg-btn-group input[type="radio"]:checked + span {
+ background: var(--md-primary-fg-color);
+ color: #fff;
+ border-color: var(--md-primary-fg-color);
+}
+
body[data-md-color-scheme="slate"] .cg-checkbox label {
color: #ccc;
}
diff --git a/docs/static/js/config-generator.js b/docs/static/js/config-generator.js
index 5c1bfcd7..529a6bdd 100644
--- a/docs/static/js/config-generator.js
+++ b/docs/static/js/config-generator.js
@@ -4,38 +4,32 @@
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: "auth-file", env: "NTFY_AUTH_FILE", section: "auth" },
+ { key: "auth-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth", def: "read-write" },
{ 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-cache-dir", env: "NTFY_ATTACHMENT_CACHE_DIR", section: "attach" },
{ 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-file", env: "NTFY_CACHE_FILE", section: "cache" },
{ 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-file", env: "NTFY_WEB_PUSH_FILE", section: "webpush" },
{ 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-listen", env: "NTFY_SMTP_SERVER_LISTEN", section: "smtp-in" },
{ 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",
@@ -80,6 +74,7 @@
val = el.value.trim();
if (!val) return;
}
+ if (val && c.def && val === c.def) return;
if (val) values[c.key] = val;
});
@@ -146,6 +141,7 @@
upstream: "# Upstream",
};
var lastSection = "";
+ var hadAuth = false;
CONFIG.forEach(function (c) {
if (!(c.key in values)) return;
@@ -154,6 +150,7 @@
if (sections[c.section]) lines.push(sections[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");
@@ -162,36 +159,54 @@
}
});
+ // Find where auth section ends to insert users/acls/tokens there
+ var authInsertIdx = lines.length;
+ if (hadAuth) {
+ for (var 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; }
+ authInsertIdx = j + 1;
+ }
+ break;
+ }
+ }
+ }
+
+ var authExtra = [];
if (values["_auth-users"]) {
- if (lastSection !== "auth") { lines.push(""); lines.push("# Access control"); }
- lines.push("auth-users:");
+ if (!hadAuth) { authExtra.push(""); authExtra.push("# Access control"); hadAuth = true; }
+ authExtra.push("auth-users:");
values["_auth-users"].forEach(function (u) {
- lines.push(' - "' + u.username + ":" + u.password + ":" + u.role + '"');
+ authExtra.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:");
+ if (!hadAuth) { authExtra.push(""); authExtra.push("# Access control"); hadAuth = true; }
+ authExtra.push("auth-access:");
values["_auth-acls"].forEach(function (a) {
- lines.push(' - "' + (a.user || "*") + ":" + a.topic + ":" + a.permission + '"');
+ authExtra.push(' - "' + (a.user || "*") + ":" + a.topic + ":" + a.permission + '"');
});
}
if (values["_auth-tokens"]) {
- lines.push("auth-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;
- lines.push(' - "' + entry + '"');
+ authExtra.push(' - "' + entry + '"');
});
}
- return lines.join("\n");
- }
+ // Splice auth extras into the right position
+ if (authExtra.length) {
+ lines.splice.apply(lines, [authInsertIdx, 0].concat(authExtra));
+ }
- function dockerPath(p) {
- return DOCKER_PATH_MAP[p] || p;
+ return lines.join("\n");
}
function generateDockerCompose(values) {
@@ -207,8 +222,6 @@
var val = values[c.key];
if (c.type === "bool") {
val = "true";
- } else {
- val = dockerPath(val);
}
if (val.indexOf("$") !== -1) {
val = val.replace(/\$/g, "$$$$");
@@ -243,12 +256,11 @@
}
lines.push(" volumes:");
- lines.push(" - ./:/var/lib/ntfy");
+ lines.push(" - /var/cache/ntfy:/var/cache/ntfy");
+ lines.push(" - /etc/ntfy:/etc/ntfy");
lines.push(" ports:");
-
- var listen = values["listen-http"] || ":80";
- var port = listen.replace(/.*:/, "");
- lines.push(' - "8080:' + port + '"');
+ lines.push(' - "80:80"');
+ lines.push(" restart: unless-stopped");
return lines.join("\n");
}
@@ -449,10 +461,6 @@
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");
@@ -514,6 +522,14 @@
var emailInSection = modal.querySelector("#cg-email-in-section");
if (emailInSection) emailInSection.style.display = smtpInEnabled ? "" : "none";
+ // Show/hide configure buttons next to feature checkboxes
+ modal.querySelectorAll(".cg-btn-configure").forEach(function (btn) {
+ var row = btn.closest(".cg-feature-row");
+ if (!row) return;
+ var cb = row.querySelector('input[type="checkbox"]');
+ btn.style.display = (cb && cb.checked) ? "" : "none";
+ });
+
// If active nav tab got hidden, switch to General
var activeNav = modal.querySelector(".cg-nav-tab.active");
if (activeNav && activeNav.style.display === "none") {
@@ -554,6 +570,22 @@
proxyCheckbox.checked = proxyYes.checked;
}
+ // 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/signup radios → hidden checkboxes
+ var loginYes = modal.querySelector('input[name="cg-enable-login"][value="yes"]');
+ var loginHidden = modal.querySelector("#cg-enable-login-hidden");
+ if (loginYes && loginHidden) loginHidden.checked = loginYes.checked;
+
+ 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");
@@ -563,9 +595,17 @@
if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db");
}
if (isPrivate) {
- prefillSelect(modal, "auth-default-access", "deny-all");
+ // Set default access select to deny-all
+ if (accessSelect) accessSelect.value = "deny-all";
+ if (accessHidden) accessHidden.value = "deny-all";
+ // Enable login
+ var loginYesRadio = modal.querySelector('input[name="cg-enable-login"][value="yes"]');
+ if (loginYesRadio) loginYesRadio.checked = true;
+ if (loginHidden) loginHidden.checked = true;
} else {
- prefillSelect(modal, "auth-default-access", "read-write");
+ // Open server: reset default access to read-write
+ if (accessSelect) accessSelect.value = "read-write";
+ if (accessHidden) accessHidden.value = "read-write";
}
if (cacheEnabled) {
@@ -661,8 +701,54 @@
document.body.style.overflow = "";
}
+ var resetBtn = document.getElementById("cg-reset-btn");
+
+ function resetAll() {
+ // Reset all text/password inputs
+ modal.querySelectorAll('input[type="text"], input[type="password"]').forEach(function (el) {
+ el.value = "";
+ });
+ // Uncheck all checkboxes
+ modal.querySelectorAll('input[type="checkbox"]').forEach(function (el) {
+ 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) {
@@ -679,6 +765,14 @@
});
});
+ // Configure buttons in feature grid
+ modal.querySelectorAll(".cg-btn-configure").forEach(function (btn) {
+ btn.addEventListener("click", function () {
+ var 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 () {