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="text" name="new_name" value="{p}"
pattern="[a-z_][a-z0-9_\\-]{{0,31}}" required>
<button type="submit">Save</button>
<button type="button" class="persona-rename-cancel">Cancel</button>
<button type="submit" class="btn-save">Save</button>
<button type="button" class="btn-cancel persona-rename-cancel">Cancel</button>
</form>
</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():
# Category header spanning all columns
escaped_cat = _html.escape(category)
rows.append(
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>'
)
rows.append(f'<tr class="tool-cat-row"><td colspan="4">{escaped_cat}</td></tr>')
for tool in tools:
risk = TOOL_RISK.get(tool, "medium")
risk_cls = f"risk-{risk}"

View File

@@ -8,56 +8,10 @@
<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">
<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>
<style>
: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;
}
[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; }
.page { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
/* ── Header ── */
header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--pg-border); }
@@ -134,15 +88,16 @@
</style>
</head>
<body>
<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>
<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">
<header>
<h1>Help &amp; Reference</h1>
<p id="persona-label"></p>

View File

@@ -7,204 +7,53 @@
<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>
: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);
/* ── Test action buttons ── */
.test-btn-row { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.test-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;
}
[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: 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-btn:hover { border-color: var(--pg-action); color: var(--pg-accent); }
.test-btn:disabled { opacity: 0.5; cursor: default; }
.test-result {
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
border-radius: 6px;
font-size: 0.82rem;
line-height: 1.5;
display: none;
margin-top: 0.75rem; padding: 0.6rem 0.8rem; 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.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 blocks */
/* ── Channel config collapsible blocks ── */
details.channel-block {
border: 1px solid var(--pg-border);
border-radius: 8px;
margin-bottom: 0.75rem;
overflow: hidden;
border: 1px solid var(--pg-border); border-radius: 8px;
margin-bottom: 0.75rem; overflow: hidden;
}
details.channel-block summary {
padding: 0.75rem 1rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--pg-muted);
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5rem;
user-select: none;
background: var(--pg-bg);
padding: 0.75rem 1rem; font-size: 0.85rem; font-weight: 600;
color: var(--pg-muted); 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::before {
content: '▶';
font-size: 0.65rem;
color: var(--pg-dimmer);
transition: transform 0.15s;
flex-shrink: 0;
content: '▶'; font-size: 0.65rem; 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 { border-bottom: 1px solid var(--pg-border); }
.channel-block-body {
padding: 1rem 1rem 0.25rem;
}
.channel-block-body { padding: 1rem 1rem 0.25rem; }
.channel-hint {
font-size: 0.75rem;
color: var(--pg-dimmer);
margin-top: -0.6rem;
margin-bottom: 1rem;
line-height: 1.5;
font-size: 0.75rem; color: var(--pg-dimmer);
margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
}
</style>
</head>
<body>
<div class="card">
<nav class="page-nav">
<a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ help_href }}" class="nav-link">Help</a>
@@ -214,11 +63,9 @@
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="logo">
<h1>Notifications</h1>
<p>How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
</div>
<div class="page-wrap">
<h1 class="page-title">Notifications</h1>
<p class="page-subtitle">How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
<!-- SUCCESS -->
<!-- ERROR -->
@@ -254,7 +101,7 @@
<!-- Nextcloud Talk -->
<div class="section">
<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.
<strong>Sending</strong> requires the bot URL, secret, and notification room.
<strong>Reading history</strong> (<code>nc_talk_history</code> tool) additionally
@@ -266,7 +113,7 @@
<div class="channel-block-body">
<p class="channel-hint">
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>
<div class="field">
<label for="nc_url">Nextcloud URL</label>
@@ -322,7 +169,7 @@
<!-- Home Assistant -->
<div class="section">
<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
(read states, control devices). Webhook ID is the shared secret used in your
HA <code>rest_command</code> URL.
@@ -374,7 +221,7 @@
<!-- Google Chat -->
<div class="section">
<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.
Incoming messages are handled separately via the Google Chat Add-on.
</p>
@@ -383,8 +230,7 @@
<summary>Outbound webhook</summary>
<div class="channel-block-body">
<p class="channel-hint">
Create a webhook in your Google Chat space → Manage webhooks.
Paste the full URL here.
Create a webhook in your Google Chat space → Manage webhooks. Paste the full URL here.
</p>
<div class="field">
<label for="gc_outbound_webhook">Webhook URL</label>
@@ -397,19 +243,19 @@
</details>
</div>
<button type="submit">Save notification settings</button>
<button type="submit" class="btn-submit">Save notification settings</button>
</form>
<!-- Test -->
<div class="section" style="margin-top:2rem;">
<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
immediately — no need to wait for the daily 09:00 scheduler job.
</p>
<div class="btn-row">
<button class="btn" id="btn-test-notify">Send Test Notification</button>
<button class="btn" id="btn-check-reminders">Check Reminders Now</button>
<div class="test-btn-row">
<button class="test-btn" id="btn-test-notify">Send Test Notification</button>
<button class="test-btn" id="btn-check-reminders">Check Reminders Now</button>
</div>
<div class="test-result" id="test-result"></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,259 +7,81 @@
<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>
: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);
}
[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;
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.persona-list li {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
list-style: none; display: flex; 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-link {
display: inline-block;
padding: 0.3rem 0.75rem;
background: var(--pg-bg);
border: 1px solid var(--pg-border);
border-radius: 20px;
color: #a78bfa;
font-size: 0.85rem;
text-decoration: none;
transition: border-color 0.15s;
display: inline-block; padding: 0.3rem 0.75rem;
background: var(--pg-bg); border: 1px solid var(--pg-border);
border-radius: 20px; color: var(--pg-accent); 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-rename-toggle {
background: none;
border: none;
color: var(--pg-muted);
font-size: 0.85rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s;
background: none; border: 1px solid var(--pg-border);
border-radius: 6px; color: var(--pg-muted); font-size: 0.8rem;
padding: 0.3rem 0.6rem; margin-top: 0.25rem;
cursor: pointer; 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 input[type="text"] {
width: 12rem;
padding: 0.3rem 0.6rem;
background: var(--pg-bg);
border: 1px solid #7c3aed;
border-radius: 6px;
color: var(--pg-text);
font-size: 0.9rem;
outline: none;
width: 12rem; padding: 0.3rem 0.6rem;
border-color: var(--pg-action); font-size: 0.9rem;
}
.persona-rename-form button[type="submit"] {
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); }
.persona-rename-form .btn-save { padding: 0.3rem 0.75rem; font-size: 0.85rem; }
.add-persona {
display: inline-block;
margin-top: 0.75rem;
font-size: 0.8rem;
color: var(--pg-muted);
text-decoration: none;
display: inline-block; margin-top: 0.75rem;
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 {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
display: inline-block; padding: 0.25rem 0.75rem;
border-radius: 20px; font-size: 0.78rem; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.06em;
}
.role-badge.role-admin {
background: rgba(124, 58, 237, 0.15);
color: #a78bfa;
background: rgba(124,58,237,0.15); color: var(--pg-accent);
border: 1px solid rgba(124,58,237,0.4);
}
.role-badge.role-user {
background: rgba(100, 116, 139, 0.12);
color: var(--pg-muted);
background: rgba(100,116,139,0.12); color: var(--pg-muted);
border: 1px solid var(--pg-border);
}
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.88rem;
font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace;
line-height: 1.55;
resize: vertical;
outline: none;
transition: border-color 0.15s;
/* ── OpenRouter quickstart warning card ── */
#openrouter-quickstart {
display: none; background: #1c1a0a; border: 1px solid #78350f;
border-radius: 8px; padding: 1rem; margin-bottom: 1rem;
}
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>
</head>
<body>
<div class="card">
<nav class="page-nav">
<a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ help_href }}" class="nav-link">Help</a>
@@ -269,11 +91,9 @@
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="logo">
<h1>Account Settings</h1>
<p>Manage your account and personas.</p>
</div>
<div class="page-wrap">
<h1 class="page-title">Account Settings</h1>
<p class="page-subtitle">Manage your account and personas.</p>
<!-- SUCCESS -->
<!-- ERROR -->
@@ -289,26 +109,21 @@
<label>Role</label>
<span class="role-badge role-{{ user_role }}">{{ user_role }}</span>
</div>
<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;">
<button type="button" id="show-rename-user" class="persona-rename-toggle">
✏ Change username
</button>
<form id="rename-user-form" method="POST" action="/settings/username"
style="display:none; margin-top:0.75rem;">
<form id="rename-user-form" method="POST" action="/settings/username" style="display:none; margin-top:0.75rem;">
<div class="field">
<label for="new_username">New username</label>
<input type="text" id="new_username" name="new_username"
value="{{ username }}"
pattern="[a-z_][a-z0-9_\-]{0,31}" required autofocus
autocomplete="off" data-form-type="other">
<p style="font-size:0.75rem; color:var(--pg-muted); margin-top:0.3rem;">
Lowercase letters, digits, _ or - only. You will be logged out after renaming.
</p>
<p class="hint">Lowercase letters, digits, _ or - only. You will be logged out after renaming.</p>
</div>
<div style="display:flex; gap:0.5rem;">
<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="button" id="cancel-rename-user"
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 class="btn-row">
<button type="submit" class="btn-save">Save</button>
<button type="button" id="cancel-rename-user" class="btn-cancel">Cancel</button>
</div>
</form>
</div>
@@ -322,18 +137,15 @@
placeholder="No Google account linked"
style="{{ google_email == '' and 'color:var(--pg-dimmer)' or '' }}">
</div>
<p style="font-size:0.75rem; color:var(--pg-muted); margin-top:-0.5rem;">
To link or change your Google account, contact Scott.
</p>
<p class="hint" style="margin-top:-0.5rem;">To link or change your Google account, contact Scott.</p>
</div>
<!-- Email Allowlist -->
<div class="section">
<h2>Email Allowlist</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
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>
tool will only send to addresses that match at least one pattern.
Leave blank to block all outbound email.
<p class="section-note">
One regex pattern per line. The <code>email_send</code> tool will only send to addresses
that match at least one pattern. Leave blank to block all outbound email.
</p>
<form method="POST" action="/settings/email-allowlist">
<div class="field">
@@ -342,17 +154,16 @@
placeholder=".*@example\.com&#10;alice@example\.com"
spellcheck="false">{{ email_allowlist }}</textarea>
</div>
<button type="submit">Save allowlist</button>
<button type="submit" class="btn-submit">Save allowlist</button>
</form>
</div>
<!-- HTTP POST Allowlist -->
<div class="section">
<h2>HTTP POST Allowlist</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
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>
tool will only POST to URLs that start with a listed prefix.
Leave blank to block all outbound POST requests.
<p class="section-note">
One URL prefix per line. The <code>http_post</code> tool will only POST to URLs that
start with a listed prefix. Leave blank to block all outbound POST requests.
</p>
<form method="POST" action="/settings/http-allowlist">
<div class="field">
@@ -361,103 +172,73 @@
placeholder="https://ha.dgrzone.com/api/webhook/&#10;https://n8n.dgrzone.com/webhook/"
spellcheck="false">{{ http_allowlist }}</textarea>
</div>
<button type="submit">Save allowlist</button>
<button type="submit" class="btn-submit">Save allowlist</button>
</form>
</div>
<!-- Notifications -->
<div class="section">
<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.
</p>
<a href="/settings/notifications"
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>
<a href="/settings/notifications" class="action-link">Notification settings →</a>
</div>
<!-- Tool Permissions → moved to /settings/tools -->
<!-- Tool Permissions → /settings/tools -->
<div class="section">
<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.
</p>
<a href="/settings/tools"
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>
<a href="/settings/tools" class="action-link">Tool settings →</a>
</div>
<!-- Browser cache -->
<!-- Usage summary -->
<div class="section" id="usage-section">
<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).
Claude CLI calls are not metered.
</p>
<div id="usage-table-wrap" style="overflow-x:auto;">
<p style="font-size:0.8rem; color:var(--pg-muted);">Loading…</p>
<div id="usage-table-wrap" class="usage-wrap">
<p class="section-note">Loading…</p>
</div>
</div>
<!-- Browser Cache -->
<div class="section">
<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,
theme, font size, and context tier. Does not sign you out.
</p>
<button type="button" id="clear-ls-btn"
style="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;">
Clear browser cache
</button>
<span id="clear-ls-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;">
Cleared.
</span>
<button type="button" id="clear-ls-btn" class="btn-secondary">Clear browser cache</button>
<span id="clear-ls-ok" class="result-ok">Cleared.</span>
</div>
<!-- Model Registry link -->
<!-- Model Registry -->
<div class="section">
<h2>Model Registry</h2>
<!-- Quick-start card: shown only when no model is configured for chat role -->
<div id="openrouter-quickstart" style="display:none; background:#1c1a0a; border:1px solid #78350f;
border-radius:8px; padding:1rem; margin-bottom:1rem;">
<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;">
<div id="openrouter-quickstart">
<p class="qs-title">⚡ You're on the server default model</p>
<p class="qs-body">
You can chat now, but adding your own model gives you more choices, lets you pick
role-specific models, and tracks your usage separately.
OpenRouter is the easiest way to get started — one key, many models.
</p>
<a href="/setup/model"
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>
<a href="/setup/model" class="action-link action-link-amber">Set up OpenRouter →</a>
</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.),
and assign models to roles — chat, orchestrator, distill, and more.
</p>
<a href="/settings/models"
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>
<a href="/settings/models" class="action-link">Manage models →</a>
</div>
<!-- Change password -->
<!-- Change Password -->
<div class="section">
<h2>Change Password</h2>
<form method="POST" action="/settings/password" id="password-form">
@@ -476,27 +257,22 @@
<input type="password" id="confirm_password" name="confirm_password"
autocomplete="new-password" required>
</div>
<button type="submit">Update password</button>
<button type="submit" class="btn-submit">Update password</button>
</form>
</div>
<!-- Personas -->
<!-- Sessions -->
<div class="section">
<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.
Only unnamed sessions are affected — existing names are left alone.
</p>
<button type="button" id="backfill-names-btn"
style="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;">
Auto-name old sessions
</button>
<span id="backfill-names-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;"></span>
<button type="button" id="backfill-names-btn" class="btn-secondary">Auto-name old sessions</button>
<span id="backfill-names-ok" class="result-ok"></span>
</div>
<!-- Personas -->
<div class="section">
<h2>Personas</h2>
<ul class="persona-list">
@@ -528,16 +304,6 @@
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)
document.getElementById('clear-ls-btn').addEventListener('click', () => {
localStorage.clear();
@@ -548,8 +314,7 @@
(async () => {
try {
const d = await fetch('/backend').then(r => r.json());
const roles = d.available_roles || [];
if (roles.length === 0) {
if ((d.available_roles || []).length === 0) {
document.getElementById('openrouter-quickstart').style.display = 'block';
}
} catch (_) {}
@@ -563,7 +328,7 @@
if (!resp.ok) throw new Error(resp.statusText);
const rows_data = await resp.json();
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;
}
const fmt = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
@@ -572,27 +337,22 @@
? `<span title="${d.key}">${d.label}</span>`
: `<span>${d.key}</span>`;
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 style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${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 style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${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>${labelCell}</td>
<td>${d.calls}</td>
<td>${fmt(d.prompt_tokens)}</td>
<td>${fmt(d.completion_tokens)}</td>
<td>${fmt(d.total_tokens)}</td>
</tr>`;
}).join('');
wrap.innerHTML = `<table style="border-collapse:collapse; width:100%; min-width:360px;">
<thead>
<tr style="border-bottom:1px solid var(--pg-border);">
<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 style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Calls</th>
<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>
wrap.innerHTML = `<table class="usage-table">
<thead><tr>
<th style="text-align:left">Model</th>
<th>Calls</th><th>Prompt</th><th>Output</th><th>Total</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
} 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 name="viewport" content="width=device-width, initial-scale=1.0">
<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>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
: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 cards (bordered sections on tools page) ── */
.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;
}
.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-note { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.35rem; line-height: 1.5; }
select, input[type="text"] {
background: var(--pg-bg); border: 1px solid var(--pg-border);
border-radius: 0.375rem; color: var(--pg-text);
/* Compact selects and inputs inside policy cards */
.policy-card select, .policy-card input[type="text"] {
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 {
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;
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; }
/* ── Tool table ── */
.section-head {
.table-section-label {
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--pg-dimmer);
margin: 1.75rem 0 0.6rem;
}
.tool-table {
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;
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 tr:last-child td { border-bottom: none; }
.tool-table tr:hover td { background: rgba(124,58,237,0.04); }
.tool-name { font-family: monospace; font-size: 0.82rem; }
/* Risk badges */
.risk { display: inline-block; font-size: 0.7rem; font-weight: 700;
padding: 0.15rem 0.45rem; border-radius: 9999px; letter-spacing: 0.04em; }
.risk-low { background: rgba(34,197,94,0.15); color: #16a34a; }
.risk-medium { background: rgba(234,179, 8,0.15); color: #ca8a04; }
.risk-high { background: rgba(239,68, 68,0.15); color: #dc2626; }
@media (prefers-color-scheme: dark) {
.risk-low { background: rgba(34,197,94,0.12); color: #4ade80; }
.risk-medium { background: rgba(234,179,8,0.12); color: #fbbf24; }
.risk-high { background: rgba(239,68,68,0.12); color: #f87171; }
}
[data-theme="light"] .risk-low { background: rgba(34,197,94,0.15); color: #16a34a; }
[data-theme="light"] .risk-medium { background: rgba(234,179,8,0.15); color: #ca8a04; }
[data-theme="light"] .risk-high { background: rgba(239,68,68,0.15); color: #dc2626; }
/* Auto status pill */
.auto-pill {
display: inline-block; font-size: 0.68rem; font-weight: 600;
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); }
@media (prefers-color-scheme: dark) {
.auto-on { color: #a78bfa; }
}
[data-theme="light"] .auto-on { color: #7c3aed; }
/* Override select */
.override-sel {
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-off { border-color: #dc2626; color: #dc2626; }
@@ -129,7 +95,7 @@
/* Legend */
.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.on { background: #7c3aed; }
.legend-dot.on { background: #a78bfa; }
.legend-dot.off { background: var(--pg-dimmer); }
</style>
</head>
@@ -146,8 +112,8 @@
</nav>
<div class="page-wrap">
<h1>Tool Settings</h1>
<p class="page-lead">
<h1 class="page-title">Tool Settings</h1>
<p class="page-subtitle">
Control which orchestrator tools are available. The risk level sets an automatic threshold;
whitelist and blacklist let you fine-tune individual tools beyond that.
</p>
@@ -157,10 +123,9 @@
<form method="POST" action="/settings/tools" id="tools-form">
<!-- ── Risk policy ── -->
<!-- Risk policy -->
<div class="policy-card">
<h2>Risk Policy</h2>
<div class="policy-row">
<span class="policy-label">Max risk level</span>
<select name="max_risk" id="max-risk-sel">
@@ -175,53 +140,42 @@
<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).
</p>
<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.
Use the override column to force-include or force-exclude individual tools.
</p>
</div>
<!-- ── Legend ── -->
<!-- Legend -->
<div class="legend">
<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>
</div>
<!-- ── Tool table ── -->
<!-- Tool table -->
{{ tool_table_html }}
<!-- ── Confirmation gate ── -->
<!-- Confirmation gate -->
<div class="policy-card" style="margin-top:1.75rem;">
<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>
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>
<div class="policy-row" style="align-items:flex-start; gap:1.5rem; flex-wrap:wrap;">
<div style="flex:1; min-width:200px;">
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;">
Allow list — bypass confirmation
</label>
<div class="col-split" style="margin-top:0.85rem;">
<div class="col-half">
<label>Allow list — bypass confirmation</label>
<textarea name="allow_list" rows="4"
placeholder="reminders_clear&#10;cron_remove"
autocomplete="off" spellcheck="false"
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border);
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>
autocomplete="off" spellcheck="false">{{ tool_allow }}</textarea>
<p class="hint">One tool name per line. These tools skip the confirmation prompt.</p>
</div>
<div style="flex:1; min-width:200px;">
<label style="display:block; font-size:0.8rem; font-weight:600; margin-bottom:0.35rem;">
Deny list — always block
</label>
<div class="col-half">
<label>Deny list — always block</label>
<textarea name="deny_list" rows="4"
placeholder="shell_exec&#10;file_write"
autocomplete="off" spellcheck="false"
style="width:100%; background:var(--pg-bg); border:1px solid var(--pg-border);
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>
autocomplete="off" spellcheck="false">{{ tool_deny }}</textarea>
<p class="hint">These tools are always blocked regardless of risk policy.</p>
</div>
</div>
</div>