mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-03-18 21:30:44 +01:00
config generator
This commit is contained in:
164
docs/config-generator-plan.md
Normal file
164
docs/config-generator-plan.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Config Generator for ntfy Docs
|
||||
|
||||
## Context
|
||||
|
||||
The ntfy config page (`docs/config.md`) documents 60+ server configuration options across many sections. Users often struggle to assemble a working config file. An interactive config generator helps users build a `server.yml`, `docker-compose.yml`, or env vars file by answering guided questions, with real-time preview.
|
||||
|
||||
## Files Created
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/static/js/config-generator.js` | All generator logic (vanilla JS, ~300 lines) |
|
||||
| `docs/static/css/config-generator.css` | All generator styles (~250 lines) |
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `docs/config.md` | Inserted `## Config generator` HTML block at line 137 (before `## Database options`) |
|
||||
| `mkdocs.yml` | Added `config-generator.js` to `extra_javascript` and `config-generator.css` to `extra_css` |
|
||||
|
||||
## UI Layout
|
||||
|
||||
Two-panel side-by-side layout:
|
||||
|
||||
- **Left panel**: Collapsible accordion sections with form inputs for config options
|
||||
- **Right panel** (sticky): Tabs for `server.yml` / `docker-compose.yml` / `Environment variables`, with a copy button. Updates in real-time as the user changes options.
|
||||
- **Responsive**: Stacks vertically on screens < 900px
|
||||
|
||||
## Configuration Sections (Left Panel)
|
||||
|
||||
### 1. Basic Setup
|
||||
- `base-url` (text, placeholder: `https://ntfy.example.com`)
|
||||
- `listen-http` (text, placeholder: `:80`)
|
||||
- `behind-proxy` (checkbox)
|
||||
|
||||
### 2. Database
|
||||
- Radio: SQLite (default) vs PostgreSQL
|
||||
- SQLite → `cache-file` (text, placeholder: `/var/cache/ntfy/cache.db`)
|
||||
- PostgreSQL → `database-url` (text, placeholder: `postgres://user:pass@host:5432/ntfy`)
|
||||
|
||||
### 3. Access Control
|
||||
- "Enable access control" (checkbox) → shows:
|
||||
- `auth-file` (text, placeholder: `/var/lib/ntfy/auth.db`) — hidden if PostgreSQL
|
||||
- `auth-default-access` (select: `read-write`, `read-only`, `write-only`, `deny-all`)
|
||||
- `enable-login` (checkbox)
|
||||
- `enable-signup` (checkbox)
|
||||
- Provisioned users (repeatable rows): username, password hash, role (admin/user)
|
||||
- Provisioned ACLs (repeatable rows): username, topic pattern, permission (rw/ro/wo/deny)
|
||||
- Provisioned tokens (repeatable rows): username, token, label
|
||||
|
||||
### 4. Attachments
|
||||
- "Enable attachments" (checkbox) → shows:
|
||||
- `attachment-cache-dir` (text, placeholder: `/var/cache/ntfy/attachments`)
|
||||
- `attachment-file-size-limit` (text, placeholder: `15M`)
|
||||
- `attachment-total-size-limit` (text, placeholder: `5G`)
|
||||
- `attachment-expiry-duration` (text, placeholder: `3h`)
|
||||
|
||||
### 5. Message Cache
|
||||
- `cache-duration` (text, placeholder: `12h`)
|
||||
|
||||
### 6. Web Push
|
||||
- "Enable web push" (checkbox) → shows:
|
||||
- `web-push-public-key` (text)
|
||||
- `web-push-private-key` (text)
|
||||
- `web-push-file` (text, placeholder: `/var/lib/ntfy/webpush.db`) — hidden if PostgreSQL
|
||||
- `web-push-email-address` (text)
|
||||
|
||||
### 7. Email Notifications (Outgoing)
|
||||
- "Enable email sending" (checkbox) → shows:
|
||||
- `smtp-sender-addr` (text, placeholder: `smtp.example.com:587`)
|
||||
- `smtp-sender-from` (text, placeholder: `ntfy@example.com`)
|
||||
- `smtp-sender-user` (text)
|
||||
- `smtp-sender-pass` (text, type=password)
|
||||
|
||||
### 8. Email Publishing (Incoming)
|
||||
- "Enable email publishing" (checkbox) → shows:
|
||||
- `smtp-server-listen` (text, placeholder: `:25`)
|
||||
- `smtp-server-domain` (text, placeholder: `ntfy.example.com`)
|
||||
- `smtp-server-addr-prefix` (text, placeholder: `ntfy-`)
|
||||
|
||||
### 9. Upstream Server
|
||||
- "iOS users will use this server" (checkbox) → sets `upstream-base-url: https://ntfy.sh`
|
||||
|
||||
### 10. Monitoring
|
||||
- "Enable Prometheus metrics" (checkbox) → sets `enable-metrics: true`
|
||||
|
||||
## JavaScript Architecture (`config-generator.js`)
|
||||
|
||||
Single vanilla JS file, no dependencies. Key structure:
|
||||
|
||||
1. **Config definitions array (`CONFIG`)**: Each entry has `key`, `env`, `type`, `def`, `section`. This is the single source of truth for all three output formats.
|
||||
|
||||
2. **`collectValues()`**: Reads all form inputs, returns an object of key→value, filtering out empty values. Handles repeatable rows for auth-users/access/tokens. Respects conditional visibility (skips hidden fields).
|
||||
|
||||
3. **`generateServerYml(values)`**: Outputs YAML with section comments. Provisioned users/ACLs/tokens rendered as YAML arrays.
|
||||
|
||||
4. **`generateDockerCompose(values)`**: Wraps values as env vars in Docker Compose format. Key transformations:
|
||||
- Uses `NTFY_` prefixed env var names from config definitions
|
||||
- File paths adjusted for Docker via `DOCKER_PATH_MAP`
|
||||
- `$` doubled to `$$` in bcrypt hashes (with comment)
|
||||
- Standard boilerplate: `services:`, `image:`, `volumes:`, `ports:`, `command: serve`
|
||||
- Provisioned users/ACLs/tokens use indexed env vars (e.g., `NTFY_AUTH_USERS_0_USERNAME`)
|
||||
|
||||
5. **`generateEnvVars(values)`**: Simple `export NTFY_KEY="value"` format. Single quotes for values containing `$`.
|
||||
|
||||
6. **`updateOutput()`**: Called on every input change. Runs the active tab's generator and updates the `<code>` element.
|
||||
|
||||
7. **`initGenerator()`**: Called on DOMContentLoaded, no-ops if `#config-generator` is missing. Sets up event listeners for tabs, accordions, repeatable row add/remove, conditional toggles, and special checkboxes (upstream, metrics).
|
||||
|
||||
### Conditional visibility
|
||||
- Database = PostgreSQL → hide `cache-file`, `auth-file`, `web-push-file`; show `database-url`
|
||||
- Access control unchecked → hide all auth fields
|
||||
- Each "enable X" checkbox controls visibility of its section's detail fields via `data-toggle` attribute
|
||||
|
||||
### Docker Compose path mapping
|
||||
- `/var/cache/ntfy/cache.db` → `/var/lib/ntfy/cache.db`
|
||||
- `/var/cache/ntfy/attachments` → `/var/lib/ntfy/attachments`
|
||||
- `/var/lib/ntfy/*` → unchanged
|
||||
- Volume: `./:/var/lib/ntfy`
|
||||
|
||||
## CSS Architecture (`config-generator.css`)
|
||||
|
||||
Key aspects:
|
||||
|
||||
- **Flex layout** with `gap: 24px` for the two-panel layout
|
||||
- **Sticky right panel**: `position: sticky; top: 76px; align-self: flex-start; max-height: calc(100vh - 100px); overflow-y: auto`
|
||||
- **Dark mode**: Uses `body[data-md-color-scheme="slate"]` selectors (matching existing pattern in `extra.css`)
|
||||
- **Accent color**: Uses `var(--md-primary-fg-color)` (`#338574`) for focus states, active tabs, and interactive elements
|
||||
- **Responsive**: `@media (max-width: 900px)` → column layout, static right panel
|
||||
- **Accordion**: `.cg-section-header` click toggles `.cg-section.open` which shows/hides `.cg-section-body`
|
||||
- **Tabs**: `.cg-tab.active` gets accent color border-bottom highlight
|
||||
- **Code output**: Dark background (`#1e1e1e`) with light text, consistent in both light and dark mode
|
||||
|
||||
## HTML Block (in config.md)
|
||||
|
||||
~250 lines of HTML inserted at line 137. Pure HTML (no markdown inside). Structure:
|
||||
|
||||
```
|
||||
## Config generator
|
||||
<div id="config-generator">
|
||||
<div id="cg-left">
|
||||
<!-- 10 accordion sections with form fields -->
|
||||
</div>
|
||||
<div id="cg-right">
|
||||
<!-- Tab bar + code output + copy button -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
The docs build was verified with `mkdocs build` — completed successfully with no errors. The generated HTML at `server/docs/config/index.html` contains the config-generator elements.
|
||||
|
||||
Manual verification checklist:
|
||||
1. `cd ntfy && mkdocs serve` — open config page in browser
|
||||
2. Verify all 10 sections expand/collapse
|
||||
3. Fill in basic setup → check server.yml output updates
|
||||
4. Switch tabs → verify docker-compose.yml and env vars render correctly
|
||||
5. Toggle access control → verify auth fields show/hide
|
||||
6. Add provisioned users/ACLs/tokens → verify repeatable rows work
|
||||
7. Switch database to PostgreSQL → verify SQLite fields hide
|
||||
8. Toggle dark mode → verify styles look correct
|
||||
9. Resize to mobile width → verify column layout
|
||||
10. Click copy button → verify clipboard content
|
||||
239
docs/config.md
239
docs/config.md
@@ -135,6 +135,245 @@ using Docker Compose (i.e. `docker-compose.yml`):
|
||||
command: serve
|
||||
```
|
||||
|
||||
## Config generator
|
||||
|
||||
Use this interactive tool to build your ntfy configuration. Select options below and copy the generated config.
|
||||
|
||||
<div id="config-generator-app">
|
||||
<div id="cg-left">
|
||||
<div class="cg-section">
|
||||
<div class="cg-section-header">Basic Setup</div>
|
||||
<div class="cg-section-body">
|
||||
<div class="cg-field">
|
||||
<label>Base 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-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 class="cg-section">
|
||||
<div class="cg-section-header">Database</div>
|
||||
<div class="cg-section-body">
|
||||
<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 id="cg-sqlite-fields">
|
||||
<div class="cg-field">
|
||||
<label>Cache file</label>
|
||||
<input type="text" data-key="cache-file" placeholder="/var/cache/ntfy/cache.db">
|
||||
</div>
|
||||
</div>
|
||||
<div id="cg-postgres-fields" style="display:none">
|
||||
<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>
|
||||
</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">
|
||||
<label>Auth file</label>
|
||||
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Default access</label>
|
||||
<select data-key="auth-default-access">
|
||||
<option value="">-- select --</option>
|
||||
<option value="read-write">read-write</option>
|
||||
<option value="read-only">read-only</option>
|
||||
<option value="write-only">write-only</option>
|
||||
<option value="deny-all">deny-all</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="cg-checkbox">
|
||||
<input type="checkbox" data-key="enable-login" id="cg-enable-login">
|
||||
<label for="cg-enable-login">Enable login</label>
|
||||
</div>
|
||||
<div class="cg-checkbox">
|
||||
<input type="checkbox" data-key="enable-signup" id="cg-enable-signup">
|
||||
<label for="cg-enable-signup">Enable signup</label>
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Provisioned users</label>
|
||||
<div class="cg-repeatable-container" id="cg-auth-users-container"></div>
|
||||
<button type="button" class="cg-btn-add" data-add-type="user">+ Add user</button>
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Provisioned ACLs</label>
|
||||
<div class="cg-repeatable-container" id="cg-auth-acls-container"></div>
|
||||
<button type="button" class="cg-btn-add" data-add-type="acl">+ Add ACL</button>
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Provisioned tokens</label>
|
||||
<div class="cg-repeatable-container" id="cg-auth-tokens-container"></div>
|
||||
<button type="button" class="cg-btn-add" data-add-type="token">+ Add token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cg-section">
|
||||
<div class="cg-section-header">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">
|
||||
<label>Cache directory</label>
|
||||
<input type="text" data-key="attachment-cache-dir" placeholder="/var/cache/ntfy/attachments">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>File size limit</label>
|
||||
<input type="text" data-key="attachment-file-size-limit" placeholder="15M">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Total size limit</label>
|
||||
<input type="text" data-key="attachment-total-size-limit" placeholder="5G">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Expiry duration</label>
|
||||
<input type="text" data-key="attachment-expiry-duration" placeholder="3h">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<label>Public key</label>
|
||||
<input type="text" data-key="web-push-public-key" placeholder="Public key">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Private key</label>
|
||||
<input type="text" data-key="web-push-private-key" placeholder="Private key">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Web push file</label>
|
||||
<input type="text" data-key="web-push-file" placeholder="/var/lib/ntfy/webpush.db">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Email address</label>
|
||||
<input type="text" data-key="web-push-email-address" placeholder="admin@example.com">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<label>SMTP server address</label>
|
||||
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Sender email</label>
|
||||
<input type="text" data-key="smtp-sender-from" placeholder="ntfy@example.com">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>SMTP username</label>
|
||||
<input type="text" data-key="smtp-sender-user" placeholder="Username">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>SMTP password</label>
|
||||
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<label>Listen address</label>
|
||||
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Domain</label>
|
||||
<input type="text" data-key="smtp-server-domain" placeholder="ntfy.example.com">
|
||||
</div>
|
||||
<div class="cg-field">
|
||||
<label>Address prefix</label>
|
||||
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
||||
</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">
|
||||
</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">
|
||||
</div>
|
||||
</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-tab" data-format="env-vars">Environment variables</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Database options
|
||||
ntfy uses a database for storing messages ([message cache](#message-cache)), users and [access control](#access-control), and [web push](#web-push) subscriptions.
|
||||
You can choose between **SQLite** and **PostgreSQL** as the database backend.
|
||||
|
||||
365
docs/static/css/config-generator.css
vendored
Normal file
365
docs/static/css/config-generator.css
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
/* Config Generator */
|
||||
|
||||
#config-generator-app {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin: 1em 0 2em;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
#cg-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#cg-right {
|
||||
width: 420px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Accordion sections */
|
||||
.cg-section {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cg-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.88rem;
|
||||
background: #f5f5f5;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.cg-section-header:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.cg-section-header::after {
|
||||
content: '\25B6';
|
||||
font-size: 0.65em;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.cg-section.open .cg-section-header::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.cg-section-body {
|
||||
display: none;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cg-section.open .cg-section-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.cg-field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cg-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cg-field label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 3px;
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.cg-field input[type="text"],
|
||||
.cg-field input[type="password"],
|
||||
.cg-field select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.82rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cg-field input[type="text"]:focus,
|
||||
.cg-field input[type="password"]:focus,
|
||||
.cg-field select:focus {
|
||||
border-color: var(--md-primary-fg-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
|
||||
}
|
||||
|
||||
.cg-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cg-checkbox input[type="checkbox"] {
|
||||
accent-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.cg-checkbox label {
|
||||
font-weight: 500;
|
||||
font-size: 0.82rem;
|
||||
margin: 0;
|
||||
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 */
|
||||
.cg-repeatable-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cg-repeatable-row input,
|
||||
.cg-repeatable-row select {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
padding: 5px 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cg-repeatable-row input:focus,
|
||||
.cg-repeatable-row select:focus {
|
||||
border-color: var(--md-primary-fg-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cg-btn-remove {
|
||||
background: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 3px 7px;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cg-btn-remove:hover {
|
||||
background: #fee;
|
||||
border-color: #c66;
|
||||
color: #c33;
|
||||
}
|
||||
|
||||
.cg-btn-add {
|
||||
background: none;
|
||||
border: 1px dashed #bbb;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.78rem;
|
||||
color: #777;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cg-btn-add:hover {
|
||||
border-color: var(--md-primary-fg-color);
|
||||
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 {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cg-output-wrap pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
min-height: 120px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.cg-btn-copy {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: #444;
|
||||
color: #ddd;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.cg-btn-copy:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.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-section {
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-section-header {
|
||||
background: #2e303e;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-section-header:hover {
|
||||
background: #363849;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-section-body {
|
||||
border-top-color: #444;
|
||||
}
|
||||
|
||||
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;
|
||||
border-color: #555;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-repeatable-row input,
|
||||
body[data-md-color-scheme="slate"] .cg-repeatable-row select {
|
||||
background: #1e1e2e;
|
||||
border-color: #555;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-remove {
|
||||
border-color: #555;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="slate"] .cg-btn-add {
|
||||
border-color: #555;
|
||||
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: #161620;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
#config-generator-app {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#cg-right {
|
||||
width: 100%;
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
502
docs/static/js/config-generator.js
vendored
Normal file
502
docs/static/js/config-generator.js
vendored
Normal file
@@ -0,0 +1,502 @@
|
||||
// Config Generator for ntfy
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var CONFIG = [
|
||||
{ key: "base-url", env: "NTFY_BASE_URL", section: "basic" },
|
||||
{ key: "listen-http", env: "NTFY_LISTEN_HTTP", section: "basic", def: ":80" },
|
||||
{ key: "behind-proxy", env: "NTFY_BEHIND_PROXY", section: "basic", type: "bool" },
|
||||
{ key: "cache-file", env: "NTFY_CACHE_FILE", section: "database", def: "/var/cache/ntfy/cache.db" },
|
||||
{ key: "database-url", env: "NTFY_DATABASE_URL", section: "database" },
|
||||
{ key: "auth-file", env: "NTFY_AUTH_FILE", section: "auth", def: "/var/lib/ntfy/auth.db" },
|
||||
{ key: "auth-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth" },
|
||||
{ key: "enable-login", env: "NTFY_ENABLE_LOGIN", section: "auth", type: "bool" },
|
||||
{ key: "enable-signup", env: "NTFY_ENABLE_SIGNUP", section: "auth", type: "bool" },
|
||||
{ key: "attachment-cache-dir", env: "NTFY_ATTACHMENT_CACHE_DIR", section: "attach", def: "/var/cache/ntfy/attachments" },
|
||||
{ key: "attachment-file-size-limit", env: "NTFY_ATTACHMENT_FILE_SIZE_LIMIT", section: "attach", def: "15M" },
|
||||
{ key: "attachment-total-size-limit", env: "NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT", section: "attach", def: "5G" },
|
||||
{ key: "attachment-expiry-duration", env: "NTFY_ATTACHMENT_EXPIRY_DURATION", section: "attach", def: "3h" },
|
||||
{ key: "cache-duration", env: "NTFY_CACHE_DURATION", section: "cache", def: "12h" },
|
||||
{ key: "web-push-public-key", env: "NTFY_WEB_PUSH_PUBLIC_KEY", section: "webpush" },
|
||||
{ key: "web-push-private-key", env: "NTFY_WEB_PUSH_PRIVATE_KEY", section: "webpush" },
|
||||
{ key: "web-push-file", env: "NTFY_WEB_PUSH_FILE", section: "webpush", def: "/var/lib/ntfy/webpush.db" },
|
||||
{ key: "web-push-email-address", env: "NTFY_WEB_PUSH_EMAIL_ADDRESS", section: "webpush" },
|
||||
{ key: "smtp-sender-addr", env: "NTFY_SMTP_SENDER_ADDR", section: "smtp-out" },
|
||||
{ key: "smtp-sender-from", env: "NTFY_SMTP_SENDER_FROM", section: "smtp-out" },
|
||||
{ key: "smtp-sender-user", env: "NTFY_SMTP_SENDER_USER", section: "smtp-out" },
|
||||
{ key: "smtp-sender-pass", env: "NTFY_SMTP_SENDER_PASS", section: "smtp-out" },
|
||||
{ key: "smtp-server-listen", env: "NTFY_SMTP_SERVER_LISTEN", section: "smtp-in", def: ":25" },
|
||||
{ key: "smtp-server-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 = {
|
||||
"/var/cache/ntfy/cache.db": "/var/lib/ntfy/cache.db",
|
||||
"/var/cache/ntfy/attachments": "/var/lib/ntfy/attachments",
|
||||
};
|
||||
|
||||
function collectValues() {
|
||||
var values = {};
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return values;
|
||||
|
||||
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||
isPostgres = isPostgres && isPostgres.checked;
|
||||
|
||||
CONFIG.forEach(function (c) {
|
||||
var el = gen.querySelector('[data-key="' + c.key + '"]');
|
||||
if (!el) return;
|
||||
|
||||
// Skip hidden fields
|
||||
var container = el.closest(".cg-conditional");
|
||||
if (container && !container.classList.contains("visible")) return;
|
||||
var field = el.closest(".cg-field");
|
||||
if (field && field.style.display === "none") return;
|
||||
|
||||
var val;
|
||||
if (c.type === "bool") {
|
||||
if (el.checked) val = "true";
|
||||
} else {
|
||||
val = el.value.trim();
|
||||
if (!val) return;
|
||||
}
|
||||
if (val) values[c.key] = val;
|
||||
});
|
||||
|
||||
// Provisioned users
|
||||
var userRows = gen.querySelectorAll(".cg-auth-user-row");
|
||||
var users = [];
|
||||
userRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
var p = row.querySelector('[data-field="password"]');
|
||||
var r = row.querySelector('[data-field="role"]');
|
||||
if (u && p && u.value.trim() && p.value.trim()) {
|
||||
users.push({ username: u.value.trim(), password: p.value.trim(), role: r ? r.value : "user" });
|
||||
}
|
||||
});
|
||||
if (users.length) values["_auth-users"] = users;
|
||||
|
||||
// Provisioned ACLs
|
||||
var aclRows = gen.querySelectorAll(".cg-auth-acl-row");
|
||||
var acls = [];
|
||||
aclRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
var t = row.querySelector('[data-field="topic"]');
|
||||
var p = row.querySelector('[data-field="permission"]');
|
||||
if (u && t && t.value.trim()) {
|
||||
acls.push({ user: u.value.trim(), topic: t.value.trim(), permission: p ? p.value : "read-write" });
|
||||
}
|
||||
});
|
||||
if (acls.length) values["_auth-acls"] = acls;
|
||||
|
||||
// Provisioned tokens
|
||||
var tokenRows = gen.querySelectorAll(".cg-auth-token-row");
|
||||
var tokens = [];
|
||||
tokenRows.forEach(function (row) {
|
||||
var u = row.querySelector('[data-field="username"]');
|
||||
var t = row.querySelector('[data-field="token"]');
|
||||
var l = row.querySelector('[data-field="label"]');
|
||||
if (u && t && u.value.trim() && t.value.trim()) {
|
||||
tokens.push({ user: u.value.trim(), token: t.value.trim(), label: l ? l.value.trim() : "" });
|
||||
}
|
||||
});
|
||||
if (tokens.length) values["_auth-tokens"] = tokens;
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function generateServerYml(values) {
|
||||
var lines = [];
|
||||
var sections = {
|
||||
basic: "# Server",
|
||||
database: "# Database",
|
||||
auth: "# Access control",
|
||||
attach: "# Attachments",
|
||||
cache: "# Message cache",
|
||||
webpush: "# Web push",
|
||||
"smtp-out": "# Email notifications (outgoing)",
|
||||
"smtp-in": "# Email publishing (incoming)",
|
||||
upstream: "# Upstream",
|
||||
metrics: "# Monitoring",
|
||||
};
|
||||
var lastSection = "";
|
||||
|
||||
CONFIG.forEach(function (c) {
|
||||
if (!(c.key in values)) return;
|
||||
if (c.section !== lastSection) {
|
||||
if (lines.length) lines.push("");
|
||||
if (sections[c.section]) lines.push(sections[c.section]);
|
||||
lastSection = c.section;
|
||||
}
|
||||
var val = values[c.key];
|
||||
if (c.type === "bool") {
|
||||
lines.push(c.key + ": true");
|
||||
} else {
|
||||
lines.push(c.key + ': "' + val + '"');
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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:");
|
||||
values["_auth-acls"].forEach(function (a) {
|
||||
lines.push(" - user: " + (a.user || "*"));
|
||||
lines.push(" topic: " + a.topic);
|
||||
lines.push(" permission: " + 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);
|
||||
});
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function dockerPath(p) {
|
||||
return DOCKER_PATH_MAP[p] || p;
|
||||
}
|
||||
|
||||
function generateDockerCompose(values) {
|
||||
var lines = [];
|
||||
lines.push("services:");
|
||||
lines.push(" ntfy:");
|
||||
lines.push(' image: binwiederhier/ntfy');
|
||||
lines.push(" command: serve");
|
||||
lines.push(" environment:");
|
||||
|
||||
CONFIG.forEach(function (c) {
|
||||
if (!(c.key in values)) return;
|
||||
var val = values[c.key];
|
||||
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");
|
||||
}
|
||||
lines.push(" " + c.env + ": " + val);
|
||||
});
|
||||
|
||||
// Auth users in Docker
|
||||
if (values["_auth-users"]) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push(" volumes:");
|
||||
lines.push(" - ./:/var/lib/ntfy");
|
||||
lines.push(" ports:");
|
||||
|
||||
var listen = values["listen-http"] || ":80";
|
||||
var port = listen.replace(/.*:/, "");
|
||||
lines.push(' - "8080:' + port + '"');
|
||||
|
||||
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() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return;
|
||||
|
||||
var values = collectValues();
|
||||
var codeEl = gen.querySelector("#cg-code");
|
||||
if (!codeEl) return;
|
||||
|
||||
var activeTab = gen.querySelector(".cg-tab.active");
|
||||
var format = activeTab ? activeTab.getAttribute("data-format") : "server-yml";
|
||||
|
||||
var output = "";
|
||||
var hasValues = false;
|
||||
for (var k in values) {
|
||||
if (values.hasOwnProperty(k)) { hasValues = true; break; }
|
||||
}
|
||||
|
||||
if (!hasValues) {
|
||||
codeEl.innerHTML = '<span class="cg-empty-msg">Configure options on the left to generate your config...</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "server-yml") {
|
||||
output = generateServerYml(values);
|
||||
} else if (format === "docker-compose") {
|
||||
output = generateDockerCompose(values);
|
||||
} else {
|
||||
output = generateEnvVars(values);
|
||||
}
|
||||
|
||||
codeEl.textContent = output;
|
||||
}
|
||||
|
||||
function updateConditionalVisibility() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
if (!gen) return;
|
||||
|
||||
var isPostgres = gen.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||
isPostgres = isPostgres && isPostgres.checked;
|
||||
|
||||
// Database fields
|
||||
var sqliteFields = gen.querySelector("#cg-sqlite-fields");
|
||||
var pgFields = gen.querySelector("#cg-postgres-fields");
|
||||
if (sqliteFields) sqliteFields.style.display = isPostgres ? "none" : "block";
|
||||
if (pgFields) pgFields.style.display = isPostgres ? "block" : "none";
|
||||
|
||||
// Hide auth-file and web-push-file if PostgreSQL
|
||||
var authFile = gen.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"]');
|
||||
if (wpFile) {
|
||||
var wpField = wpFile.closest(".cg-field");
|
||||
if (wpField) wpField.style.display = isPostgres ? "none" : "";
|
||||
}
|
||||
|
||||
// Conditional sections (checkboxes that show/hide detail fields)
|
||||
var toggles = gen.querySelectorAll("[data-toggle]");
|
||||
toggles.forEach(function (toggle) {
|
||||
var target = gen.querySelector("#" + toggle.getAttribute("data-toggle"));
|
||||
if (target) {
|
||||
if (toggle.checked) {
|
||||
target.classList.add("visible");
|
||||
} else {
|
||||
target.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addRepeatableRow(container, type) {
|
||||
var row = document.createElement("div");
|
||||
row.className = "cg-repeatable-row cg-auth-" + type + "-row";
|
||||
|
||||
if (type === "user") {
|
||||
row.innerHTML =
|
||||
'<input type="text" data-field="username" placeholder="Username">' +
|
||||
'<input type="text" data-field="password" placeholder="Password hash (bcrypt)">' +
|
||||
'<select data-field="role"><option value="user">user</option><option value="admin">admin</option></select>' +
|
||||
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
||||
} else if (type === "acl") {
|
||||
row.innerHTML =
|
||||
'<input type="text" data-field="username" placeholder="Username (* for everyone)">' +
|
||||
'<input type="text" data-field="topic" placeholder="Topic pattern">' +
|
||||
'<select data-field="permission"><option value="read-write">read-write</option><option value="read-only">read-only</option><option value="write-only">write-only</option><option value="deny">deny</option></select>' +
|
||||
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
||||
} else if (type === "token") {
|
||||
row.innerHTML =
|
||||
'<input type="text" data-field="username" placeholder="Username">' +
|
||||
'<input type="text" data-field="token" placeholder="Token">' +
|
||||
'<input type="text" data-field="label" placeholder="Label (optional)">' +
|
||||
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
||||
}
|
||||
|
||||
row.querySelector(".cg-btn-remove").addEventListener("click", function () {
|
||||
row.remove();
|
||||
updateOutput();
|
||||
});
|
||||
row.querySelectorAll("input, select").forEach(function (el) {
|
||||
el.addEventListener("input", updateOutput);
|
||||
});
|
||||
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function initGenerator() {
|
||||
var gen = document.getElementById("config-generator-app");
|
||||
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
|
||||
gen.querySelectorAll(".cg-tab").forEach(function (tab) {
|
||||
tab.addEventListener("click", function () {
|
||||
gen.querySelectorAll(".cg-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) {
|
||||
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
||||
el.addEventListener(evt, function () {
|
||||
updateConditionalVisibility();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
// Add buttons for repeatable rows
|
||||
gen.querySelectorAll(".cg-btn-add").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var type = btn.getAttribute("data-add-type");
|
||||
var container = btn.previousElementSibling;
|
||||
if (!container) container = btn.parentElement.querySelector(".cg-repeatable-container");
|
||||
addRepeatableRow(container, type);
|
||||
});
|
||||
});
|
||||
|
||||
// Copy button
|
||||
var copyBtn = gen.querySelector("#cg-copy-btn");
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener("click", function () {
|
||||
var code = gen.querySelector("#cg-code");
|
||||
if (code && code.textContent) {
|
||||
navigator.clipboard.writeText(code.textContent).then(function () {
|
||||
copyBtn.textContent = "Copied!";
|
||||
setTimeout(function () { copyBtn.textContent = "Copy"; }, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Upstream checkbox special handling
|
||||
var upstreamCheck = gen.querySelector("#cg-upstream-check");
|
||||
if (upstreamCheck) {
|
||||
upstreamCheck.addEventListener("change", function () {
|
||||
var input = gen.querySelector('[data-key="upstream-base-url"]');
|
||||
if (input) input.value = upstreamCheck.checked ? "https://ntfy.sh" : "";
|
||||
updateOutput();
|
||||
});
|
||||
}
|
||||
|
||||
// Metrics checkbox special handling
|
||||
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();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initGenerator);
|
||||
} else {
|
||||
initGenerator();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user