From 288b21cec98421fda57028a0c8c9d835cfbb14b0 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 22 May 2026 17:07:31 +0900 Subject: feat: add/remove keys from UI, backend URL setting, user service, Docker fix - Add POST /models/add-key and POST /models/remove-key API endpoints - Add 'Add New Key' modal (page-level) with provider selection - Add remove button per key in Model Status view - Add configurable backend URL setting in Settings panel with localStorage persistence - Convert systemd service from system to user service (systemctl --user) - Fix Docker entrypoint to chown all nested node_modules dirs - Update dispatch.toml credential paths to use -pro/-max naming - Make API port configurable via PORT env var (default 3000, prod 18390) --- packages/frontend/src/App.svelte | 80 ++++++++++++++++++++++ .../frontend/src/lib/components/ModelStatus.svelte | 49 ++++++++++++- .../src/lib/components/SettingsPanel.svelte | 46 +++++++++++++ .../src/lib/components/SidebarPanel.svelte | 4 +- packages/frontend/src/lib/config.ts | 36 ++++++++-- 5 files changed, 209 insertions(+), 6 deletions(-) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 33415bd..d96cb63 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -22,6 +22,40 @@ let modelsData = $state<{ keys: KeyInfo[] }>({ let sidebarOpen = $state(true); +// Add Key modal state (rendered at page level to escape sidebar transform) +let showAddKeyModal = $state(false); +let addKeyProvider = $state("anthropic"); +let addKeyId = $state(""); +let addKeyError = $state(null); +let addKeySaving = $state(false); + +async function addNewKey(): Promise { + if (!addKeyId.trim()) return; + addKeySaving = true; + addKeyError = null; + try { + const res = await fetch(`${config.apiBase}/models/add-key`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: addKeyId.trim(), provider: addKeyProvider }), + }); + const data = (await res.json()) as { success?: boolean; error?: string }; + if (!res.ok || !data.success) { + addKeyError = data.error ?? "Failed to add key"; + } else { + showAddKeyModal = false; + addKeyId = ""; + addKeyProvider = "anthropic"; + await new Promise((r) => setTimeout(r, 500)); + window.location.reload(); + } + } catch (e) { + addKeyError = e instanceof Error ? e.message : "Network error"; + } finally { + addKeySaving = false; + } +} + async function fetchModels() { try { const res = await fetch(`${config.apiBase}/models`); @@ -112,6 +146,7 @@ onMount(() => { }} onAgentChange={(agent) => tabStore.setAgent(agent)} onWorkingDirectoryChange={(dir) => tabStore.setWorkingDirectory(dir)} + onAddKey={() => { showAddKeyModal = true; addKeyId = ""; addKeyProvider = "anthropic"; addKeyError = null; }} /> @@ -144,3 +179,48 @@ onMount(() => {
+ + +{#if showAddKeyModal} + +{/if} diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte index 1270fcc..d6ff0f5 100644 --- a/packages/frontend/src/lib/components/ModelStatus.svelte +++ b/packages/frontend/src/lib/components/ModelStatus.svelte @@ -20,10 +20,12 @@ const { keys = [], currentModel, apiBase = "", + onAddKey = () => {}, }: { keys?: KeyInfo[]; currentModel?: string; apiBase?: string; + onAddKey?: () => void; } = $props(); const activeKeys = $derived(keys.filter((k) => k.status === "active").length); @@ -44,6 +46,9 @@ let showKeyModal = $state(null); // keyId or null let keyModalValue = $state(""); let keyModalError = $state(null); let keyModalSaving = $state(false); +let removingKey = $state(null); + + async function loadCredentialStatus(): Promise { try { @@ -129,6 +134,27 @@ async function saveApiKey(): Promise { } } +async function removeKey(keyId: string): Promise { + if (!confirm(`Remove key "${keyId}" from config?`)) return; + removingKey = keyId; + try { + const res = await fetch(`${apiBase}/models/remove-key`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: keyId }), + }); + const data = (await res.json()) as { success?: boolean; error?: string }; + if (res.ok && data.success) { + await new Promise((r) => setTimeout(r, 500)); + window.location.reload(); + } + } catch { + // ignore + } finally { + removingKey = null; + } +} + $effect(() => { void loadCredentialStatus(); void loadApiKeyStatus(); @@ -209,8 +235,21 @@ function truncate(str: string | null, max: number): string { > {key.status} - {key.id} + {key.id} {key.provider} + {#if key.status === "exhausted"}
@@ -270,6 +309,13 @@ function truncate(str: string | null, max: number): string {
{/if} {/if} + {#if showKeyModal} {/if} + diff --git a/packages/frontend/src/lib/components/SettingsPanel.svelte b/packages/frontend/src/lib/components/SettingsPanel.svelte index 79574d2..c19fe45 100644 --- a/packages/frontend/src/lib/components/SettingsPanel.svelte +++ b/packages/frontend/src/lib/components/SettingsPanel.svelte @@ -1,4 +1,5 @@