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:
Scott Idem
2026-05-15 21:03:11 -04:00
parent 070f1ce156
commit 7a27190ffe
13 changed files with 1224 additions and 953 deletions

View File

@@ -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&#10;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/&#10;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;