- 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
2109 lines
118 KiB
HTML
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>
|
|
<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: <key></code> or <code>Authorization: Bearer <key></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 & 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 & 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 & 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><|im_start|></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 - Name: Your Name - Location: Your City, Country - 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 & 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 & 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> <message></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 %}
|