refactor: extract shared settings stylesheet (pg.css) and clean up inline styles

- Create cortex/static/pg.css with shared variables, nav, containers, form
  elements, button classes, text utilities, and feedback messages
- All four settings pages (settings, notifications, tools, help) now link to
  pg.css and have page-specific-only <style> blocks
- Style block line counts reduced: settings 244→70, notifications 189→42,
  tools 126→88, help 122→75
- Inline style= attributes reduced: settings 45→4, notifications 7→3,
  tools 12→4, help 0
- Introduced shared CSS classes: .btn-submit, .btn-save, .btn-cancel,
  .btn-secondary, .action-link, .hint, .section-note, .page-title,
  .page-subtitle, .page-wrap, .usage-table, .tool-cat-row
- Fix tools_settings.py: replace server-generated inline styles on category
  header rows with .tool-cat-row CSS class
- Fix settings.py: add btn-save/btn-cancel/persona-rename-cancel classes to
  server-rendered persona rename form buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-11 23:22:59 -04:00
parent 8ab1942514
commit 4c3d9a7a65
7 changed files with 415 additions and 718 deletions

View File

@@ -127,8 +127,8 @@ def _settings_page(username: str, personas: list[str], back_persona: str = "", s
<input type="hidden" name="old_name" value="{p}"> <input type="hidden" name="old_name" value="{p}">
<input type="text" name="new_name" value="{p}" <input type="text" name="new_name" value="{p}"
pattern="[a-z_][a-z0-9_\\-]{{0,31}}" required> pattern="[a-z_][a-z0-9_\\-]{{0,31}}" required>
<button type="submit">Save</button> <button type="submit" class="btn-save">Save</button>
<button type="button" class="persona-rename-cancel">Cancel</button> <button type="button" class="btn-cancel persona-rename-cancel">Cancel</button>
</form> </form>
</li>''' for p in personas </li>''' for p in personas
) )

View File

@@ -53,13 +53,7 @@ def _build_tool_table(policy: dict) -> str:
for category, tools in TOOL_CATEGORIES.items(): for category, tools in TOOL_CATEGORIES.items():
# Category header spanning all columns # Category header spanning all columns
escaped_cat = _html.escape(category) escaped_cat = _html.escape(category)
rows.append( rows.append(f'<tr class="tool-cat-row"><td colspan="4">{escaped_cat}</td></tr>')
f'<tr><td colspan="4" style="padding:0.75rem 0.9rem 0.3rem;'
f'font-size:0.72rem;font-weight:700;letter-spacing:0.07em;'
f'text-transform:uppercase;color:var(--pg-dimmer);'
f'border-bottom:1px solid var(--pg-border);">'
f'{escaped_cat}</td></tr>'
)
for tool in tools: for tool in tools:
risk = TOOL_RISK.get(tool, "medium") risk = TOOL_RISK.get(tool, "medium")
risk_cls = f"risk-{risk}" risk_cls = f"risk-{risk}"

View File

@@ -8,56 +8,10 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="/static/pg.css">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script> <script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style> <style>
:root { .page { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
--pg-bg: #0f1117; --pg-surface: #1a1d27;
--pg-border: #2d3148;
--pg-text: #e2e8f0; --pg-muted: #94a3b8;
--pg-dim: #64748b; --pg-dimmer: #475569;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
--pg-accent: #a78bfa;
}
[data-theme="light"] {
--pg-bg: #f4f2fa; --pg-surface: #ffffff;
--pg-border: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
--pg-accent: #7c3aed;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
background: var(--pg-bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--pg-text);
padding: 2rem 1.5rem;
}
.page { max-width: 720px; margin: 0 auto; }
/* ── Page nav ── */
.page-nav {
display: flex; align-items: center; gap: 0.25rem;
margin-bottom: 1.75rem; flex-wrap: wrap;
}
.nav-link {
display: inline-flex; align-items: center;
padding: 0.3rem 0.6rem; border-radius: 6px;
font-size: 0.8rem; font-weight: 500; color: var(--pg-dim);
text-decoration: none; transition: color 0.15s, background 0.15s; white-space: nowrap;
}
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: var(--pg-accent); }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
/* ── Header ── */ /* ── Header ── */
header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--pg-border); } header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--pg-border); }
@@ -134,15 +88,16 @@
</style> </style>
</head> </head>
<body> <body>
<nav class="page-nav" id="page-nav">
<a id="nav-chat" href="/" class="nav-link">← Chat</a>
<a href="/help" class="nav-link active">Help</a>
<a href="/settings" class="nav-link" id="nav-settings">Settings</a>
<a href="/settings/notifications" class="nav-link">Notifications</a>
<a href="/settings/tools" class="nav-link">Tools</a>
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="page"> <div class="page">
<nav class="page-nav" id="page-nav">
<a id="nav-chat" href="/" class="nav-link">← Chat</a>
<a href="/help" class="nav-link active">Help</a>
<a href="/settings" class="nav-link" id="nav-settings">Settings</a>
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<header> <header>
<h1>Help &amp; Reference</h1> <h1>Help &amp; Reference</h1>
<p id="persona-label"></p> <p id="persona-label"></p>

View File

