Files
oai-web/server/web/templates/settings.html
Rune Olsen 7b0a9ccc2b 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
2026-04-10 12:06:23 +02:00

2109 lines
118 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ agent_name }} — Settings{% endblock %}
{% block content %}
<div class="page">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<h1>Settings</h1>
<div id="flash-msg" style="
font-size:13px;color:var(--green);
opacity:0;transition:opacity 0.3s;
"></div>
</div>
{% if current_user.is_admin %}
<!-- ── Admin Tabs ── -->
<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:28px">
<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>
<button type="button" class="tab-btn" id="stab-system" onclick="switchSettingsTab('system')">Personality</button>
<button type="button" class="tab-btn" id="stab-brain" onclick="switchSettingsTab('brain')">2nd Brain</button>
<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 %}
<!-- ── User Tabs ── -->
<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:28px">
<button type="button" class="tab-btn active" id="ustab-apikeys" onclick="switchUserTab('apikeys')">API Keys</button>
<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 / 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 %}
{% if current_user.is_admin %}
<!-- ══════════════════════════════════════════════════════════
TAB: General
═══════════════════════════════════════════════════════════ -->
<div id="spane-general">
<!-- Agent control -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Agent Control</h2>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">
<button type="button" class="btn {% if is_paused %}btn-ghost paused{% else %}btn-primary{% endif %}"
id="settings-pause-btn" onclick="togglePause()">
<span class="btn-icon">{% if is_paused %}▶{% else %}⏸{% endif %}</span>
{% if is_paused %}Resume agent{% else %}Pause agent{% endif %}
</button>
<span style="font-size:12px;color:var(--text-dim)">
Status: <span id="status-label">{% if is_paused %}Paused{% else %}Running{% endif %}</span>
&nbsp;<span class="status-dot {% if is_paused %}paused{% endif %}" id="status-dot"></span>
</span>
</div>
</section>
<!-- Trusted proxy IPs -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Trusted Proxy IPs</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:8px">
If {{ agent_name }} is accessed through a reverse proxy (e.g. Zoraxy, nginx),
enter the proxy's IP address here. This lets {{ agent_name }} log the real visitor IP
instead of the proxy's internal IP.
</p>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Leave as <code>127.0.0.1</code> if the proxy runs on the same machine.
Use a LAN IP (e.g. <code>192.168.1.50</code>) if the proxy is on a different server.
Subnet notation is supported (e.g. <code>192.168.1.0/24</code>).
Separate multiple entries with commas.
</p>
<form id="proxy-trust-form" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;max-width:520px">
<div class="form-group" style="flex:1;min-width:200px;margin-bottom:0">
<label>Trusted proxy IP(s)</label>
<input type="text" id="proxy-trust-ips" class="form-input"
placeholder="127.0.0.1" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Save</button>
</form>
<div style="
margin-top:14px;padding:10px 14px;
background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
font-size:12px;color:var(--text-dim);max-width:520px;line-height:1.6
">
<strong style="color:var(--text)">Restart required after saving.</strong>
The setting is stored immediately, but only takes effect after a server restart.<br>
On Docker: run <code>docker compose restart</code> on the server.
On local: stop and restart uvicorn.
</div>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- Runtime limits -->
<section>
<h2 class="settings-section-title">Runtime Limits</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
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: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">
</div>
<div class="form-group" style="flex:1;min-width:180px;margin-bottom:0">
<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>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px;margin-top:32px"></div>
<!-- Default Models -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">Default Chat Models</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Pre-select a model in the chat picker. Falls back to <code>DEFAULT_CHAT_MODEL</code> in <code>.env</code>, then the first available model.
Changes take effect immediately — no restart needed.
</p>
<form id="default-models-form" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;max-width:640px">
<div class="form-group" style="flex:1;min-width:220px;margin-bottom:0">
<label>Default model</label>
<select id="dm-default" class="form-input"></select>
</div>
<div class="form-group" style="flex:1;min-width:220px;margin-bottom:0">
<label>Default model — free tier <span style="font-size:11px;color:var(--text-dim)">(users without own API key)</span></label>
<select id="dm-free" class="form-input"></select>
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Save</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- Audit Log -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">Audit Log</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Automatic rotation deletes old entries daily at 03:00. Set to <em>Keep forever</em> to disable rotation.
</p>
<form id="audit-retention-form" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;max-width:520px">
<div class="form-group" style="flex:1;min-width:180px;margin-bottom:0">
<label>Retention period</label>
<select id="audit-retention-days" class="form-input">
<option value="0">Keep forever</option>
<option value="7">7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="180">180 days</option>
<option value="365">1 year</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Save</button>
</form>
<div style="margin-top:16px">
<p style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Manual clear — immediately deletes entries:</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-ghost" onclick="clearAuditLog(30)">Delete older than 30 days</button>
<button class="btn btn-ghost" onclick="clearAuditLog(7)">Delete older than 7 days</button>
<button class="btn btn-danger" onclick="clearAuditLog(0)">Clear entire log</button>
</div>
</div>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- API Key -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">API Key</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Protect the REST API for external access (scripts, home automations, other services).
The <strong>web UI always works without a key</strong> - any browser that loads the app gets a session
cookie automatically. The key is only needed for external programmatic access and Swagger.
The raw key is shown <strong>once</strong> at generation time - copy it to your external tool.
</p>
<div id="apikey-status" style="font-size:13px;margin-bottom:16px;display:flex;gap:12px;align-items:center">
Loading...
</div>
<!-- One-time reveal box (hidden until a key is generated) -->
<div id="apikey-reveal-box" style="display:none;margin-bottom:16px;max-width:600px">
<div style="
background:var(--bg3);border:1px solid var(--accent-dim);border-radius:var(--radius);
padding:12px 14px;margin-bottom:8px;
">
<div style="font-size:11px;color:var(--yellow);margin-bottom:6px;font-weight:600">
Copy this key for your external tool - it will not be shown again
</div>
<div style="display:flex;gap:8px;align-items:center">
<code id="apikey-reveal-value" style="
flex:1;font-family:var(--mono);font-size:12px;
word-break:break-all;color:var(--text);
"></code>
<button class="btn btn-ghost" onclick="copyApiKey()" style="white-space:nowrap;flex-shrink:0">
Copy
</button>
</div>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-primary" onclick="generateApiKey(false)">Generate Key</button>
<button id="apikey-regen-btn" class="btn btn-ghost" onclick="generateApiKey(true)" style="display:none">Regenerate</button>
<button id="apikey-revoke-btn" class="btn btn-danger" onclick="revokeApiKey()" style="display:none">Revoke</button>
</div>
<p style="font-size:11px;color:var(--text-dim);margin-top:12px">
External requests: use header <code>X-API-Key: &lt;key&gt;</code> or <code>Authorization: Bearer &lt;key&gt;</code>.<br>
The Swagger UI at <a href="/docs" style="color:var(--accent)">/docs</a> has an Authorize button where you can enter the key.
The web UI itself never needs the key - it uses a session cookie set automatically on page load.
</p>
</section>
<!-- Users Base Folder -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Users Base Folder</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Absolute path on the server where each user's personal folder will be provisioned.
Non-admin users can only access their own subfolder (<code>{base}/{username}/</code>).
If unset, non-admin users have no filesystem access.
</p>
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="users-base-folder" class="form-input" style="max-width:480px"
placeholder="/data/users (absolute path on server)">
<button class="btn btn-primary" onclick="saveUsersBaseFolder()">Save</button>
<button class="btn btn-ghost" onclick="clearUsersBaseFolder()">Clear</button>
</div>
</section>
<!-- Provider API Keys (admin) -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Provider API Keys</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Global API keys used by all agents and users. Changes take effect immediately —
no restart required. If a user sets their own key, it takes precedence over these.
</p>
<div style="display:flex;flex-direction:column;gap:16px;max-width:520px">
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:8px">Anthropic</div>
<div id="provider-key-anthropic-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="provider-key-anthropic-input" class="form-input" placeholder="sk-ant-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveProviderKey('anthropic')">Save</button>
<button class="btn btn-ghost" onclick="clearProviderKey('anthropic')">Clear</button>
</div>
</div>
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:8px">OpenRouter</div>
<div id="provider-key-openrouter-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="provider-key-openrouter-input" class="form-input" placeholder="sk-or-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveProviderKey('openrouter')">Save</button>
<button class="btn btn-ghost" onclick="clearProviderKey('openrouter')">Clear</button>
</div>
</div>
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:8px">OpenAI</div>
<div id="provider-key-openai-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="provider-key-openai-input" class="form-input" placeholder="sk-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveProviderKey('openai')">Save</button>
<button class="btn btn-ghost" onclick="clearProviderKey('openai')">Clear</button>
</div>
</div>
</div>
</section>
</div><!-- /spane-general -->
<!-- ══════════════════════════════════════════════════════════
TAB: Whitelists
═══════════════════════════════════════════════════════════ -->
<div id="spane-whitelists" style="display:none">
<!-- Email whitelist -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">Email Whitelist</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
The agent can only send email to addresses listed here. Daily limit 0 = unlimited.
</p>
<div class="table-wrap" style="margin-bottom:16px">
<table>
<thead>
<tr>
<th>Email address</th>
<th>Daily limit</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="email-whitelist-list">
<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
<form id="email-whitelist-form" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;max-width:520px">
<div class="form-group" style="flex:1;min-width:200px;margin-bottom:0">
<label>Add address</label>
<input type="email" id="ew-email" class="form-input"
placeholder="you@example.com" required autocomplete="off">
</div>
<div class="form-group" style="width:120px;margin-bottom:0">
<label>Daily limit <span style="color:var(--text-dim)">(0=∞)</span></label>
<input type="number" id="ew-limit" class="form-input" value="0" min="0">
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Add</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:36px"></div>
<!-- Web whitelist -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">Web Whitelist (Tier 1)</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Domains always accessible without user confirmation. Subdomains are automatically included
(e.g. <code>wikipedia.org</code> covers <code>en.wikipedia.org</code>).
</p>
<div class="table-wrap" style="margin-bottom:16px">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Note</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="web-whitelist-list">
<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
<form id="web-whitelist-form" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;max-width:560px">
<div class="form-group" style="flex:1;min-width:160px;margin-bottom:0">
<label>Add domain</label>
<input type="text" id="ww-domain" class="form-input"
placeholder="example.com" required autocomplete="off">
</div>
<div class="form-group" style="flex:2;min-width:180px;margin-bottom:0">
<label>Note <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="ww-note" class="form-input" placeholder="e.g. Company intranet">
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Add</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:36px"></div>
<!-- Filesystem sandbox -->
<section>
<h2 class="settings-section-title">Filesystem Sandbox</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
The agent can only read and write files inside these directories. Subdirectories are included automatically.
</p>
<div class="table-wrap" style="margin-bottom:16px">
<table>
<thead>
<tr>
<th>Directory</th>
<th>Note</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="fs-whitelist-list">
<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
<form id="fs-whitelist-form" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;max-width:640px">
<div class="form-group" style="flex:2;min-width:220px;margin-bottom:0">
<label>Add directory</label>
<input type="text" id="fs-path" class="form-input"
placeholder="/home/rune/documents" required autocomplete="off">
</div>
<div class="form-group" style="flex:2;min-width:180px;margin-bottom:0">
<label>Note <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="fs-note" class="form-input" placeholder="e.g. Work documents">
</div>
<button type="button" class="btn btn-ghost" style="margin-bottom:0"
onclick="openFsBrowser()">Browse…</button>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Add</button>
</form>
</section>
</div><!-- /spane-whitelists -->
<!-- ══════════════════════════════════════════════════════════
TAB: Credentials
═══════════════════════════════════════════════════════════ -->
<div id="spane-credentials" style="display:none">
<!-- Stored credentials -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Encrypted Credential Store</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
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>Key</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="cred-list">
<tr><td colspan="4" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
</section>
</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
═══════════════════════════════════════════════════════════ -->
<div id="spane-inbox" style="display:none">
<!-- Connection status + config -->
<section style="margin-bottom:36px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<h2 class="settings-section-title" style="margin-bottom:0">Inbox Configuration</h2>
<span id="inbox-status-badge" style="font-size:12px;padding:2px 10px;border-radius:20px;background:var(--bg2);color:var(--text-dim)">Not configured</span>
<button type="button" class="btn btn-ghost" id="inbox-action-btn" style="padding:4px 12px;font-size:12px" onclick="reconnectInbox()">Reconnect</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
IMAP/SMTP credentials for the system inbox account (e.g. <code>jarvis@example.com</code>).
SMTP username and password default to the IMAP values if left blank.
</p>
<form id="inbox-config-form" style="max-width:560px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="form-group" style="margin-bottom:0">
<label>IMAP Host</label>
<input type="text" id="inbox-imap-host" class="form-input" placeholder="mail.example.com" autocomplete="off">
</div>
<div class="form-group" style="margin-bottom:0">
<label>IMAP Username</label>
<input type="text" id="inbox-imap-user" class="form-input" placeholder="jarvis@example.com" autocomplete="off">
</div>
<div class="form-group" style="margin-bottom:0">
<label>IMAP Password</label>
<input type="password" id="inbox-imap-pass" class="form-input" placeholder="Leave blank to keep current" autocomplete="new-password">
</div>
<div class="form-group" style="margin-bottom:0">
<label>SMTP Host <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="inbox-smtp-host" class="form-input" placeholder="Defaults to IMAP host" autocomplete="off">
</div>
<div class="form-group" style="margin-bottom:0">
<label>SMTP Port <span style="color:var(--text-dim)">(optional)</span></label>
<input type="number" id="inbox-smtp-port" class="form-input" placeholder="465" min="1" max="65535">
</div>
<div class="form-group" style="margin-bottom:0">
<label>SMTP Username <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="inbox-smtp-user" class="form-input" placeholder="Defaults to IMAP username" autocomplete="off">
</div>
<div class="form-group" style="margin-bottom:0">
<label>SMTP Password <span style="color:var(--text-dim)">(optional)</span></label>
<input type="password" id="inbox-smtp-pass" class="form-input" placeholder="Defaults to IMAP password" autocomplete="new-password">
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:16px">Save &amp; Reconnect</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:36px"></div>
<!-- Trigger rules -->
<section>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px">
<h2 class="settings-section-title" style="margin-bottom:0">Trigger Rules</h2>
<button type="button" class="btn btn-primary" style="padding:4px 14px;font-size:13px" onclick="openTriggerModal()">+ Add Rule</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
When the trigger word is found in an incoming email body, the linked agent runs and its
response is sent back to the sender.
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Trigger word</th>
<th>Agent</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="inbox-trigger-list">
<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
</section>
</div><!-- /spane-inbox -->
<!-- ══════════════════════════════════════════════════════════
TAB: Email Accounts (admin)
═══════════════════════════════════════════════════════════ -->
<div id="spane-emailaccounts" style="display:none">
<section>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px">
<h2 class="settings-section-title" style="margin-bottom:0">Email Handling Accounts</h2>
<button type="button" class="btn btn-primary" style="padding:4px 14px;font-size:13px" onclick="openEmailAccountModal()">+ Add</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Accounts monitored for automated email handling. Each new email is dispatched to the assigned agent.
<strong>No sending from these accounts.</strong>
</p>
<div class="table-wrap">
<table>
<thead><tr><th>Label</th><th>Account</th><th>Status</th><th>Model</th><th>Actions</th></tr></thead>
<tbody id="email-accounts-tbody"><tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
</div><!-- /spane-emailaccounts -->
<!-- ══════════════════════════════════════════════════════════
TAB: Telegram
═══════════════════════════════════════════════════════════ -->
<div id="spane-telegram" style="display:none">
<!-- Bot config + status -->
<section style="margin-bottom:36px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<h2 class="settings-section-title" style="margin-bottom:0">Bot Configuration</h2>
<span id="telegram-status-badge" style="font-size:12px;padding:2px 10px;border-radius:20px;background:var(--bg2);color:var(--text-dim)">Not configured</span>
<button type="button" class="btn btn-ghost" id="telegram-action-btn" style="padding:4px 12px;font-size:12px" onclick="reconnectTelegram()">Reconnect</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
Enter your Telegram Bot API token (from <code>@BotFather</code>). The bot will long-poll for new messages.
</p>
<form id="telegram-config-form" style="max-width:520px">
<div class="form-group">
<label>Bot Token</label>
<input type="password" id="telegram-bot-token" class="form-input"
placeholder="Leave blank to keep current" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary">Save &amp; Reconnect</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:36px"></div>
<!-- Chat ID whitelist -->
<section style="margin-bottom:36px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px">
<h2 class="settings-section-title" style="margin-bottom:0">Chat ID Whitelist</h2>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
The bot will only respond to messages from these chat IDs. To find your chat ID, message
<code>@userinfobot</code> on Telegram.
</p>
<div class="table-wrap" style="margin-bottom:16px">
<table>
<thead>
<tr>
<th>Chat ID</th>
<th>Label</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="telegram-whitelist-list">
<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
<form id="telegram-whitelist-form" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;max-width:520px">
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Chat ID</label>
<input type="text" id="tg-wl-chatid" class="form-input"
placeholder="e.g. 123456789" required autocomplete="off">
</div>
<div class="form-group" style="flex:2;min-width:160px;margin-bottom:0">
<label>Label <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="tg-wl-label" class="form-input" placeholder="e.g. My phone">
</div>
<button type="submit" class="btn btn-primary" style="margin-bottom:0">Add</button>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:36px"></div>
<!-- Trigger rules -->
<section>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px">
<h2 class="settings-section-title" style="margin-bottom:0">Trigger Rules</h2>
<button type="button" class="btn btn-primary" style="padding:4px 14px;font-size:13px" onclick="openTgTriggerModal()">+ Add Rule</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
When the trigger word is found in an incoming Telegram message, the linked agent runs and
its response is sent back to the sender.
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Trigger word</th>
<th>Agent</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="telegram-trigger-list">
<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
</section>
<!-- Default agent -->
<section style="margin-bottom:36px">
<h2 class="settings-section-title">Default Agent</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
When a message from a whitelisted user matches no trigger word, this agent runs.
Leave blank to silently drop unmatched messages.
</p>
<div style="display:flex;gap:10px;align-items:flex-end;max-width:520px">
<div class="form-group" style="flex:1;margin-bottom:0">
<label>Default agent</label>
<select id="tg-default-agent" class="form-input">
<option value="">— none (drop unmatched messages) —</option>
</select>
</div>
<button type="button" class="btn btn-primary" onclick="saveTgDefaultAgent()">Save</button>
</div>
</section>
</div><!-- /spane-telegram -->
<!-- ══════════════════════════════════════════════════════════
TAB: Personality
═══════════════════════════════════════════════════════════ -->
<div id="spane-system" style="display:none">
<!-- SOUL.md -->
<section style="margin-bottom:36px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h2 class="settings-section-title" style="margin-bottom:0">Identity &amp; Personality <code style="font-size:11px;font-weight:400;color:var(--text-dim)">SOUL.md</code></h2>
<button type="button" class="btn btn-primary" style="padding:4px 14px;font-size:13px" onclick="saveSystemPrompt('soul')">Save</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Defines the agent's name, personality, values, and communication style.
Injected as the first part of every system prompt. Changes take effect immediately.
<strong>Note:</strong> the agent name shown in the sidebar updates after a container restart.
</p>
<textarea id="sp-soul" class="form-input" rows="16"
placeholder="Loading…" style="resize:vertical;font-family:monospace;font-size:12px"></textarea>
</section>
<!-- USER.md -->
<section style="margin-bottom:36px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h2 class="settings-section-title" style="margin-bottom:0">Owner Context <code style="font-size:11px;font-weight:400;color:var(--text-dim)">USER.md</code></h2>
<button type="button" class="btn btn-primary" style="padding:4px 14px;font-size:13px" onclick="saveSystemPrompt('user')">Save</button>
</div>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Your name, location, preferences, and context injected after the date/time block.
Optional — leave blank and the section is omitted entirely.
Changes take effect immediately.
</p>
<textarea id="sp-user" class="form-input" rows="12"
placeholder="Loading…" style="resize:vertical;font-family:monospace;font-size:12px"></textarea>
</section>
</div><!-- /spane-system -->
<!-- ══════════════════════════════════════════════════════════
TAB: 2nd Brain
═══════════════════════════════════════════════════════════ -->
<div id="spane-brain" style="display:none">
<!-- Status + stats -->
<section style="margin-bottom:32px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<h2 class="settings-section-title" style="margin-bottom:0">Status</h2>
<span id="brain-status-badge" style="font-size:12px;padding:2px 10px;border-radius:20px;background:var(--bg2);color:var(--text-dim)">Loading…</span>
</div>
<div id="brain-stats" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;max-width:640px"></div>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- Quick capture -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Quick Capture</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Add a thought directly. Type and tags are extracted automatically.
</p>
<form id="brain-capture-form" style="max-width:640px">
<div class="form-group" style="margin-bottom:8px">
<textarea id="brain-capture-text" class="form-input" rows="3"
placeholder="What's on your mind?" style="resize:vertical"></textarea>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<button type="submit" class="btn btn-primary">Capture</button>
<div id="brain-capture-result" style="font-size:12px;color:var(--green);white-space:pre-line"></div>
</div>
</form>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- Search + Recent thoughts -->
<section style="margin-bottom:32px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:16px">
<h2 class="settings-section-title" style="margin-bottom:0" id="brain-table-heading">Recent Thoughts</h2>
<div style="display:flex;gap:8px;align-items:center;max-width:360px;flex:1">
<input id="brain-search-input" class="form-input" type="search"
placeholder="Semantic search…" style="flex:1;padding:6px 10px;font-size:13px"
oninput="onBrainSearchInput(this.value)">
<button id="brain-search-clear" class="btn btn-small" style="display:none" onclick="clearBrainSearch()">Clear</button>
</div>
</div>
<div class="table-wrap">
<table id="brain-recent-table">
<thead>
<tr>
<th>Content</th>
<th style="width:100px">Type</th>
<th style="width:150px">Captured</th>
</tr>
</thead>
<tbody>
<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Loading…</td></tr>
</tbody>
</table>
</div>
</section>
<!-- Thought detail modal -->
<div id="brain-thought-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);align-items:center;justify-content:center">
<div style="background:var(--bg1);border:1px solid var(--border);border-radius:10px;max-width:680px;width:90%;max-height:80vh;display:flex;flex-direction:column;overflow:hidden">
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="display:flex;gap:8px;align-items:center">
<span id="btm-type" class="badge badge-blue" style="font-size:11px"></span>
<span id="btm-date" style="font-size:12px;color:var(--text-dim)"></span>
<span id="btm-sim" style="font-size:12px;color:var(--text-dim)"></span>
</div>
<button class="btn btn-small" onclick="closeBrainThoughtModal()"></button>
</div>
<div style="padding:20px;overflow-y:auto;flex:1">
<pre id="btm-content" style="white-space:pre-wrap;word-break:break-word;font-family:inherit;font-size:14px;line-height:1.6;margin:0"></pre>
</div>
<div id="btm-tags" style="padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:6px;flex-wrap:wrap"></div>
</div>
</div>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- Auto-approve -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Jarvis Access</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
When enabled, Jarvis can access the 2nd Brain at any time without asking for permission first.
Jarvis will proactively search and capture without interrupting the conversation.
</p>
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;max-width:640px">
<input type="checkbox" id="brain-auto-approve-toggle" onchange="saveBrainAutoApprove(this.checked)">
<span style="font-size:14px">Automatically approve all 2nd Brain access</span>
</label>
<div id="brain-auto-approve-flash" style="font-size:12px;color:var(--green);margin-top:8px;display:none"></div>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<!-- MCP Access Key -->
<section>
<h2 class="settings-section-title">MCP Access Key</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Use this key to connect third-party MCP clients (Claude Desktop, Claude Code, Cursor) to your personal 2nd Brain.
All queries and captures are automatically scoped to your account.
</p>
<div style="max-width:640px">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
<input id="brain-mcp-key" type="password" class="form-input" style="flex:1;font-family:monospace" readonly>
<button class="btn btn-small" onclick="toggleBrainKeyVisibility(this, 'brain-mcp-key')" title="Show/hide">Show</button>
<button class="btn btn-small" onclick="copyBrainKey('brain-mcp-key')" title="Copy">Copy</button>
<button class="btn btn-small" style="color:var(--red)" onclick="regenerateBrainKey('brain-mcp-key', 'brain-mcp-cmd')" title="Regenerate">Regenerate</button>
</div>
<div style="background:var(--bg2);border-radius:6px;padding:12px;font-size:12px;color:var(--text-dim)">
<strong style="color:var(--text)">Connect with Claude Code:</strong><br>
<code id="brain-mcp-cmd" style="font-size:11px;word-break:break-all"></code>
</div>
</div>
</section>
</div><!-- /spane-brain -->
<!-- ══════════════════════════════════════════════════════════
TAB: MCP Servers
═══════════════════════════════════════════════════════════ -->
<div id="spane-mcp" style="display:none">
<section>
<h2 class="settings-section-title">MCP Servers</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Connect external MCP servers to give {{ agent_name }} new tools.
Tools are discovered at startup and after saving changes here.
Use the name <code>mcp__servername__toolname</code> in agent tool lists.
</p>
<button class="btn btn-primary" style="margin-bottom:16px" onclick="showMcpModal()">+ Add MCP Server</button>
<div class="table-wrap">
<table id="mcp-server-table">
<thead><tr>
<th>Name</th><th>URL</th><th>Transport</th><th>Tools</th><th>Status</th><th>Actions</th>
</tr></thead>
<tbody><tr><td colspan="6" style="text-align:center;color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</div>
</section>
</div><!-- /spane-mcp -->
<!-- ══════════════════════════════════════════════════════════
TAB: Security
═══════════════════════════════════════════════════════════ -->
<div id="spane-security" style="display:none">
<form id="security-form">
<!-- Enhanced Sanitization -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Enhanced Input Sanitization</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Extends the built-in prompt injection filter with additional patterns:
jailbreak phrases ("disregard your training", "pretend to be", "DAN mode"),
LLM special tokens (<code>[INST]</code>, <code>&lt;|im_start|&gt;</code>, Llama/Mistral tokens),
and role-play framing. Base64 blobs are flagged in logs but not redacted
(email signatures contain legitimate base64).
Also sanitizes the Telegram and inbox email listeners, which previously
passed raw external text directly to the agent.
</p>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px">
<input type="checkbox" id="sec-sanitize-enhanced">
Enable enhanced sanitization
</label>
</section>
<!-- Structured Truncation -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Structured Content Truncation</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Enforces configurable character limits on content returned from web, email,
and filesystem tools. Reduces the injection surface area by limiting how much
external content the agent sees per tool call. Truncated content is always
labelled so the agent knows data was cut. Filesystem behavior also changes
from <em>reject large files</em> to <em>truncate and notify</em>.
</p>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;margin-bottom:16px">
<input type="checkbox" id="sec-truncation-enabled">
Enable structured truncation
</label>
<div style="display:flex;gap:12px;flex-wrap:wrap;max-width:640px">
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Max web chars <span style="color:var(--text-dim);font-size:11px">(default 20,000)</span></label>
<input type="number" id="sec-max-web-chars" class="form-input" min="1000" max="500000" step="1000">
</div>
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Max email body chars <span style="color:var(--text-dim);font-size:11px">(default 6,000)</span></label>
<input type="number" id="sec-max-email-chars" class="form-input" min="500" max="100000" step="500">
</div>
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Max file chars <span style="color:var(--text-dim);font-size:11px">(default 20,000)</span></label>
<input type="number" id="sec-max-file-chars" class="form-input" min="1000" max="500000" step="1000">
</div>
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Max email subject chars <span style="color:var(--text-dim);font-size:11px">(default 200)</span></label>
<input type="number" id="sec-max-subject-chars" class="form-input" min="50" max="2000" step="10">
</div>
</div>
</section>
<!-- Output Validation -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Output Validation</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Rule-based guard for actions triggered by external-origin sessions
(Telegram and email inbox). Blocks the most common exfiltration attack:
injected instructions telling the agent to email content back to the
attacker's address. Specifically: if an inbox-triggered session attempts
to send an email <em>to the same address that sent the triggering email</em>,
the action is blocked and logged. Interactive chat sessions are never affected.
</p>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px">
<input type="checkbox" id="sec-output-validation">
Enable output validation
</label>
</section>
<!-- Canary Token -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Canary Token</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Injects a secret token into the system prompt with instructions never to
repeat it. Before each tool call, the agent's arguments are scanned for
the token. If found, the call is blocked, logged as
<code>security:canary_blocked</code>, and a Pushover alert is sent.
The token rotates daily and is stored in the credential store.
Requires Pushover credentials (<code>pushover_app_token</code> and
<code>pushover_user_key</code>) to send alerts — alerts silently fail
if unconfigured, but the block still happens.
</p>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px">
<input type="checkbox" id="sec-canary-enabled">
Enable canary token
</label>
</section>
<!-- LLM Content Screening -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">LLM Content Screening</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px">
Runs external content (web pages, emails, files) through a cheap, fast model
before it reaches the main agent. Asks: <em>"Does this content attempt to instruct an AI?"</em>
In <strong>flag mode</strong> (default) suspected content is prefixed with a warning but still passed through.
In <strong>block mode</strong> suspected content is rejected entirely.
Requires an OpenRouter API key. Cost is negligible — under $0.02/day at typical personal agent usage.
</p>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;margin-bottom:16px">
<input type="checkbox" id="sec-llm-screen-enabled">
Enable LLM content screening
</label>
<div style="display:flex;gap:12px;flex-wrap:wrap;max-width:640px">
<div class="form-group" style="flex:2;min-width:200px;margin-bottom:0">
<label>Screening model</label>
<select id="sec-llm-screen-model" class="form-input">
<option value="google/gemini-flash-1.5">Gemini Flash 1.5 — cheapest (~$0.04/1k calls)</option>
<option value="meta-llama/llama-3.1-8b-instruct">Llama 3.1 8B — cheapest (~$0.025/1k calls)</option>
<option value="anthropic/claude-haiku-3">Claude Haiku 3 — most accurate (~$0.13/1k calls)</option>
<option value="openai/gpt-4o-mini">GPT-4o mini (~$0.075/1k calls)</option>
</select>
</div>
<div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
<label>Mode</label>
<select id="sec-llm-screen-mode" class="form-input">
<option value="flag">Flag — warn but allow</option>
<option value="block">Block — reject suspected content</option>
</select>
</div>
</div>
</section>
<button type="submit" class="btn btn-primary">Save security settings</button>
</form>
<!-- Login lockouts -->
<section class="settings-section" style="margin-top:24px">
<h3>Login Lockouts</h3>
<p style="color:var(--text-dim);font-size:13px;margin-bottom:12px">
IPs are locked for 30 minutes after 5 failed attempts. A second lockout within 24 hours results in a permanent block until manually unlocked here.
</p>
<div id="lockout-list" style="margin-bottom:12px"></div>
<div style="display:flex;gap:8px">
<button class="btn btn-ghost btn-small" onclick="loadLockouts()">Refresh</button>
<button class="btn btn-ghost btn-small" style="color:var(--danger,#dc3c3c)" onclick="unlockAllIps()">Unlock all</button>
</div>
</section>
</div><!-- /spane-security -->
<!-- ══════════════════════════════════════════════════════════
TAB: Branding
═══════════════════════════════════════════════════════════ -->
<div id="spane-branding" style="display:none">
<h2 class="settings-section-title">Sidebar Name</h2>
<p style="color:var(--text-dim);font-size:13px;margin-bottom:12px">
Replaces the agent name in the sidebar. Leave blank to use the default name from <code>SOUL.md</code>.
</p>
<form id="branding-name-form" onsubmit="saveBrandingName(event)" style="display:flex;gap:8px;max-width:400px">
<input type="text" id="brand-name-input" class="form-input" placeholder="e.g. Jarvis"
style="flex:1" autocomplete="off">
<button type="submit" class="btn btn-primary">Save</button>
</form>
<h2 class="settings-section-title" style="margin-top:28px">Sidebar Logo</h2>
<p style="color:var(--text-dim);font-size:13px;margin-bottom:12px">
Replaces the default logo. Supports PNG, JPG, GIF, SVG, WebP. Leave unset to use the default.
</p>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px">
<img id="brand-logo-preview" src="" alt="logo preview"
style="width:36px;height:36px;object-fit:contain;border-radius:4px;background:var(--surface2);padding:2px">
<div style="display:flex;gap:8px">
<label class="btn btn-secondary" style="cursor:pointer">
Upload
<input type="file" id="brand-logo-file" accept="image/*" style="display:none" onchange="uploadBrandLogo()">
</label>
<button type="button" class="btn btn-secondary" id="brand-logo-reset-btn" onclick="resetBrandLogo()">Reset to default</button>
</div>
</div>
</div><!-- /spane-branding -->
<!-- ══════════════════════════════════════════════════════════
TAB: Profile
═══════════════════════════════════════════════════════════ -->
<div id="spane-mfa" style="display:none">
<!-- Theme picker -->
<section style="max-width:640px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Theme</h2>
<div id="theme-picker" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:4px">
<span style="color:var(--text-dim);font-size:13px">Loading…</span>
</div>
</section>
<!-- Account info -->
<section style="max-width:480px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Account</h2>
<div id="profile-load-area">
<div class="form-group">
<label>Username</label>
<input type="text" id="profile-username" class="form-input" disabled style="opacity:0.6;cursor:default">
</div>
<div class="form-group">
<label>Email</label>
<input type="text" id="profile-email" class="form-input" disabled style="opacity:0.6;cursor:default">
</div>
<div class="form-group">
<label>Display name <span style="font-size:11px;color:var(--text-dim)">(optional, shown instead of username)</span></label>
<input type="text" id="profile-display-name" class="form-input" placeholder="Your name" maxlength="80">
</div>
<div id="profile-save-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
<button class="btn btn-primary" onclick="saveMyProfile()">Save</button>
</div>
</section>
<!-- Change password -->
<section style="max-width:480px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Change Password</h2>
<div class="form-group">
<label>Current password</label>
<input type="password" id="prof-pw-current" class="form-input" autocomplete="current-password">
</div>
<div class="form-group">
<label>New password</label>
<input type="password" id="prof-pw-new" class="form-input" autocomplete="new-password" placeholder="min 8 characters">
</div>
<div class="form-group">
<label>Confirm new password</label>
<input type="password" id="prof-pw-new2" class="form-input" autocomplete="new-password">
</div>
<div id="prof-pw-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
<button class="btn btn-primary" onclick="changeMyProfilePassword()">Change Password</button>
</section>
<!-- MFA -->
<section style="max-width:480px">
<h2 class="settings-section-title">Two-Factor Authentication (TOTP)</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
Use an authenticator app (e.g. Google Authenticator, Authy) to generate one-time codes.
</p>
<div id="mfa-status-area">Loading…</div>
</section>
<section>
<h3 class="settings-section-title">Data Folder</h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:8px;line-height:1.5">
Your personal folder for email agent memory and reasoning files.
The base path is set by the administrator via <code>system:users_base_folder</code> in Credentials.
</p>
<span id="data-folder-hint" style="font-size:12px;color:var(--text-dim)">Loading…</span>
</section>
</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
═══════════════════════════════════════════════════════════ -->
<div id="uspane-apikeys">
<section style="margin-bottom:32px">
<h2 class="settings-section-title">My API Keys</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Set your own API keys. If left blank, the system defaults are used.
</p>
<div id="my-api-keys-tier-note" style="display:none;margin-bottom:20px;padding:13px 16px;background:rgba(224,166,50,0.08);border:1px solid rgba(224,166,50,0.35);border-left:4px solid var(--yellow);border-radius:var(--radius);font-size:13px;color:var(--text);line-height:1.6"></div>
<div style="display:flex;flex-direction:column;gap:20px;max-width:520px">
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:4px">Anthropic API Key</div>
<div id="my-apikey-anthropic-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="my-apikey-anthropic-input" class="form-input" placeholder="sk-ant-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveMyProviderKey('anthropic')">Save</button>
<button class="btn btn-ghost" onclick="clearMyProviderKey('anthropic')">Clear</button>
</div>
</div>
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:4px">OpenRouter API Key</div>
<div id="my-apikey-openrouter-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="my-apikey-openrouter-input" class="form-input" placeholder="sk-or-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveMyProviderKey('openrouter')">Save</button>
<button class="btn btn-ghost" onclick="clearMyProviderKey('openrouter')">Clear</button>
</div>
</div>
<div>
<div style="font-size:13px;font-weight:500;margin-bottom:4px">OpenAI API Key</div>
<div id="my-apikey-openai-status" style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Loading...</div>
<div style="display:flex;gap:8px">
<input type="password" id="my-apikey-openai-input" class="form-input" placeholder="sk-..." autocomplete="off" style="flex:1">
<button class="btn btn-primary" onclick="saveMyProviderKey('openai')">Save</button>
<button class="btn btn-ghost" onclick="clearMyProviderKey('openai')">Clear</button>
</div>
</div>
</div>
</section>
</div><!-- /uspane-apikeys -->
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: Personality
═══════════════════════════════════════════════════════════ -->
<div id="uspane-personality" style="display:none">
<section style="margin-bottom:32px">
<h2 class="settings-section-title">My Personality</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
These files define how the assistant behaves and what it knows about you.
They were pre-filled from the system defaults when your account was created — edit them to make the assistant yours.
</p>
<div class="form-group" style="margin-bottom:20px">
<label style="font-weight:600">Agent persona <code style="font-size:11px;font-weight:400;color:var(--text-dim)">soul.md</code></label>
<p style="font-size:12px;color:var(--text-dim);margin:4px 0 8px">
Defines the agent's name, personality, values, and communication style.
</p>
<textarea id="my-personality-soul" class="form-input" rows="10"
style="resize:vertical;font-family:var(--mono);font-size:12px"
placeholder="You are Jarvis, a personal AI assistant..."></textarea>
</div>
<div class="form-group" style="margin-bottom:20px">
<label style="font-weight:600">About me <code style="font-size:11px;font-weight:400;color:var(--text-dim)">user.md</code></label>
<p style="font-size:12px;color:var(--text-dim);margin:4px 0 8px">
Your name, location, role, and preferences. The assistant uses this as context for every response.
</p>
<textarea id="my-personality-user" class="form-input" rows="8"
style="resize:vertical;font-family:var(--mono);font-size:12px"
placeholder="# About me&#10;&#10;- Name: Your Name&#10;- Location: Your City, Country&#10;- Role: What you do"></textarea>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-primary" onclick="saveMyPersonality()">Save</button>
<button class="btn btn-ghost" onclick="clearMyPersonality()">Reset to system defaults</button>
</div>
</section>
</div><!-- /uspane-personality -->
<div id="uspane-inbox" style="display:none">
<!-- Section 1: Trigger Account -->
<section style="margin-bottom:36px;padding-bottom:28px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Trigger Account</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
This account listens for command emails. Messages matching a trigger phrase dispatch an agent.
Replies can be sent from this account.
</p>
<div id="my-inbox-status" style="font-size:12px;margin-bottom:16px"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px">
<div class="form-group"><label>IMAP Host</label><input type="text" id="my-inbox-imap-host" class="form-input" placeholder="imap.example.com"></div>
<div class="form-group"><label>IMAP Port</label><input type="text" id="my-inbox-imap-port" class="form-input" placeholder="993"></div>
<div class="form-group"><label>IMAP Username</label><input type="text" id="my-inbox-imap-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>IMAP Password</label><input type="password" id="my-inbox-imap-password" class="form-input" placeholder="Leave blank to keep existing"></div>
<div class="form-group"><label>SMTP Host</label><input type="text" id="my-inbox-smtp-host" class="form-input" placeholder="smtp.example.com"></div>
<div class="form-group"><label>SMTP Port</label><input type="text" id="my-inbox-smtp-port" class="form-input" placeholder="465"></div>
<div class="form-group"><label>SMTP Username</label><input type="text" id="my-inbox-smtp-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>SMTP Password</label><input type="password" id="my-inbox-smtp-password" class="form-input" placeholder="Leave blank to keep existing"></div>
</div>
<div style="display:flex;gap:8px;margin-bottom:28px">
<button class="btn btn-primary" onclick="saveMyInboxConfig()">Save &amp; Connect</button>
</div>
<h3 style="margin-bottom:12px;font-size:14px">Trigger Rules</h3>
<button class="btn btn-small btn-ghost" style="margin-bottom:12px" onclick="openMyInboxTriggerModal()">+ Add trigger</button>
<table class="data-table">
<thead><tr><th>Trigger word</th><th>Agent</th><th>Status</th><th></th></tr></thead>
<tbody id="my-inbox-triggers-tbody"><tr><td colspan="4" style="color:var(--text-dim)">No triggers configured.</td></tr></tbody>
</table>
</section>
</div><!-- /uspane-inbox -->
<!-- Email Accounts tab -->
<div id="uspane-emailaccounts" style="display:none">
<section>
<h2 class="settings-section-title">Email Handling Accounts</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
These accounts are monitored continuously. Each new email is handed to the assigned agent for
automated organisation. <strong>No emails can be sent from these accounts.</strong>
The agent only has access to email tools - no web, filesystem, or other capabilities.
</p>
<button class="btn btn-primary btn-small" style="margin-bottom:16px" onclick="openEmailAccountModal()">+ Add handling account</button>
<table class="data-table" id="email-accounts-table">
<thead><tr><th>Label</th><th>Account</th><th>Status</th><th>Model</th><th></th></tr></thead>
<tbody id="email-accounts-tbody"><tr><td colspan="5" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</section>
</div><!-- /uspane-emailaccounts -->
<!-- CalDAV / CardDAV tab -->
<div id="uspane-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">
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 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-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-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>
<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 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">
<section style="margin-bottom:32px">
<h2 class="settings-section-title">My Telegram Bot</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Connect your own Telegram bot. Create a bot via @BotFather and paste the token below.
</p>
<div id="my-telegram-status" style="font-size:12px;margin-bottom:12px"></div>
<div class="form-group">
<label>Bot Token</label>
<input type="password" id="my-telegram-bot-token" class="form-input" placeholder="123456:ABCdef...">
</div>
<div style="display:flex;gap:8px;margin-bottom:28px">
<button class="btn btn-primary" onclick="saveMyTelegramConfig()">Save &amp; Connect</button>
<button class="btn btn-danger btn-small" onclick="deleteMyTelegramConfig()">Remove bot</button>
</div>
<h3 style="margin-bottom:12px;font-size:14px">Chat ID Whitelist</h3>
<div style="display:flex;gap:8px;margin-bottom:12px">
<input type="text" id="my-tg-chat-id" class="form-input" style="max-width:200px" placeholder="Chat ID">
<input type="text" id="my-tg-chat-label" class="form-input" style="max-width:160px" placeholder="Label (optional)">
<button class="btn btn-primary btn-small" onclick="addMyTelegramWhitelist()">Add</button>
</div>
<table class="data-table" style="margin-bottom:24px">
<thead><tr><th>Chat ID</th><th>Label</th><th></th></tr></thead>
<tbody id="my-telegram-whitelist-tbody"><tr><td colspan="3" style="color:var(--text-dim)">No whitelisted chats.</td></tr></tbody>
</table>
<h3 style="margin-bottom:12px;font-size:14px">Trigger Rules</h3>
<button class="btn btn-small btn-ghost" style="margin-bottom:12px" onclick="openMyTelegramTriggerModal()">+ Add trigger</button>
<table class="data-table">
<thead><tr><th>Trigger word</th><th>Agent</th><th>Status</th><th></th></tr></thead>
<tbody id="my-telegram-triggers-tbody"><tr><td colspan="4" style="color:var(--text-dim)">No triggers configured.</td></tr></tbody>
</table>
</section>
</div><!-- /uspane-telegram -->
<div id="uspane-mcp" style="display:none">
<section style="margin-bottom:32px">
<h2 class="settings-section-title">My MCP Servers</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Connect your own external MCP servers. These are available only to you during your chat sessions.
</p>
<button class="btn btn-primary btn-small" style="margin-bottom:16px" onclick="openMyMcpModal()">+ Add server</button>
<table class="data-table" id="my-mcp-table">
<thead><tr><th>Name</th><th>URL</th><th>Transport</th><th>Status</th><th></th></tr></thead>
<tbody id="my-mcp-tbody"><tr><td colspan="5" style="color:var(--text-dim)">Loading…</td></tr></tbody>
</table>
</section>
</div><!-- /uspane-mcp -->
<!-- ══════════════════════════════════════════════════════════
USER SETTINGS: 2nd Brain
═══════════════════════════════════════════════════════════ -->
<div id="uspane-brain" style="display:none">
<!-- Auto-approve -->
<section style="margin-bottom:32px">
<h2 class="settings-section-title">Jarvis Access</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
When enabled, Jarvis can access the 2nd Brain at any time without asking for permission first.
Jarvis will proactively search and capture without interrupting the conversation.
</p>
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;max-width:640px">
<input type="checkbox" id="ubrain-auto-approve-toggle" onchange="saveBrainAutoApprove(this.checked)">
<span style="font-size:14px">Automatically approve all 2nd Brain access</span>
</label>
<div id="ubrain-auto-approve-flash" style="font-size:12px;color:var(--green);margin-top:8px;display:none"></div>
</section>
<div style="border-top:1px solid var(--border);margin-bottom:32px"></div>
<section style="margin-bottom:32px">
<h2 class="settings-section-title">2nd Brain MCP Access Key</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">
Use this key to connect third-party MCP clients (Claude Desktop, Claude Code, Cursor) to your personal 2nd Brain.
All queries and captures are automatically scoped to your account.
</p>
<div style="max-width:640px">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
<input id="ubrain-mcp-key" type="password" class="form-input" style="flex:1;font-family:monospace" readonly>
<button class="btn btn-small" onclick="toggleBrainKeyVisibility(this, 'ubrain-mcp-key')" title="Show/hide">Show</button>
<button class="btn btn-small" onclick="copyBrainKey('ubrain-mcp-key')" title="Copy">Copy</button>
<button class="btn btn-small" style="color:var(--red)" onclick="regenerateBrainKey('ubrain-mcp-key', 'ubrain-mcp-cmd')" title="Regenerate">Regenerate</button>
</div>
<div style="background:var(--bg2);border-radius:6px;padding:12px;font-size:12px;color:var(--text-dim)">
<strong style="color:var(--text)">Connect with Claude Code:</strong><br>
<code id="ubrain-mcp-cmd" style="font-size:11px;word-break:break-all"></code>
</div>
</div>
</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
═══════════════════════════════════════════════════════════ -->
<div id="uspane-mfa" style="display:none">
<!-- Theme picker -->
<section style="max-width:640px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Theme</h2>
<div id="theme-picker" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:4px">
<span style="color:var(--text-dim);font-size:13px">Loading…</span>
</div>
</section>
<!-- Account info -->
<section style="max-width:480px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Account</h2>
<div class="form-group">
<label>Username</label>
<input type="text" id="profile-username" class="form-input" disabled style="opacity:0.6;cursor:default">
</div>
<div class="form-group">
<label>Email</label>
<input type="text" id="profile-email" class="form-input" disabled style="opacity:0.6;cursor:default">
</div>
<div class="form-group">
<label>Display name <span style="font-size:11px;color:var(--text-dim)">(optional, shown instead of username)</span></label>
<input type="text" id="profile-display-name" class="form-input" placeholder="Your name" maxlength="80">
</div>
<div id="profile-save-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
<button class="btn btn-primary" onclick="saveMyProfile()">Save</button>
</section>
<!-- Change password -->
<section style="max-width:480px;margin-bottom:36px;padding-bottom:32px;border-bottom:1px solid var(--border)">
<h2 class="settings-section-title">Change Password</h2>
<div class="form-group">
<label>Current password</label>
<input type="password" id="prof-pw-current" class="form-input" autocomplete="current-password">
</div>
<div class="form-group">
<label>New password</label>
<input type="password" id="prof-pw-new" class="form-input" autocomplete="new-password" placeholder="min 8 characters">
</div>
<div class="form-group">
<label>Confirm new password</label>
<input type="password" id="prof-pw-new2" class="form-input" autocomplete="new-password">
</div>
<div id="prof-pw-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
<button class="btn btn-primary" onclick="changeMyProfilePassword()">Change Password</button>
</section>
<!-- MFA -->
<section style="max-width:480px">
<h2 class="settings-section-title">Two-Factor Authentication (TOTP)</h2>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:20px">
Use an authenticator app (e.g. Google Authenticator, Authy) to generate one-time codes.
</p>
<div id="mfa-status-area">Loading…</div>
</section>
<section style="max-width:480px">
<h3 class="settings-section-title">Data Folder</h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:8px;line-height:1.5">
Your personal folder for email agent memory and reasoning files.
The base path is set by the administrator via <code>system:users_base_folder</code> in Credentials.
</p>
<span id="data-folder-hint" style="font-size:12px;color:var(--text-dim)">Loading…</span>
</section>
</div><!-- /uspane-mfa -->
{% endif %}
</div>
<!-- ── MCP server modal ── -->
<div class="modal-overlay hidden" id="mcp-modal">
<div class="modal" style="max-width:520px;width:100%">
<h3 style="margin-bottom:20px" id="mcp-modal-title">Add MCP Server</h3>
<input type="hidden" id="mcp-modal-id">
<div class="form-group">
<label>Name</label>
<input type="text" id="mcp-name" class="form-input" placeholder="e.g. GitHub">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="mcp-url" class="form-input" placeholder="https://mcp.example.com/sse">
</div>
<div class="form-group">
<label>Transport</label>
<select id="mcp-transport" class="form-input">
<option value="sse">SSE (most public servers)</option>
<option value="streamable_http">Streamable HTTP</option>
</select>
</div>
<div class="form-group">
<label>API Key <span style="color:var(--text-dim)">(optional)</span></label>
<input type="password" id="mcp-api-key" class="form-input" placeholder="Bearer token or API key">
</div>
<div class="form-group">
<label>Extra Headers <span style="color:var(--text-dim)">(optional JSON object)</span></label>
<input type="text" id="mcp-headers" class="form-input" placeholder='{"x-custom": "value"}'>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="mcp-enabled" checked> Enabled
</label>
</div>
<div class="modal-buttons">
<button class="btn btn-ghost" onclick="closeMcpModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveMcpServer()">Save</button>
</div>
</div>
</div>
<!-- ── My Inbox trigger modal ── -->
<div class="modal-overlay hidden" id="my-inbox-trigger-modal">
<div class="modal" style="max-width:480px;width:100%">
<h3 style="margin-bottom:20px" id="my-inbox-trigger-modal-title">Add Inbox Trigger</h3>
<input type="hidden" id="my-inbox-trigger-id">
<div class="form-group"><label>Trigger word</label><input type="text" id="my-inbox-trigger-word" class="form-input" placeholder="e.g. JARVIS"></div>
<div class="form-group"><label>Agent</label><select id="my-inbox-trigger-agent" class="form-input"></select></div>
<div class="form-group"><label>Description (optional)</label><input type="text" id="my-inbox-trigger-desc" class="form-input"></div>
<div class="modal-buttons">
<button class="btn btn-ghost" onclick="closeMyInboxTriggerModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveMyInboxTrigger()">Save</button>
</div>
</div>
</div>
<!-- ── My Telegram trigger modal ── -->
<div class="modal-overlay hidden" id="my-telegram-trigger-modal">
<div class="modal" style="max-width:480px;width:100%">
<h3 style="margin-bottom:20px" id="my-telegram-trigger-modal-title">Add Telegram Trigger</h3>
<input type="hidden" id="my-telegram-trigger-id">
<div class="form-group"><label>Trigger word</label><input type="text" id="my-telegram-trigger-word" class="form-input" placeholder="e.g. JARVIS"></div>
<div class="form-group"><label>Agent</label><select id="my-telegram-trigger-agent" class="form-input"></select></div>
<div class="form-group"><label>Description (optional)</label><input type="text" id="my-telegram-trigger-desc" class="form-input"></div>
<div class="modal-buttons">
<button class="btn btn-ghost" onclick="closeMyTelegramTriggerModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveMyTelegramTrigger()">Save</button>
</div>
</div>
</div>
<!-- ── My MCP server modal ── -->
<div class="modal-overlay hidden" id="my-mcp-modal">
<div class="modal" style="max-width:520px;width:100%">
<h3 style="margin-bottom:20px" id="my-mcp-modal-title">Add MCP Server</h3>
<div class="form-group">
<label>Name</label>
<input type="text" id="my-mcp-name" class="form-input" placeholder="e.g. My GitHub">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="my-mcp-url" class="form-input" placeholder="https://mcp.example.com/sse">
</div>
<div class="form-group">
<label>Transport</label>
<select id="my-mcp-transport" class="form-input">
<option value="sse">SSE (most public servers)</option>
<option value="streamable_http">Streamable HTTP</option>
</select>
</div>
<div class="form-group">
<label>API Key <span style="color:var(--text-dim)">(optional)</span></label>
<input type="password" id="my-mcp-apikey" class="form-input" placeholder="Bearer token or API key">
</div>
<div class="modal-buttons">
<button class="btn btn-ghost" onclick="closeMyMcpModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveMyMcpServer()">Save</button>
</div>
</div>
</div>
<!-- ── Email Handling Account modal ── -->
<div class="modal-overlay" id="email-account-modal" style="display:none">
<div class="modal" style="max-width:820px;width:100%">
<h3 id="eam-title">Add Handling Account</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:16px">
<!-- Left column: IMAP account -->
<div>
<p style="font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:14px;font-weight:600">Account</p>
<div class="form-group"><label>Label</label>
<input type="text" id="eam-label" class="form-input" placeholder="e.g. Work Email"></div>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
<div class="form-group"><label>IMAP Host</label>
<input type="text" id="eam-imap-host" class="form-input" placeholder="imap.example.com"></div>
<div class="form-group"><label>Port</label>
<input type="number" id="eam-imap-port" class="form-input" value="993" min="1" max="65535"></div>
</div>
<div class="form-group"><label>Username</label>
<input type="text" id="eam-imap-username" class="form-input" placeholder="user@example.com"></div>
<div class="form-group"><label>Password</label>
<input type="password" id="eam-imap-password" class="form-input" placeholder="Leave blank to keep existing"></div>
<div class="form-group"><label>Initial load limit <span style="color:var(--text-dim);font-size:11px">(emails on first connect)</span></label>
<input type="number" id="eam-initial-load-limit" class="form-input" value="200" min="0" max="5000"></div>
<div class="form-group">
<label>Monitored folders</label>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
<span id="eam-folders-display" style="font-size:13px;color:var(--text-dim)">INBOX</span>
<button type="button" class="btn btn-ghost btn-small" id="eam-load-folders-btn" onclick="loadEamFolders()">Load folders</button>
</div>
<div id="eam-folders-checklist" style="display:none;max-height:160px;overflow-y:auto;background:var(--bg2);padding:8px 12px;border-radius:var(--radius);border:1px solid var(--border)"></div>
<input type="hidden" id="eam-folders-hidden" value='["INBOX"]'>
</div>
</div>
<!-- Right column: Handling agent -->
<div>
<p style="font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:14px;font-weight:600">Handling Agent</p>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:14px;line-height:1.5">
A dedicated agent is created for this account. It can use email tools plus any notification tools you enable below.
</p>
<div class="form-group"><label>Model</label>
<select id="eam-agent-model" class="form-input"><option value="">Loading…</option></select>
</div>
<div class="form-group" style="display:flex;flex-direction:column;flex:1">
<label>Agent prompt</label>
<textarea id="eam-agent-prompt" class="form-input" rows="8"
style="resize:vertical;font-size:13px;line-height:1.5"
placeholder="Describe how the agent should handle incoming emails…"></textarea>
</div>
<div class="form-group">
<label>Notification tools <span style="font-size:11px;color:var(--text-dim)">(optional)</span></label>
<div id="eam-extra-tools-area" style="margin-top:6px;display:flex;flex-direction:column;gap:8px">
<span style="font-size:12px;color:var(--text-dim)">Loading…</span>
</div>
</div>
<div class="form-group" id="eam-keyword-row" style="display:none">
<label>Telegram reply keyword</label>
<input type="text" id="eam-telegram-keyword" class="form-input"
placeholder="e.g. work" maxlength="20"
style="text-transform:lowercase"
oninput="this.value=this.value.toLowerCase().replace(/[^a-z0-9-]/g,'');const p=document.getElementById('eam-keyword-preview');if(p)p.textContent=this.value||'keyword';">
<span style="font-size:11px;color:var(--text-dim);margin-top:4px;display:block">
Users reply with <code>/<span id="eam-keyword-preview">keyword</span> &lt;message&gt;</code>
</span>
</div>
<div id="eam-pause-row" style="display:none;margin-top:8px">
<button type="button" class="btn btn-ghost btn-small" id="eam-pause-btn"
onclick="togglePauseEmailAccount()"></button>
<span id="eam-pause-status" style="font-size:12px;color:var(--text-dim);margin-left:8px"></span>
</div>
</div>
</div><!-- /grid -->
<div class="modal-buttons" style="margin-top:20px">
<button class="btn btn-ghost" onclick="closeEmailAccountModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveEmailAccount()">Save</button>
</div>
</div>
</div>
<!-- ── Inbox trigger modal ── -->
<div class="modal-overlay hidden" id="trigger-modal">
<div class="modal" style="max-width:480px;width:100%">
<h3 style="margin-bottom:20px" id="trigger-modal-title">Add Trigger Rule</h3>
<input type="hidden" id="trigger-modal-id">
<div class="form-group">
<label>Trigger word</label>
<input type="text" id="trigger-modal-word" class="form-input"
placeholder="e.g. JARVIS" autocomplete="off">
</div>
<div class="form-group">
<label>Agent</label>
<select id="trigger-modal-agent" class="form-input"></select>
</div>
<div class="form-group">
<label>Description <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="trigger-modal-desc" class="form-input"
placeholder="What does this rule do?">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="trigger-modal-enabled" checked>
<label for="trigger-modal-enabled" style="font-size:13px;cursor:pointer">Enabled</label>
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-ghost" onclick="closeTriggerModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveTrigger()">Save</button>
</div>
</div>
</div>
<!-- ── Telegram trigger modal ── -->
<div class="modal-overlay hidden" id="tg-trigger-modal">
<div class="modal" style="max-width:480px;width:100%">
<h3 style="margin-bottom:20px" id="tg-trigger-modal-title">Add Telegram Trigger</h3>
<input type="hidden" id="tg-trigger-modal-id">
<div class="form-group">
<label>Trigger word</label>
<input type="text" id="tg-trigger-modal-word" class="form-input"
placeholder="e.g. JARVIS" autocomplete="off">
</div>
<div class="form-group">
<label>Agent</label>
<select id="tg-trigger-modal-agent" class="form-input"></select>
</div>
<div class="form-group">
<label>Description <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="tg-trigger-modal-desc" class="form-input"
placeholder="What does this rule do?">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="tg-trigger-modal-enabled" checked>
<label for="tg-trigger-modal-enabled" style="font-size:13px;cursor:pointer">Enabled</label>
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-ghost" onclick="closeTgTriggerModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveTgTrigger()">Save</button>
</div>
</div>
</div>
<!-- ── Credential modal ── -->
<div class="modal-overlay hidden" id="cred-modal">
<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>Key name</label>
<input type="text" id="cred-modal-key-custom" class="form-input"
placeholder="e.g. my_api_key or service:token" autocomplete="off">
</div>
<div class="form-group">
<label>Value</label>
<div style="position:relative;display:flex;align-items:center">
<input type="password" id="cred-modal-value" class="form-input"
placeholder="Secret value" autocomplete="new-password"
style="padding-right:40px">
<button type="button" tabindex="-1" onclick="toggleCredVisibility()"
style="position:absolute;right:10px;background:none;border:none;cursor:pointer;color:var(--text-dim);font-size:15px;padding:0;line-height:1"
title="Show / hide">👁</button>
</div>
</div>
<div class="form-group">
<label>Description <span style="color:var(--text-dim)">(optional)</span></label>
<input type="text" id="cred-modal-desc" class="form-input"
placeholder="What is this credential for?">
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-ghost" onclick="closeCredModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveCredModal()">Save</button>
</div>
</div>
</div>
<!-- ── Directory browser modal ── -->
<div class="modal-overlay hidden" id="fs-browser-modal">
<div class="modal" style="max-width:560px;width:100%">
<h3 style="margin-bottom:12px">Browse Server Directories</h3>
<div id="fs-breadcrumb" style="
font-size:12px;color:var(--text-dim);
background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
padding:8px 12px;margin-bottom:12px;word-break:break-all;font-family:monospace
">/</div>
<div style="
border:1px solid var(--border);border-radius:var(--radius);
max-height:320px;overflow-y:auto;margin-bottom:16px
">
<div id="fs-browser-list" style="padding:4px 0"></div>
</div>
<div class="modal-buttons">
<button class="btn btn-ghost" onclick="closeFsBrowser()">Cancel</button>
<button class="btn btn-primary" onclick="selectFsPath()">Select this folder</button>
</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 %}
{% endblock %}