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

View File

@@ -137,20 +137,28 @@ using Docker Compose (i.e. `docker-compose.yml`):
## Config generator
This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point. Please refer to the relevant sections in the doc for more details.
<button type="button" id="cg-open-btn" class="cg-open-btn">Open config generator</button>
<div id="cg-modal" class="cg-modal" style="display:none">
<div class="cg-modal-backdrop"></div>
<div class="cg-modal-dialog">
<div class="cg-modal-header">
<div class="cg-modal-header-left">
<span class="cg-modal-title">Config generator</span>
<span class="cg-modal-desc">This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point.</span>
</div>
<div class="cg-modal-header-actions">
<button type="button" id="cg-reset-btn" class="cg-modal-reset" title="Reset all values">Reset</button>
<button type="button" id="cg-close-btn" class="cg-modal-close" title="Close">&times;</button>
</div>
</div>
<div class="cg-modal-body">
<div id="cg-left">
<div class="cg-nav">
<div class="cg-nav-tab active" data-panel="cg-panel-general">General</div>
<div class="cg-nav-tab" data-panel="cg-panel-database" id="cg-nav-database" style="display:none">Database</div>
<div class="cg-nav-tab" data-panel="cg-panel-auth" id="cg-nav-auth" style="display:none">Access Control</div>
<div class="cg-nav-tab" data-panel="cg-panel-auth" id="cg-nav-auth" style="display:none">Users</div>
<div class="cg-nav-tab" data-panel="cg-panel-cache" id="cg-nav-cache" style="display:none">Message Cache</div>
<div class="cg-nav-tab" data-panel="cg-panel-attach" id="cg-nav-attach" style="display:none">Attachments</div>
<div class="cg-nav-tab" data-panel="cg-panel-webpush" id="cg-nav-webpush" style="display:none">Web Push</div>
@@ -158,129 +166,137 @@ using Docker Compose (i.e. `docker-compose.yml`):
</div>
<div class="cg-panels">
<div class="cg-panel active" id="cg-panel-general">
<div class="cg-field">
<label>What's your ntfy service URL?</label>
<div class="cg-field cg-inline-field">
<label>What URL will ntfy be reachable on?</label>
<input type="text" data-key="base-url" placeholder="https://ntfy.example.com">
</div>
<div class="cg-field">
<label>Listen address</label>
<input type="text" data-key="listen-http" placeholder=":80">
</div>
<div class="cg-field">
<label>Will ntfy run behind a proxy (e.g. nginx, Caddy, Apache2)?</label>
<div class="cg-radio-group">
<label><input type="radio" name="cg-proxy" value="no" checked> No</label>
<label><input type="radio" name="cg-proxy" value="yes"> Yes</label>
<div class="cg-field cg-inline-field">
<label>Will ntfy run behind a proxy (e.g. nginx, Caddy)? <a href="/config/#behind-a-proxy-tls-etc" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-proxy" value="no" checked><span>No</span></label>
<label><input type="radio" name="cg-proxy" value="yes"><span>Yes</span></label>
</div>
</div>
<div class="cg-field">
<label>Is this an open server or a private server?</label>
<div class="cg-radio-group">
<label><input type="radio" name="cg-server-type" value="open" checked> Open (anyone can read/write)</label>
<label><input type="radio" name="cg-server-type" value="private"> Private (requires authentication)</label>
<div class="cg-field cg-inline-field">
<label>Is this an open or private server? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-server-type" value="open" checked><span>Open</span></label>
<label><input type="radio" name="cg-server-type" value="private"><span>Private</span></label>
</div>
</div>
<div class="cg-field">
<label>Will iOS/iPhone users use this server?</label>
<div class="cg-radio-group">
<label><input type="radio" name="cg-ios" value="no" checked> No</label>
<label><input type="radio" name="cg-ios" value="yes"> Yes</label>
<div class="cg-field cg-inline-field">
<label>Will iOS/iPhone users use this server? <a href="/config/#ios-instant-notifications" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-ios" value="no" checked><span>No</span></label>
<label><input type="radio" name="cg-ios" value="yes"><span>Yes</span></label>
</div>
</div>
<div class="cg-field">
<label>Do you want to use ntfy as a UnifiedPush distributor?</label>
<div class="cg-radio-group">
<label><input type="radio" name="cg-unifiedpush" value="no" checked> No</label>
<label><input type="radio" name="cg-unifiedpush" value="yes"> Yes</label>
<div class="cg-field cg-inline-field">
<label>Do you want to use ntfy as a UnifiedPush distributor? <a href="/config/#example-unifiedpush" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-unifiedpush" value="no" checked><span>No</span></label>
<label><input type="radio" name="cg-unifiedpush" value="yes"><span>Yes</span></label>
</div>
</div>
<div class="cg-field">
<label>Which features do you want to enable?</label>
<div class="cg-feature-grid">
<label><input type="checkbox" id="cg-feat-auth"> User management and access control</label>
<label><input type="checkbox" id="cg-feat-cache"> Persistent message cache</label>
<label><input type="checkbox" id="cg-feat-attach"> Attachments</label>
<label><input type="checkbox" id="cg-feat-webpush"> Web push</label>
<label><input type="checkbox" id="cg-feat-smtp-out"> Email notifications (outgoing)</label>
<label><input type="checkbox" id="cg-feat-smtp-in"> Email publishing (incoming)</label>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-auth"> User management and access control</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-auth" style="display:none">Configure</button></div>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-cache"> Persistent message cache</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-cache" style="display:none">Configure</button></div>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-attach"> Attachments</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-attach" style="display:none">Configure</button></div>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-webpush"> Web push</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-webpush" style="display:none">Configure</button></div>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-smtp-out"> Email notifications</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-email" style="display:none">Configure</button></div>
<div class="cg-feature-row"><label><input type="checkbox" id="cg-feat-smtp-in"> Email publishing</label><button type="button" class="cg-btn-configure" data-panel="cg-panel-email" style="display:none">Configure</button></div>
</div>
</div>
<div class="cg-field" id="cg-wizard-db" style="display:none">
<label>Which database backend would you like to use?</label>
<div class="cg-radio-group">
<label><input type="radio" name="cg-db-type" value="sqlite" checked> SQLite</label>
<label><input type="radio" name="cg-db-type" value="postgres"> PostgreSQL</label>
<div class="cg-field cg-inline-field" id="cg-wizard-db" style="display:none">
<label>Which database backend? <a href="/config/#database-options" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-db-type" value="sqlite" checked><span>SQLite</span></label>
<label><input type="radio" name="cg-db-type" value="postgres"><span>PostgreSQL</span></label>
</div>
</div>
</div>
<div class="cg-panel" id="cg-panel-auth">
<div class="cg-field">
<label>Auth file</label>
<div class="cg-panel-desc">Configure user management, access control, and provisioned users/ACLs. See <a href="/config/#access-control" target="_blank">access control</a> for details.</div>
<div class="cg-field cg-inline-field">
<label>Where should the user database be stored?</label>
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
</div>
<div class="cg-field">
<label>Default access</label>
<select data-key="auth-default-access">
<option value="">-- select --</option>
<option value="read-write">read-write</option>
<option value="read-only">read-only</option>
<option value="write-only">write-only</option>
<option value="deny-all">deny-all</option>
<div class="cg-field cg-inline-field">
<label>What is the default access policy? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<select id="cg-default-access-select">
<option value="read-write" selected>Read &amp; Write</option>
<option value="read-only">Read Only</option>
<option value="write-only">Write Only</option>
<option value="deny-all">Deny All</option>
</select>
</div>
<div class="cg-checkbox">
<input type="checkbox" data-key="enable-login" id="cg-enable-login">
<label for="cg-enable-login">Enable login</label>
<div class="cg-field cg-inline-field">
<label>Enable login page?</label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-enable-login" value="no" checked><span>No</span></label>
<label><input type="radio" name="cg-enable-login" value="yes"><span>Yes</span></label>
</div>
<div class="cg-checkbox">
<input type="checkbox" data-key="enable-signup" id="cg-enable-signup">
<label for="cg-enable-signup">Enable signup</label>
</div>
<div class="cg-field cg-inline-field">
<label>Enable signup?</label>
<div class="cg-btn-group">
<label><input type="radio" name="cg-enable-signup" value="no" checked><span>No</span></label>
<label><input type="radio" name="cg-enable-signup" value="yes"><span>Yes</span></label>
</div>
</div>
<input type="hidden" data-key="auth-default-access">
<input type="checkbox" data-key="enable-login" id="cg-enable-login-hidden" style="display:none">
<input type="checkbox" data-key="enable-signup" id="cg-enable-signup-hidden" style="display:none">
<div class="cg-field">
<label>Provisioned users</label>
<label>Provisioned users <a href="/config/#users-and-roles" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-repeatable-container" id="cg-auth-users-container"></div>
<button type="button" class="cg-btn-add" data-add-type="user">+ Add user</button>
</div>
<div class="cg-field">
<label>Provisioned ACLs</label>
<label>Provisioned ACLs <a href="/config/#access-control-list-acl" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-repeatable-container" id="cg-auth-acls-container"></div>
<button type="button" class="cg-btn-add" data-add-type="acl">+ Add ACL</button>
</div>
<div class="cg-field">
<label>Provisioned tokens</label>
<label>Provisioned tokens <a href="/config/#access-tokens" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
<div class="cg-repeatable-container" id="cg-auth-tokens-container"></div>
<button type="button" class="cg-btn-add" data-add-type="token">+ Add token</button>
</div>
</div>
<div class="cg-panel" id="cg-panel-cache">
<div class="cg-field" id="cg-cache-file-field">
<label>Cache file</label>
<div class="cg-panel-desc">Configure the message cache to allow devices to retrieve missed notifications. See <a href="/config/#message-cache" target="_blank">message cache</a> for details.</div>
<div class="cg-field cg-inline-field" id="cg-cache-file-field">
<label>Where should the cache be stored?</label>
<input type="text" data-key="cache-file" placeholder="/var/cache/ntfy/cache.db">
</div>
<div class="cg-field">
<label>Cache duration</label>
<div class="cg-field cg-inline-field">
<label>How long should messages be cached?</label>
<input type="text" data-key="cache-duration" placeholder="12h">
</div>
</div>
<div class="cg-panel" id="cg-panel-attach">
<div class="cg-field">
<label>Cache directory</label>
<div class="cg-panel-desc">Allow users to upload and attach files to notifications. See <a href="/config/#attachments" target="_blank">attachments</a> for details.</div>
<div class="cg-field cg-inline-field">
<label>Where should attachments be stored?</label>
<input type="text" data-key="attachment-cache-dir" placeholder="/var/cache/ntfy/attachments">
</div>
<div class="cg-field">
<label>File size limit</label>
<div class="cg-field cg-inline-field">
<label>Max file size per attachment?</label>
<input type="text" data-key="attachment-file-size-limit" placeholder="15M">
</div>
<div class="cg-field">
<label>Total size limit</label>
<div class="cg-field cg-inline-field">
<label>Total attachment storage limit?</label>
<input type="text" data-key="attachment-total-size-limit" placeholder="5G">
</div>
<div class="cg-field">
<label>Expiry duration</label>
<div class="cg-field cg-inline-field">
<label>How long before attachments expire?</label>
<input type="text" data-key="attachment-expiry-duration" placeholder="3h">
</div>
</div>
<div class="cg-panel" id="cg-panel-webpush">
<div class="cg-panel-desc">Enable browser push notifications via the Web Push API. VAPID keys are generated automatically. See <a href="/config/#web-push" target="_blank">web push</a> for details.</div>
<div class="cg-field">
<label>Public key</label>
<input type="text" data-key="web-push-public-key" placeholder="Public key" readonly>
@@ -300,6 +316,7 @@ using Docker Compose (i.e. `docker-compose.yml`):
</div>
</div>
<div class="cg-panel" id="cg-panel-email">
<div class="cg-panel-desc">Configure outgoing email notifications and/or incoming email publishing. See <a href="/config/#e-mail-notifications" target="_blank">email notifications</a> and <a href="/config/#e-mail-publishing" target="_blank">email publishing</a> for details.</div>
<div id="cg-email-out-section" style="display:none">
<div class="cg-field"><label><strong>Outgoing (notifications)</strong></label></div>
<div class="cg-field">
@@ -336,6 +353,7 @@ using Docker Compose (i.e. `docker-compose.yml`):
</div>
</div>
<div class="cg-panel" id="cg-panel-database">
<div class="cg-panel-desc">Configure the PostgreSQL connection. See <a href="/config/#postgresql-experimental" target="_blank">PostgreSQL</a> for details.</div>
<div class="cg-field">
<label>Database URL</label>
<input type="text" data-key="database-url" placeholder="postgres://user:pass@host:5432/ntfy">

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;
}

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 () {