mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-03-18 13:20:48 +01:00
Continued
This commit is contained in:
132
docs/config.md
132
docs/config.md
@@ -137,65 +137,83 @@ using Docker Compose (i.e. `docker-compose.yml`):
|
||||
|
||||
## Config generator
|
||||
|
||||
Use this interactive tool to build your ntfy configuration. Select options below and copy the generated config.
|
||||
|
||||
<div id="config-generator-app">
|
||||
<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">
|
||||
<span class="cg-modal-title">Config generator</span>
|
||||
<button type="button" id="cg-close-btn" class="cg-modal-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="cg-modal-body">
|
||||
<div id="cg-left">
|
||||
<div class="cg-wizard">
|
||||
<div class="cg-wizard-step">
|
||||
<label class="cg-wizard-label">What's your ntfy service URL?</label>
|
||||
<input type="text" data-key="base-url" placeholder="https://ntfy.example.com" class="cg-wizard-input">
|
||||
<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-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>
|
||||
<div class="cg-nav-tab" data-panel="cg-panel-email" id="cg-nav-email" style="display:none">Email</div>
|
||||
</div>
|
||||
<div class="cg-wizard-step">
|
||||
<label class="cg-wizard-label">Are you running ntfy behind a proxy?</label>
|
||||
<div class="cg-wizard-toggle">
|
||||
<label><input type="checkbox" data-key="behind-proxy" id="cg-behind-proxy"> Yes, behind nginx/Apache/Caddy</label>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="cg-wizard-step">
|
||||
<label class="cg-wizard-label">Is this an open server or a private server?</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="cg-wizard-step">
|
||||
<label class="cg-wizard-label">Which features do you want to enable?</label>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<label><input type="checkbox" id="cg-feat-upstream"> Upstream server (iOS push)</label>
|
||||
<label><input type="checkbox" id="cg-feat-metrics"> Prometheus metrics</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-wizard-step" id="cg-wizard-db" style="display:none">
|
||||
<label class="cg-wizard-label">Which database backend would you like to use?</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cg-details">
|
||||
<div class="cg-detail-section" id="cg-detail-basic">
|
||||
<div class="cg-detail-heading">Server</div>
|
||||
<div class="cg-field">
|
||||
<label>Listen address</label>
|
||||
<input type="text" data-key="listen-http" placeholder=":80">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-db-postgres" style="display:none">
|
||||
<div class="cg-detail-heading">Database (PostgreSQL)</div>
|
||||
<div class="cg-field">
|
||||
<label>Database URL</label>
|
||||
<input type="text" data-key="database-url" placeholder="postgres://user:pass@host:5432/ntfy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-auth" style="display:none">
|
||||
<div class="cg-detail-heading">Access Control</div>
|
||||
<div class="cg-panel" id="cg-panel-auth">
|
||||
<div class="cg-field">
|
||||
<label>Auth file</label>
|
||||
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
||||
@@ -234,8 +252,7 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<button type="button" class="cg-btn-add" data-add-type="token">+ Add token</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-cache" style="display:none">
|
||||
<div class="cg-detail-heading">Message Cache</div>
|
||||
<div class="cg-panel" id="cg-panel-cache">
|
||||
<div class="cg-field" id="cg-cache-file-field">
|
||||
<label>Cache file</label>
|
||||
<input type="text" data-key="cache-file" placeholder="/var/cache/ntfy/cache.db">
|
||||
@@ -245,8 +262,7 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<input type="text" data-key="cache-duration" placeholder="12h">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-attach" style="display:none">
|
||||
<div class="cg-detail-heading">Attachments</div>
|
||||
<div class="cg-panel" id="cg-panel-attach">
|
||||
<div class="cg-field">
|
||||
<label>Cache directory</label>
|
||||
<input type="text" data-key="attachment-cache-dir" placeholder="/var/cache/ntfy/attachments">
|
||||
@@ -264,16 +280,16 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<input type="text" data-key="attachment-expiry-duration" placeholder="3h">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-webpush" style="display:none">
|
||||
<div class="cg-detail-heading">Web Push</div>
|
||||
<div class="cg-panel" id="cg-panel-webpush">
|
||||
<div class="cg-field">
|
||||
<label>Public key</label>
|
||||
<input type="text" data-key="web-push-public-key" placeholder="Public key">
|
||||
<input type="text" data-key="web-push-public-key" placeholder="Public key" readonly>
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Private key</label>
|
||||
<input type="text" data-key="web-push-private-key" placeholder="Private key">
|
||||
<input type="text" data-key="web-push-private-key" placeholder="Private key" readonly>
|
||||
</div>
|
||||
<button type="button" id="cg-regen-keys" class="cg-btn-add" style="margin-bottom:12px">Regenerate keys</button>
|
||||
<div class="cg-field">
|
||||
<label>Web push file</label>
|
||||
<input type="text" data-key="web-push-file" placeholder="/var/lib/ntfy/webpush.db">
|
||||
@@ -283,8 +299,9 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<input type="text" data-key="web-push-email-address" placeholder="admin@example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-smtp-out" style="display:none">
|
||||
<div class="cg-detail-heading">Email Notifications (Outgoing)</div>
|
||||
<div class="cg-panel" id="cg-panel-email">
|
||||
<div id="cg-email-out-section" style="display:none">
|
||||
<div class="cg-field"><label><strong>Outgoing (notifications)</strong></label></div>
|
||||
<div class="cg-field">
|
||||
<label>SMTP server address</label>
|
||||
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
||||
@@ -302,8 +319,8 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-detail-section" id="cg-detail-smtp-in" style="display:none">
|
||||
<div class="cg-detail-heading">Email Publishing (Incoming)</div>
|
||||
<div id="cg-email-in-section" style="display:none">
|
||||
<div class="cg-field"><label><strong>Incoming (publishing)</strong></label></div>
|
||||
<div class="cg-field">
|
||||
<label>Listen address</label>
|
||||
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
||||
@@ -317,18 +334,29 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
||||
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-panel" id="cg-panel-database">
|
||||
<div class="cg-field">
|
||||
<label>Database URL</label>
|
||||
<input type="text" data-key="database-url" placeholder="postgres://user:pass@host:5432/ntfy">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" data-key="upstream-base-url">
|
||||
<input type="checkbox" data-key="enable-metrics" style="display:none">
|
||||
<input type="checkbox" data-key="behind-proxy" id="cg-behind-proxy" style="display:none">
|
||||
</div>
|
||||
</div>
|
||||
<div id="cg-right">
|
||||
<div class="cg-tabs">
|
||||
<div class="cg-tab active" data-format="server-yml">server.yml</div>
|
||||
<div class="cg-tab" data-format="docker-compose">docker-compose.yml</div>
|
||||
<div class="cg-output-tabs">
|
||||
<div class="cg-output-tab active" data-format="server-yml">server.yml</div>
|
||||
<div class="cg-output-tab" data-format="docker-compose">docker-compose.yml</div>
|
||||
<div class="cg-output-tab" data-format="env-vars">Env variables</div>
|
||||
<button type="button" id="cg-copy-btn" class="cg-btn-copy" title="Copy to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
||||
</div>
|
||||
<div class="cg-output-wrap">
|
||||
<pre><code id="cg-code"><span class="cg-empty-msg">Configure options on the left to generate your config...</span></code></pre>
|
||||
<div id="cg-warnings" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
563
docs/static/css/config-generator.css
vendored
563
docs/static/css/config-generator.css
vendored
@@ -1,156 +1,245 @@
|
||||
/* Config Generator */
|
||||
|
||||
#config-generator-app {
|
||||
/* Open button */
|
||||
.cg-open-btn {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
background: var(--md-primary-fg-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.cg-open-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Modal overlay */
|
||||
.cg-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cg-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cg-modal-dialog {
|
||||
position: absolute;
|
||||
inset: 24px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin: 1em 0 2em;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#cg-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#cg-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 76px;
|
||||
align-self: flex-start;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Wizard questions */
|
||||
.cg-wizard {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.cg-wizard-step {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cg-wizard-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cg-wizard-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cg-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cg-modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cg-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cg-modal-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cg-wizard-input {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.82rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cg-wizard-input:focus {
|
||||
border-color: var(--md-primary-fg-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
|
||||
}
|
||||
|
||||
.cg-wizard-toggle label {
|
||||
/* Modal body: left + right */
|
||||
.cg-modal-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cg-wizard-toggle input[type="checkbox"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.cg-radio-group {
|
||||
/* Left panel */
|
||||
#cg-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #ddd;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cg-nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.cg-radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 400;
|
||||
font-size: 0.82rem;
|
||||
.cg-nav-tab {
|
||||
padding: 9px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: #777;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
user-select: none;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cg-radio-group input[type="radio"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
.cg-nav-tab:hover {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.cg-feature-grid {
|
||||
.cg-nav-tab.active {
|
||||
color: var(--md-primary-fg-color);
|
||||
border-bottom-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.cg-panels {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.cg-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cg-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Right panel */
|
||||
#cg-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cg-feature-grid label {
|
||||
.cg-output-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.cg-output-tab {
|
||||
padding: 9px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
color: #777;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cg-output-tab:hover {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.cg-output-tab.active {
|
||||
color: var(--md-primary-fg-color);
|
||||
border-bottom-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.cg-btn-copy {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
color: #777;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
padding: 9px 10px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.cg-feature-grid input[type="checkbox"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
.cg-btn-copy:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Detail sections */
|
||||
#cg-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.cg-output-wrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.cg-detail-section {
|
||||
.cg-output-wrap pre {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
min-height: 100px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.cg-detail-heading {
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--md-primary-fg-color);
|
||||
.cg-empty-msg {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cg-warning {
|
||||
padding: 6px 10px;
|
||||
margin-top: 8px;
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.cg-field {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cg-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cg-field label {
|
||||
.cg-field > label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 3px;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.78rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@@ -161,7 +250,7 @@
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.78rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
@@ -179,7 +268,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cg-checkbox input[type="checkbox"] {
|
||||
@@ -188,11 +277,48 @@
|
||||
|
||||
.cg-checkbox label {
|
||||
font-weight: 500;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.78rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cg-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cg-radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 400;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cg-radio-group input[type="radio"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.cg-feature-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.cg-feature-grid label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cg-feature-grid input[type="checkbox"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
/* Repeatable rows */
|
||||
.cg-repeatable-row {
|
||||
display: flex;
|
||||
@@ -209,7 +335,7 @@
|
||||
padding: 5px 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -243,7 +369,7 @@
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.75rem;
|
||||
color: #777;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -253,127 +379,96 @@
|
||||
color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.cg-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.cg-tab {
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
color: #777;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cg-tab:hover {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.cg-tab.active {
|
||||
color: var(--md-primary-fg-color);
|
||||
border-bottom-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
/* Output panel */
|
||||
.cg-output-wrap {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cg-output-wrap pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
min-height: 120px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.cg-btn-copy {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
color: #777;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.cg-btn-copy:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cg-empty-msg {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
body[data-md-color-scheme="slate"] #cg-right {
|
||||
background: #2e303e;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-wizard {
|
||||
background: #2e303e;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-wizard-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-wizard-input {
|
||||
body[data-md-color-scheme="slate"] .cg-modal-dialog {
|
||||
background: #1e1e2e;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-modal-header {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-modal-title {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-wizard-toggle label,
|
||||
body[data-md-color-scheme="slate"] .cg-radio-group label,
|
||||
body[data-md-color-scheme="slate"] .cg-feature-grid label {
|
||||
color: #ccc;
|
||||
body[data-md-color-scheme="slate"] .cg-modal-close {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-detail-section {
|
||||
body[data-md-color-scheme="slate"] .cg-modal-close:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] #cg-left {
|
||||
border-right-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-nav {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-nav-tab {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-nav-tab:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-output-tabs {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-output-tab {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-output-tab:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-copy {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-copy:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-output-wrap pre {
|
||||
background: #161620;
|
||||
color: #ddd;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-detail-heading {
|
||||
color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-field label {
|
||||
body[data-md-color-scheme="slate"] .cg-field > label {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-field input[type="text"],
|
||||
body[data-md-color-scheme="slate"] .cg-field input[type="password"],
|
||||
body[data-md-color-scheme="slate"] .cg-field select {
|
||||
background: #1e1e2e;
|
||||
background: #2a2a3a;
|
||||
border-color: #555;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-checkbox label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-radio-group label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-feature-grid label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-repeatable-row input,
|
||||
body[data-md-color-scheme="slate"] .cg-repeatable-row select {
|
||||
background: #1e1e2e;
|
||||
background: #2a2a3a;
|
||||
border-color: #555;
|
||||
color: #ddd;
|
||||
}
|
||||
@@ -388,46 +483,28 @@ body[data-md-color-scheme="slate"] .cg-btn-add {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-tabs {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-tab {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-tab:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-output-wrap pre {
|
||||
background: #1e1e2e;
|
||||
color: #ddd;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-copy {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-copy:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-checkbox label {
|
||||
color: #ccc;
|
||||
body[data-md-color-scheme="slate"] .cg-warning {
|
||||
background: #3a2e00;
|
||||
color: #ffc107;
|
||||
border-color: #665200;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
#config-generator-app {
|
||||
.cg-modal-dialog {
|
||||
inset: 8px;
|
||||
}
|
||||
|
||||
.cg-modal-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#cg-right {
|
||||
width: 100%;
|
||||
position: static;
|
||||
max-height: none;
|
||||
#cg-left {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#cg-right {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
541
docs/static/js/config-generator.js
vendored
541
docs/static/js/config-generator.js
vendored
@@ -29,7 +29,6 @@
|
||||
{ key: "smtp-server-domain", env: "NTFY_SMTP_SERVER_DOMAIN", section: "smtp-in" },
|
||||
{ key: "smtp-server-addr-prefix", env: "NTFY_SMTP_SERVER_ADDR_PREFIX", section: "smtp-in" },
|
||||
{ key: "upstream-base-url", env: "NTFY_UPSTREAM_BASE_URL", section: "upstream" },
|
||||
{ key: "enable-metrics", env: "NTFY_ENABLE_METRICS", section: "metrics", type: "bool" },
|
||||
];
|
||||
|
||||
var DOCKER_PATH_MAP = {
|
||||
@@ -37,31 +36,42 @@
|
||||
"/var/cache/ntfy/attachments": "/var/lib/ntfy/attachments",
|
||||
};
|
||||
|
||||
// Feature checkbox ID → detail section ID
|
||||
var FEATURE_MAP = {
|
||||
"cg-feat-cache": "cg-detail-cache",
|
||||
"cg-feat-attach": "cg-detail-attach",
|
||||
"cg-feat-webpush": "cg-detail-webpush",
|
||||
"cg-feat-smtp-out": "cg-detail-smtp-out",
|
||||
"cg-feat-smtp-in": "cg-detail-smtp-in",
|
||||
// Feature checkbox → nav tab ID
|
||||
var 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 gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return values;
|
||||
var modal = document.getElementById("cg-modal");
|
||||
if (!modal) return values;
|
||||
|
||||
CONFIG.forEach(function (c) {
|
||||
var el = gen.querySelector('[data-key="' + c.key + '"]');
|
||||
var el = modal.querySelector('[data-key="' + c.key + '"]');
|
||||
if (!el) return;
|
||||
|
||||
// Skip fields in hidden detail sections
|
||||
var section = el.closest(".cg-detail-section");
|
||||
if (section && section.style.display === "none") return;
|
||||
// Skip fields in hidden panels (feature not enabled)
|
||||
var panel = el.closest(".cg-panel");
|
||||
if (panel) {
|
||||
// Panel hidden directly (e.g. PostgreSQL panel when SQLite selected)
|
||||
if (panel.style.display === "none") 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip hidden individual fields (e.g. auth-file when using PostgreSQL)
|
||||
var field = el.closest(".cg-field");
|
||||
if (field && field.style.display === "none") return;
|
||||
// Skip hidden individual fields or sections
|
||||
var ancestor = el.parentElement;
|
||||
while (ancestor && ancestor !== modal) {
|
||||
if (ancestor.style.display === "none") return;
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
|
||||
var val;
|
||||
if (c.type === "bool") {
|
||||
@@ -74,7 +84,7 @@
|
||||
});
|
||||
|
||||
// Provisioned users
|
||||
var userRows = gen.querySelectorAll(".cg-auth-user-row");
|
||||
var userRows = modal.querySelectorAll(".cg-auth-user-row");
|
||||
var users = [];
|
||||
userRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
@@ -87,7 +97,7 @@
|
||||
if (users.length) values["_auth-users"] = users;
|
||||
|
||||
// Provisioned ACLs
|
||||
var aclRows = gen.querySelectorAll(".cg-auth-acl-row");
|
||||
var aclRows = modal.querySelectorAll(".cg-auth-acl-row");
|
||||
var acls = [];
|
||||
aclRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
@@ -100,7 +110,7 @@
|
||||
if (acls.length) values["_auth-acls"] = acls;
|
||||
|
||||
// Provisioned tokens
|
||||
var tokenRows = gen.querySelectorAll(".cg-auth-token-row");
|
||||
var tokenRows = modal.querySelectorAll(".cg-auth-token-row");
|
||||
var tokens = [];
|
||||
tokenRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
@@ -112,6 +122,13 @@
|
||||
});
|
||||
if (tokens.length) values["_auth-tokens"] = tokens;
|
||||
|
||||
// UnifiedPush ACL
|
||||
var 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" });
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -127,7 +144,6 @@
|
||||
"smtp-out": "# Email notifications (outgoing)",
|
||||
"smtp-in": "# Email publishing (incoming)",
|
||||
upstream: "# Upstream",
|
||||
metrics: "# Monitoring",
|
||||
};
|
||||
var lastSection = "";
|
||||
|
||||
@@ -146,34 +162,28 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Auth users
|
||||
if (values["_auth-users"]) {
|
||||
if (lastSection !== "auth") { lines.push(""); lines.push("# Access control"); }
|
||||
lines.push("auth-users:");
|
||||
values["_auth-users"].forEach(function (u) {
|
||||
lines.push(" - username: " + u.username);
|
||||
lines.push(" password: " + u.password);
|
||||
lines.push(" role: " + u.role);
|
||||
lines.push(' - "' + u.username + ":" + u.password + ":" + u.role + '"');
|
||||
});
|
||||
}
|
||||
|
||||
// Auth ACLs
|
||||
if (values["_auth-acls"]) {
|
||||
if (!values["_auth-users"] && lastSection !== "auth") { lines.push(""); lines.push("# Access control"); }
|
||||
lines.push("auth-access:");
|
||||
values["_auth-acls"].forEach(function (a) {
|
||||
lines.push(" - user: " + (a.user || "*"));
|
||||
lines.push(" topic: " + a.topic);
|
||||
lines.push(" permission: " + a.permission);
|
||||
lines.push(' - "' + (a.user || "*") + ":" + a.topic + ":" + a.permission + '"');
|
||||
});
|
||||
}
|
||||
|
||||
// Auth tokens
|
||||
if (values["_auth-tokens"]) {
|
||||
lines.push("auth-tokens:");
|
||||
values["_auth-tokens"].forEach(function (t) {
|
||||
lines.push(" - user: " + t.user);
|
||||
lines.push(" token: " + t.token);
|
||||
if (t.label) lines.push(" label: " + t.label);
|
||||
var entry = t.user + ":" + t.token;
|
||||
if (t.label) entry += ":" + t.label;
|
||||
lines.push(' - "' + entry + '"');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,10 +208,8 @@
|
||||
if (c.type === "bool") {
|
||||
val = "true";
|
||||
} else {
|
||||
// Adjust paths for Docker
|
||||
val = dockerPath(val);
|
||||
}
|
||||
// Double $ in bcrypt hashes
|
||||
if (val.indexOf("$") !== -1) {
|
||||
val = val.replace(/\$/g, "$$$$");
|
||||
lines.push(" # Note: $ is doubled to $$ for docker-compose");
|
||||
@@ -209,34 +217,29 @@
|
||||
lines.push(" " + c.env + ": " + val);
|
||||
});
|
||||
|
||||
// Auth users in Docker
|
||||
if (values["_auth-users"]) {
|
||||
var usersVal = values["_auth-users"].map(function (u) {
|
||||
return u.username + ":" + u.password + ":" + u.role;
|
||||
}).join(",");
|
||||
usersVal = usersVal.replace(/\$/g, "$$$$");
|
||||
lines.push(" # Note: $ is doubled to $$ for docker-compose");
|
||||
values["_auth-users"].forEach(function (u, i) {
|
||||
var pw = u.password.replace(/\$/g, "$$$$");
|
||||
lines.push(" NTFY_AUTH_USERS_" + i + "_USERNAME: " + u.username);
|
||||
lines.push(" NTFY_AUTH_USERS_" + i + "_PASSWORD: " + pw);
|
||||
lines.push(" NTFY_AUTH_USERS_" + i + "_ROLE: " + u.role);
|
||||
});
|
||||
lines.push(" NTFY_AUTH_USERS: " + usersVal);
|
||||
}
|
||||
|
||||
// Auth ACLs in Docker
|
||||
if (values["_auth-acls"]) {
|
||||
values["_auth-acls"].forEach(function (a, i) {
|
||||
lines.push(" NTFY_AUTH_ACCESS_" + i + "_USER: " + (a.user || "*"));
|
||||
lines.push(" NTFY_AUTH_ACCESS_" + i + "_TOPIC: " + a.topic);
|
||||
lines.push(" NTFY_AUTH_ACCESS_" + i + "_PERMISSION: " + a.permission);
|
||||
});
|
||||
var aclsVal = values["_auth-acls"].map(function (a) {
|
||||
return (a.user || "*") + ":" + a.topic + ":" + a.permission;
|
||||
}).join(",");
|
||||
lines.push(" NTFY_AUTH_ACCESS: " + aclsVal);
|
||||
}
|
||||
|
||||
// Auth tokens in Docker
|
||||
if (values["_auth-tokens"]) {
|
||||
values["_auth-tokens"].forEach(function (t, i) {
|
||||
var tok = t.token.replace(/\$/g, "$$$$");
|
||||
lines.push(" NTFY_AUTH_TOKENS_" + i + "_USER: " + t.user);
|
||||
lines.push(" NTFY_AUTH_TOKENS_" + i + "_TOKEN: " + tok);
|
||||
if (t.label) lines.push(" NTFY_AUTH_TOKENS_" + i + "_LABEL: " + t.label);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
lines.push(" volumes:");
|
||||
@@ -250,15 +253,84 @@
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function generateEnvVars(values) {
|
||||
var lines = [];
|
||||
|
||||
CONFIG.forEach(function (c) {
|
||||
if (!(c.key in values)) return;
|
||||
var val = values[c.key];
|
||||
if (c.type === "bool") val = "true";
|
||||
var q = val.indexOf("$") !== -1 ? "'" : '"';
|
||||
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);
|
||||
}
|
||||
|
||||
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 + '"');
|
||||
}
|
||||
|
||||
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 + '"');
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Web Push VAPID key generation (P-256 ECDH)
|
||||
function generateVAPIDKeys() {
|
||||
return crypto.subtle.generateKey(
|
||||
{ name: "ECDH", namedCurve: "P-256" },
|
||||
true,
|
||||
["deriveBits"]
|
||||
).then(function (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]);
|
||||
// Extract raw 32-byte private key from PKCS#8 (last 32 bytes of the DER)
|
||||
var privBytes = privPkcs8.slice(privPkcs8.length - 32);
|
||||
return {
|
||||
publicKey: arrayToBase64Url(pubBytes),
|
||||
privateKey: arrayToBase64Url(privBytes)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function arrayToBase64Url(arr) {
|
||||
var str = "";
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
str += String.fromCharCode(arr[i]);
|
||||
}
|
||||
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function updateOutput() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return;
|
||||
var modal = document.getElementById("cg-modal");
|
||||
if (!modal) return;
|
||||
|
||||
var values = collectValues();
|
||||
var codeEl = gen.querySelector("#cg-code");
|
||||
var codeEl = modal.querySelector("#cg-code");
|
||||
if (!codeEl) return;
|
||||
|
||||
var activeTab = gen.querySelector(".cg-tab.active");
|
||||
var activeTab = modal.querySelector(".cg-output-tab.active");
|
||||
var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml";
|
||||
|
||||
var hasValues = false;
|
||||
@@ -274,150 +346,266 @@
|
||||
var output = "";
|
||||
if (format === "docker-compose") {
|
||||
output = generateDockerCompose(values);
|
||||
} else if (format === "env-vars") {
|
||||
output = generateEnvVars(values);
|
||||
} else {
|
||||
output = generateServerYml(values);
|
||||
}
|
||||
|
||||
codeEl.textContent = output;
|
||||
|
||||
// Validation warnings
|
||||
var warnings = validate(modal, values);
|
||||
var warningsEl = modal.querySelector("#cg-warnings");
|
||||
if (warningsEl) {
|
||||
if (warnings.length) {
|
||||
warningsEl.innerHTML = warnings.map(function (w) {
|
||||
return '<div class="cg-warning">' + w + '</div>';
|
||||
}).join("");
|
||||
warningsEl.style.display = "";
|
||||
} else {
|
||||
warningsEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set a field's value only if it is currently empty
|
||||
function prefill(gen, key, value) {
|
||||
var el = gen.querySelector('[data-key="' + key + '"]');
|
||||
function validate(modal, values) {
|
||||
var warnings = [];
|
||||
var baseUrl = values["base-url"] || "";
|
||||
|
||||
// base-url format
|
||||
if (baseUrl) {
|
||||
if (baseUrl.indexOf("http://") !== 0 && baseUrl.indexOf("https://") !== 0) {
|
||||
warnings.push("base-url must start with http:// or https://");
|
||||
} else {
|
||||
try {
|
||||
var u = new URL(baseUrl);
|
||||
if (u.pathname !== "/" && u.pathname !== "") {
|
||||
warnings.push("base-url must not have a path, ntfy does not support sub-paths");
|
||||
}
|
||||
} catch (e) {
|
||||
warnings.push("base-url is not a valid URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"];
|
||||
if (wpPublic || wpPrivate || wpEmail) {
|
||||
var 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(", "));
|
||||
}
|
||||
}
|
||||
|
||||
// SMTP sender requires base-url and smtp-sender-from
|
||||
if (values["smtp-sender-addr"]) {
|
||||
var 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(", "));
|
||||
}
|
||||
}
|
||||
|
||||
// SMTP server requires domain
|
||||
if (values["smtp-server-listen"] && !values["smtp-server-domain"]) {
|
||||
warnings.push("Email publishing requires smtp-server-domain");
|
||||
}
|
||||
|
||||
// Attachments require base-url
|
||||
if (values["attachment-cache-dir"] && !baseUrl) {
|
||||
warnings.push("Attachments require base-url to be set");
|
||||
}
|
||||
|
||||
// Upstream requires base-url and can't equal it
|
||||
if (values["upstream-base-url"]) {
|
||||
if (!baseUrl) {
|
||||
warnings.push("Upstream server requires base-url to be set");
|
||||
} else if (baseUrl === values["upstream-base-url"]) {
|
||||
warnings.push("base-url and upstream-base-url cannot be the same");
|
||||
}
|
||||
}
|
||||
|
||||
// enable-signup requires enable-login
|
||||
if (values["enable-signup"] && !values["enable-login"]) {
|
||||
warnings.push("Enable signup requires enable-login to also be set");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function prefill(modal, key, value) {
|
||||
var el = modal.querySelector('[data-key="' + key + '"]');
|
||||
if (el && !el.value.trim()) el.value = value;
|
||||
}
|
||||
|
||||
// Set a select's value (always, to reflect wizard state)
|
||||
function prefillSelect(gen, key, value) {
|
||||
var el = gen.querySelector('[data-key="' + key + '"]');
|
||||
function prefillSelect(modal, key, value) {
|
||||
var el = modal.querySelector('[data-key="' + key + '"]');
|
||||
if (el) el.value = value;
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return;
|
||||
var modal = document.getElementById("cg-modal");
|
||||
if (!modal) return;
|
||||
|
||||
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||
var isPostgres = modal.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||
isPostgres = isPostgres && isPostgres.checked;
|
||||
|
||||
var isPrivate = gen.querySelector('input[name="cg-server-type"][value="private"]');
|
||||
var isPrivate = modal.querySelector('input[name="cg-server-type"][value="private"]');
|
||||
isPrivate = isPrivate && isPrivate.checked;
|
||||
|
||||
var cacheEnabled = gen.querySelector("#cg-feat-cache");
|
||||
var isUnifiedPush = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]');
|
||||
isUnifiedPush = isUnifiedPush && isUnifiedPush.checked;
|
||||
|
||||
// Auto-check auth when private or UnifiedPush is selected
|
||||
var authCheck = modal.querySelector("#cg-feat-auth");
|
||||
if (authCheck) {
|
||||
var authForced = isPrivate || isUnifiedPush || isPostgres;
|
||||
if (authForced) authCheck.checked = true;
|
||||
authCheck.disabled = authForced;
|
||||
}
|
||||
|
||||
var authEnabled = authCheck && authCheck.checked;
|
||||
|
||||
var cacheEnabled = modal.querySelector("#cg-feat-cache");
|
||||
cacheEnabled = cacheEnabled && cacheEnabled.checked;
|
||||
|
||||
var attachEnabled = gen.querySelector("#cg-feat-attach");
|
||||
var attachEnabled = modal.querySelector("#cg-feat-attach");
|
||||
attachEnabled = attachEnabled && attachEnabled.checked;
|
||||
|
||||
var webpushEnabled = gen.querySelector("#cg-feat-webpush");
|
||||
var webpushEnabled = modal.querySelector("#cg-feat-webpush");
|
||||
webpushEnabled = webpushEnabled && webpushEnabled.checked;
|
||||
|
||||
var smtpOutEnabled = gen.querySelector("#cg-feat-smtp-out");
|
||||
var smtpOutEnabled = modal.querySelector("#cg-feat-smtp-out");
|
||||
smtpOutEnabled = smtpOutEnabled && smtpOutEnabled.checked;
|
||||
|
||||
var smtpInEnabled = gen.querySelector("#cg-feat-smtp-in");
|
||||
var smtpInEnabled = modal.querySelector("#cg-feat-smtp-in");
|
||||
smtpInEnabled = smtpInEnabled && smtpInEnabled.checked;
|
||||
|
||||
// Show database question only if a DB-dependent feature is selected
|
||||
var needsDb = isPrivate || cacheEnabled || webpushEnabled;
|
||||
var dbStep = gen.querySelector("#cg-wizard-db");
|
||||
var needsDb = authEnabled || cacheEnabled || webpushEnabled;
|
||||
var dbStep = modal.querySelector("#cg-wizard-db");
|
||||
if (dbStep) dbStep.style.display = needsDb ? "" : "none";
|
||||
|
||||
// Database detail section (PostgreSQL only; SQLite needs no extra config)
|
||||
var pgSection = gen.querySelector("#cg-detail-db-postgres");
|
||||
if (pgSection) pgSection.style.display = (needsDb && isPostgres) ? "" : "none";
|
||||
// Nav tabs for features
|
||||
for (var featId in NAV_MAP) {
|
||||
var checkbox = modal.querySelector("#" + featId);
|
||||
var navTab = modal.querySelector("#" + NAV_MAP[featId]);
|
||||
if (checkbox && navTab) {
|
||||
navTab.style.display = checkbox.checked ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Hide cache-file in message cache section when PostgreSQL
|
||||
var cacheFileField = gen.querySelector("#cg-cache-file-field");
|
||||
if (cacheFileField) cacheFileField.style.display = isPostgres ? "none" : "";
|
||||
// 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";
|
||||
|
||||
// Auth detail section
|
||||
var authSection = gen.querySelector("#cg-detail-auth");
|
||||
if (authSection) authSection.style.display = isPrivate ? "" : "none";
|
||||
// If active nav tab got hidden, switch to General
|
||||
var activeNav = modal.querySelector(".cg-nav-tab.active");
|
||||
if (activeNav && activeNav.style.display === "none") {
|
||||
switchPanel(modal, "cg-panel-general");
|
||||
}
|
||||
|
||||
// Hide auth-file and web-push-file if PostgreSQL
|
||||
var authFile = gen.querySelector('[data-key="auth-file"]');
|
||||
var authFile = modal.querySelector('[data-key="auth-file"]');
|
||||
if (authFile) {
|
||||
var authField = authFile.closest(".cg-field");
|
||||
if (authField) authField.style.display = isPostgres ? "none" : "";
|
||||
}
|
||||
var wpFile = gen.querySelector('[data-key="web-push-file"]');
|
||||
var wpFile = modal.querySelector('[data-key="web-push-file"]');
|
||||
if (wpFile) {
|
||||
var wpField = wpFile.closest(".cg-field");
|
||||
if (wpField) wpField.style.display = isPostgres ? "none" : "";
|
||||
}
|
||||
|
||||
// Feature toggles → detail sections
|
||||
for (var featId in FEATURE_MAP) {
|
||||
var checkbox = gen.querySelector("#" + featId);
|
||||
var section = gen.querySelector("#" + FEATURE_MAP[featId]);
|
||||
if (checkbox && section) {
|
||||
section.style.display = checkbox.checked ? "" : "none";
|
||||
}
|
||||
// Hide cache-file when PostgreSQL
|
||||
var cacheFileField = modal.querySelector("#cg-cache-file-field");
|
||||
if (cacheFileField) cacheFileField.style.display = isPostgres ? "none" : "";
|
||||
|
||||
// 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"]');
|
||||
if (iosYes && upstreamInput) {
|
||||
upstreamInput.value = iosYes.checked ? "https://ntfy.sh" : "";
|
||||
}
|
||||
|
||||
// Upstream special handling
|
||||
var upstreamCheck = gen.querySelector("#cg-feat-upstream");
|
||||
var upstreamInput = gen.querySelector('[data-key="upstream-base-url"]');
|
||||
if (upstreamCheck && upstreamInput) {
|
||||
upstreamInput.value = upstreamCheck.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;
|
||||
}
|
||||
|
||||
// Metrics special handling
|
||||
var metricsCheck = gen.querySelector("#cg-feat-metrics");
|
||||
var metricsInput = gen.querySelector('[data-key="enable-metrics"]');
|
||||
if (metricsCheck && metricsInput) {
|
||||
metricsInput.checked = metricsCheck.checked;
|
||||
}
|
||||
|
||||
// --- Pre-fill defaults based on wizard selections ---
|
||||
|
||||
// Database
|
||||
// --- Pre-fill defaults ---
|
||||
if (isPostgres) {
|
||||
prefill(gen, "database-url", "postgres://user:pass@host:5432/ntfy");
|
||||
prefill(modal, "database-url", "postgres://user:pass@host:5432/ntfy");
|
||||
}
|
||||
|
||||
// Access control: always sync default-access with open/private
|
||||
if (authEnabled) {
|
||||
if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db");
|
||||
}
|
||||
if (isPrivate) {
|
||||
prefillSelect(gen, "auth-default-access", "deny-all");
|
||||
if (!isPostgres) prefill(gen, "auth-file", "/var/lib/ntfy/auth.db");
|
||||
prefillSelect(modal, "auth-default-access", "deny-all");
|
||||
} else {
|
||||
prefillSelect(gen, "auth-default-access", "read-write");
|
||||
prefillSelect(modal, "auth-default-access", "read-write");
|
||||
}
|
||||
|
||||
// Persistent message cache
|
||||
if (cacheEnabled) {
|
||||
if (!isPostgres) prefill(gen, "cache-file", "/var/cache/ntfy/cache.db");
|
||||
prefill(gen, "cache-duration", "12h");
|
||||
if (!isPostgres) prefill(modal, "cache-file", "/var/cache/ntfy/cache.db");
|
||||
prefill(modal, "cache-duration", "12h");
|
||||
}
|
||||
|
||||
// Attachments
|
||||
if (attachEnabled) {
|
||||
prefill(gen, "attachment-cache-dir", "/var/cache/ntfy/attachments");
|
||||
prefill(gen, "attachment-file-size-limit", "15M");
|
||||
prefill(gen, "attachment-total-size-limit", "5G");
|
||||
prefill(gen, "attachment-expiry-duration", "3h");
|
||||
prefill(modal, "attachment-cache-dir", "/var/cache/ntfy/attachments");
|
||||
prefill(modal, "attachment-file-size-limit", "15M");
|
||||
prefill(modal, "attachment-total-size-limit", "5G");
|
||||
prefill(modal, "attachment-expiry-duration", "3h");
|
||||
}
|
||||
|
||||
// Web push
|
||||
if (webpushEnabled) {
|
||||
if (!isPostgres) prefill(gen, "web-push-file", "/var/lib/ntfy/webpush.db");
|
||||
prefill(gen, "web-push-email-address", "admin@example.com");
|
||||
if (!isPostgres) prefill(modal, "web-push-file", "/var/lib/ntfy/webpush.db");
|
||||
prefill(modal, "web-push-email-address", "admin@example.com");
|
||||
}
|
||||
|
||||
// Email notifications (outgoing)
|
||||
if (smtpOutEnabled) {
|
||||
prefill(gen, "smtp-sender-addr", "smtp.example.com:587");
|
||||
prefill(gen, "smtp-sender-from", "ntfy@example.com");
|
||||
prefill(modal, "smtp-sender-addr", "smtp.example.com:587");
|
||||
prefill(modal, "smtp-sender-from", "ntfy@example.com");
|
||||
}
|
||||
|
||||
// Email publishing (incoming)
|
||||
if (smtpInEnabled) {
|
||||
prefill(gen, "smtp-server-listen", ":25");
|
||||
prefill(gen, "smtp-server-domain", "ntfy.example.com");
|
||||
prefill(modal, "smtp-server-listen", ":25");
|
||||
prefill(modal, "smtp-server-domain", "ntfy.example.com");
|
||||
}
|
||||
}
|
||||
|
||||
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"); });
|
||||
|
||||
var navTab = modal.querySelector('[data-panel="' + panelId + '"]');
|
||||
var panel = modal.querySelector("#" + panelId);
|
||||
if (navTab) navTab.classList.add("active");
|
||||
if (panel) panel.classList.add("active");
|
||||
}
|
||||
|
||||
function addRepeatableRow(container, type) {
|
||||
var row = document.createElement("div");
|
||||
row.className = "cg-repeatable-row cg-auth-" + type + "-row";
|
||||
@@ -454,20 +642,54 @@
|
||||
}
|
||||
|
||||
function initGenerator() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return;
|
||||
var modal = document.getElementById("cg-modal");
|
||||
if (!modal) return;
|
||||
|
||||
// Tab switching
|
||||
gen.querySelectorAll(".cg-tab").forEach(function (tab) {
|
||||
var openBtn = document.getElementById("cg-open-btn");
|
||||
var closeBtn = document.getElementById("cg-close-btn");
|
||||
var backdrop = modal.querySelector(".cg-modal-backdrop");
|
||||
|
||||
function openModal() {
|
||||
modal.style.display = "";
|
||||
document.body.style.overflow = "hidden";
|
||||
updateVisibility();
|
||||
updateOutput();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.style.display = "none";
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
if (openBtn) openBtn.addEventListener("click", openModal);
|
||||
if (closeBtn) closeBtn.addEventListener("click", closeModal);
|
||||
if (backdrop) backdrop.addEventListener("click", closeModal);
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && modal.style.display !== "none") {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Left nav tab switching
|
||||
modal.querySelectorAll(".cg-nav-tab").forEach(function (tab) {
|
||||
tab.addEventListener("click", function () {
|
||||
gen.querySelectorAll(".cg-tab").forEach(function (t) { t.classList.remove("active"); });
|
||||
var panelId = tab.getAttribute("data-panel");
|
||||
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"); });
|
||||
tab.classList.add("active");
|
||||
updateOutput();
|
||||
});
|
||||
});
|
||||
|
||||
// All form inputs trigger update
|
||||
gen.querySelectorAll("input, select").forEach(function (el) {
|
||||
modal.querySelectorAll("input, select").forEach(function (el) {
|
||||
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
||||
el.addEventListener(evt, function () {
|
||||
updateVisibility();
|
||||
@@ -476,7 +698,7 @@
|
||||
});
|
||||
|
||||
// Add buttons for repeatable rows
|
||||
gen.querySelectorAll(".cg-btn-add").forEach(function (btn) {
|
||||
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;
|
||||
@@ -486,12 +708,12 @@
|
||||
});
|
||||
|
||||
// Copy button
|
||||
var copyBtn = gen.querySelector("#cg-copy-btn");
|
||||
var copyBtn = modal.querySelector("#cg-copy-btn");
|
||||
if (copyBtn) {
|
||||
var copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
||||
var checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
||||
copyBtn.addEventListener("click", function () {
|
||||
var code = gen.querySelector("#cg-code");
|
||||
var code = modal.querySelector("#cg-code");
|
||||
if (code && code.textContent) {
|
||||
navigator.clipboard.writeText(code.textContent).then(function () {
|
||||
copyBtn.innerHTML = checkIcon;
|
||||
@@ -505,8 +727,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
if (regenBtn) {
|
||||
regenBtn.addEventListener("click", function () {
|
||||
fillVAPIDKeys();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-generate keys when web push is first enabled
|
||||
var webpushFeat = modal.querySelector("#cg-feat-webpush");
|
||||
if (webpushFeat) {
|
||||
webpushFeat.addEventListener("change", function () {
|
||||
if (webpushFeat.checked && !vapidKeysGenerated) {
|
||||
vapidKeysGenerated = true;
|
||||
fillVAPIDKeys();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pre-fill base-url if not on ntfy.sh
|
||||
var baseUrlInput = gen.querySelector('[data-key="base-url"]');
|
||||
var baseUrlInput = modal.querySelector('[data-key="base-url"]');
|
||||
if (baseUrlInput && !baseUrlInput.value.trim()) {
|
||||
var host = window.location.hostname;
|
||||
if (host && host.indexOf("ntfy.sh") === -1) {
|
||||
@@ -514,8 +767,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibility();
|
||||
updateOutput();
|
||||
// Auto-open if URL hash points to config generator
|
||||
if (window.location.hash === "#config-generator") {
|
||||
openModal();
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
Reference in New Issue
Block a user