diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 17:07:31 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 17:07:31 +0900 |
| commit | 288b21cec98421fda57028a0c8c9d835cfbb14b0 (patch) | |
| tree | 9ce3ec38acdcf7be96e28f1e0d5deccf6b46c917 /packages/frontend/src/lib/components | |
| parent | 45a4890031192f4e7409443f98e824dad17ba175 (diff) | |
| download | dispatch-288b21cec98421fda57028a0c8c9d835cfbb14b0.tar.gz dispatch-288b21cec98421fda57028a0c8c9d835cfbb14b0.zip | |
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)
Diffstat (limited to 'packages/frontend/src/lib/components')
3 files changed, 97 insertions, 2 deletions
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<string | null>(null); // keyId or null let keyModalValue = $state(""); let keyModalError = $state<string | null>(null); let keyModalSaving = $state(false); +let removingKey = $state<string | null>(null); + + async function loadCredentialStatus(): Promise<void> { try { @@ -129,6 +134,27 @@ async function saveApiKey(): Promise<void> { } } +async function removeKey(keyId: string): Promise<void> { + 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} </span> - <span class="text-xs font-mono">{key.id}</span> + <span class="text-xs font-mono flex-1">{key.id}</span> <span class="text-xs text-base-content/40">{key.provider}</span> + <button + type="button" + class="btn btn-xs btn-ghost btn-square text-base-content/30 hover:text-error" + disabled={removingKey === key.id} + onclick={() => removeKey(key.id)} + title="Remove key" + > + {#if removingKey === key.id} + <span class="loading loading-spinner loading-xs"></span> + {:else} + ✕ + {/if} + </button> </div> {#if key.status === "exhausted"} <div class="pl-2 flex flex-col gap-0.5"> @@ -270,6 +309,13 @@ function truncate(str: string | null, max: number): string { </div> {/if} {/if} + <button + type="button" + class="btn btn-sm btn-primary btn-outline w-full mt-2" + onclick={onAddKey} + > + + Add New Key + </button> <!-- Import Key Modal --> {#if showKeyModal} <div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" role="dialog"> @@ -308,4 +354,5 @@ function truncate(str: string | null, max: number): string { </div> </div> {/if} + </div> 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 @@ <script lang="ts"> +import { config } from "../config.js"; import { appSettings } from "../settings.svelte.js"; import type { KeyInfo } from "../types.js"; @@ -15,6 +16,24 @@ let titleModelId = $state<string | null>(null); let availableModels = $state<string[]>([]); let loadingModels = $state(false); let autoExpandThinking = $state(appSettings.autoExpandThinking); +let backendUrl = $state(config.apiBase); +let backendUrlSaved = $state(false); + +function saveBackendUrl(): void { + const trimmed = backendUrl.trim().replace(/\/+$/, ""); + if (!trimmed) return; + config.setApiBase(trimmed); + backendUrl = trimmed; + backendUrlSaved = true; + setTimeout(() => { backendUrlSaved = false; }, 2000); +} + +function resetBackendUrl(): void { + config.setApiBase(config.defaultApiBase); + backendUrl = config.defaultApiBase; + backendUrlSaved = true; + setTimeout(() => { backendUrlSaved = false; }, 2000); +} async function loadSettings(): Promise<void> { try { @@ -143,5 +162,32 @@ $effect(() => { /> <span class="text-xs text-base-content/70">Auto-expand thinking</span> </label> + + <div class="divider my-0"></div> + + <p class="text-xs text-base-content/70">Backend URL</p> + <p class="text-xs text-base-content/40">API server address. Default: {config.defaultApiBase}</p> + <div class="flex gap-1"> + <input + type="text" + class="input input-bordered input-sm flex-1" + bind:value={backendUrl} + placeholder={config.defaultApiBase} + /> + <button type="button" class="btn btn-sm btn-primary" onclick={saveBackendUrl}> + Save + </button> + </div> + <button + type="button" + class="btn btn-xs btn-ghost btn-outline w-full" + disabled={config.apiBase === config.defaultApiBase} + onclick={resetBackendUrl} + > + Reset to default + </button> + {#if backendUrlSaved} + <p class="text-xs text-success">Saved. Reload the page to apply.</p> + {/if} </div> </div> diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 041679a..e89e351 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -25,6 +25,7 @@ const { onReasoningChange, onAgentChange = (_agent: any) => {}, onWorkingDirectoryChange = (_dir: string | null) => {}, + onAddKey = () => {}, }: { keys?: KeyInfo[]; tasks?: TaskItem[]; @@ -40,6 +41,7 @@ const { onReasoningChange: (effort: string) => void; onAgentChange?: (agent: any) => void; onWorkingDirectoryChange?: (dir: string | null) => void; + onAddKey?: () => void; } = $props(); interface Panel { @@ -129,7 +131,7 @@ function contentClass(selected: string): string { {:else if panel.selected === "Claude Reset"} <ClaudeReset {apiBase} /> {:else if panel.selected === "Model Status"} - <ModelStatus {keys} {apiBase} /> + <ModelStatus {keys} {apiBase} {onAddKey} /> {:else if panel.selected === "Tasks"} <TaskListPanel {tasks} /> {:else if panel.selected === "Config"} |
