Settings: add dedicated DAV/Pushover tabs, fix CalDAV/CardDAV bugs

- Add admin DAV tab (rename from CalDAV/CardDAV) and Pushover tab
  - Add per-user Pushover tab (User Key only; App Token stays admin-managed)
  - Remove system-wide CalDAV/CardDAV fallback — per-user config only
  - Rewrite contacts_tool.py using httpx directly (caldav 2.x dropped AddressBook)
  - Fix CardDAV REPORT/PROPFIND using SOGo URL pattern
  - Fix CalDAV/CardDAV test endpoints (POST method, URL scheme normalization)
  - Fix Show Password button — API now returns actual credential values
  - Convert Credentials tab to generic key-value store; dedicated keys
    (CalDAV, Pushover, trusted_proxy) excluded via _DEDICATED_CRED_KEYS
This commit is contained in:
2026-04-10 12:06:23 +02:00
parent a9ca08f13d
commit 7b0a9ccc2b
25 changed files with 4011 additions and 235 deletions

View File

@@ -17,6 +17,8 @@
<button type="button" class="tab-btn active" id="stab-general" onclick="switchSettingsTab('general')">General</button>
<button type="button" class="tab-btn" id="stab-whitelists" onclick="switchSettingsTab('whitelists')">Whitelists</button>
<button type="button" class="tab-btn" id="stab-credentials" onclick="switchSettingsTab('credentials')">Credentials</button>
<button type="button" class="tab-btn" id="stab-caldav" onclick="switchSettingsTab('caldav')">DAV</button>
<button type="button" class="tab-btn" id="stab-pushover" onclick="switchSettingsTab('pushover')">Pushover</button>
<button type="button" class="tab-btn" id="stab-inbox" onclick="switchSettingsTab('inbox')">Inbox</button>
<button type="button" class="tab-btn" id="stab-emailaccounts" onclick="switchSettingsTab('emailaccounts')">Email Accounts</button>
<button type="button" class="tab-btn" id="stab-telegram" onclick="switchSettingsTab('telegram')">Telegram</button>
@@ -25,6 +27,7 @@
<button type="button" class="tab-btn" id="stab-mcp" onclick="switchSettingsTab('mcp')">MCP Servers</button>
<button type="button" class="tab-btn" id="stab-security" onclick="switchSettingsTab('security')">Security</button>
<button type="button" class="tab-btn" id="stab-branding" onclick="switchSettingsTab('branding')">Branding</button>
<button type="button" class="tab-btn" id="stab-webhooks" onclick="switchSettingsTab('webhooks')">Webhooks</button>
<button type="button" class="tab-btn" id="stab-mfa" onclick="switchSettingsTab('mfa')">Profile</button>
</div>
{% else %}
@@ -34,10 +37,12 @@
<button type="button" class="tab-btn" id="ustab-personality" onclick="switchUserTab('personality')">Personality</button>
<button type="button" class="tab-btn" id="ustab-inbox" onclick="switchUserTab('inbox')">Inbox</button>
<button type="button" class="tab-btn" id="ustab-emailaccounts" onclick="switchUserTab('emailaccounts')">Email Accounts</button>
<button type="button" class="tab-btn" id="ustab-caldav" onclick="switchUserTab('caldav')">CalDAV</button>
<button type="button" class="tab-btn" id="ustab-caldav" onclick="switchUserTab('caldav')">CalDAV / CardDAV</button>
<button type="button" class="tab-btn" id="ustab-telegram" onclick="switchUserTab('telegram')">Telegram</button>
<button type="button" class="tab-btn" id="ustab-mcp" onclick="switchUserTab('mcp')">MCP Servers</button>
<button type="button" class="tab-btn" id="ustab-brain" onclick="switchUserTab('brain')">2nd Brain</button>
<button type="button" class="tab-btn" id="ustab-pushover" onclick="switchUserTab('pushover')">Pushover</button>
<button type="button" class="tab-btn" id="ustab-webhooks" onclick="switchUserTab('webhooks')">Webhooks</button>
<button type="button" class="tab-btn" id="ustab-mfa" onclick="switchUserTab('mfa')">Profile</button>
</div>
{% endif %}
@@ -107,7 +112,7 @@
Override the defaults from <code>.env</code>. Changes take effect immediately — no restart needed.
Individual agents can further override <em>Max tool calls</em> on their own settings page.
</p>
<form id="limits-form" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;max-width:520px">
<form id="limits-form" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;max-width:680px">
<div class="form-group" style="flex:1;min-width:160px;margin-bottom:0">
<label>Max tool calls per run</label>
<input type="number" id="lim-tool-calls" class="form-input" min="1" max="200">
@@ -116,6 +121,10 @@
<label>Max autonomous runs / hour</label>
<input type="number" id="lim-runs-per-hour" class="form-input" min="1" max="1000">
</div>
<div class="form-group" style="flex:1;min-width:160px;margin-bottom:0">
<label>Max concurrent runs</label>
<input type="number" id="lim-concurrent-runs" class="form-input" min="1" max="20">
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Save</button>
</form>
<div id="limits-defaults" style="font-size:11px;color:var(--text-dim);margin-top:10px"></div>
@@ -404,17 +413,18 @@
<!-- Stored credentials -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Encrypted Credentials</h2>
<h2 class="settings-section-title">Encrypted Credential Store</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Credentials for CalDAV, Email, and Pushover. All values are stored AES-256-GCM encrypted.
Inbox, Telegram, and other integration credentials are managed in their respective Settings tabs.
Generic key-value store for any credentials not managed in a dedicated settings tab.
All values are stored AES-256-GCM encrypted.
CalDAV, CardDAV, Pushover, Inbox, and Telegram credentials are managed in their own tabs.
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Credential</th>
<th>Used By</th>
<th>Key</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
@@ -428,6 +438,102 @@
</div><!-- /spane-credentials -->
<!-- ══════════════════════════════════════════════════════════
TAB: CalDAV / CardDAV (admin system credentials)
═══════════════════════════════════════════════════════════ -->
<div id="spane-caldav" style="display:none">
<!-- CalDAV (Calendar) -->
<section style="max-width:560px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">My CalDAV <span style="font-weight:400;font-size:13px;color:var(--text-dim)">(Calendar)</span></h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Your personal CalDAV credentials. Each user configures their own — there is no shared fallback.
</p>
<div class="form-group"><label>Mailcow Host</label>
<input type="text" id="adm-caldav-host" class="form-input" placeholder="mail.example.com"></div>
<div class="form-group"><label>Username</label>
<input type="text" id="adm-caldav-username" class="form-input" placeholder="jarvis@example.com"></div>
<div class="form-group"><label>Password</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="adm-caldav-password" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('adm-caldav-password', this)">Show</button>
</div>
</div>
<div class="form-group"><label>Calendar name <span style="color:var(--text-dim);font-size:11px">(optional)</span></label>
<input type="text" id="adm-caldav-calendar-name" class="form-input" placeholder="Leave blank to use first found"></div>
<div style="margin-top:4px">
<button class="btn btn-ghost btn-small" onclick="testAdminCaldav()" id="adm-caldav-test-btn">Test CalDAV</button>
<div id="adm-caldav-test-result" style="margin-top:8px;font-size:13px"></div>
</div>
</section>
<!-- CardDAV (Contacts) -->
<section style="max-width:560px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">System CardDAV <span style="font-weight:400;font-size:13px;color:var(--text-dim)">(Contacts)</span></h2>
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<input type="checkbox" id="adm-carddav-same" style="width:auto" onchange="onAdmCarddavSameChange()">
<label for="adm-carddav-same" style="margin:0;cursor:pointer;font-size:13px">Same server as CalDAV (use CalDAV credentials)</label>
</div>
<div id="adm-carddav-custom-fields">
<div class="form-group"><label>CardDAV Server URL</label>
<input type="text" id="adm-carddav-url" class="form-input" placeholder="https://contacts.example.com"></div>
<div class="form-group"><label>Username</label>
<input type="text" id="adm-carddav-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>Password</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="adm-carddav-password" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('adm-carddav-password', this)">Show</button>
</div>
</div>
</div>
<div class="form-group" style="display:flex;align-items:center;gap:10px;padding-top:12px;border-top:1px solid var(--border)">
<input type="checkbox" id="adm-contacts-allow-write" style="width:auto">
<label for="adm-contacts-allow-write" style="margin:0;cursor:pointer;font-size:13px">
Allow contact writes <span style="color:var(--text-dim)">(agents can create/update/delete contacts)</span>
</label>
</div>
<div style="margin-top:8px">
<button class="btn btn-ghost btn-small" onclick="testAdminCarddav()" id="adm-carddav-test-btn">Test CardDAV</button>
<div id="adm-carddav-test-result" style="margin-top:8px;font-size:13px"></div>
</div>
</section>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-primary" onclick="saveAdminCaldav()">Save all</button>
</div>
</div><!-- /spane-caldav -->
<!-- ══════════════════════════════════════════════════════════
TAB: Pushover (admin)
═══════════════════════════════════════════════════════════ -->
<div id="spane-pushover" style="display:none">
<section style="max-width:520px">
<h2 class="settings-section-title">Pushover</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
Pushover delivers push notifications to iOS/Android devices.
The <strong>App Token</strong> is created at <a href="https://pushover.net" target="_blank" style="color:var(--accent)">pushover.net</a>
and is shared across all users of this installation.
Each user sets their own <strong>User Key</strong> in their personal Settings → Pushover tab.
</p>
<div class="form-group"><label>App Token</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="adm-pushover-app-token" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('adm-pushover-app-token', this)">Show</button>
</div>
<div id="adm-pushover-app-token-status" style="font-size:12px;color:var(--text-dim);margin-top:4px"></div>
</div>
<div class="form-group"><label>Your User Key <span style="color:var(--text-dim);font-size:11px">(admin's own notifications)</span></label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="adm-pushover-user-key" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('adm-pushover-user-key', this)">Show</button>
</div>
<div id="adm-pushover-user-key-status" style="font-size:12px;color:var(--text-dim);margin-top:4px"></div>
</div>
<button class="btn btn-primary" onclick="saveAdminPushover()">Save</button>
</section>
</div><!-- /spane-pushover -->
<!-- ══════════════════════════════════════════════════════════
TAB: Inbox
═══════════════════════════════════════════════════════════ -->
@@ -1081,6 +1187,127 @@
</div><!-- /spane-mfa -->
<!-- ══════════════════════════════════════════════════════════
ADMIN: Webhooks
═══════════════════════════════════════════════════════════ -->
<div id="spane-webhooks" style="display:none">
<!-- Inbound Endpoints -->
<section style="margin-bottom:40px">
<h2 class="settings-section-title">Inbound Webhook Triggers</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Each endpoint has a secret token. POST <code>/webhook/{token}</code> with <code>{"message":"..."}</code>
— or GET <code>/webhook/{token}?q=...</code> for iOS Shortcuts — to trigger the associated agent.
</p>
<button class="btn btn-primary btn-small" onclick="openWebhookModal(null)" style="margin-bottom:16px">+ Add Endpoint</button>
<div class="table-wrap">
<table>
<thead><tr>
<th>Name</th>
<th>Agent</th>
<th>Status</th>
<th>Triggers</th>
<th>Last triggered</th>
<th style="text-align:right">Actions</th>
</tr></thead>
<tbody id="webhooks-tbody"><tr><td colspan="6" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
<!-- Outbound Targets -->
<section>
<h2 class="settings-section-title">Outbound Webhook Targets</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Named targets agents can POST to using the <code>webhook</code> tool.
</p>
<button class="btn btn-primary btn-small" onclick="openWebhookTargetModal(null)" style="margin-bottom:16px">+ Add Target</button>
<div class="table-wrap">
<table>
<thead><tr>
<th>Name</th>
<th>URL</th>
<th>Status</th>
<th style="text-align:right">Actions</th>
</tr></thead>
<tbody id="webhook-targets-tbody"><tr><td colspan="4" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
</div><!-- /spane-webhooks -->
<!-- Webhook endpoint modal -->
<div class="modal-overlay" id="webhook-modal" style="display:none">
<div class="modal" style="max-width:500px;width:100%">
<h3 id="webhook-modal-title" style="margin-bottom:20px">Add Webhook Endpoint</h3>
<input type="hidden" id="webhook-modal-id">
<div class="form-group">
<label>Name</label>
<input type="text" id="webhook-modal-name" class="form-input" placeholder="e.g. GitHub Push" autocomplete="off">
</div>
<div class="form-group">
<label>Description <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="webhook-modal-desc" class="form-input" placeholder="What triggers this?">
</div>
<div class="form-group">
<label>Agent to trigger</label>
<select id="webhook-modal-agent" class="form-input"></select>
</div>
<div class="form-group" style="display:flex;align-items:center;gap:10px">
<input type="checkbox" id="webhook-modal-allow-get" checked style="width:auto">
<label for="webhook-modal-allow-get" style="margin:0;cursor:pointer">Allow GET requests (for iOS Shortcuts)</label>
</div>
<div class="modal-buttons" style="margin-top:20px">
<button type="button" class="btn btn-ghost" onclick="closeWebhookModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveWebhook()">Save</button>
</div>
</div>
</div>
<!-- Token reveal modal (shown once after create or rotate) -->
<div class="modal-overlay" id="webhook-token-modal" style="display:none">
<div class="modal" style="max-width:520px;width:100%">
<h3 style="margin-bottom:8px">Webhook Token</h3>
<p style="font-size:12px;color:var(--yellow);margin-bottom:16px">Copy this token now — it will not be shown again.</p>
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="webhook-token-value" class="form-input" readonly style="font-family:var(--mono);font-size:12px">
<button class="btn btn-ghost btn-small" onclick="copyWebhookToken()">Copy</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-top:14px">
POST: <code id="webhook-token-url-post"></code><br>
GET: <code id="webhook-token-url-get"></code>
</p>
<div class="modal-buttons" style="margin-top:20px">
<button type="button" class="btn btn-primary" onclick="closeWebhookTokenModal()">Done</button>
</div>
</div>
</div>
<!-- Outbound target modal -->
<div class="modal-overlay" id="webhook-target-modal" style="display:none">
<div class="modal" style="max-width:480px;width:100%">
<h3 id="webhook-target-modal-title" style="margin-bottom:20px">Add Webhook Target</h3>
<input type="hidden" id="webhook-target-modal-id">
<div class="form-group">
<label>Name <span style="color:var(--text-dim)">(used by agents)</span></label>
<input type="text" id="webhook-target-modal-name" class="form-input" placeholder="e.g. home-assistant" autocomplete="off">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="webhook-target-modal-url" class="form-input" placeholder="https://...">
</div>
<div class="form-group">
<label>Secret header value <span style="color:var(--text-dim)">(optional — sent as Authorization: Bearer)</span></label>
<input type="password" id="webhook-target-modal-secret" class="form-input" placeholder="Leave blank to omit">
</div>
<div class="modal-buttons" style="margin-top:20px">
<button type="button" class="btn btn-ghost" onclick="closeWebhookTargetModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveWebhookTarget()">Save</button>
</div>
</div>
</div>
{% else %}
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: API Keys
@@ -1211,38 +1438,79 @@
</section>
</div><!-- /uspane-emailaccounts -->
<!-- CalDAV tab -->
<!-- CalDAV / CardDAV tab -->
<div id="uspane-caldav" style="display:none">
<section>
<h2 class="settings-section-title">My CalDAV</h2>
<!-- ── CalDAV (Calendar) ── -->
<section style="max-width:560px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">My CalDAV <span style="font-weight:400;font-size:13px;color:var(--text-dim)">(Calendar)</span></h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Your personal CalDAV server for calendar access. Leave blank to use the system CalDAV server
configured by the admin.
Used by the <code>caldav</code> tool for reading and writing calendar events.
Used by the <code>caldav</code> tool for reading and writing calendar events.
</p>
<div style="max-width:520px">
<div class="form-group"><label>CalDAV Server URL</label>
<input type="text" id="my-caldav-url" class="form-input" placeholder="https://mail.example.com"></div>
<div class="form-group"><label>CalDAV Server URL</label>
<input type="text" id="my-caldav-url" class="form-input" placeholder="https://mail.example.com"></div>
<div class="form-group"><label>Username</label>
<input type="text" id="my-caldav-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>Password</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="my-caldav-password" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('my-caldav-password', this)">Show</button>
</div>
</div>
<div class="form-group"><label>Calendar name <span style="color:var(--text-dim);font-size:11px">(optional — uses first found if blank)</span></label>
<input type="text" id="my-caldav-calendar-name" class="form-input" placeholder="Personal"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px">
<button class="btn btn-ghost btn-small" onclick="testMyCaldavConfig()" id="caldav-test-btn">Test CalDAV</button>
</div>
<div id="caldav-test-result" style="margin-top:10px;font-size:13px"></div>
</section>
<!-- ── CardDAV (Contacts) ── -->
<section style="max-width:560px;margin-bottom:32px">
<h2 class="settings-section-title">My CardDAV <span style="font-weight:400;font-size:13px;color:var(--text-dim)">(Contacts)</span></h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Used by the <code>contacts</code> tool for reading (and optionally writing) address book contacts.
</p>
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<input type="checkbox" id="my-carddav-same" style="width:auto" onchange="onCarddavSameChange()">
<label for="my-carddav-same" style="margin:0;cursor:pointer;font-size:13px">
Same server as CalDAV (use CalDAV credentials for contacts)
</label>
</div>
<div id="carddav-custom-fields">
<div class="form-group"><label>CardDAV Server URL</label>
<input type="text" id="my-carddav-url" class="form-input" placeholder="https://contacts.example.com"></div>
<div class="form-group"><label>Username</label>
<input type="text" id="my-caldav-username" class="form-input" placeholder="user@example.com"></div>
<input type="text" id="my-carddav-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>Password</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="my-caldav-password" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('my-caldav-password', this)">Show</button>
<input type="password" id="my-carddav-password" class="form-input" placeholder="Leave blank to keep existing">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('my-carddav-password', this)">Show</button>
</div>
</div>
<div class="form-group"><label>Calendar name <span style="color:var(--text-dim);font-size:11px">(optional — uses first found if blank)</span></label>
<input type="text" id="my-caldav-calendar-name" class="form-input" placeholder="Personal"></div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<button class="btn btn-primary" onclick="saveMyCaldavConfig()">Save</button>
<button class="btn btn-ghost btn-small" onclick="testMyCaldavConfig()" id="caldav-test-btn">Test connection</button>
<button class="btn btn-danger btn-small" onclick="deleteMyCaldavConfig()">Clear / use system CalDAV</button>
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-top:8px;padding-top:16px;border-top:1px solid var(--border)">
<input type="checkbox" id="my-contacts-allow-write" style="width:auto">
<label for="my-contacts-allow-write" style="margin:0;cursor:pointer;font-size:13px">
Allow contact writes <span style="color:var(--text-dim)">(create, update, delete — requires confirmation)</span>
</label>
</div>
<div id="caldav-test-result" style="margin-top:12px;font-size:13px"></div>
<p style="margin-top:16px;font-size:12px;color:var(--text-dim)">
If left blank, the system CalDAV server will be used (configured by the admin in Credentials).
</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px">
<button class="btn btn-ghost btn-small" onclick="testMyCarddavConfig()" id="carddav-test-btn">Test CardDAV</button>
</div>
<div id="carddav-test-result" style="margin-top:10px;font-size:13px"></div>
</section>
<!-- ── Save / Clear ── -->
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<button class="btn btn-primary" onclick="saveMyCaldavConfig()">Save all</button>
<button class="btn btn-danger btn-small" onclick="deleteMyCaldavConfig()">Clear all</button>
</div>
<p style="margin-top:12px;font-size:12px;color:var(--text-dim)">
Each user must configure their own CalDAV / CardDAV credentials. There is no shared fallback.
</p>
</div><!-- /uspane-caldav -->
<div id="uspane-telegram" style="display:none">
@@ -1335,6 +1603,83 @@
</section>
</div><!-- /uspane-brain -->
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: Pushover
═══════════════════════════════════════════════════════════ -->
<div id="uspane-pushover" style="display:none">
<section style="max-width:520px">
<h2 class="settings-section-title">My Pushover Notifications</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Set your personal Pushover <strong>User Key</strong> to receive push notifications on your devices.
The App Token is shared and managed by the admin.
Find your User Key at <a href="https://pushover.net" target="_blank" style="color:var(--accent)">pushover.net</a> → your user page.
</p>
<div id="my-pushover-status" style="font-size:12px;margin-bottom:12px"></div>
<div class="form-group"><label>Your User Key</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="my-pushover-user-key" class="form-input" placeholder="uXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">
<button type="button" class="btn btn-ghost btn-small" onclick="togglePasswordVisibility('my-pushover-user-key', this)">Show</button>
</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-primary" onclick="saveMyPushover()">Save</button>
<button class="btn btn-danger btn-small" onclick="deleteMyPushover()">Remove</button>
</div>
</section>
</div><!-- /uspane-pushover -->
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: Webhooks
═══════════════════════════════════════════════════════════ -->
<div id="uspane-webhooks" style="display:none">
<!-- Inbound endpoints -->
<section style="margin-bottom:40px">
<h2 class="settings-section-title">Inbound Endpoints</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Webhook endpoints that trigger your agents. Each has a secret token —
POST <code>/webhook/{token}</code> with <code>{"message":"..."}</code>
or GET <code>/webhook/{token}?q=...</code> for iOS Shortcuts.
</p>
<button class="btn btn-primary btn-small" onclick="openMyWebhookModal(null)" style="margin-bottom:16px">+ Add Endpoint</button>
<div class="table-wrap">
<table>
<thead><tr>
<th>Name</th>
<th>Agent</th>
<th>Status</th>
<th>Triggers</th>
<th>Last triggered</th>
<th style="text-align:right">Actions</th>
</tr></thead>
<tbody id="my-webhooks-tbody"><tr><td colspan="6" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
<!-- Outbound targets -->
<section>
<h2 class="settings-section-title">Outbound Targets</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Named targets your agents can POST to using the <code>webhook</code> tool.
Targets you define here are only visible to your own agents.
</p>
<button class="btn btn-primary btn-small" onclick="openMyWebhookTargetModal(null)" style="margin-bottom:16px">+ Add Target</button>
<div class="table-wrap">
<table>
<thead><tr>
<th>Name</th>
<th>URL</th>
<th>Status</th>
<th style="text-align:right">Actions</th>
</tr></thead>
<tbody id="my-webhook-targets-tbody"><tr><td colspan="4" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
</div><!-- /uspane-webhooks -->
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: Profile
═══════════════════════════════════════════════════════════ -->
@@ -1657,31 +2002,9 @@
<div class="modal" style="max-width:480px;width:100%">
<h3 style="margin-bottom:20px" id="cred-modal-title">Add Credential</h3>
<div class="form-group" id="cred-modal-key-group">
<label>Credential</label>
<select id="cred-modal-key-select" class="form-input" onchange="onCredKeySelectChange()">
<option value="">- choose a credential -</option>
<optgroup label="CalDAV">
<option value="mailcow_host">Mailcow Host</option>
<option value="mailcow_username">Mailcow Username</option>
<option value="mailcow_password">Mailcow Password</option>
<option value="caldav_calendar_name">Calendar Name</option>
</optgroup>
<optgroup label="Email">
<option value="mailcow_imap_host">IMAP Host</option>
<option value="mailcow_smtp_host">SMTP Host</option>
<option value="mailcow_smtp_port">SMTP Port</option>
</optgroup>
<optgroup label="Pushover">
<option value="pushover_user_key">User Key</option>
<option value="pushover_app_token">App Token</option>
</optgroup>
<option value="__custom__">Custom…</option>
</select>
</div>
<div class="form-group" id="cred-modal-custom-group" style="display:none">
<label>Custom credential name</label>
<label>Key name</label>
<input type="text" id="cred-modal-key-custom" class="form-input"
placeholder="e.g. my_api_key" autocomplete="off">
placeholder="e.g. my_api_key or service:token" autocomplete="off">
</div>
<div class="form-group">
<label>Value</label>
@@ -1727,6 +2050,58 @@
</div>
</div>
</div>
<!-- User webhook target modal -->
<div class="modal-overlay" id="my-webhook-target-modal" style="display:none">
<div class="modal" style="max-width:480px;width:100%">
<h3 id="my-webhook-target-modal-title" style="margin-bottom:20px">Add Outbound Target</h3>
<input type="hidden" id="my-webhook-target-modal-id">
<div class="form-group">
<label>Name <span style="color:var(--text-dim)">(used by agents)</span></label>
<input type="text" id="my-webhook-target-modal-name" class="form-input" placeholder="e.g. home-assistant" autocomplete="off">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="my-webhook-target-modal-url" class="form-input" placeholder="https://...">
</div>
<div class="form-group">
<label>Secret header value <span style="color:var(--text-dim)">(optional — sent as Authorization: Bearer)</span></label>
<input type="password" id="my-webhook-target-modal-secret" class="form-input" placeholder="Leave blank to omit">
</div>
<div class="modal-buttons" style="margin-top:20px">
<button type="button" class="btn btn-ghost" onclick="closeMyWebhookTargetModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveMyWebhookTarget()">Save</button>
</div>
</div>
</div>
<!-- User webhook modal -->
<div class="modal-overlay" id="my-webhook-modal" style="display:none">
<div class="modal" style="max-width:500px;width:100%">
<h3 id="my-webhook-modal-title" style="margin-bottom:20px">Add Webhook</h3>
<input type="hidden" id="my-webhook-modal-id">
<div class="form-group">
<label>Name</label>
<input type="text" id="my-webhook-modal-name" class="form-input" placeholder="e.g. iOS Shortcut" autocomplete="off">
</div>
<div class="form-group">
<label>Description <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="my-webhook-modal-desc" class="form-input" placeholder="What triggers this?">
</div>
<div class="form-group">
<label>Agent to trigger</label>
<select id="my-webhook-modal-agent" class="form-input"></select>
</div>
<div class="form-group" style="display:flex;align-items:center;gap:10px">
<input type="checkbox" id="my-webhook-modal-allow-get" checked style="width:auto">
<label for="my-webhook-modal-allow-get" style="margin:0;cursor:pointer">Allow GET requests (for iOS Shortcuts)</label>
</div>
<div class="modal-buttons" style="margin-top:20px">
<button type="button" class="btn btn-ghost" onclick="closeMyWebhookModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveMyWebhook()">Save</button>
</div>
</div>
</div>
{% block extra_scripts %}
<script>window.IS_ADMIN = {{ current_user.is_admin | tojson }};</script>
{% endblock %}