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:
@@ -106,6 +106,18 @@ GOOGLE_CATALOG: list[dict] = [
|
||||
{"id": "gemini-3.1-flash-lite-preview", "label": "Gemini 3.1 Flash-Lite (preview)", "context_k": 1000},
|
||||
]
|
||||
|
||||
# Known OpenAI-compatible cloud inference services.
|
||||
# All use host_type "openai" (/chat/completions + /models paths).
|
||||
CLOUD_API_CATALOG: list[dict] = [
|
||||
{"id": "openrouter", "label": "OpenRouter", "api_url": "https://openrouter.ai/api/v1"},
|
||||
{"id": "openai", "label": "OpenAI", "api_url": "https://api.openai.com/v1"},
|
||||
{"id": "groq", "label": "Groq", "api_url": "https://api.groq.com/openai/v1"},
|
||||
{"id": "xai", "label": "X.ai / Grok", "api_url": "https://api.x.ai/v1"},
|
||||
{"id": "together", "label": "Together.ai", "api_url": "https://api.together.xyz/v1"},
|
||||
{"id": "fireworks", "label": "Fireworks.ai", "api_url": "https://api.fireworks.ai/inference/v1"},
|
||||
{"id": "custom", "label": "Custom", "api_url": ""},
|
||||
]
|
||||
|
||||
|
||||
# ── Built-in model definitions ────────────────────────────────────────────────
|
||||
|
||||
@@ -148,6 +160,8 @@ _ROLE_LAST_RESORT: dict[str, str] = {
|
||||
|
||||
PRIORITY_KEYS = ["primary", "backup_1", "backup_2", "backup_3", "backup_4"]
|
||||
|
||||
REQUIRED_ROLES: list[str] = ["chat", "orchestrator", "distill"]
|
||||
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -565,6 +579,8 @@ def get_catalog(provider: str, username: str | None = None) -> list[dict]:
|
||||
return list(ANTHROPIC_CATALOG)
|
||||
if provider == "google":
|
||||
return list(GOOGLE_CATALOG)
|
||||
if provider == "cloud":
|
||||
return list(CLOUD_API_CATALOG)
|
||||
return []
|
||||
|
||||
|
||||
@@ -851,6 +867,52 @@ def remove_model(username: str, model_id: str) -> bool:
|
||||
return len(data["models"]) < before
|
||||
|
||||
|
||||
def get_custom_roles(username: str) -> list[str]:
|
||||
"""
|
||||
Return the user's custom (non-required) roles.
|
||||
Falls back to config-defined roles minus required ones for migration.
|
||||
"""
|
||||
registry = _load(username)
|
||||
if "custom_roles" in registry:
|
||||
return [r for r in registry["custom_roles"] if r and r not in REQUIRED_ROLES]
|
||||
from config import settings as _cfg
|
||||
return [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES]
|
||||
|
||||
|
||||
def get_all_roles(username: str) -> list[str]:
|
||||
"""Return required roles followed by the user's custom roles."""
|
||||
return list(REQUIRED_ROLES) + get_custom_roles(username)
|
||||
|
||||
|
||||
def add_custom_role(username: str, role_name: str) -> bool:
|
||||
"""Add a custom role. Returns False if the name is invalid or already a required role."""
|
||||
role_name = role_name.strip().lower()
|
||||
if not role_name or role_name in REQUIRED_ROLES:
|
||||
return False
|
||||
data = _load(username)
|
||||
if "custom_roles" not in data:
|
||||
from config import settings as _cfg
|
||||
data["custom_roles"] = [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES]
|
||||
if role_name not in data["custom_roles"]:
|
||||
data["custom_roles"].append(role_name)
|
||||
_save(username, data)
|
||||
return True
|
||||
|
||||
|
||||
def remove_custom_role(username: str, role_name: str) -> bool:
|
||||
"""Remove a custom role. Required roles cannot be removed."""
|
||||
if role_name in REQUIRED_ROLES:
|
||||
return False
|
||||
data = _load(username)
|
||||
if "custom_roles" not in data:
|
||||
from config import settings as _cfg
|
||||
data["custom_roles"] = [r for r in _cfg.get_defined_roles() if r not in REQUIRED_ROLES]
|
||||
if role_name in data["custom_roles"]:
|
||||
data["custom_roles"].remove(role_name)
|
||||
_save(username, data)
|
||||
return True
|
||||
|
||||
|
||||
def set_role(username: str, role: str, priority: str, model_id: str | None) -> bool:
|
||||
"""
|
||||
Assign a model to a role priority slot.
|
||||
|
||||
Reference in New Issue
Block a user