From 7b140d4079a93a4fbaf0172dce70187571005489 Mon Sep 17 00:00:00 2001 From: Rune Olsen Date: Mon, 13 Apr 2026 12:45:50 +0200 Subject: [PATCH] Changes, bugfixes and updates to the matrix. Added 1.2v batteries --- .env.example | 23 ++------- server/config.py | 14 ++---- server/providers/registry.py | 17 +++++-- server/web/routes.py | 27 +++++++++++ server/web/static/app.js | 75 ++++++++++++++++++++++++------ server/web/templates/audit.html | 13 ++---- server/web/templates/base.html | 2 +- server/web/templates/help.html | 51 ++++++++++++++++++++ server/web/templates/settings.html | 15 +++++- server/webhooks/endpoints.py | 4 +- 10 files changed, 179 insertions(+), 62 deletions(-) diff --git a/.env.example b/.env.example index c0e0c85..18eb164 100644 --- a/.env.example +++ b/.env.example @@ -2,32 +2,15 @@ # Copy this file to .env and fill in your values. # Never commit .env to version control. -# AI provider selection — keys are configured via Settings → Credentials (stored encrypted in DB) -# Set DEFAULT_PROVIDER to the provider you'll use as the default -DEFAULT_PROVIDER=openrouter # anthropic | openrouter | openai - -# Override the model (leave empty to use the provider's default) -# DEFAULT_MODEL=claude-sonnet-4-6 - -# Available models shown in the chat model selector (comma-separated) -# AVAILABLE_MODELS=claude-sonnet-4-6,claude-opus-4-6,claude-haiku-4-5-20251001 - -# Default model pre-selected in chat UI (defaults to first in AVAILABLE_MODELS) -# DEFAULT_CHAT_MODEL=claude-sonnet-4-6 - # Master password for the encrypted credential store (required) # Choose a strong passphrase — all credentials are encrypted with this. DB_MASTER_PASSWORD=change-me-to-a-strong-passphrase -# Server +# Server port PORT=8080 -# Agent limits -MAX_TOOL_CALLS=20 -MAX_AUTONOMOUS_RUNS_PER_HOUR=10 - -# Timezone for display (stored internally as UTC) -TIMEZONE=Europe/Oslo +# Default model pre-selected in chat UI (leave empty to use the first available model) +# DEFAULT_CHAT_MODEL=claude-sonnet-4-6 # Main app database — PostgreSQL (shared postgres service) AIDE_DB_URL=postgresql://aide:change-me@postgres:5432/aide diff --git a/server/config.py b/server/config.py index 12dfbe8..7ecb769 100644 --- a/server/config.py +++ b/server/config.py @@ -97,16 +97,12 @@ def _load() -> Settings: max_runs = int(_optional("MAX_AUTONOMOUS_RUNS_PER_HOUR", "10")) timezone = _optional("TIMEZONE", "Europe/Oslo") - def _normalize_model(m: str) -> str: - """Prepend default_provider if model has no provider prefix.""" - parts = m.split(":", 1) - if len(parts) == 2 and parts[0] in _known_providers: - return m - return f"{default_provider}:{m}" - - available_models: list[str] = [] # unused; kept for backward compat + available_models: list[str] = [] default_chat_model_raw = _optional("DEFAULT_CHAT_MODEL", "") - default_chat_model = _normalize_model(default_chat_model_raw) if default_chat_model_raw else "" + if default_chat_model_raw and ":" not in default_chat_model_raw: + default_chat_model = f"{default_provider}:{default_chat_model_raw}" + else: + default_chat_model = default_chat_model_raw aide_db_url = _require("AIDE_DB_URL") diff --git a/server/providers/registry.py b/server/providers/registry.py index 63ff54f..32d9bef 100644 --- a/server/providers/registry.py +++ b/server/providers/registry.py @@ -24,10 +24,19 @@ async def _resolve_key(provider: str, user_id: str | None = None) -> str: return await credential_store.get(f"system:{provider}_api_key") or "" +async def _get_default_provider() -> str: + """Return the default provider name: credential_store → settings → 'anthropic'.""" + from ..database import credential_store + from ..config import settings + val = await credential_store.get("system:default_provider") + if val and val in {"anthropic", "openrouter", "openai"}: + return val + return settings.default_provider + + async def get_provider(user_id: str | None = None) -> AIProvider: """Return the default provider, with keys resolved for the given user.""" - from ..config import settings - return await get_provider_for_name(settings.default_provider, user_id=user_id) + return await get_provider_for_name(await _get_default_provider(), user_id=user_id) async def get_provider_for_name(name: str, user_id: str | None = None) -> AIProvider: @@ -64,15 +73,13 @@ async def get_provider_for_model(model_str: str, user_id: str | None = None) -> "openrouter:openai/gpt-4o" → (OpenRouterProvider, "openai/gpt-4o") "claude-sonnet-4-6" → (default_provider, "claude-sonnet-4-6") """ - from ..config import settings - _known = {"anthropic", "openrouter", "openai"} if ":" in model_str: prefix, bare = model_str.split(":", 1) if prefix in _known: return await get_provider_for_name(prefix, user_id=user_id), bare # No recognised prefix — use default provider, full string as model ID - return await get_provider_for_name(settings.default_provider, user_id=user_id), model_str + return await get_provider_for_name(await _get_default_provider(), user_id=user_id), model_str async def get_available_providers(user_id: str | None = None) -> list[str]: diff --git a/server/web/routes.py b/server/web/routes.py index 197bbc6..b49b4ec 100644 --- a/server/web/routes.py +++ b/server/web/routes.py @@ -380,6 +380,33 @@ async def get_queue_status(request: Request): return agent_runner.queue_status +@router.get("/settings/provider") +async def get_default_provider(request: Request): + _require_admin(request) + from ..providers.registry import get_available_providers + from ..config import settings as _settings + val = await credential_store.get("system:default_provider") + available = await get_available_providers() + current = val or _settings.default_provider + # If the saved provider no longer has a key, fall back to the first available + if current not in available and available: + current = available[0] + return {"default_provider": current, "available_providers": available} + + +class ProviderIn(BaseModel): + default_provider: str + + +@router.post("/settings/provider") +async def set_default_provider(request: Request, body: ProviderIn): + _require_admin(request) + if body.default_provider not in {"anthropic", "openrouter", "openai"}: + raise HTTPException(status_code=400, detail="Invalid provider. Use: anthropic, openrouter, openai") + await credential_store.set("system:default_provider", body.default_provider, "Default AI provider") + return {"default_provider": body.default_provider} + + @router.get("/settings/default-models") async def get_default_models(request: Request): _require_admin(request) diff --git a/server/web/static/app.js b/server/web/static/app.js index 65c01c1..e3f629f 100644 --- a/server/web/static/app.js +++ b/server/web/static/app.js @@ -1202,8 +1202,6 @@ function initAudit() { const el = document.getElementById(id); if (el) el.value = ""; }); - const cb = document.getElementById("filter-confirmed"); - if (cb) cb.checked = false; const banner = document.getElementById("audit-task-filter-banner"); if (banner) { banner.textContent = `Showing entries for run ${_auditTaskId.slice(0, 8)}…`; @@ -1218,7 +1216,6 @@ function initAudit() { const el = document.getElementById(id); if (el) el.value = ""; }); - document.getElementById("filter-confirmed").checked = false; _auditTaskId = ""; const banner = document.getElementById("audit-task-filter-banner"); if (banner) banner.style.display = "none"; @@ -1235,8 +1232,6 @@ async function loadAudit(page) { const end = document.getElementById("filter-end")?.value; const tool = document.getElementById("filter-tool")?.value; const session = document.getElementById("filter-session")?.value; - const confirmed = document.getElementById("filter-confirmed")?.checked; - const startISO = parseEuDate(start); const endISO = parseEuDate(end, true); if (startISO) params.set("start", startISO); @@ -1244,7 +1239,6 @@ async function loadAudit(page) { if (tool) params.set("tool_name", tool); if (session) params.set("session_id", session); if (_auditTaskId) params.set("task_id", _auditTaskId); - if (confirmed) params.set("confirmed_only", "true"); params.set("page", auditPage); params.set("per_page", 50); @@ -1265,7 +1259,6 @@ function renderAuditTable(data) { ${esc(e.tool_name)} ${esc(JSON.stringify(e.arguments).slice(0, 80))} ${esc((e.result_summary || "").slice(0, 80))} - ${e.confirmed ? '' : ''} ${esc((e.session_id || "").slice(0,8))} `; tr.addEventListener("click", () => openAuditDetail(e)); @@ -1292,9 +1285,6 @@ function openAuditDetail(e) { document.getElementById("audit-detail-ts").textContent = formatDate(e.timestamp); document.getElementById("audit-detail-session").textContent = e.session_id || "—"; document.getElementById("audit-detail-task").textContent = e.task_id || "—"; - document.getElementById("audit-detail-confirmed").innerHTML = e.confirmed - ? 'Yes' - : 'No'; let _args = e.arguments; if (typeof _args === "string") { try { _args = JSON.parse(_args); } catch {} } document.getElementById("audit-detail-args").textContent = @@ -1411,6 +1401,7 @@ async function saveProviderKey(provider) { document.getElementById(`provider-key-${provider}-input`).value = ""; showFlash(`${provider} key saved.`); loadProviderKeys(); + loadDefaultProvider(); } else { const d = await r.json().catch(() => ({})); showFlash(d.detail || "Error saving key."); @@ -1420,7 +1411,7 @@ async function saveProviderKey(provider) { async function clearProviderKey(provider) { if (!confirm(`Clear the ${provider} API key? This may break agent runs that rely on it.`)) return; const r = await fetch(`/api/settings/provider-keys/${provider}`, { method: "DELETE" }); - if (r.ok) { showFlash(`${provider} key cleared.`); loadProviderKeys(); } + if (r.ok) { showFlash(`${provider} key cleared.`); loadProviderKeys(); loadDefaultProvider(); } else showFlash("Error clearing key."); } @@ -2448,6 +2439,7 @@ function initSettings() { loadApiKeyStatus(); loadProviderKeys(); loadUsersBaseFolder(); + loadDefaultProvider(); loadLimits(); loadDefaultModels(); loadProxyTrust(); @@ -2785,13 +2777,56 @@ async function loadLimits() { if (mcrEl) mcrEl.value = data.max_concurrent_runs; if (defaultsEl) { defaultsEl.textContent = - `.env defaults: max_tool_calls=${data.defaults.max_tool_calls}, ` + - `max_autonomous_runs_per_hour=${data.defaults.max_autonomous_runs_per_hour}, ` + - `max_concurrent_runs=${data.defaults.max_concurrent_runs}`; + `Built-in defaults: tool_calls=${data.defaults.max_tool_calls}, ` + + `runs/hour=${data.defaults.max_autonomous_runs_per_hour}, ` + + `concurrent=${data.defaults.max_concurrent_runs}`; } } catch { /* ignore */ } } +async function loadDefaultProvider() { + const sel = document.getElementById("default-provider-select"); + if (!sel) return; + try { + const r = await fetch("/api/settings/provider"); + if (!r.ok) return; + const data = await r.json(); + const labels = { anthropic: "Anthropic", openrouter: "OpenRouter", openai: "OpenAI" }; + const available = data.available_providers || []; + sel.innerHTML = ""; + if (available.length === 0) { + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = "No providers configured"; + sel.appendChild(opt); + sel.disabled = true; + sel.closest("section")?.querySelector(".btn")?.setAttribute("disabled", ""); + return; + } + for (const p of available) { + const opt = document.createElement("option"); + opt.value = p; + opt.textContent = labels[p] || p; + sel.appendChild(opt); + } + sel.disabled = false; + sel.closest("section")?.querySelector(".btn")?.removeAttribute("disabled"); + sel.value = data.default_provider || available[0]; + } catch { /* ignore */ } +} + +async function saveDefaultProvider() { + const sel = document.getElementById("default-provider-select"); + if (!sel) return; + const r = await fetch("/api/settings/provider", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ default_provider: sel.value }), + }); + if (r.ok) showFlash("Default provider saved ✓"); + else showFlash("Error saving default provider"); +} + async function loadDefaultModels() { const defSel = document.getElementById("dm-default"); const freeSel = document.getElementById("dm-free"); @@ -5725,3 +5760,15 @@ document.addEventListener("DOMContentLoaded", () => { initNav(); _initPage(location.pathname); }); + +// Close any open modal on Escape +document.addEventListener("keydown", e => { + if (e.key !== "Escape") return; + const overlay = [...document.querySelectorAll(".modal-overlay")] + .find(el => el.style.display !== "none" && !el.classList.contains("hidden")); + if (!overlay) return; + // Find the ✕ close button, or any button whose onclick closes the modal + const closeBtn = [...overlay.querySelectorAll("button")] + .find(b => b.textContent.trim() === "✕" || /^close/i.test((b.getAttribute("onclick") || "").trim())); + closeBtn?.click(); +}); diff --git a/server/web/templates/audit.html b/server/web/templates/audit.html index 8e98805..8d214e4 100644 --- a/server/web/templates/audit.html +++ b/server/web/templates/audit.html @@ -30,11 +30,6 @@ -
- -
@@ -55,12 +50,11 @@ Tool Arguments Result - Confirmed Session - Loading… + Loading…
@@ -72,15 +66,14 @@