@@ -7,218 +7,65 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/pg.css">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script> <script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style> <style>
:root { /* ── Test action buttons ── */
--pg-bg: #0f1117; --pg-surface: #1a1d27; .test-btn-row { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
--pg-border: #2d3148; .test-btn {
--pg-text: #e2e8f0; --pg-muted: #94a3b8; flex: 1; padding: 0.6rem 0.75rem;
--pg-dim: #64748b; --pg-dimmer: #475569; border: 1px solid var(--pg-border); border-radius: 6px;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05); background: var(--pg-bg); color: var(--pg-text);
font-size: 0.85rem; font-weight: 500; cursor: pointer;
transition: border-color 0.15s, color 0.15s; text-align: center;
} }
[data-theme="light"] { .test-btn:hover { border-color: var(--pg-action); color: var(--pg-accent); }
--pg-bg: #f4f2fa; --pg-surface: #ffffff; .test-btn:disabled { opacity: 0.5; cursor: default; }
--pg-border: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--pg-bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--pg-text);
padding: 1.5rem;
}
.card {
background: var(--pg-surface);
border: 1px solid var(--pg-border);
border-radius: 12px;
padding: 2.5rem 2rem;
width: 100%;
max-width: 520px;
}
.page-nav {
display: flex;
align-items: center;
gap: 0.25rem;
margin-bottom: 1.75rem;
flex-wrap: wrap;
}
.nav-link {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.6rem;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-dim);
text-decoration: none;
transition: color 0.15s, background 0.15s;
white-space: nowrap;
}
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: #a78bfa; }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
.logo { margin-bottom: 1.75rem; }
.logo h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
.logo p { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.2rem; }
h2 {
font-size: 0.9rem;
font-weight: 600;
color: var(--pg-muted);
margin-bottom: 1rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--pg-border);
}
.section { margin-bottom: 2rem; }
label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-muted);
margin-bottom: 0.4rem;
}
input, select {
width: 100%;
padding: 0.65rem 0.85rem;
background: var(--pg-bg);
border: 1px solid var(--pg-border);
border-radius: 6px;
color: var(--pg-text);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s;
}
input:focus, select:focus { border-color: #7c3aed; }
input[type="password"] { font-family: monospace; letter-spacing: 0.05em; }
.field { margin-bottom: 1rem; }
button[type="submit"] {
width: 100%;
padding: 0.7rem;
margin-top: 0.25rem;
background: #7c3aed;
border: none;
border-radius: 6px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
button[type="submit"]:hover { background: #6d28d9; }
.error { color: #f87171; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
.success { color: #4ade80; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
.btn-row { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.btn {
flex: 1;
padding: 0.6rem 0.75rem;
border: 1px solid var(--pg-border);
border-radius: 6px;
background: var(--pg-bg);
color: var(--pg-text);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
text-align: center;
}
.btn:hover { border-color: #7c3aed; color: #a78bfa; }
.btn:disabled { opacity: 0.5; cursor: default; }
.test-result { .test-result {
margin-top: 0.75rem; margin-top: 0.75rem; padding: 0.6rem 0.8rem; border-radius: 6px;
padding: 0.6rem 0.8rem; font-size: 0.82rem; line-height: 1.5; display: none;
border-radius: 6px;
font-size: 0.82rem;
line-height: 1.5;
display: none;
} }
.test-result.ok { background: rgba(74, 222, 128, 0.1); color: #4ade80; border: 1px solid rgba(74, 222, 128, 0.25); } .test-result.ok { background: rgba(74,222,128,0.1); color: #4ade80; border: 1px solid rgba(74,222,128,0.25); }
.test-result.err { background: rgba(248, 113, 113, 0.1); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.25); } .test-result.err { background: rgba(248,113,113,0.1); color: #f87171; border: 1px solid rgba(248,113,113,0.25); }
.hint { font-size: 0.78rem; color: var(--pg-dim); margin-top: 0.35rem; line-height: 1.5; } /* ── Channel config collapsible blocks ── */
/* Channel config blocks */
details.channel-block { details.channel-block {
border: 1px solid var(--pg-border); border: 1px solid var(--pg-border); border-radius: 8px;
border-radius: 8px; margin-bottom: 0.75rem; overflow: hidden;
margin-bottom: 0.75rem;
overflow: hidden;
} }
details.channel-block summary { details.channel-block summary {
padding: 0.75rem 1rem; padding: 0.75rem 1rem; font-size: 0.85rem; font-weight: 600;
font-size: 0.85rem; color: var(--pg-muted); cursor: pointer; list-style: none;
font-weight: 600; display: flex; align-items: center; gap: 0.5rem;
color: var(--pg-muted); user-select: none; background: var(--pg-bg);
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5rem;
user-select: none;
background: var(--pg-bg);
} }
details.channel-block summary::-webkit-details-marker { display: none; } details.channel-block summary::-webkit-details-marker { display: none; }
details.channel-block summary::before { details.channel-block summary::before {
content: '▶'; content: '▶'; font-size: 0.65rem; color: var(--pg-dimmer);
font-size: 0.65rem; transition: transform 0.15s; flex-shrink: 0;
color: var(--pg-dimmer);
transition: transform 0.15s;
flex-shrink: 0;
} }
details.channel-block[open] summary::before { transform: rotate(90deg); } details.channel-block[open] summary::before { transform: rotate(90deg); }
details.channel-block[open] summary { border-bottom: 1px solid var(--pg-border); } details.channel-block[open] summary { border-bottom: 1px solid var(--pg-border); }
.channel-block-body { .channel-block-body { padding: 1rem 1rem 0.25rem; }
padding: 1rem 1rem 0.25rem;
}
.channel-hint { .channel-hint {
font-size: 0.75rem; font-size: 0.75rem; color: var(--pg-dimmer);
color: var(--pg-dimmer); margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
margin-top: -0.6rem;
margin-bottom: 1rem;
line-height: 1.5;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <nav class="page-nav">
<nav class="page-nav"> <a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ back_href }}" class="nav-link">← Chat</a> <a href="{{ help_href }}" class="nav-link">Help</a>
<a href="{{ help_href }}" class="nav-link">Help</a> <a href="/settings" class="nav-link">Settings</a>
<a href="/settings" class="nav-link">Settings</a> <a href="/settings/notifications" class="nav-link active">Notifications</a>
<a href="/settings/notifications" class="nav-link active">Notifications</a> <a href="/settings/tools" class="nav-link">Tools</a>
<a href="/settings/tools" class="nav-link">Tools</a> <span class="nav-spacer"></span>
<span class="nav-spacer"></span> <a href="/logout" class="nav-link nav-logout">Sign out</a>
<a href="/logout" class="nav-link nav-logout">Sign out</a> </nav>
</nav> <div class="page-wrap">
<h1 class="page-title">Notifications</h1>
<div class="logo"> <p class="page-subtitle">How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
<h1>Notifications</h1>
<p>How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
</div>
<!-- SUCCESS --> <!-- SUCCESS -->
<!-- ERROR --> <!-- ERROR -->
@@ -254,7 +101,7 @@
<!-- Nextcloud Talk --> <!-- Nextcloud Talk -->
<div class="section"> <div class="section">
<h2>Nextcloud Talk</h2> <h2>Nextcloud Talk</h2>
<p class="hint" style="margin-bottom:1rem;"> <p class="section-note">
Configure to send and receive messages via your Nextcloud Talk bot. Configure to send and receive messages via your Nextcloud Talk bot.
<strong>Sending</strong> requires the bot URL, secret, and notification room. <strong>Sending</strong> requires the bot URL, secret, and notification room.
<strong>Reading history</strong> (<code>nc_talk_history</code> tool) additionally <strong>Reading history</strong> (<code>nc_talk_history</code> tool) additionally
@@ -266,7 +113,7 @@
<div class="channel-block-body"> <div class="channel-block-body">
<p class="channel-hint"> <p class="channel-hint">
Set these up in your Nextcloud Talk room → Bot settings. Set these up in your Nextcloud Talk room → Bot settings.
See the <a href="/help" style="color:#a78bfa;">setup guide</a> for step-by-step instructions. See the <a href="/help" style="color:var(--pg-accent);">setup guide</a> for step-by-step instructions.
</p> </p>
<div class="field"> <div class="field">
<label for="nc_url">Nextcloud URL</label> <label for="nc_url">Nextcloud URL</label>
@@ -322,7 +169,7 @@
<!-- Home Assistant --> <!-- Home Assistant -->
<div class="section"> <div class="section">
<h2>Home Assistant</h2> <h2>Home Assistant</h2>
<p class="hint" style="margin-bottom:1rem;"> <p class="section-note">
Receive events from HA automations and let Inara call the HA REST API Receive events from HA automations and let Inara call the HA REST API
(read states, control devices). Webhook ID is the shared secret used in your (read states, control devices). Webhook ID is the shared secret used in your
HA <code>rest_command</code> URL. HA <code>rest_command</code> URL.
@@ -374,7 +221,7 @@
<!-- Google Chat --> <!-- Google Chat -->
<div class="section"> <div class="section">
<h2>Google Chat</h2> <h2>Google Chat</h2>
<p class="hint" style="margin-bottom:1rem;"> <p class="section-note">
Outbound webhook for proactive messages to a Google Chat space. Outbound webhook for proactive messages to a Google Chat space.
Incoming messages are handled separately via the Google Chat Add-on. Incoming messages are handled separately via the Google Chat Add-on.
</p> </p>
@@ -383,8 +230,7 @@
<summary>Outbound webhook</summary> <summary>Outbound webhook</summary>
<div class="channel-block-body"> <div class="channel-block-body">
<p class="channel-hint"> <p class="channel-hint">
Create a webhook in your Google Chat space → Manage webhooks. Create a webhook in your Google Chat space → Manage webhooks. Paste the full URL here.
Paste the full URL here.
</p> </p>
<div class="field"> <div class="field">
<label for="gc_outbound_webhook">Webhook URL</label> <label for="gc_outbound_webhook">Webhook URL</label>
@@ -397,19 +243,19 @@
</details> </details>
</div> </div>
<button type="submit">Save notification settings</button> <button type="submit" class="btn-submit">Save notification settings</button>
</form> </form>
<!-- Test --> <!-- Test -->
<div class="section" style="margin-top:2rem;"> <div class="section" style="margin-top:2rem;">
<h2>Test</h2> <h2>Test</h2>
<p class="hint" style="margin-bottom:0.85rem"> <p class="section-note">
Fire a notification via your configured channel or run the reminder check Fire a notification via your configured channel or run the reminder check
immediately — no need to wait for the daily 09:00 scheduler job. immediately — no need to wait for the daily 09:00 scheduler job.
</p> </p>
<div class="btn-row"> <div class="test-btn-row">
<button class="btn" id="btn-test-notify">Send Test Notification</button> <button class="test-btn" id="btn-test-notify">Send Test Notification</button>
<button class="btn" id="btn-check-reminders">Check Reminders Now</button> <button class="test-btn" id="btn-check-reminders">Check Reminders Now</button>
</div> </div>
<div class="test-result" id="test-result"></div> <div class="test-result" id="test-result"></div>
</div> </div>

188
cortex/static/pg.css Normal file
View File

@@ -0,0 +1,188 @@
/* ─── Cortex settings pages — shared stylesheet ───────────────────────────── */
/* ── Variables ── */
:root {
--pg-bg: #0f1117; --pg-surface: #1a1d27; --pg-border: #2d3148;
--pg-text: #e2e8f0; --pg-muted: #94a3b8;
--pg-dim: #64748b; --pg-dimmer: #475569;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
--pg-accent: #a78bfa; /* heading/highlight purple */
--pg-action: #7c3aed; /* button/focus purple */
}
[data-theme="light"] {
--pg-bg: #f4f2fa; --pg-surface: #ffffff; --pg-border: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
--pg-accent: #7c3aed;
--pg-action: #6d28d9;
}
/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Base ── */
body {
min-height: 100vh;
background: var(--pg-bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--pg-text);
}
/* ── Top nav ── */
.page-nav {
display: flex; align-items: center; gap: 0.25rem;
padding: 0.5rem 1rem; background: var(--pg-surface);
border-bottom: 1px solid var(--pg-border); flex-wrap: wrap;
}
.nav-link {
display: inline-flex; align-items: center;
padding: 0.3rem 0.6rem; border-radius: 6px;
font-size: 0.8rem; font-weight: 500; color: var(--pg-dim);
text-decoration: none; transition: color 0.15s, background 0.15s;
white-space: nowrap;
}
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: var(--pg-accent); }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
/* ── Page container ── */
.page-wrap {
max-width: 860px; margin: 0 auto;
padding: 2rem 1.5rem 4rem; width: 100%;
}
/* ── Page heading ── */
.page-title {
font-size: 1.4rem; font-weight: 700; color: var(--pg-accent);
}
.page-subtitle {
font-size: 0.8rem; color: var(--pg-muted);
margin-top: 0.2rem; margin-bottom: 1.75rem; line-height: 1.5;
}
/* ── Sections (settings-style, bottom-bordered h2) ── */
.section { margin-bottom: 2rem; }
.section > h2 {
font-size: 0.9rem; font-weight: 600; color: var(--pg-muted);
margin-bottom: 1rem; padding-bottom: 0.4rem;
border-bottom: 1px solid var(--pg-border);
}
/* ── Form elements ── */
.field { margin-bottom: 1rem; }
label {
display: block; font-size: 0.8rem; font-weight: 500;
color: var(--pg-muted); margin-bottom: 0.4rem;
}
input, select, textarea {
width: 100%; padding: 0.65rem 0.85rem;
background: var(--pg-bg); border: 1px solid var(--pg-border);
border-radius: 6px; color: var(--pg-text); font-size: 0.95rem;
font-family: inherit; outline: none; transition: border-color 0.15s;
}
input:focus, select:focus, textarea:focus { border-color: var(--pg-action); }
input[readonly] { color: var(--pg-muted); cursor: default; }
input[type="password"] { font-family: monospace; letter-spacing: 0.05em; }
textarea {
font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace;
font-size: 0.88rem; line-height: 1.55; resize: vertical;
}
/* ── Buttons ── */
/* Full-width primary form submit */
.btn-submit {
width: 100%; padding: 0.7rem; margin-top: 0.25rem;
background: var(--pg-action); border: none; border-radius: 6px;
color: #fff; font-size: 1rem; font-weight: 600;
cursor: pointer; transition: background 0.15s;
}
.btn-submit:hover { opacity: 0.88; }
/* Compact inline primary (e.g. rename save, inline forms) */
.btn-save {
padding: 0.4rem 0.9rem; background: var(--pg-action); border: none;
border-radius: 6px; color: #fff; font-size: 0.9rem;
font-weight: 600; cursor: pointer; transition: opacity 0.15s;
}
.btn-save:hover { opacity: 0.88; }
/* Outline secondary (e.g. clear cache, cancel, test actions) */
.btn-secondary {
padding: 0.5rem 1rem; background: none;
border: 1px solid var(--pg-border); border-radius: 6px;
color: var(--pg-muted); font-size: 0.88rem; font-weight: 500;
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.btn-secondary:hover { border-color: var(--pg-muted); color: var(--pg-text); }
.btn-secondary:disabled { opacity: 0.5; cursor: default; }
/* Inline cancel */
.btn-cancel {
padding: 0.4rem 0.75rem; background: none;
border: 1px solid var(--pg-border); border-radius: 6px;
color: var(--pg-muted); font-size: 0.9rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.btn-cancel:hover { border-color: var(--pg-muted); color: var(--pg-text); }
/* Button-styled link (purple, used for "Settings →" style CTAs) */
.action-link {
display: inline-block; padding: 0.5rem 1rem;
background: var(--pg-action); border-radius: 6px;
color: #fff; font-size: 0.88rem; font-weight: 600;
text-decoration: none; transition: opacity 0.15s;
}
.action-link:hover { opacity: 0.88; }
/* Inline button row */
.btn-row { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
/* ── Text utilities ── */
/* Small muted helper text below inputs */
.hint { font-size: 0.78rem; color: var(--pg-dim); margin-top: 0.35rem; line-height: 1.5; }
/* Section-level description paragraph */
.section-note { font-size: 0.8rem; color: var(--pg-muted); margin-bottom: 0.85rem; line-height: 1.55; }
/* Inline code */
code {
font-size: 0.82rem; font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace;
background: var(--pg-bg); border: 1px solid var(--pg-border);
padding: 0.1rem 0.35rem; border-radius: 4px; color: var(--pg-accent);
}
/* ── Feedback messages ── */
.success { color: #4ade80; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
.error { color: #f87171; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
/* ── Usage table (JS-rendered in settings) ── */
.usage-table { border-collapse: collapse; width: 100%; min-width: 360px; }
.usage-table th {
padding: 0.35rem 0.5rem; font-size: 0.75rem; color: var(--pg-muted);
font-weight: 600; text-align: right; border-bottom: 1px solid var(--pg-border);
}
.usage-table th:first-child { padding-left: 0; text-align: left; }
.usage-table td {
padding: 0.4rem 0.5rem; font-size: 0.82rem; color: var(--pg-muted); text-align: right;
}
.usage-table td:first-child { padding-left: 0; color: var(--pg-text); text-align: left; white-space: nowrap; }
.usage-table td:last-child { color: var(--pg-text); font-weight: 600; }
/* ── Tool category header row (tools_settings.py generated) ── */
.tool-cat-row td {
padding: 0.75rem 0.9rem 0.3rem;
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.07em;
text-transform: uppercase; color: var(--pg-dimmer);
border-bottom: 1px solid var(--pg-border);
}

View File

@@ -7,273 +7,93 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/pg.css">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script> <script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style> <style>
:root { /* ── Persona list ── */
--pg-bg: #0f1117; --pg-surface: #1a1d27;
--pg-border: #2d3148;
--pg-text: #e2e8f0; --pg-muted: #94a3b8;
--pg-dim: #64748b; --pg-dimmer: #475569;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
}
[data-theme="light"] {
--pg-bg: #f4f2fa; --pg-surface: #ffffff;
--pg-border: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--pg-bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--pg-text);
padding: 1.5rem;
}
.card {
background: var(--pg-surface);
border: 1px solid var(--pg-border);
border-radius: 12px;
padding: 2.5rem 2rem;
width: 100%;
max-width: 480px;
}
.page-nav {
display: flex;
align-items: center;
gap: 0.25rem;
margin-bottom: 1.75rem;
flex-wrap: wrap;
}
.nav-link {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.6rem;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-dim);
text-decoration: none;
transition: color 0.15s, background 0.15s;
white-space: nowrap;
}
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: #a78bfa; }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
.logo {
margin-bottom: 1.75rem;
}
.logo h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
.logo p { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.2rem; }
h2 {
font-size: 0.9rem;
font-weight: 600;
color: var(--pg-muted);
margin-bottom: 1rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--pg-border);
}
.section { margin-bottom: 2rem; }
label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-muted);
margin-bottom: 0.4rem;
}
input {
width: 100%;
padding: 0.65rem 0.85rem;
background: var(--pg-bg);
border: 1px solid var(--pg-border);
border-radius: 6px;
color: var(--pg-text);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s;
}
input:focus { border-color: #7c3aed; }
input[readonly] { color: var(--pg-muted); cursor: default; }
.field { margin-bottom: 1rem; }
button[type="submit"] {
width: 100%;
padding: 0.7rem;
margin-top: 0.25rem;
background: #7c3aed;
border: none;
border-radius: 6px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
button[type="submit"]:hover { background: #6d28d9; }
.error {
color: #f87171;
font-size: 0.85rem;
text-align: center;
margin-bottom: 1rem;
}
.success {
color: #4ade80;
font-size: 0.85rem;
text-align: center;
margin-bottom: 1rem;
}
.persona-list { .persona-list {
list-style: none; list-style: none; display: flex; flex-direction: column;
display: flex; gap: 0.5rem; margin-top: 0.5rem;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.persona-list li {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
} }
.persona-list li { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.persona-link { .persona-link {
display: inline-block; display: inline-block; padding: 0.3rem 0.75rem;
padding: 0.3rem 0.75rem; background: var(--pg-bg); border: 1px solid var(--pg-border);
background: var(--pg-bg); border-radius: 20px; color: var(--pg-accent); font-size: 0.85rem;
border: 1px solid var(--pg-border); text-decoration: none; transition: border-color 0.15s;
border-radius: 20px;
color: #a78bfa;
font-size: 0.85rem;
text-decoration: none;
transition: border-color 0.15s;
} }
.persona-link:hover { border-color: #7c3aed; } .persona-link:hover { border-color: var(--pg-action); }
.persona-list li em { color: var(--pg-muted); font-size: 0.85rem; } .persona-list li em { color: var(--pg-muted); font-size: 0.85rem; }
.persona-rename-toggle { .persona-rename-toggle {
background: none; background: none; border: 1px solid var(--pg-border);
border: none; border-radius: 6px; color: var(--pg-muted); font-size: 0.8rem;
color: var(--pg-muted); padding: 0.3rem 0.6rem; margin-top: 0.25rem;
font-size: 0.85rem; cursor: pointer; opacity: 0.7; transition: opacity 0.15s, color 0.15s;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s;
} }
.persona-rename-toggle:hover { opacity: 1; color: #a78bfa; } .persona-rename-toggle:hover { opacity: 1; color: var(--pg-accent); }
.persona-rename-form { display: flex; align-items: center; gap: 0.4rem; } .persona-rename-form { display: flex; align-items: center; gap: 0.4rem; }
.persona-rename-form input[type="text"] { .persona-rename-form input[type="text"] {
width: 12rem; width: 12rem; padding: 0.3rem 0.6rem;
padding: 0.3rem 0.6rem; border-color: var(--pg-action); font-size: 0.9rem;
background: var(--pg-bg);
border: 1px solid #7c3aed;
border-radius: 6px;
color: var(--pg-text);
font-size: 0.9rem;
outline: none;
} }
.persona-rename-form button[type="submit"] { .persona-rename-form .btn-save { padding: 0.3rem 0.75rem; font-size: 0.85rem; }
width: auto;
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
margin-top: 0;
}
.persona-rename-cancel {
background: none;
border: 1px solid var(--pg-border);
border-radius: 6px;
color: var(--pg-muted);
font-size: 0.85rem;
padding: 0.3rem 0.6rem;
cursor: pointer;
}
.persona-rename-cancel:hover { border-color: var(--pg-muted); color: var(--pg-text); }
.add-persona { .add-persona {
display: inline-block; display: inline-block; margin-top: 0.75rem;
margin-top: 0.75rem; font-size: 0.8rem; color: var(--pg-muted); text-decoration: none;
font-size: 0.8rem;
color: var(--pg-muted);
text-decoration: none;
} }
.add-persona:hover { color: #a78bfa; } .add-persona:hover { color: var(--pg-accent); }
/* ── Role badge ── */
.role-badge { .role-badge {
display: inline-block; display: inline-block; padding: 0.25rem 0.75rem;
padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.78rem; font-weight: 600;
border-radius: 20px; text-transform: uppercase; letter-spacing: 0.06em;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
} }
.role-badge.role-admin { .role-badge.role-admin {
background: rgba(124, 58, 237, 0.15); background: rgba(124,58,237,0.15); color: var(--pg-accent);
color: #a78bfa; border: 1px solid rgba(124,58,237,0.4);
border: 1px solid rgba(124, 58, 237, 0.4);
} }
.role-badge.role-user { .role-badge.role-user {
background: rgba(100, 116, 139, 0.12); background: rgba(100,116,139,0.12); color: var(--pg-muted);
color: var(--pg-muted);
border: 1px solid var(--pg-border); border: 1px solid var(--pg-border);
} }
textarea { /* ── OpenRouter quickstart warning card ── */
width: 100%; #openrouter-quickstart {
padding: 0.65rem 0.85rem; display: none; background: #1c1a0a; border: 1px solid #78350f;
background: var(--pg-bg); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;
border: 1px solid var(--pg-border);
border-radius: 6px;
color: var(--pg-text);
font-size: 0.88rem;
font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace;
line-height: 1.55;
resize: vertical;
outline: none;
transition: border-color 0.15s;
} }
textarea:focus { border-color: #7c3aed; } #openrouter-quickstart .qs-title {
font-size: 0.82rem; color: #fbbf24; font-weight: 600; margin-bottom: 0.4rem;
}
#openrouter-quickstart .qs-body {
font-size: 0.8rem; color: #d97706; margin-bottom: 0.75rem; line-height: 1.5;
}
.action-link.action-link-amber {
background: #92400e; color: #fef3c7; font-size: 0.85rem; padding: 0.5rem 0.9rem;
}
.action-link.action-link-amber:hover { opacity: 0.9; background: #78350f; }
/* ── Inline result feedback spans ── */
.result-ok { display: none; margin-left: 0.75rem; font-size: 0.8rem; color: #4ade80; }
/* ── Usage table wrapper ── */
.usage-wrap { overflow-x: auto; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <nav class="page-nav">
<nav class="page-nav"> <a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ back_href }}" class="nav-link">← Chat</a> <a href="{{ help_href }}" class="nav-link">Help</a>
<a href="{{ help_href }}" class="nav-link">Help</a> <a href="/settings" class="nav-link active">Settings</a>
<a href="/settings" class="nav-link active">Settings</a> <a href="/settings/notifications" class="nav-link">Notifications</a>
<a href="/settings/notifications" class="nav-link">Notifications</a> <a href="/settings/tools" class="nav-link">Tools</a>
<a href="/settings/tools" class="nav-link">Tools</a> <span class="nav-spacer"></span>
<span class="nav-spacer"></span> <a href="/logout" class="nav-link nav-logout">Sign out</a>
<a href="/logout" class="nav-link nav-logout">Sign out</a> </nav>
</nav> <div class="page-wrap">
<h1 class="page-title">Account Settings</h1>
<div class="logo"> <p class="page-subtitle">Manage your account and personas.</p>
<h1>Account Settings</h1>
<p>Manage your account and personas.</p>
</div>
<!-- SUCCESS --> <!-- SUCCESS -->
<!-- ERROR --> <!-- ERROR -->
@@ -289,26 +109,21 @@
<label>Role</label> <label>Role</label>
<span class="role-badge role-{{ user_role }}">{{ user_role }}</span> <span class="role-badge role-{{ user_role }}">{{ user_role }}</span>
</div> </div>
<button type="button" id="show-rename-user" class="persona-rename-toggle" <button type="button" id="show-rename-user" class="persona-rename-toggle">
style="opacity:0.7; font-size:0.8rem; padding:0.3rem 0.6rem; border:1px solid var(--pg-border); border-radius:6px; margin-top:0.25rem;">
✏ Change username ✏ Change username
</button> </button>
<form id="rename-user-form" method="POST" action="/settings/username" <form id="rename-user-form" method="POST" action="/settings/username" style="display:none; margin-top:0.75rem;">
style="display:none; margin-top:0.75rem;">
<div class="field"> <div class="field">
<label for="new_username">New username</label> <label for="new_username">New username</label>
<input type="text" id="new_username" name="new_username" <input type="text" id="new_username" name="new_username"
value="{{ username }}" value="{{ username }}"
pattern="[a-z_][a-z0-9_\-]{0,31}" required autofocus pattern="[a-z_][a-z0-9_\-]{0,31}" required autofocus
autocomplete="off" data-form-type="other"> autocomplete="off" data-form-type="other">
<p style="font-size:0.75rem; color:var(--pg-muted); margin-top:0.3rem;"> <p class="hint">Lowercase letters, digits, _ or - only. You will be logged out after renaming.</p>
Lowercase letters, digits, _ or - only. You will be logged out after renaming.
</p>
</div> </div>
<div style="display:flex; gap:0.5rem;"> <div class="btn-row">
<button type="submit" style="flex:1; padding:0.5rem; background:#7c3aed; border:none; border-radius:6px; color:#fff; font-size:0.9rem; font-weight:600; cursor:pointer;">Save</button> <button type="submit" class="btn-save">Save</button>
<button type="button" id="cancel-rename-user" <button type="button" id="cancel-rename-user" class="btn-cancel">Cancel</button>
style="padding:0.5rem 0.9rem; background:none; border:1px solid var(--pg-border); border-radius:6px; color:var(--pg-muted); font-size:0.9rem; cursor:pointer;">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
@@ -322,18 +137,15 @@
placeholder="No Google account linked" placeholder="No Google account linked"
style="{{ google_email == '' and 'color:var(--pg-dimmer)' or '' }}"> style="{{ google_email == '' and 'color:var(--pg-dimmer)' or '' }}">
</div> </div>
<p style="font-size:0.75rem; color:var(--pg-muted); margin-top:-0.5rem;"> <p class="hint" style="margin-top:-0.5rem;">To link or change your Google account, contact Scott.</p>
To link or change your Google account, contact Scott.
</p>
</div> </div>
<!-- Email Allowlist --> <!-- Email Allowlist -->
<div class="section"> <div class="section">
<h2>Email Allowlist</h2> <h2>Email Allowlist</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
One regex pattern per line. The <code style="font-size:0.82rem; background:var(--pg-bg); padding:0.1rem 0.35rem; border-radius:4px;">email_send</code> One regex pattern per line. The <code>email_send</code> tool will only send to addresses
tool will only send to addresses that match at least one pattern. that match at least one pattern. Leave blank to block all outbound email.
Leave blank to block all outbound email.
</p> </p>
<form method="POST" action="/settings/email-allowlist"> <form method="POST" action="/settings/email-allowlist">
<div class="field"> <div class="field">
@@ -342,17 +154,16 @@
placeholder=".*@example\.com&#10;alice@example\.com" placeholder=".*@example\.com&#10;alice@example\.com"
spellcheck="false">{{ email_allowlist }}</textarea> spellcheck="false">{{ email_allowlist }}</textarea>
</div> </div>
<button type="submit">Save allowlist</button> <button type="submit" class="btn-submit">Save allowlist</button>
</form> </form>
</div> </div>
<!-- HTTP POST Allowlist --> <!-- HTTP POST Allowlist -->
<div class="section"> <div class="section">
<h2>HTTP POST Allowlist</h2> <h2>HTTP POST Allowlist</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
One URL prefix per line. The <code style="font-size:0.82rem; background:var(--pg-bg); padding:0.1rem 0.35rem; border-radius:4px;">http_post</code> One URL prefix per line. The <code>http_post</code> tool will only POST to URLs that
tool will only POST to URLs that start with a listed prefix. start with a listed prefix. Leave blank to block all outbound POST requests.
Leave blank to block all outbound POST requests.
</p> </p>
<form method="POST" action="/settings/http-allowlist"> <form method="POST" action="/settings/http-allowlist">
<div class="field"> <div class="field">
@@ -361,103 +172,73 @@
placeholder="https://ha.dgrzone.com/api/webhook/&#10;https://n8n.dgrzone.com/webhook/" placeholder="https://ha.dgrzone.com/api/webhook/&#10;https://n8n.dgrzone.com/webhook/"
spellcheck="false">{{ http_allowlist }}</textarea> spellcheck="false">{{ http_allowlist }}</textarea>
</div> </div>
<button type="submit">Save allowlist</button> <button type="submit" class="btn-submit">Save allowlist</button>
</form> </form>
</div> </div>
<!-- Notifications --> <!-- Notifications -->
<div class="section"> <div class="section">
<h2>Notifications</h2> <h2>Notifications</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
Configure how Inara reaches out proactively — reminders, cron jobs, and memory digests. Configure how Inara reaches out proactively — reminders, cron jobs, and memory digests.
</p> </p>
<a href="/settings/notifications" <a href="/settings/notifications" class="action-link">Notification settings →</a>
style="display:inline-block; padding:0.55rem 1rem; background:#7c3aed; border-radius:6px;
color:#fff; font-size:0.88rem; font-weight:600; text-decoration:none;
transition:background 0.15s;">
Notification settings →
</a>
</div> </div>
<!-- Tool Permissions → moved to /settings/tools --> <!-- Tool Permissions → /settings/tools -->
<div class="section"> <div class="section">
<h2>Tool Permissions</h2> <h2>Tool Permissions</h2>
<p style="font-size:0.85rem; color:var(--pg-muted); margin-bottom:1rem; line-height:1.55;"> <p class="section-note">
Configure tool access, risk policy, and confirmation gate overrides on the Tools page. Configure tool access, risk policy, and confirmation gate overrides on the Tools page.
</p> </p>
<a href="/settings/tools" <a href="/settings/tools" class="action-link">Tool settings →</a>
style="display:inline-block; padding:0.45rem 1.1rem; background:var(--pg-accent,#7c3aed);
color:#fff; border-radius:0.5rem; font-size:0.875rem; font-weight:600;
text-decoration:none;">
Tool Settings →
</a>
</div> </div>
<!-- Browser cache -->
<!-- Usage summary --> <!-- Usage summary -->
<div class="section" id="usage-section"> <div class="section" id="usage-section">
<h2>Usage</h2> <h2>Usage</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
Token consumption tracked for API-backed models (Gemini API, local OpenAI-compatible). Token consumption tracked for API-backed models (Gemini API, local OpenAI-compatible).
Claude CLI calls are not metered. Claude CLI calls are not metered.
</p> </p>
<div id="usage-table-wrap" style="overflow-x:auto;"> <div id="usage-table-wrap" class="usage-wrap">
<p style="font-size:0.8rem; color:var(--pg-muted);">Loading…</p> <p class="section-note">Loading…</p>
</div> </div>
</div> </div>
<!-- Browser Cache -->
<div class="section"> <div class="section">
<h2>Browser Cache</h2> <h2>Browser Cache</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
Clears UI preferences stored in this browser: active mode, session ID, memory toggles, Clears UI preferences stored in this browser: active mode, session ID, memory toggles,
theme, font size, and context tier. Does not sign you out. theme, font size, and context tier. Does not sign you out.
</p> </p>
<button type="button" id="clear-ls-btn" <button type="button" id="clear-ls-btn" class="btn-secondary">Clear browser cache</button>
style="padding:0.5rem 1rem; background:none; border:1px solid var(--pg-border); border-radius:6px; <span id="clear-ls-ok" class="result-ok">Cleared.</span>
color:var(--pg-muted); font-size:0.88rem; font-weight:500; cursor:pointer;
transition:border-color 0.15s, color 0.15s;">
Clear browser cache
</button>
<span id="clear-ls-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;">
Cleared.
</span>
</div> </div>
<!-- Model Registry link --> <!-- Model Registry -->
<div class="section"> <div class="section">
<h2>Model Registry</h2> <h2>Model Registry</h2>
<!-- Quick-start card: shown only when no model is configured for chat role --> <div id="openrouter-quickstart">
<div id="openrouter-quickstart" style="display:none; background:#1c1a0a; border:1px solid #78350f; <p class="qs-title">⚡ You're on the server default model</p>
border-radius:8px; padding:1rem; margin-bottom:1rem;"> <p class="qs-body">
<p style="font-size:0.82rem; color:#fbbf24; font-weight:600; margin-bottom:0.4rem;">
⚡ You're on the server default model
</p>
<p style="font-size:0.8rem; color:#d97706; margin-bottom:0.75rem; line-height:1.5;">
You can chat now, but adding your own model gives you more choices, lets you pick You can chat now, but adding your own model gives you more choices, lets you pick
role-specific models, and tracks your usage separately. role-specific models, and tracks your usage separately.
OpenRouter is the easiest way to get started — one key, many models. OpenRouter is the easiest way to get started — one key, many models.
</p> </p>
<a href="/setup/model" <a href="/setup/model" class="action-link action-link-amber">Set up OpenRouter →</a>
style="display:inline-block; padding:0.5rem 0.9rem; background:#92400e; border-radius:6px;
color:#fef3c7; font-size:0.85rem; font-weight:600; text-decoration:none;">
Set up OpenRouter →
</a>
</div> </div>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
Configure AI providers (Anthropic, Google), local hosts (Open WebUI, Ollama, OpenRouter, etc.), Configure AI providers (Anthropic, Google), local hosts (Open WebUI, Ollama, OpenRouter, etc.),
and assign models to roles — chat, orchestrator, distill, and more. and assign models to roles — chat, orchestrator, distill, and more.
</p> </p>
<a href="/settings/models" <a href="/settings/models" class="action-link">Manage models →</a>
style="display:inline-block; padding:0.55rem 1rem; background:#7c3aed; border-radius:6px;
color:#fff; font-size:0.88rem; font-weight:600; text-decoration:none;
transition:background 0.15s;">
Manage models →
</a>
</div> </div>
<!-- Change password --> <!-- Change Password -->
<div class="section"> <div class="section">
<h2>Change Password</h2> <h2>Change Password</h2>
<form method="POST" action="/settings/password" id="password-form"> <form method="POST" action="/settings/password" id="password-form">
@@ -476,27 +257,22 @@
<input type="password" id="confirm_password" name="confirm_password" <input type="password" id="confirm_password" name="confirm_password"
autocomplete="new-password" required> autocomplete="new-password" required>
</div> </div>
<button type="submit">Update password</button> <button type="submit" class="btn-submit">Update password</button>
</form> </form>
</div> </div>
<!-- Personas -->
<!-- Sessions --> <!-- Sessions -->
<div class="section"> <div class="section">
<h2>Sessions</h2> <h2>Sessions</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;"> <p class="section-note">
Auto-name any sessions that still show a random ID, using their first message as the name. Auto-name any sessions that still show a random ID, using their first message as the name.
Only unnamed sessions are affected — existing names are left alone. Only unnamed sessions are affected — existing names are left alone.
</p> </p>
<button type="button" id="backfill-names-btn" <button type="button" id="backfill-names-btn" class="btn-secondary">Auto-name old sessions</button>
style="padding:0.5rem 1rem; background:none; border:1px solid var(--pg-border); border-radius:6px; <span id="backfill-names-ok" class="result-ok"></span>
color:var(--pg-muted); font-size:0.88rem; font-weight:500; cursor:pointer;
transition:border-color 0.15s, color 0.15s;">
Auto-name old sessions
</button>
<span id="backfill-names-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;"></span>
</div> </div>
<!-- Personas -->
<div class="section"> <div class="section">
<h2>Personas</h2> <h2>Personas</h2>
<ul class="persona-list"> <ul class="persona-list">
@@ -528,16 +304,6 @@
document.getElementById('show-rename-user').style.display = ''; document.getElementById('show-rename-user').style.display = '';
}); });
// Gemini key — "remove" link clears the input and submits the form
const geminiRemove = document.getElementById('gemini-remove-link');
if (geminiRemove) {
geminiRemove.addEventListener('click', e => {
e.preventDefault();
document.getElementById('gemini_api_key').value = '';
document.querySelector('form[action="/settings/gemini-key"]').submit();
});
}
// Clear localStorage (keeps JWT cookie — no sign-out) // Clear localStorage (keeps JWT cookie — no sign-out)
document.getElementById('clear-ls-btn').addEventListener('click', () => { document.getElementById('clear-ls-btn').addEventListener('click', () => {
localStorage.clear(); localStorage.clear();
@@ -548,8 +314,7 @@
(async () => { (async () => {
try { try {
const d = await fetch('/backend').then(r => r.json()); const d = await fetch('/backend').then(r => r.json());
const roles = d.available_roles || []; if ((d.available_roles || []).length === 0) {
if (roles.length === 0) {
document.getElementById('openrouter-quickstart').style.display = 'block'; document.getElementById('openrouter-quickstart').style.display = 'block';
} }
} catch (_) {} } catch (_) {}
@@ -563,7 +328,7 @@
if (!resp.ok) throw new Error(resp.statusText); if (!resp.ok) throw new Error(resp.statusText);
const rows_data = await resp.json(); const rows_data = await resp.json();
if (!rows_data.length) { if (!rows_data.length) {
wrap.innerHTML = '<p style="font-size:0.8rem;color:var(--pg-muted);">No usage recorded yet.</p>'; wrap.innerHTML = '<p class="section-note">No usage recorded yet.</p>';
return; return;
} }
const fmt = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); const fmt = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
@@ -572,27 +337,22 @@
? `<span title="${d.key}">${d.label}</span>` ? `<span title="${d.key}">${d.label}</span>`
: `<span>${d.key}</span>`; : `<span>${d.key}</span>`;
return `<tr> return `<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0; font-size:0.82rem; color:var(--pg-text); white-space:nowrap;">${labelCell}</td> <td>${labelCell}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${d.calls}</td> <td>${d.calls}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${fmt(d.prompt_tokens)}</td> <td>${fmt(d.prompt_tokens)}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${fmt(d.completion_tokens)}</td> <td>${fmt(d.completion_tokens)}</td>
<td style="padding:0.4rem 0 0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-text); text-align:right; font-weight:600;">${fmt(d.total_tokens)}</td> <td>${fmt(d.total_tokens)}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
wrap.innerHTML = `<table style="border-collapse:collapse; width:100%; min-width:360px;"> wrap.innerHTML = `<table class="usage-table">
<thead> <thead><tr>
<tr style="border-bottom:1px solid var(--pg-border);"> <th style="text-align:left">Model</th>
<th style="padding:0.35rem 0.75rem 0.35rem 0; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:left;">Model</th> <th>Calls</th><th>Prompt</th><th>Output</th><th>Total</th>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Calls</th> </tr></thead>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Prompt</th>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Output</th>
<th style="padding:0.35rem 0 0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Total</th>
</tr>
</thead>
<tbody>${rows}</tbody> <tbody>${rows}</tbody>
</table>`; </table>`;
} catch (e) { } catch (e) {
wrap.innerHTML = `<p style="font-size:0.8rem;color:var(--pg-muted);">Could not load usage data.</p>`; wrap.innerHTML = '<p class="section-note">Could not load usage data.</p>';
} }
})(); })();

View File

@@ -4,51 +4,15 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool Settings — Cortex</title> <title>Tool Settings — Cortex</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/pg.css">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* ── Policy cards (bordered sections on tools page) ── */
:root {
color-scheme: light dark;
--pg-bg: #f8fafc; --pg-card: #ffffff; --pg-border: #e2e8f0;
--pg-text: #1e293b; --pg-muted: #64748b; --pg-dimmer: #94a3b8;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
--pg-accent: #7c3aed;
}
@media (prefers-color-scheme: dark) {
:root {
--pg-bg: #0f0a1e; --pg-card: #1a1228; --pg-border: #2d2040;
--pg-text: #e2d9f3; --pg-muted: #9d8ec4; --pg-dimmer: #6b5d8a;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
}
}
body { font-family: system-ui, sans-serif; background: var(--pg-bg); color: var(--pg-text); min-height: 100vh; }
.page-nav {
display: flex; align-items: center; gap: 0.25rem;
padding: 0.5rem 1rem; background: var(--pg-card);
border-bottom: 1px solid var(--pg-border); flex-wrap: wrap;
}
.nav-link {
padding: 0.35rem 0.7rem; border-radius: 0.375rem; font-size: 0.875rem;
color: var(--pg-muted); text-decoration: none; white-space: nowrap;
}
.nav-link:hover { color: var(--pg-text); background: var(--pg-nav-hover); }
.nav-link.active { color: #a78bfa; }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
.page-wrap { max-width: 860px; margin: 0 auto; padding: 2rem 1rem 4rem; }
h1 { font-size: 1.4rem; font-weight: 700; margin-bottom: 0.25rem; }
.page-lead { color: var(--pg-muted); font-size: 0.9rem; margin-bottom: 2rem; }
.success { color: #22c55e; font-size: 0.9rem; margin: 0.5rem 0; }
.error { color: #f87171; font-size: 0.9rem; margin: 0.5rem 0; }
/* ── Risk policy card ── */
.policy-card { .policy-card {
background: var(--pg-card); border: 1px solid var(--pg-border); background: var(--pg-surface); border: 1px solid var(--pg-border);
border-radius: 0.75rem; padding: 1.25rem 1.5rem; margin-bottom: 1.75rem; border-radius: 0.75rem; padding: 1.25rem 1.5rem; margin-bottom: 1.75rem;
} }
.policy-card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; } .policy-card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; }
@@ -56,30 +20,37 @@
.policy-label { font-size: 0.875rem; font-weight: 500; min-width: 6rem; } .policy-label { font-size: 0.875rem; font-weight: 500; min-width: 6rem; }
.policy-note { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.35rem; line-height: 1.5; } .policy-note { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.35rem; line-height: 1.5; }
select, input[type="text"] { /* Compact selects and inputs inside policy cards */
background: var(--pg-bg); border: 1px solid var(--pg-border); .policy-card select, .policy-card input[type="text"] {
border-radius: 0.375rem; color: var(--pg-text);
padding: 0.4rem 0.65rem; font-size: 0.875rem; padding: 0.4rem 0.65rem; font-size: 0.875rem;
} }
select:focus, input:focus { outline: 2px solid var(--pg-accent); border-color: transparent; }
/* Two-column layout for allow/deny textareas */
.col-split { display: flex; gap: 1.5rem; flex-wrap: wrap; align-items: flex-start; }
.col-half { flex: 1; min-width: 200px; }
.col-half label { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.35rem; }
.col-half textarea {
font-size: 0.82rem; border-radius: 0.375rem; padding: 0.45rem 0.65rem;
}
/* Save button (compact, not full-width) */
.save-btn { .save-btn {
background: var(--pg-accent); color: #fff; border: none; background: var(--pg-action); color: #fff; border: none;
border-radius: 0.5rem; padding: 0.5rem 1.4rem; border-radius: 0.5rem; padding: 0.5rem 1.4rem;
font-size: 0.875rem; font-weight: 600; cursor: pointer; font-size: 0.875rem; font-weight: 600; cursor: pointer;
margin-top: 0.5rem; margin-top: 0.5rem; transition: opacity 0.15s;
} }
.save-btn:hover { opacity: 0.88; } .save-btn:hover { opacity: 0.88; }
/* ── Tool table ── */ /* ── Tool table ── */
.section-head { .table-section-label {
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--pg-dimmer); text-transform: uppercase; color: var(--pg-dimmer);
margin: 1.75rem 0 0.6rem; margin: 1.75rem 0 0.6rem;
} }
.tool-table { .tool-table {
width: 100%; border-collapse: collapse; width: 100%; border-collapse: collapse;
background: var(--pg-card); border: 1px solid var(--pg-border); background: var(--pg-surface); border: 1px solid var(--pg-border);
border-radius: 0.75rem; overflow: hidden; margin-bottom: 0.5rem; border-radius: 0.75rem; overflow: hidden; margin-bottom: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -92,36 +63,31 @@
.tool-table td { padding: 0.5rem 0.9rem; border-bottom: 1px solid var(--pg-border); vertical-align: middle; } .tool-table td { padding: 0.5rem 0.9rem; border-bottom: 1px solid var(--pg-border); vertical-align: middle; }
.tool-table tr:last-child td { border-bottom: none; } .tool-table tr:last-child td { border-bottom: none; }
.tool-table tr:hover td { background: rgba(124,58,237,0.04); } .tool-table tr:hover td { background: rgba(124,58,237,0.04); }
.tool-name { font-family: monospace; font-size: 0.82rem; } .tool-name { font-family: monospace; font-size: 0.82rem; }
/* Risk badges */ /* Risk badges */
.risk { display: inline-block; font-size: 0.7rem; font-weight: 700; .risk { display: inline-block; font-size: 0.7rem; font-weight: 700;
padding: 0.15rem 0.45rem; border-radius: 9999px; letter-spacing: 0.04em; } padding: 0.15rem 0.45rem; border-radius: 9999px; letter-spacing: 0.04em; }
.risk-low { background: rgba(34,197,94,0.15); color: #16a34a; } .risk-low { background: rgba(34,197,94,0.12); color: #4ade80; }
.risk-medium { background: rgba(234,179, 8,0.15); color: #ca8a04; } .risk-medium { background: rgba(234,179,8,0.12); color: #fbbf24; }
.risk-high { background: rgba(239,68, 68,0.15); color: #dc2626; } .risk-high { background: rgba(239,68,68,0.12); color: #f87171; }
@media (prefers-color-scheme: dark) { [data-theme="light"] .risk-low { background: rgba(34,197,94,0.15); color: #16a34a; }
.risk-low { background: rgba(34,197,94,0.12); color: #4ade80; } [data-theme="light"] .risk-medium { background: rgba(234,179,8,0.15); color: #ca8a04; }
.risk-medium { background: rgba(234,179, 8,0.12); color: #fbbf24; } [data-theme="light"] .risk-high { background: rgba(239,68,68,0.15); color: #dc2626; }
.risk-high { background: rgba(239,68, 68,0.12); color: #f87171; }
}
/* Auto status pill */ /* Auto status pill */
.auto-pill { .auto-pill {
display: inline-block; font-size: 0.68rem; font-weight: 600; display: inline-block; font-size: 0.68rem; font-weight: 600;
padding: 0.12rem 0.4rem; border-radius: 9999px; padding: 0.12rem 0.4rem; border-radius: 9999px;
} }
.auto-on { background: rgba(124,58,237,0.12); color: #7c3aed; } .auto-on { background: rgba(124,58,237,0.12); color: #a78bfa; }
.auto-off { background: rgba(148,163,184,0.12); color: var(--pg-dimmer); } .auto-off { background: rgba(148,163,184,0.12); color: var(--pg-dimmer); }
@media (prefers-color-scheme: dark) { [data-theme="light"] .auto-on { color: #7c3aed; }
.auto-on { color: #a78bfa; }
}
/* Override select */ /* Override select */
.override-sel { .override-sel {
font-size: 0.78rem; padding: 0.25rem 0.5rem; font-size: 0.78rem; padding: 0.25rem 0.5rem;
border-radius: 0.3rem; min-width: 7rem; border-radius: 0.3rem; min-width: 7rem; width: auto;
} }
.override-sel.forced-on { border-color: #7c3aed; color: #7c3aed; } .override-sel.forced-on { border-color: #7c3aed; color: #7c3aed; }
.override-sel.forced-off { border-color: #dc2626; color: #dc2626; } .override-sel.forced-off { border-color: #dc2626; color: #dc2626; }
@@ -129,7 +95,7 @@
/* Legend */ /* Legend */
.legend { display: flex; gap: 1.25rem; flex-wrap: wrap; margin-bottom: 1.25rem; font-size: 0.8rem; color: var(--pg-muted); } .legend { display: flex; gap: 1.25rem; flex-wrap: wrap; margin-bottom: 1.25rem; font-size: 0.8rem; color: var(--pg-muted); }
.legend-dot { display: inline-block; width: 0.55rem; height: 0.55rem; border-radius: 50%; margin-right: 0.3rem; } .legend-dot { display: inline-block; width: 0.55rem; height: 0.55rem; border-radius: 50%; margin-right: 0.3rem; }
.legend-dot.on { background: #7c3aed; } .legend-dot.on { background: #a78bfa; }
.legend-dot.off { background: var(--pg-dimmer); } .legend-dot.off { background: var(--pg-dimmer); }
</style> </style>
</head> </head>
@@ -146,8 +112,8 @@
</nav> </nav>
<div class="page-wrap"> <div class="page-wrap">
<h1>Tool Settings</h1> <h1 class="page-title">Tool Settings</h1>
<p class="page-lead"> <p class="page-subtitle">
Control which orchestrator tools are available. The risk level sets an automatic threshold; Control which orchestrator tools are available. The risk level sets an automatic threshold;
whitelist and blacklist let you fine-tune individual tools beyond that. whitelist and blacklist let you fine-tune individual tools beyond that.
</p> </p>
@@ -157,15 +123,14 @@
<form method="POST" action="/settings/tools" id="tools-form"> <form method="POST" action="/settings/tools" id="tools-form">
<!-- ── Risk policy ── --> <!-- Risk policy -->
<div class="policy-card"> <div class="policy-card">
<h2>Risk Policy</h2> <h2>Risk Policy</h2>
<div class="policy-row"> <div class="policy-row">
<span class="policy-label">Max risk level</span> <span class="policy-label">Max risk level</span>
<select name="max_risk" id="max-risk-sel"> <select name="max_risk" id="max-risk-sel">
<option value="" {{ sel_none }}>No filter — use all role-permitted tools</option> <option value="" {{ sel_none }}>No filter — use all role-permitted tools</option>
<option value="low" {{ sel_low }}>Low — read-only and sandboxed tools only</option> <option value="low" {{ sel_low }}>Low — read-only and sandboxed tools only</option>
<option value="medium" {{ sel_medium }}>Medium — low + medium risk (recommended)</option> <option value="medium" {{ sel_medium }}>Medium — low + medium risk (recommended)</option>
<option value="high" {{ sel_high }}>High — all tools including destructive ones</option> <option value="high" {{ sel_high }}>High — all tools including destructive ones</option>
</select> </select>
@@ -175,53 +140,42 @@
<strong>Medium</strong> tools write to local data or send notifications to you (cron jobs, scratch, task management).<br> <strong>Medium</strong> tools write to local data or send notifications to you (cron jobs, scratch, task management).<br>
<strong>High</strong> tools affect external systems or the host (shell exec, email, device control, service restart). <strong>High</strong> tools affect external systems or the host (shell exec, email, device control, service restart).
</p> </p>
<p class="policy-note" style="margin-top:0.75rem;"> <p class="policy-note" style="margin-top:0.75rem;">
The <em>Auto</em> column below shows each tool's status at your current max risk level. The <em>Auto</em> column below shows each tool's status at your current max risk level.
Use the override column to force-include or force-exclude individual tools. Use the override column to force-include or force-exclude individual tools.
</p> </p>
</div> </div>
<!-- ── Legend ── --> <!-- Legend -->
<div class="legend"> <div class="legend">
<span><span class="legend-dot on"></span>Auto-included by risk level</span> <span><span class="legend-dot on"></span>Auto-included by risk level</span>
<span><span class="legend-dot off"></span>Auto-excluded by risk level</span> <span><span class="legend-dot off"></span>Auto-excluded by risk level</span>
</div> </div>
<!-- ── Tool table ── --> <!-- Tool table -->
{{ tool_table_html }} {{ tool_table_html }}
<!-- ── Confirmation gate ── --> <!-- Confirmation gate -->
<div class="policy-card" style="margin-top:1.75rem;"> <div class="policy-card" style="margin-top:1.75rem;">
<h2>Confirmation Gate</h2> <h2>Confirmation Gate</h2>
<p class="policy-note" style="margin-bottom:0.85rem;"> <p class="policy-note">
Some tools require explicit confirmation before executing. Override the defaults here.<br> Some tools require explicit confirmation before executing. Override the defaults here.<br>
Tools requiring confirmation by default: <code style="font-size:0.78rem;">{{ confirm_required_tools }}</code> Tools requiring confirmation by default: <code>{{ confirm_required_tools }}</code>
</p> </p>
<div class="policy-row" style="align-items:flex-start; gap:1.5rem; flex-wrap:wrap;"> <div class="col-split" style="margin-top:0.85rem;">
<div style="flex:1; min-width:200px;"> <div class="col-half">
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;"> <label>Allow list — bypass confirmation</label>
Allow list — bypass confirmation
</label>
<textarea name="allow_list" rows="4" <textarea name="allow_list" rows="4"
placeholder="reminders_clear&#10;cron_remove" placeholder="reminders_clear&#10;cron_remove"
autocomplete="off" spellcheck="false" autocomplete="off" spellcheck="false">{{ tool_allow }}</textarea>
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border); <p class="hint">One tool name per line. These tools skip the confirmation prompt.</p>
border-radius:0.375rem; color:var(--pg-text); padding:0.45rem 0.65rem;
font-size:0.82rem; font-family:monospace; resize:vertical;">{{ tool_allow }}</textarea>
<p class="policy-note">One tool name per line. These tools skip the confirmation prompt.</p>
</div> </div>
<div style="flex:1; min-width:200px;"> <div class="col-half">
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;"> <label>Deny list — always block</label>
Deny list — always block
</label>
<textarea name="deny_list" rows="4" <textarea name="deny_list" rows="4"
placeholder="shell_exec&#10;file_write" placeholder="shell_exec&#10;file_write"
autocomplete="off" spellcheck="false" autocomplete="off" spellcheck="false">{{ tool_deny }}</textarea>
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border); <p class="hint">These tools are always blocked regardless of risk policy.</p>
border-radius:0.375rem; color:var(--pg-text); padding:0.45rem 0.65rem;
font-size:0.82rem; font-family:monospace; resize:vertical;">{{ tool_deny }}</textarea>
<p class="policy-note">These tools are always blocked regardless of risk policy.</p>
</div> </div>
</div> </div>
</div> </div>