feat: custom roles, Tailwind settings pages, pg.css fixes, doc cleanup
Model Registry: - Add per-user custom roles (add/remove via UI); required roles chat/orchestrator/distill are always present and cannot be removed - Auto-migrate legacy .env-defined roles to custom_roles on first access - Role config panel (gear): Remove role button moved inside panel; required badge below name - Role select: Primary + Backup slots only (was three) Settings pages — Tailwind CSS migration (CDN, preflight: false): - local_llm.html, settings.html, help.html, notifications.html, tools_settings.html, crons.html, integrations.html all migrated; pg-* color tokens; dark/light via data-theme pg.css fixes: - input[type=checkbox/radio]: width: auto — prevents pg.css width:100% from stretching checkboxes - btn-submit: responsive sizing via Tailwind w-full md:w-96 on each button (no longer full-width on desktop) Documentation: - MASTER.md, TODO__Agents.md: remove "/ Inara" from titles; description updated to "per-user AI personas" - HELP.md: persona-agnostic language throughout (NC Talk, Google Chat, push, schedules, HA sections); roles section restructured to show required vs. custom roles with examples - notifications.html: subtitle and HA description use "your persona" not "Inara" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -268,17 +268,24 @@ The label and context window size auto-fill from the catalog — edit them if yo
|
||||
|
||||
### Step 3 — Assign models to roles
|
||||
|
||||
Scroll to **Role Assignments** at the bottom of the page. Each role has **Primary**, **Backup 1**, and **Backup 2** slots — Primary is tried first, then backups in order. Changes save automatically.
|
||||
Scroll to **Role Assignments** at the bottom of the page. Each role has **Primary** and **Backup** slots — Primary is tried first, then backup. Changes save automatically.
|
||||
|
||||
**Required roles** (always present, cannot be removed):
|
||||
|
||||
| Role | Used for |
|
||||
|---|---|
|
||||
| **Chat** | Regular conversation |
|
||||
| **Orchestrator** | Agent mode tool loop |
|
||||
| **Distill** | Memory distillation (short / mid / long) |
|
||||
| **Coder** | Code-focused tasks |
|
||||
| **Research** | Long-context research tasks |
|
||||
|
||||
Leave all slots empty to use the server default.
|
||||
**Custom roles** — Click **+ Add custom role** to create your own. Each custom role gets its own model selection, tool set, and system prompt addition. Good examples:
|
||||
|
||||
| Example | Purpose |
|
||||
|---|---|
|
||||
| **Coder** | Code-focused tasks — larger context window, code-aware model |
|
||||
| **Research** | Long-context research — high-token model, web tools prioritized |
|
||||
|
||||
Switch roles via the **Role** selector in the Context & Memory panel (⚙). Leave all slots empty to use the server default.
|
||||
|
||||
**Per-role tool sets:** Expand any role card to configure which tool categories the orchestrator can use when that role is active. Unchecked categories are hidden from the model entirely — reducing token overhead on every orchestrated call. Leaving all categories unchecked means all tools the user has access to are available (the default).
|
||||
|
||||
@@ -288,7 +295,7 @@ Leave all slots empty to use the server default.
|
||||
|
||||
## Nextcloud Talk Bot
|
||||
|
||||
Inara is registered as a bot in Nextcloud Talk.
|
||||
The Cortex bot is registered in Nextcloud Talk.
|
||||
|
||||
- Messages sent in enabled Talk conversations are received by Cortex, processed, and replied to.
|
||||
- The webhook returns `200 OK` immediately; the reply happens asynchronously.
|
||||
@@ -299,12 +306,12 @@ Inara is registered as a bot in Nextcloud Talk.
|
||||
|
||||
## Google Chat Bot
|
||||
|
||||
Inara is available as a bot in Google Chat (One Sky IT Workspace).
|
||||
The Cortex bot is available in Google Chat (One Sky IT Workspace).
|
||||
|
||||
- Send Inara a direct message in Google Chat to start a conversation.
|
||||
- Send the bot a direct message in Google Chat to start a conversation.
|
||||
- Each DM thread is its own session (`gc_spaces/*` prefix) — history persists across messages.
|
||||
- Responses are synchronous — Google Chat displays the reply directly in the thread.
|
||||
- To add Inara to a space: open the space, add a person/app, search for **Inara**.
|
||||
- To add the bot to a space: open the space, click **Add people & apps**, and search for the Cortex bot.
|
||||
- Sessions from Google Chat appear as `gc_*` prefixed IDs in the Sessions panel.
|
||||
|
||||
---
|
||||
@@ -339,9 +346,9 @@ Cortex can send browser push notifications — even when the tab is closed.
|
||||
- Open **☰ → Enable notifications** and accept the browser permission prompt.
|
||||
- Once enabled, the button shows **Notifications on** (in accent colour).
|
||||
- Click again to disable. Subscriptions are stored per-device.
|
||||
- The orchestrator's `web_push` tool lets Inara send you a push proactively (e.g. when a long task completes).
|
||||
- The orchestrator's `web_push` tool lets your persona send you a push proactively (e.g. when a long task completes).
|
||||
|
||||
**Notification channel settings:** ☰ → **Account** → **Notification settings →** — choose Browser Push, Email, Nextcloud Talk, or Google Chat as the channel Inara uses for scheduled reminders, cron job completions, and memory digests. Use the **Send Test Notification** button to verify your setup, or **Check Reminders Now** to trigger the reminder check immediately.
|
||||
**Notification channel settings:** ☰ → **Account** → **Notification settings →** — choose Browser Push, Email, Nextcloud Talk, or Google Chat as the channel your persona uses for scheduled reminders, cron job completions, and memory digests. Use the **Send Test Notification** button to verify your setup, or **Check Reminders Now** to trigger the reminder check immediately.
|
||||
|
||||
---
|
||||
|
||||
@@ -389,7 +396,7 @@ Distillation builds up the memory layers from raw session logs. Runs automatical
|
||||
|
||||
## Scheduled Jobs
|
||||
|
||||
Cortex can run recurring jobs on a schedule — reminders, daily briefings, automated research, and more. Manage them by asking Inara to set them up, or go directly to **☰ → Account → Schedules**.
|
||||
Cortex can run recurring jobs on a schedule — reminders, daily briefings, automated research, and more. Manage them by asking your persona to set them up, or go directly to **☰ → Account → Schedules**.
|
||||
|
||||
### Job Types
|
||||
|
||||
@@ -424,7 +431,7 @@ Schedules take effect immediately when added or edited — no restart needed. Pa
|
||||
|
||||
### Home Assistant Integration
|
||||
|
||||
HA automations can trigger Inara via webhook. Configure in **Notifications → Home Assistant → Inbound webhook**:
|
||||
HA automations can trigger your persona via webhook. Configure in **Notifications → Home Assistant → Inbound webhook**:
|
||||
|
||||
- Set a **Webhook ID** (long random string — this is your secret URL component)
|
||||
- Your endpoint: `https://cortex.dgrzone.com/webhook/ha/{username}/{webhook_id}`
|
||||
|
||||
@@ -7,9 +7,36 @@
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
/* ── Server-generated table + badges ── */
|
||||
.cron-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 0.82rem;
|
||||
margin-bottom: 1.5rem;
|
||||
@@ -47,15 +74,14 @@
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-cron:hover { border-color: var(--pg-accent); color: var(--pg-accent); }
|
||||
.btn-cron-del:hover { border-color: var(--pg-danger, #ef4444); color: var(--pg-danger, #ef4444); }
|
||||
.btn-cron-del { color: var(--pg-dimmer); }
|
||||
.btn-cron-del:hover { border-color: #ef4444; color: #ef4444; }
|
||||
|
||||
.payload-cell {
|
||||
max-width: 240px; overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; color: var(--pg-dimmer);
|
||||
}
|
||||
|
||||
.persona-group { margin-bottom: 0.25rem; }
|
||||
.persona-group-label {
|
||||
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.06em; color: var(--pg-dimmer); margin: 1.25rem 0 0.5rem;
|
||||
@@ -67,11 +93,6 @@
|
||||
border: 1px dashed var(--pg-border); border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.add-form-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 0 0.75rem;
|
||||
}
|
||||
.add-form-grid .field-full { grid-column: 1 / -1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -79,6 +100,7 @@
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link active">Schedules</a>
|
||||
@@ -99,11 +121,11 @@
|
||||
<!-- Cron list -->
|
||||
{{ cron_list_html }}
|
||||
|
||||
<!-- Add new cron -->
|
||||
<!-- Add new schedule -->
|
||||
<div class="section">
|
||||
<h2>Add schedule</h2>
|
||||
<form method="POST" action="/settings/crons/add">
|
||||
<div class="add-form-grid">
|
||||
<div class="grid grid-cols-2 gap-x-3">
|
||||
<div class="field">
|
||||
<label for="add_persona">Persona</label>
|
||||
<select id="add_persona" name="persona">
|
||||
@@ -136,13 +158,13 @@
|
||||
monthly · monthly:DD · monthly:DD:HH:MM · yearly:MM:DD · yearly:MM:DD:HH:MM
|
||||
</p>
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<div class="field col-span-2">
|
||||
<label for="add_payload">Payload / prompt</label>
|
||||
<textarea id="add_payload" name="payload" rows="3"
|
||||
placeholder="Check my open tasks and send a summary." required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Add schedule</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Add schedule</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,38 +8,40 @@
|
||||
<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>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
.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); }
|
||||
header h1 { font-size: 1.5rem; font-weight: 700; color: var(--pg-accent); }
|
||||
header p { font-size: 0.85rem; color: var(--pg-muted); margin-top: 0.25rem; }
|
||||
|
||||
/* ── Tabs ── */
|
||||
.tab-bar {
|
||||
display: flex; gap: 0.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--pg-border);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 0.45rem 1rem;
|
||||
font-size: 0.85rem; font-weight: 500;
|
||||
color: var(--pg-dim);
|
||||
background: none; border: none; border-bottom: 2px solid transparent;
|
||||
cursor: pointer; transition: color 0.15s, border-color 0.15s;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tab-btn:hover { color: var(--pg-bright); }
|
||||
.tab-btn.active { color: var(--pg-accent); border-bottom-color: var(--pg-accent); }
|
||||
|
||||
/* ── Tab panels (JS-toggled display) ── */
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
/* ── Content ── */
|
||||
/* ── Dynamically-rendered markdown content ── */
|
||||
.help-body { line-height: 1.7; }
|
||||
|
||||
details {
|
||||
@@ -83,8 +85,6 @@
|
||||
.help-body pre { background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 6px; padding: 0.75rem 1rem; overflow-x: auto; margin: 0.5rem 0; }
|
||||
.help-body pre code { background: none; border: none; padding: 0; font-size: 0.85em; color: var(--pg-muted); }
|
||||
.help-body hr { border: none; border-top: 1px solid var(--pg-border); margin: 0.5rem 0; }
|
||||
|
||||
.empty-state { color: var(--pg-dim); font-size: 0.9rem; padding: 2rem 0; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -92,6 +92,7 @@
|
||||
<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/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
@@ -99,23 +100,33 @@
|
||||
<span class="nav-spacer"></span>
|
||||
<a href="/logout" class="nav-link nav-logout">Sign out</a>
|
||||
</nav>
|
||||
<div class="page">
|
||||
<header>
|
||||
<h1>Help & Reference</h1>
|
||||
<p id="persona-label"></p>
|
||||
</header>
|
||||
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn active" data-tab="ui">UI Guide</button>
|
||||
<button class="tab-btn" data-tab="tools">Tools</button>
|
||||
<button class="tab-btn" data-tab="persona" id="tab-btn-persona">Persona</button>
|
||||
<div class="max-w-3xl mx-auto px-6 py-8 pb-16">
|
||||
<div class="mb-6 pb-4 border-b border-pg-border">
|
||||
<h1 class="text-xl font-bold text-pg-accent">Help & Reference</h1>
|
||||
<p id="persona-label" class="text-xs text-pg-muted mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div id="tab-ui" class="tab-panel active"><div class="help-body"><p class="empty-state">Loading…</p></div></div>
|
||||
<div id="tab-tools" class="tab-panel"> <div class="help-body"><p class="empty-state">Loading…</p></div></div>
|
||||
<div id="tab-persona" class="tab-panel"> <div class="help-body"><p class="empty-state">Loading…</p></div></div>
|
||||
<!-- Tab bar -->
|
||||
<div class="flex gap-1 mb-5 border-b border-pg-border -mb-px">
|
||||
<button class="tab-btn px-4 py-2 text-sm font-medium text-pg-dim border-b-2 border-transparent -mb-px cursor-pointer transition-colors hover:text-pg-bright active"
|
||||
data-tab="ui">UI Guide</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm font-medium text-pg-dim border-b-2 border-transparent -mb-px cursor-pointer transition-colors hover:text-pg-bright"
|
||||
data-tab="tools">Tools</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm font-medium text-pg-dim border-b-2 border-transparent -mb-px cursor-pointer transition-colors hover:text-pg-bright"
|
||||
data-tab="persona" id="tab-btn-persona">Persona</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-ui" class="tab-panel active"><div class="help-body"><p class="text-pg-dimmer text-sm text-center py-8">Loading…</p></div></div>
|
||||
<div id="tab-tools" class="tab-panel"> <div class="help-body"><p class="text-pg-dimmer text-sm text-center py-8">Loading…</p></div></div>
|
||||
<div id="tab-persona" class="tab-panel"> <div class="help-body"><p class="text-pg-dimmer text-sm text-center py-8">Loading…</p></div></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Active tab indicator — must be CSS since Tailwind can't match .active sibling state */
|
||||
.tab-btn.active { color: var(--pg-accent); border-bottom-color: var(--pg-accent); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const cfg = window.HELP_CONFIG || {};
|
||||
const user = cfg.user || 'scott';
|
||||
@@ -186,13 +197,13 @@
|
||||
fetch('/static/HELP.md')
|
||||
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
||||
.then(md => render('tab-ui', md, false, UI_OPEN))
|
||||
.catch(e => { document.querySelector('#tab-ui .help-body').innerHTML = `<p class="empty-state">Failed to load: ${e}</p>`; });
|
||||
.catch(e => { document.querySelector('#tab-ui .help-body').innerHTML = `<p class="text-pg-dimmer text-sm text-center py-8">Failed to load: ${e}</p>`; });
|
||||
|
||||
// Tools
|
||||
fetch('/static/TOOLS.md')
|
||||
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
||||
.then(md => render('tab-tools', md, true, null))
|
||||
.catch(e => { document.querySelector('#tab-tools .help-body').innerHTML = `<p class="empty-state">Failed to load: ${e}</p>`; });
|
||||
.catch(e => { document.querySelector('#tab-tools .help-body').innerHTML = `<p class="text-pg-dimmer text-sm text-center py-8">Failed to load: ${e}</p>`; });
|
||||
|
||||
// Persona-specific HELP.md
|
||||
const personaPanel = document.querySelector('#tab-persona .help-body');
|
||||
@@ -204,13 +215,13 @@
|
||||
if (content) {
|
||||
render('tab-persona', content, true, null);
|
||||
} else {
|
||||
personaPanel.innerHTML = `<p class="empty-state">No ${persona}-specific notes yet. Edit <code>HELP.md</code> in the Files panel to add them.</p>`;
|
||||
personaPanel.innerHTML = `<p class="text-pg-dimmer text-sm text-center py-8">No ${persona}-specific notes yet. Edit <code>HELP.md</code> in the Files panel to add them.</p>`;
|
||||
}
|
||||
} else {
|
||||
personaPanel.innerHTML = `<p class="empty-state">No ${persona}-specific notes yet.</p>`;
|
||||
personaPanel.innerHTML = `<p class="text-pg-dimmer text-sm text-center py-8">No ${persona}-specific notes yet.</p>`;
|
||||
}
|
||||
} catch (_) {
|
||||
personaPanel.innerHTML = `<p class="empty-state">No ${persona}-specific notes yet.</p>`;
|
||||
personaPanel.innerHTML = `<p class="text-pg-dimmer text-sm text-center py-8">No ${persona}-specific notes yet.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,35 @@
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
details.channel-block {
|
||||
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);
|
||||
}
|
||||
details.channel-block summary::-webkit-details-marker { display: none; }
|
||||
details.channel-block summary::before {
|
||||
content: '▶'; font-size: 0.65rem; color: var(--pg-dimmer);
|
||||
@@ -27,16 +43,6 @@
|
||||
}
|
||||
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-hint {
|
||||
font-size: 0.75rem; color: var(--pg-dimmer);
|
||||
margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
|
||||
}
|
||||
.field-row {
|
||||
display: grid; grid-template-columns: 1fr auto; gap: 0.75rem; align-items: end;
|
||||
}
|
||||
.field-row .field { margin-bottom: 0; }
|
||||
.field-narrow input { max-width: 120px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -44,6 +50,7 @@
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
@@ -60,7 +67,6 @@
|
||||
|
||||
<form method="POST" action="/settings/integrations">
|
||||
|
||||
<!-- Aether Platform Database -->
|
||||
<div class="section">
|
||||
<h2>Aether Platform Database</h2>
|
||||
<p class="section-note">
|
||||
@@ -69,14 +75,18 @@
|
||||
Only SELECT, SHOW, DESCRIBE, and EXPLAIN are permitted — no writes possible.
|
||||
</p>
|
||||
|
||||
<details class="channel-block" {{ ae_db_host and 'open' or '' }}>
|
||||
<summary>Connection</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
Use the same credentials as <code>agents_sync/mcp/scripts/sql_inspector.py</code>.
|
||||
The password field is left blank in the form — leave it blank to keep the stored value.
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ ae_db_host and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
Connection
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer mb-4 -mt-1 leading-relaxed">
|
||||
Use the same credentials as
|
||||
<code class="font-mono text-pg-accent bg-pg-bg border border-pg-border rounded px-1 text-xs">agents_sync/mcp/scripts/sql_inspector.py</code>.
|
||||
Leave the password blank to keep the stored value.
|
||||
</p>
|
||||
<div class="field-row">
|
||||
<div class="grid grid-cols-[1fr_7rem] gap-3 items-start">
|
||||
<div class="field">
|
||||
<label for="ae_db_host">Host</label>
|
||||
<input type="text" id="ae_db_host" name="ae_db_host"
|
||||
@@ -84,7 +94,7 @@
|
||||
placeholder="192.168.64.5"
|
||||
autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="field field-narrow">
|
||||
<div class="field">
|
||||
<label for="ae_db_port">Port</label>
|
||||
<input type="number" id="ae_db_port" name="ae_db_port"
|
||||
value="{{ ae_db_port }}"
|
||||
@@ -117,7 +127,7 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">Save integrations</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Save integrations</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,38 +7,36 @@
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
/* ── 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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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); }
|
||||
|
||||
/* ── Channel config collapsible blocks ── */
|
||||
details.channel-block {
|
||||
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);
|
||||
}
|
||||
/* ── Channel collapsible arrow ── */
|
||||
details.channel-block summary::-webkit-details-marker { display: none; }
|
||||
details.channel-block summary::before {
|
||||
content: '▶'; font-size: 0.65rem; color: var(--pg-dimmer);
|
||||
@@ -46,11 +44,9 @@
|
||||
}
|
||||
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-hint {
|
||||
font-size: 0.75rem; color: var(--pg-dimmer);
|
||||
margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Test result feedback (JS-toggled display) ── */
|
||||
#test-result { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -58,6 +54,7 @@
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link active">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
@@ -67,7 +64,7 @@
|
||||
</nav>
|
||||
<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>
|
||||
<p class="page-subtitle">How your persona reaches out proactively — reminders, cron jobs, and memory digests.</p>
|
||||
|
||||
<!-- SUCCESS -->
|
||||
<!-- ERROR -->
|
||||
@@ -90,8 +87,9 @@
|
||||
<p class="hint">Used for reminder alerts, distillation summaries, and cron job notifications.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="notification_email">Email address override
|
||||
<span style="color:var(--pg-dim); font-weight:400;">(optional)</span>
|
||||
<label for="notification_email">
|
||||
Email address override
|
||||
<span class="font-normal text-pg-dim">(optional)</span>
|
||||
</label>
|
||||
<input type="email" id="notification_email" name="notification_email"
|
||||
value="{{ notify_email_override }}"
|
||||
@@ -110,12 +108,15 @@
|
||||
requires a Nextcloud username and app password.
|
||||
</p>
|
||||
|
||||
<details class="channel-block" {{ nc_url and 'open' or '' }}>
|
||||
<summary>Bot credentials (sending)</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ nc_url and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
Bot credentials (sending)
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer -mt-1 mb-4 leading-relaxed">
|
||||
Set these up in your Nextcloud Talk room → Bot settings.
|
||||
See the <a href="/help" style="color:var(--pg-accent);">setup guide</a> for step-by-step instructions.
|
||||
See the <a href="/help" class="text-pg-accent">setup guide</a> for step-by-step instructions.
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="nc_url">Nextcloud URL</label>
|
||||
@@ -143,10 +144,13 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="channel-block" {{ nc_username and 'open' or '' }}>
|
||||
<summary>API credentials (reading history)</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ nc_username and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
API credentials (reading history)
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer -mt-1 mb-4 leading-relaxed">
|
||||
Required for the <code>nc_talk_history</code> orchestrator tool.
|
||||
Generate an app password in Nextcloud → Settings → Security → App passwords.
|
||||
</p>
|
||||
@@ -172,15 +176,18 @@
|
||||
<div class="section">
|
||||
<h2>Home Assistant</h2>
|
||||
<p class="section-note">
|
||||
Receive events from HA automations and let Inara call the HA REST API
|
||||
Receive events from HA automations and let your persona call the HA REST API
|
||||
(read states, control devices). Webhook ID is the shared secret used in your
|
||||
HA <code>rest_command</code> URL.
|
||||
</p>
|
||||
|
||||
<details class="channel-block" {{ ha_url and 'open' or '' }}>
|
||||
<summary>Connection</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ ha_url and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
Connection
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer -mt-1 mb-4 leading-relaxed">
|
||||
HA URL and a Long-Lived Access Token (Profile → scroll to bottom →
|
||||
Long-Lived Access Tokens → Create Token).
|
||||
</p>
|
||||
@@ -201,10 +208,13 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="channel-block" {{ ha_webhook_id and 'open' or '' }}>
|
||||
<summary>Inbound webhook (HA → Cortex)</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ ha_webhook_id and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
Inbound webhook (HA → Cortex)
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer -mt-1 mb-4 leading-relaxed">
|
||||
The webhook ID is the shared secret in your HA <code>rest_command</code> URL.
|
||||
Your endpoint: <code>https://cortex.dgrzone.com/webhook/ha/{{ ha_username }}/<webhook_id></code>
|
||||
</p>
|
||||
@@ -235,10 +245,13 @@
|
||||
Incoming messages are handled separately via the Google Chat Add-on.
|
||||
</p>
|
||||
|
||||
<details class="channel-block" {{ gc_webhook and 'open' or '' }}>
|
||||
<summary>Outbound webhook</summary>
|
||||
<div class="channel-block-body">
|
||||
<p class="channel-hint">
|
||||
<details class="channel-block border border-pg-border rounded-lg overflow-hidden mb-3"
|
||||
{{ gc_webhook and 'open' or '' }}>
|
||||
<summary class="flex items-center gap-2 px-4 py-3 text-sm font-semibold text-pg-muted cursor-pointer select-none bg-pg-bg">
|
||||
Outbound webhook
|
||||
</summary>
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<p class="text-xs text-pg-dimmer -mt-1 mb-4 leading-relaxed">
|
||||
Create a webhook in your Google Chat space → Manage webhooks. Paste the full URL here.
|
||||
</p>
|
||||
<div class="field">
|
||||
@@ -252,7 +265,7 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">Save notification settings</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Save notification settings</button>
|
||||
</form>
|
||||
|
||||
<!-- Test -->
|
||||
@@ -262,11 +275,14 @@
|
||||
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="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 class="flex gap-3 mt-2">
|
||||
<button class="flex-1 px-3 py-2.5 text-sm font-medium border border-pg-border rounded-md bg-pg-bg text-pg-text hover:border-pg-action hover:text-pg-accent transition-colors cursor-pointer disabled:opacity-50"
|
||||
id="btn-test-notify">Send Test Notification</button>
|
||||
<button class="flex-1 px-3 py-2.5 text-sm font-medium border border-pg-border rounded-md bg-pg-bg text-pg-text hover:border-pg-action hover:text-pg-accent transition-colors cursor-pointer disabled:opacity-50"
|
||||
id="btn-check-reminders">Check Reminders Now</button>
|
||||
</div>
|
||||
<div class="test-result" id="test-result"></div>
|
||||
<div id="test-result"
|
||||
class="mt-3 px-3 py-2.5 rounded-md text-sm leading-relaxed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -287,7 +303,9 @@
|
||||
|
||||
function showResult(ok, msg) {
|
||||
resultEl.textContent = msg;
|
||||
resultEl.className = 'test-result ' + (ok ? 'ok' : 'err');
|
||||
resultEl.className = ok
|
||||
? 'mt-3 px-3 py-2.5 rounded-md text-sm leading-relaxed bg-green-950 text-green-400 border border-green-800'
|
||||
: 'mt-3 px-3 py-2.5 rounded-md text-sm leading-relaxed bg-red-950 text-red-400 border border-red-800';
|
||||
resultEl.style.display = 'block';
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ input, select, textarea {
|
||||
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; }
|
||||
input[type="checkbox"], input[type="radio"] { width: auto; padding: 0; }
|
||||
|
||||
textarea {
|
||||
font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace;
|
||||
@@ -99,12 +100,12 @@ textarea {
|
||||
|
||||
/* ── Buttons ── */
|
||||
|
||||
/* Full-width primary form submit */
|
||||
/* Primary form submit */
|
||||
.btn-submit {
|
||||
width: 100%; padding: 0.7rem; margin-top: 0.25rem;
|
||||
padding: 0.6rem 1.5rem; 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;
|
||||
color: #fff; font-size: 0.9rem; font-weight: 600;
|
||||
cursor: pointer; transition: opacity 0.15s;
|
||||
}
|
||||
.btn-submit:hover { opacity: 0.88; }
|
||||
|
||||
|
||||
@@ -7,10 +7,36 @@
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
/* ── Persona list ── */
|
||||
/* ── Server-generated persona list ── */
|
||||
.persona-list {
|
||||
list-style: none; display: flex; flex-direction: column;
|
||||
gap: 0.5rem; margin-top: 0.5rem;
|
||||
@@ -37,13 +63,8 @@
|
||||
border-color: var(--pg-action); font-size: 0.9rem;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.add-persona:hover { color: var(--pg-accent); }
|
||||
|
||||
/* ── Role badge ── */
|
||||
/* ── Server-generated role badge ── */
|
||||
.role-badge {
|
||||
display: inline-block; padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px; font-size: 0.78rem; font-weight: 600;
|
||||
@@ -58,26 +79,8 @@
|
||||
border: 1px solid var(--pg-border);
|
||||
}
|
||||
|
||||
/* ── OpenRouter quickstart warning card ── */
|
||||
#openrouter-quickstart {
|
||||
display: none; background: #1c1a0a; border: 1px solid #78350f;
|
||||
border-radius: 8px; padding: 1rem; margin-bottom: 1rem;
|
||||
}
|
||||
#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 ── */
|
||||
/* ── JS-toggled states ── */
|
||||
#clear-ls-ok { display: none; margin-left: 0.75rem; font-size: 0.8rem; color: #4ade80; }
|
||||
.usage-wrap { overflow-x: auto; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -86,6 +89,7 @@
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link active">Settings</a>
|
||||
<a href="/settings/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
@@ -100,6 +104,21 @@
|
||||
<!-- SUCCESS -->
|
||||
<!-- ERROR -->
|
||||
|
||||
<!-- OpenRouter quickstart (shown by JS when no model is configured) -->
|
||||
<div id="openrouter-quickstart"
|
||||
class="hidden rounded-xl border border-amber-800 bg-amber-950 p-4 mb-5">
|
||||
<p class="text-xs font-semibold text-amber-400 mb-1">⚡ You're on the server default model</p>
|
||||
<p class="text-xs text-amber-600 mb-3 leading-relaxed">
|
||||
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"
|
||||
class="inline-block px-3 py-2 rounded-md bg-amber-900 text-amber-100 text-sm font-medium hover:bg-amber-800 transition-colors">
|
||||
Set up OpenRouter →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Account info -->
|
||||
<div class="section">
|
||||
<h2>Account</h2>
|
||||
@@ -156,7 +175,7 @@
|
||||
placeholder=".*@example\.com alice@example\.com"
|
||||
spellcheck="false">{{ email_allowlist }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Save allowlist</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Save allowlist</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -174,28 +193,10 @@
|
||||
placeholder="https://ha.dgrzone.com/api/webhook/ https://n8n.dgrzone.com/webhook/"
|
||||
spellcheck="false">{{ http_allowlist }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Save allowlist</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Save allowlist</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="section">
|
||||
<h2>Notifications</h2>
|
||||
<p class="section-note">
|
||||
Configure how Inara reaches out proactively — reminders, cron jobs, and memory digests.
|
||||
</p>
|
||||
<a href="/settings/notifications" class="action-link">Notification settings →</a>
|
||||
</div>
|
||||
|
||||
<!-- Tool Permissions → /settings/tools -->
|
||||
<div class="section">
|
||||
<h2>Tool Permissions</h2>
|
||||
<p class="section-note">
|
||||
Configure tool access, risk policy, and confirmation gate overrides on the Tools page.
|
||||
</p>
|
||||
<a href="/settings/tools" class="action-link">Tool settings →</a>
|
||||
</div>
|
||||
|
||||
<!-- Usage summary -->
|
||||
<div class="section" id="usage-section">
|
||||
<h2>Usage</h2>
|
||||
@@ -216,28 +217,7 @@
|
||||
theme, font size, and context tier. Does not sign you out.
|
||||
</p>
|
||||
<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 -->
|
||||
<div class="section">
|
||||
<h2>Model Registry</h2>
|
||||
|
||||
<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" class="action-link action-link-amber">Set up OpenRouter →</a>
|
||||
</div>
|
||||
|
||||
<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" class="action-link">Manage models →</a>
|
||||
<span id="clear-ls-ok">Cleared.</span>
|
||||
</div>
|
||||
|
||||
<!-- Change Password -->
|
||||
@@ -259,7 +239,7 @@
|
||||
<input type="password" id="confirm_password" name="confirm_password"
|
||||
autocomplete="new-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Update password</button>
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -271,7 +251,9 @@
|
||||
Only unnamed sessions are affected — existing names are left alone.
|
||||
</p>
|
||||
<button type="button" id="backfill-names-btn" class="btn-secondary">Auto-name old sessions</button>
|
||||
<span id="backfill-names-ok" class="result-ok"></span>
|
||||
<span id="backfill-names-ok"
|
||||
class="ml-3 text-xs hidden"
|
||||
style="color:#4ade80"></span>
|
||||
</div>
|
||||
|
||||
<!-- Personas -->
|
||||
@@ -280,7 +262,10 @@
|
||||
<ul class="persona-list">
|
||||
{{ persona_items }}
|
||||
</ul>
|
||||
<a href="/setup/persona" class="add-persona">+ Add new persona</a>
|
||||
<a href="/setup/persona"
|
||||
class="inline-block mt-3 text-xs text-pg-muted hover:text-pg-accent transition-colors">
|
||||
+ Add new persona
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -317,7 +302,9 @@
|
||||
try {
|
||||
const d = await fetch('/backend').then(r => r.json());
|
||||
if ((d.available_roles || []).length === 0) {
|
||||
document.getElementById('openrouter-quickstart').style.display = 'block';
|
||||
const el = document.getElementById('openrouter-quickstart');
|
||||
el.classList.remove('hidden');
|
||||
el.style.display = 'block';
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
@@ -375,10 +362,12 @@
|
||||
const n = data.named ?? 0;
|
||||
ok.textContent = `Named ${n} session${n !== 1 ? 's' : ''}.`;
|
||||
ok.style.display = 'inline';
|
||||
ok.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
ok.textContent = 'Error — check console.';
|
||||
ok.style.color = '#f87171';
|
||||
ok.style.display = 'inline';
|
||||
ok.classList.remove('hidden');
|
||||
}
|
||||
btn.textContent = 'Auto-name old sessions';
|
||||
btn.disabled = false;
|
||||
|
||||
@@ -7,42 +7,36 @@
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
corePlugins: { preflight: false },
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pg: {
|
||||
bg: 'var(--pg-bg)',
|
||||
surface: 'var(--pg-surface)',
|
||||
border: 'var(--pg-border)',
|
||||
text: 'var(--pg-text)',
|
||||
muted: 'var(--pg-muted)',
|
||||
dim: 'var(--pg-dim)',
|
||||
dimmer: 'var(--pg-dimmer)',
|
||||
bright: 'var(--pg-bright)',
|
||||
accent: 'var(--pg-accent)',
|
||||
action: 'var(--pg-action)',
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
/* ── Policy cards (bordered sections on tools page) ── */
|
||||
.policy-card {
|
||||
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; }
|
||||
.policy-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
||||
.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; }
|
||||
|
||||
/* Compact selects and inputs inside policy cards */
|
||||
.policy-card select, .policy-card input[type="text"] {
|
||||
padding: 0.4rem 0.65rem; font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 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-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; transition: opacity 0.15s;
|
||||
}
|
||||
.save-btn:hover { opacity: 0.88; }
|
||||
|
||||
/* ── Tool table ── */
|
||||
/* ── Server-generated tool table ── */
|
||||
.table-section-label {
|
||||
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--pg-dimmer);
|
||||
@@ -65,7 +59,7 @@
|
||||
.tool-table tr:hover td { background: rgba(124,58,237,0.04); }
|
||||
.tool-name { font-family: monospace; font-size: 0.82rem; }
|
||||
|
||||
/* Risk badges */
|
||||
/* Risk badges (server-generated) */
|
||||
.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.12); color: #4ade80; }
|
||||
@@ -75,7 +69,7 @@
|
||||
[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-status pill (server-generated, updated by JS) */
|
||||
.auto-pill {
|
||||
display: inline-block; font-size: 0.68rem; font-weight: 600;
|
||||
padding: 0.12rem 0.4rem; border-radius: 9999px;
|
||||
@@ -84,19 +78,13 @@
|
||||
.auto-off { background: rgba(148,163,184,0.12); color: var(--pg-dimmer); }
|
||||
[data-theme="light"] .auto-on { color: #7c3aed; }
|
||||
|
||||
/* Override select */
|
||||
/* Override select (server-generated) */
|
||||
.override-sel {
|
||||
font-size: 0.78rem; padding: 0.25rem 0.5rem;
|
||||
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; }
|
||||
|
||||
/* 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: #a78bfa; }
|
||||
.legend-dot.off { background: var(--pg-dimmer); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -105,6 +93,7 @@
|
||||
<a href="{{ back_href }}" class="nav-link">← Chat</a>
|
||||
<a href="{{ help_href }}" class="nav-link">Help</a>
|
||||
<a href="/settings" class="nav-link">Settings</a>
|
||||
<a href="/settings/models" class="nav-link">Models</a>
|
||||
<a href="/settings/notifications" class="nav-link">Notifications</a>
|
||||
<a href="/settings/tools" class="nav-link active">Tools</a>
|
||||
<a href="/settings/crons" class="nav-link">Schedules</a>
|
||||
@@ -125,55 +114,55 @@
|
||||
|
||||
<form method="POST" action="/settings/tools" id="tools-form">
|
||||
|
||||
<!-- 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">
|
||||
<!-- Risk policy card -->
|
||||
<div class="rounded-xl border border-pg-border bg-pg-surface p-5 mb-5">
|
||||
<h2 class="text-sm font-semibold text-pg-bright mb-4">Risk Policy</h2>
|
||||
<div class="flex items-center gap-4 flex-wrap mb-3">
|
||||
<span class="text-sm font-medium text-pg-text min-w-[6rem]">Max risk level</span>
|
||||
<select name="max_risk" id="max-risk-sel" class="w-auto">
|
||||
<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="medium" {{ sel_medium }}>Medium — low + medium risk (recommended)</option>
|
||||
<option value="high" {{ sel_high }}>High — all tools including destructive ones</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="policy-note">
|
||||
<strong>Low</strong> tools are read-only and sandboxed (web search, project file reads, HA status checks).<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).
|
||||
<p class="text-xs text-pg-muted leading-relaxed mb-2">
|
||||
<strong class="text-pg-text">Low</strong> tools are read-only and sandboxed (web search, project file reads, HA status checks).<br>
|
||||
<strong class="text-pg-text">Medium</strong> tools write to local data or send notifications to you (cron jobs, scratch, task management).<br>
|
||||
<strong class="text-pg-text">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;">
|
||||
<p class="text-xs text-pg-muted leading-relaxed">
|
||||
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 -->
|
||||
<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 class="flex gap-5 flex-wrap mb-4 text-xs text-pg-muted">
|
||||
<span><span class="inline-block w-2 h-2 rounded-full bg-[#a78bfa] mr-1.5"></span>Auto-included by risk level</span>
|
||||
<span><span class="inline-block w-2 h-2 rounded-full bg-pg-dimmer mr-1.5"></span>Auto-excluded by risk level</span>
|
||||
</div>
|
||||
|
||||
<!-- Tool table -->
|
||||
<!-- Tool table (server-generated) -->
|
||||
{{ tool_table_html }}
|
||||
|
||||
<!-- Confirmation gate -->
|
||||
<div class="policy-card" style="margin-top:1.75rem;">
|
||||
<h2>Confirmation Gate</h2>
|
||||
<p class="policy-note">
|
||||
<!-- Confirmation gate card -->
|
||||
<div class="rounded-xl border border-pg-border bg-pg-surface p-5 mt-5 mb-5">
|
||||
<h2 class="text-sm font-semibold text-pg-bright mb-2">Confirmation Gate</h2>
|
||||
<p class="text-xs text-pg-muted leading-relaxed mb-4">
|
||||
Some tools require explicit confirmation before executing. Override the defaults here.<br>
|
||||
Tools requiring confirmation by default: <code>{{ confirm_required_tools }}</code>
|
||||
Tools requiring confirmation by default: <code class="font-mono text-pg-accent bg-pg-bg border border-pg-border rounded px-1">{{ confirm_required_tools }}</code>
|
||||
</p>
|
||||
<div class="col-split" style="margin-top:0.85rem;">
|
||||
<div class="col-half">
|
||||
<label>Allow list — bypass confirmation</label>
|
||||
<div class="flex gap-6 flex-wrap items-start">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-xs font-semibold text-pg-muted mb-1">Allow list — bypass confirmation</label>
|
||||
<textarea name="allow_list" rows="4"
|
||||
placeholder="reminders_clear cron_remove"
|
||||
autocomplete="off" spellcheck="false">{{ tool_allow }}</textarea>
|
||||
<p class="hint">One tool name per line. These tools skip the confirmation prompt.</p>
|
||||
</div>
|
||||
<div class="col-half">
|
||||
<label>Deny list — always block</label>
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-xs font-semibold text-pg-muted mb-1">Deny list — always block</label>
|
||||
<textarea name="deny_list" rows="4"
|
||||
placeholder="shell_exec file_write"
|
||||
autocomplete="off" spellcheck="false">{{ tool_deny }}</textarea>
|
||||
@@ -182,8 +171,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem;">
|
||||
<button type="submit" class="save-btn">Save tool settings</button>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn-submit w-full md:w-96">Save tool settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user