mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-03-18 21:30:44 +01:00
Configurator
This commit is contained in:
155
docs/config.md
155
docs/config.md
@@ -141,52 +141,61 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
|||||||
|
|
||||||
<div id="config-generator-app">
|
<div id="config-generator-app">
|
||||||
<div id="cg-left">
|
<div id="cg-left">
|
||||||
<div class="cg-section">
|
<div class="cg-wizard">
|
||||||
<div class="cg-section-header">Basic Setup</div>
|
<div class="cg-wizard-step">
|
||||||
<div class="cg-section-body">
|
<label class="cg-wizard-label">What's your ntfy service URL?</label>
|
||||||
<div class="cg-field">
|
<input type="text" data-key="base-url" placeholder="https://ntfy.example.com" class="cg-wizard-input">
|
||||||
<label>Base URL</label>
|
|
||||||
<input type="text" data-key="base-url" placeholder="https://ntfy.example.com">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-wizard-step">
|
||||||
<label>Listen address</label>
|
<label class="cg-wizard-label">Are you running ntfy behind a proxy?</label>
|
||||||
<input type="text" data-key="listen-http" placeholder=":80">
|
<div class="cg-wizard-toggle">
|
||||||
</div>
|
<label><input type="checkbox" data-key="behind-proxy" id="cg-behind-proxy"> Yes, behind nginx/Apache/Caddy</label>
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" data-key="behind-proxy" id="cg-behind-proxy">
|
|
||||||
<label for="cg-behind-proxy">Behind a proxy (nginx, Apache, etc.)</label>
|
|
||||||
</div>
|
</div>
|
||||||
</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-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-section">
|
</div>
|
||||||
<div class="cg-section-header">Database</div>
|
<div class="cg-wizard-step">
|
||||||
<div class="cg-section-body">
|
<label class="cg-wizard-label">Which features do you want to enable?</label>
|
||||||
|
<div class="cg-feature-grid">
|
||||||
|
<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-radio-group">
|
<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="sqlite" checked> SQLite</label>
|
||||||
<label><input type="radio" name="cg-db-type" value="postgres"> PostgreSQL</label>
|
<label><input type="radio" name="cg-db-type" value="postgres"> PostgreSQL</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="cg-sqlite-fields">
|
</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">
|
<div class="cg-field">
|
||||||
<label>Cache file</label>
|
<label>Listen address</label>
|
||||||
<input type="text" data-key="cache-file" placeholder="/var/cache/ntfy/cache.db">
|
<input type="text" data-key="listen-http" placeholder=":80">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cg-postgres-fields" style="display:none">
|
<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">
|
<div class="cg-field">
|
||||||
<label>Database URL</label>
|
<label>Database URL</label>
|
||||||
<input type="text" data-key="database-url" placeholder="postgres://user:pass@host:5432/ntfy">
|
<input type="text" data-key="database-url" placeholder="postgres://user:pass@host:5432/ntfy">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="cg-detail-section" id="cg-detail-auth" style="display:none">
|
||||||
</div>
|
<div class="cg-detail-heading">Access Control</div>
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Access Control</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-auth-toggle" data-toggle="cg-auth-fields">
|
|
||||||
<label for="cg-auth-toggle">Enable access control</label>
|
|
||||||
</div>
|
|
||||||
<div class="cg-conditional" id="cg-auth-fields">
|
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>Auth file</label>
|
<label>Auth file</label>
|
||||||
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
||||||
@@ -225,16 +234,19 @@ 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>
|
<button type="button" class="cg-btn-add" data-add-type="token">+ Add token</button>
|
||||||
</div>
|
</div>
|
||||||
</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-field" id="cg-cache-file-field">
|
||||||
|
<label>Cache file</label>
|
||||||
|
<input type="text" data-key="cache-file" placeholder="/var/cache/ntfy/cache.db">
|
||||||
|
</div>
|
||||||
|
<div class="cg-field">
|
||||||
|
<label>Cache duration</label>
|
||||||
|
<input type="text" data-key="cache-duration" placeholder="12h">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-section">
|
<div class="cg-detail-section" id="cg-detail-attach" style="display:none">
|
||||||
<div class="cg-section-header">Attachments</div>
|
<div class="cg-detail-heading">Attachments</div>
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-attach-toggle" data-toggle="cg-attach-fields">
|
|
||||||
<label for="cg-attach-toggle">Enable attachments</label>
|
|
||||||
</div>
|
|
||||||
<div class="cg-conditional" id="cg-attach-fields">
|
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>Cache directory</label>
|
<label>Cache directory</label>
|
||||||
<input type="text" data-key="attachment-cache-dir" placeholder="/var/cache/ntfy/attachments">
|
<input type="text" data-key="attachment-cache-dir" placeholder="/var/cache/ntfy/attachments">
|
||||||
@@ -252,25 +264,8 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
|||||||
<input type="text" data-key="attachment-expiry-duration" placeholder="3h">
|
<input type="text" data-key="attachment-expiry-duration" placeholder="3h">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="cg-detail-section" id="cg-detail-webpush" style="display:none">
|
||||||
</div>
|
<div class="cg-detail-heading">Web Push</div>
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Message Cache</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-field">
|
|
||||||
<label>Cache duration</label>
|
|
||||||
<input type="text" data-key="cache-duration" placeholder="12h">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Web Push</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-webpush-toggle" data-toggle="cg-webpush-fields">
|
|
||||||
<label for="cg-webpush-toggle">Enable web push</label>
|
|
||||||
</div>
|
|
||||||
<div class="cg-conditional" id="cg-webpush-fields">
|
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>Public key</label>
|
<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">
|
||||||
@@ -288,16 +283,8 @@ 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">
|
<input type="text" data-key="web-push-email-address" placeholder="admin@example.com">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="cg-detail-section" id="cg-detail-smtp-out" style="display:none">
|
||||||
</div>
|
<div class="cg-detail-heading">Email Notifications (Outgoing)</div>
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Email Notifications (Outgoing)</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-smtp-out-toggle" data-toggle="cg-smtp-out-fields">
|
|
||||||
<label for="cg-smtp-out-toggle">Enable email sending</label>
|
|
||||||
</div>
|
|
||||||
<div class="cg-conditional" id="cg-smtp-out-fields">
|
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>SMTP server address</label>
|
<label>SMTP server address</label>
|
||||||
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
||||||
@@ -315,16 +302,8 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
|||||||
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="cg-detail-section" id="cg-detail-smtp-in" style="display:none">
|
||||||
</div>
|
<div class="cg-detail-heading">Email Publishing (Incoming)</div>
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Email Publishing (Incoming)</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-smtp-in-toggle" data-toggle="cg-smtp-in-fields">
|
|
||||||
<label for="cg-smtp-in-toggle">Enable email publishing</label>
|
|
||||||
</div>
|
|
||||||
<div class="cg-conditional" id="cg-smtp-in-fields">
|
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>Listen address</label>
|
<label>Listen address</label>
|
||||||
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
||||||
@@ -338,37 +317,17 @@ Use this interactive tool to build your ntfy configuration. Select options below
|
|||||||
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Upstream Server</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-upstream-check">
|
|
||||||
<label for="cg-upstream-check">iOS users will use this server</label>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" data-key="upstream-base-url">
|
<input type="hidden" data-key="upstream-base-url">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cg-section">
|
|
||||||
<div class="cg-section-header">Monitoring</div>
|
|
||||||
<div class="cg-section-body">
|
|
||||||
<div class="cg-checkbox">
|
|
||||||
<input type="checkbox" id="cg-metrics-check">
|
|
||||||
<label for="cg-metrics-check">Enable Prometheus metrics</label>
|
|
||||||
</div>
|
|
||||||
<input type="checkbox" data-key="enable-metrics" style="display:none">
|
<input type="checkbox" data-key="enable-metrics" style="display:none">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="cg-right">
|
<div id="cg-right">
|
||||||
<div class="cg-tabs">
|
<div class="cg-tabs">
|
||||||
<div class="cg-tab active" data-format="server-yml">server.yml</div>
|
<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-tab" data-format="docker-compose">docker-compose.yml</div>
|
||||||
<div class="cg-tab" data-format="env-vars">Environment 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>
|
||||||
<div class="cg-output-wrap">
|
<div class="cg-output-wrap">
|
||||||
<button type="button" id="cg-copy-btn" class="cg-btn-copy">Copy</button>
|
|
||||||
<pre><code id="cg-code"><span class="cg-empty-msg">Configure options on the left to generate your config...</span></code></pre>
|
<pre><code id="cg-code"><span class="cg-empty-msg">Configure options on the left to generate your config...</span></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
222
docs/static/css/config-generator.css
vendored
222
docs/static/css/config-generator.css
vendored
@@ -4,7 +4,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
margin: 1em 0 2em;
|
margin: 1em 0 2em;
|
||||||
font-size: 0.82rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cg-left {
|
#cg-left {
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#cg-right {
|
#cg-right {
|
||||||
width: 420px;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 76px;
|
top: 76px;
|
||||||
@@ -25,49 +26,115 @@
|
|||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accordion sections */
|
/* Wizard questions */
|
||||||
.cg-section {
|
.cg-wizard {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
margin-bottom: 8px;
|
padding: 16px 18px;
|
||||||
overflow: hidden;
|
margin-bottom: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section-header {
|
.cg-wizard-step {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cg-wizard-step:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cg-wizard-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
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 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 6px;
|
||||||
padding: 10px 14px;
|
font-size: 0.82rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
background: #f5f5f5;
|
|
||||||
user-select: none;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section-header:hover {
|
.cg-wizard-toggle input[type="checkbox"] {
|
||||||
background: #eee;
|
accent-color: var(--md-primary-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section-header::after {
|
.cg-radio-group {
|
||||||
content: '\25B6';
|
display: flex;
|
||||||
font-size: 0.65em;
|
gap: 16px;
|
||||||
transition: transform 0.2s;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section.open .cg-section-header::after {
|
.cg-radio-group label {
|
||||||
transform: rotate(90deg);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section-body {
|
.cg-radio-group input[type="radio"] {
|
||||||
display: none;
|
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.82rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cg-feature-grid input[type="checkbox"] {
|
||||||
|
accent-color: var(--md-primary-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail sections */
|
||||||
|
#cg-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cg-detail-section {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-section.open .cg-section-body {
|
.cg-detail-heading {
|
||||||
display: block;
|
font-weight: 600;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--md-primary-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form fields */
|
/* Form fields */
|
||||||
@@ -126,33 +193,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-radio-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cg-radio-group label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-weight: 400;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cg-radio-group input[type="radio"] {
|
|
||||||
accent-color: var(--md-primary-fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cg-conditional {
|
|
||||||
display: none;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cg-conditional.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Repeatable rows */
|
/* Repeatable rows */
|
||||||
.cg-repeatable-row {
|
.cg-repeatable-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -242,15 +282,15 @@
|
|||||||
|
|
||||||
/* Output panel */
|
/* Output panel */
|
||||||
.cg-output-wrap {
|
.cg-output-wrap {
|
||||||
position: relative;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-output-wrap pre {
|
.cg-output-wrap pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #1e1e1e;
|
background: #f5f5f5;
|
||||||
color: #d4d4d4;
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
@@ -260,22 +300,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cg-btn-copy {
|
.cg-btn-copy {
|
||||||
position: absolute;
|
margin-left: auto;
|
||||||
top: 18px;
|
background: none;
|
||||||
right: 18px;
|
color: #777;
|
||||||
background: #444;
|
|
||||||
color: #ddd;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-bottom: 2px solid transparent;
|
||||||
padding: 4px 10px;
|
margin-bottom: -2px;
|
||||||
font-size: 0.72rem;
|
padding: 8px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.8;
|
line-height: 1;
|
||||||
transition: opacity 0.15s;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-btn-copy:hover {
|
.cg-btn-copy:hover {
|
||||||
opacity: 1;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cg-empty-msg {
|
.cg-empty-msg {
|
||||||
@@ -289,21 +330,33 @@ body[data-md-color-scheme="slate"] #cg-right {
|
|||||||
border-color: #444;
|
border-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-md-color-scheme="slate"] .cg-section {
|
body[data-md-color-scheme="slate"] .cg-wizard {
|
||||||
|
background: #2e303e;
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-md-color-scheme="slate"] .cg-section-header {
|
body[data-md-color-scheme="slate"] .cg-wizard-label {
|
||||||
background: #2e303e;
|
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-md-color-scheme="slate"] .cg-section-header:hover {
|
body[data-md-color-scheme="slate"] .cg-wizard-input {
|
||||||
background: #363849;
|
background: #1e1e2e;
|
||||||
|
border-color: #555;
|
||||||
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-md-color-scheme="slate"] .cg-section-body {
|
body[data-md-color-scheme="slate"] .cg-wizard-toggle label,
|
||||||
border-top-color: #444;
|
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-detail-section {
|
||||||
|
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 {
|
||||||
@@ -348,7 +401,21 @@ body[data-md-color-scheme="slate"] .cg-tab:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body[data-md-color-scheme="slate"] .cg-output-wrap pre {
|
body[data-md-color-scheme="slate"] .cg-output-wrap pre {
|
||||||
background: #161620;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@@ -362,4 +429,5 @@ body[data-md-color-scheme="slate"] .cg-output-wrap pre {
|
|||||||
position: static;
|
position: static;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
276
docs/static/js/config-generator.js
vendored
276
docs/static/js/config-generator.js
vendored
@@ -6,7 +6,6 @@
|
|||||||
{ key: "base-url", env: "NTFY_BASE_URL", section: "basic" },
|
{ key: "base-url", env: "NTFY_BASE_URL", section: "basic" },
|
||||||
{ key: "listen-http", env: "NTFY_LISTEN_HTTP", section: "basic", def: ":80" },
|
{ key: "listen-http", env: "NTFY_LISTEN_HTTP", section: "basic", def: ":80" },
|
||||||
{ key: "behind-proxy", env: "NTFY_BEHIND_PROXY", section: "basic", type: "bool" },
|
{ key: "behind-proxy", env: "NTFY_BEHIND_PROXY", section: "basic", type: "bool" },
|
||||||
{ key: "cache-file", env: "NTFY_CACHE_FILE", section: "database", def: "/var/cache/ntfy/cache.db" },
|
|
||||||
{ key: "database-url", env: "NTFY_DATABASE_URL", section: "database" },
|
{ 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-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-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth" },
|
||||||
@@ -16,6 +15,7 @@
|
|||||||
{ key: "attachment-file-size-limit", env: "NTFY_ATTACHMENT_FILE_SIZE_LIMIT", section: "attach", def: "15M" },
|
{ 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-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: "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-duration", env: "NTFY_CACHE_DURATION", section: "cache", def: "12h" },
|
{ 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-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-private-key", env: "NTFY_WEB_PUSH_PRIVATE_KEY", section: "webpush" },
|
||||||
@@ -37,21 +37,29 @@
|
|||||||
"/var/cache/ntfy/attachments": "/var/lib/ntfy/attachments",
|
"/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",
|
||||||
|
};
|
||||||
|
|
||||||
function collectValues() {
|
function collectValues() {
|
||||||
var values = {};
|
var values = {};
|
||||||
var gen = document.getElementById("config-generator-app");
|
var gen = document.getElementById("config-generator-app");
|
||||||
if (!gen) return values;
|
if (!gen) return values;
|
||||||
|
|
||||||
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
|
||||||
isPostgres = isPostgres && isPostgres.checked;
|
|
||||||
|
|
||||||
CONFIG.forEach(function (c) {
|
CONFIG.forEach(function (c) {
|
||||||
var el = gen.querySelector('[data-key="' + c.key + '"]');
|
var el = gen.querySelector('[data-key="' + c.key + '"]');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
// Skip hidden fields
|
// Skip fields in hidden detail sections
|
||||||
var container = el.closest(".cg-conditional");
|
var section = el.closest(".cg-detail-section");
|
||||||
if (container && !container.classList.contains("visible")) return;
|
if (section && section.style.display === "none") return;
|
||||||
|
|
||||||
|
// Skip hidden individual fields (e.g. auth-file when using PostgreSQL)
|
||||||
var field = el.closest(".cg-field");
|
var field = el.closest(".cg-field");
|
||||||
if (field && field.style.display === "none") return;
|
if (field && field.style.display === "none") return;
|
||||||
|
|
||||||
@@ -151,13 +159,6 @@
|
|||||||
|
|
||||||
// Auth ACLs
|
// Auth ACLs
|
||||||
if (values["_auth-acls"]) {
|
if (values["_auth-acls"]) {
|
||||||
lines.push("auth-default-access:");
|
|
||||||
lines.push(" everyone:");
|
|
||||||
values["_auth-acls"].forEach(function (a) {
|
|
||||||
// This uses the topic-level provisioning format
|
|
||||||
});
|
|
||||||
// Actually use provisioned format
|
|
||||||
lines.pop(); lines.pop();
|
|
||||||
lines.push("auth-access:");
|
lines.push("auth-access:");
|
||||||
values["_auth-acls"].forEach(function (a) {
|
values["_auth-acls"].forEach(function (a) {
|
||||||
lines.push(" - user: " + (a.user || "*"));
|
lines.push(" - user: " + (a.user || "*"));
|
||||||
@@ -249,47 +250,6 @@
|
|||||||
return lines.join("\n");
|
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";
|
|
||||||
// Use single quotes if value contains $
|
|
||||||
var q = val.indexOf("$") !== -1 ? "'" : '"';
|
|
||||||
lines.push("export " + c.env + "=" + q + val + q);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (values["_auth-users"]) {
|
|
||||||
values["_auth-users"].forEach(function (u, i) {
|
|
||||||
var q = u.password.indexOf("$") !== -1 ? "'" : '"';
|
|
||||||
lines.push("export NTFY_AUTH_USERS_" + i + '_USERNAME="' + u.username + '"');
|
|
||||||
lines.push("export NTFY_AUTH_USERS_" + i + "_PASSWORD=" + q + u.password + q);
|
|
||||||
lines.push("export NTFY_AUTH_USERS_" + i + '_ROLE="' + u.role + '"');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values["_auth-acls"]) {
|
|
||||||
values["_auth-acls"].forEach(function (a, i) {
|
|
||||||
lines.push("export NTFY_AUTH_ACCESS_" + i + '_USER="' + (a.user || "*") + '"');
|
|
||||||
lines.push("export NTFY_AUTH_ACCESS_" + i + '_TOPIC="' + a.topic + '"');
|
|
||||||
lines.push("export NTFY_AUTH_ACCESS_" + i + '_PERMISSION="' + a.permission + '"');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values["_auth-tokens"]) {
|
|
||||||
values["_auth-tokens"].forEach(function (t, i) {
|
|
||||||
var q = t.token.indexOf("$") !== -1 ? "'" : '"';
|
|
||||||
lines.push("export NTFY_AUTH_TOKENS_" + i + '_USER="' + t.user + '"');
|
|
||||||
lines.push("export NTFY_AUTH_TOKENS_" + i + "_TOKEN=" + q + t.token + q);
|
|
||||||
if (t.label) lines.push("export NTFY_AUTH_TOKENS_" + i + '_LABEL="' + t.label + '"');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOutput() {
|
function updateOutput() {
|
||||||
var gen = document.getElementById("config-generator-app");
|
var gen = document.getElementById("config-generator-app");
|
||||||
if (!gen) return;
|
if (!gen) return;
|
||||||
@@ -301,7 +261,6 @@
|
|||||||
var activeTab = gen.querySelector(".cg-tab.active");
|
var activeTab = gen.querySelector(".cg-tab.active");
|
||||||
var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml";
|
var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml";
|
||||||
|
|
||||||
var output = "";
|
|
||||||
var hasValues = false;
|
var hasValues = false;
|
||||||
for (var k in values) {
|
for (var k in values) {
|
||||||
if (values.hasOwnProperty(k)) { hasValues = true; break; }
|
if (values.hasOwnProperty(k)) { hasValues = true; break; }
|
||||||
@@ -312,29 +271,69 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === "server-yml") {
|
var output = "";
|
||||||
output = generateServerYml(values);
|
if (format === "docker-compose") {
|
||||||
} else if (format === "docker-compose") {
|
|
||||||
output = generateDockerCompose(values);
|
output = generateDockerCompose(values);
|
||||||
} else {
|
} else {
|
||||||
output = generateEnvVars(values);
|
output = generateServerYml(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
codeEl.textContent = output;
|
codeEl.textContent = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConditionalVisibility() {
|
// Set a field's value only if it is currently empty
|
||||||
|
function prefill(gen, key, value) {
|
||||||
|
var el = gen.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 + '"]');
|
||||||
|
if (el) el.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVisibility() {
|
||||||
var gen = document.getElementById("config-generator-app");
|
var gen = document.getElementById("config-generator-app");
|
||||||
if (!gen) return;
|
if (!gen) return;
|
||||||
|
|
||||||
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||||
isPostgres = isPostgres && isPostgres.checked;
|
isPostgres = isPostgres && isPostgres.checked;
|
||||||
|
|
||||||
// Database fields
|
var isPrivate = gen.querySelector('input[name="cg-server-type"][value="private"]');
|
||||||
var sqliteFields = gen.querySelector("#cg-sqlite-fields");
|
isPrivate = isPrivate && isPrivate.checked;
|
||||||
var pgFields = gen.querySelector("#cg-postgres-fields");
|
|
||||||
if (sqliteFields) sqliteFields.style.display = isPostgres ? "none" : "block";
|
var cacheEnabled = gen.querySelector("#cg-feat-cache");
|
||||||
if (pgFields) pgFields.style.display = isPostgres ? "block" : "none";
|
cacheEnabled = cacheEnabled && cacheEnabled.checked;
|
||||||
|
|
||||||
|
var attachEnabled = gen.querySelector("#cg-feat-attach");
|
||||||
|
attachEnabled = attachEnabled && attachEnabled.checked;
|
||||||
|
|
||||||
|
var webpushEnabled = gen.querySelector("#cg-feat-webpush");
|
||||||
|
webpushEnabled = webpushEnabled && webpushEnabled.checked;
|
||||||
|
|
||||||
|
var smtpOutEnabled = gen.querySelector("#cg-feat-smtp-out");
|
||||||
|
smtpOutEnabled = smtpOutEnabled && smtpOutEnabled.checked;
|
||||||
|
|
||||||
|
var smtpInEnabled = gen.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");
|
||||||
|
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";
|
||||||
|
|
||||||
|
// Hide cache-file in message cache section when PostgreSQL
|
||||||
|
var cacheFileField = gen.querySelector("#cg-cache-file-field");
|
||||||
|
if (cacheFileField) cacheFileField.style.display = isPostgres ? "none" : "";
|
||||||
|
|
||||||
|
// Auth detail section
|
||||||
|
var authSection = gen.querySelector("#cg-detail-auth");
|
||||||
|
if (authSection) authSection.style.display = isPrivate ? "" : "none";
|
||||||
|
|
||||||
// Hide auth-file and web-push-file if PostgreSQL
|
// Hide auth-file and web-push-file if PostgreSQL
|
||||||
var authFile = gen.querySelector('[data-key="auth-file"]');
|
var authFile = gen.querySelector('[data-key="auth-file"]');
|
||||||
@@ -348,18 +347,75 @@
|
|||||||
if (wpField) wpField.style.display = isPostgres ? "none" : "";
|
if (wpField) wpField.style.display = isPostgres ? "none" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conditional sections (checkboxes that show/hide detail fields)
|
// Feature toggles → detail sections
|
||||||
var toggles = gen.querySelectorAll("[data-toggle]");
|
for (var featId in FEATURE_MAP) {
|
||||||
toggles.forEach(function (toggle) {
|
var checkbox = gen.querySelector("#" + featId);
|
||||||
var target = gen.querySelector("#" + toggle.getAttribute("data-toggle"));
|
var section = gen.querySelector("#" + FEATURE_MAP[featId]);
|
||||||
if (target) {
|
if (checkbox && section) {
|
||||||
if (toggle.checked) {
|
section.style.display = checkbox.checked ? "" : "none";
|
||||||
target.classList.add("visible");
|
|
||||||
} else {
|
|
||||||
target.classList.remove("visible");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 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" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (isPostgres) {
|
||||||
|
prefill(gen, "database-url", "postgres://user:pass@host:5432/ntfy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access control: always sync default-access with open/private
|
||||||
|
if (isPrivate) {
|
||||||
|
prefillSelect(gen, "auth-default-access", "deny-all");
|
||||||
|
if (!isPostgres) prefill(gen, "auth-file", "/var/lib/ntfy/auth.db");
|
||||||
|
} else {
|
||||||
|
prefillSelect(gen, "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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email notifications (outgoing)
|
||||||
|
if (smtpOutEnabled) {
|
||||||
|
prefill(gen, "smtp-sender-addr", "smtp.example.com:587");
|
||||||
|
prefill(gen, "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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRepeatableRow(container, type) {
|
function addRepeatableRow(container, type) {
|
||||||
@@ -401,17 +457,6 @@
|
|||||||
var gen = document.getElementById("config-generator-app");
|
var gen = document.getElementById("config-generator-app");
|
||||||
if (!gen) return;
|
if (!gen) return;
|
||||||
|
|
||||||
// Accordion toggle
|
|
||||||
gen.querySelectorAll(".cg-section-header").forEach(function (header) {
|
|
||||||
header.addEventListener("click", function () {
|
|
||||||
header.parentElement.classList.toggle("open");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open first section by default
|
|
||||||
var first = gen.querySelector(".cg-section");
|
|
||||||
if (first) first.classList.add("open");
|
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching
|
||||||
gen.querySelectorAll(".cg-tab").forEach(function (tab) {
|
gen.querySelectorAll(".cg-tab").forEach(function (tab) {
|
||||||
tab.addEventListener("click", function () {
|
tab.addEventListener("click", function () {
|
||||||
@@ -425,23 +470,7 @@
|
|||||||
gen.querySelectorAll("input, select").forEach(function (el) {
|
gen.querySelectorAll("input, select").forEach(function (el) {
|
||||||
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
||||||
el.addEventListener(evt, function () {
|
el.addEventListener(evt, function () {
|
||||||
updateConditionalVisibility();
|
updateVisibility();
|
||||||
updateOutput();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Conditional toggles
|
|
||||||
gen.querySelectorAll("[data-toggle]").forEach(function (toggle) {
|
|
||||||
toggle.addEventListener("change", function () {
|
|
||||||
updateConditionalVisibility();
|
|
||||||
updateOutput();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Database radio
|
|
||||||
gen.querySelectorAll('input[name="cg-db-type"]').forEach(function (r) {
|
|
||||||
r.addEventListener("change", function () {
|
|
||||||
updateConditionalVisibility();
|
|
||||||
updateOutput();
|
updateOutput();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -459,38 +488,33 @@
|
|||||||
// Copy button
|
// Copy button
|
||||||
var copyBtn = gen.querySelector("#cg-copy-btn");
|
var copyBtn = gen.querySelector("#cg-copy-btn");
|
||||||
if (copyBtn) {
|
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 () {
|
copyBtn.addEventListener("click", function () {
|
||||||
var code = gen.querySelector("#cg-code");
|
var code = gen.querySelector("#cg-code");
|
||||||
if (code && code.textContent) {
|
if (code && code.textContent) {
|
||||||
navigator.clipboard.writeText(code.textContent).then(function () {
|
navigator.clipboard.writeText(code.textContent).then(function () {
|
||||||
copyBtn.textContent = "Copied!";
|
copyBtn.innerHTML = checkIcon;
|
||||||
setTimeout(function () { copyBtn.textContent = "Copy"; }, 2000);
|
copyBtn.style.color = "var(--md-primary-fg-color)";
|
||||||
|
setTimeout(function () {
|
||||||
|
copyBtn.innerHTML = copyIcon;
|
||||||
|
copyBtn.style.color = "";
|
||||||
|
}, 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream checkbox special handling
|
// Pre-fill base-url if not on ntfy.sh
|
||||||
var upstreamCheck = gen.querySelector("#cg-upstream-check");
|
var baseUrlInput = gen.querySelector('[data-key="base-url"]');
|
||||||
if (upstreamCheck) {
|
if (baseUrlInput && !baseUrlInput.value.trim()) {
|
||||||
upstreamCheck.addEventListener("change", function () {
|
var host = window.location.hostname;
|
||||||
var input = gen.querySelector('[data-key="upstream-base-url"]');
|
if (host && host.indexOf("ntfy.sh") === -1) {
|
||||||
if (input) input.value = upstreamCheck.checked ? "https://ntfy.sh" : "";
|
baseUrlInput.value = "https://ntfy.example.com";
|
||||||
updateOutput();
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics checkbox special handling
|
updateVisibility();
|
||||||
var metricsCheck = gen.querySelector("#cg-metrics-check");
|
|
||||||
if (metricsCheck) {
|
|
||||||
metricsCheck.addEventListener("change", function () {
|
|
||||||
var input = gen.querySelector('[data-key="enable-metrics"]');
|
|
||||||
if (input) input.checked = metricsCheck.checked;
|
|
||||||
updateOutput();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConditionalVisibility();
|
|
||||||
updateOutput();
|
updateOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user