More config generator

This commit is contained in:
binwiederhier
2026-03-10 21:11:27 -04:00
parent 19d1618bb8
commit 3402510b47
3 changed files with 431 additions and 110 deletions
+213 -4
View File
@@ -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;
}
+131 -37
View File
@@ -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 () {