summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 17:07:31 +0900
committerAdam Malczewski <[email protected]>2026-05-22 17:07:31 +0900
commit288b21cec98421fda57028a0c8c9d835cfbb14b0 (patch)
tree9ce3ec38acdcf7be96e28f1e0d5deccf6b46c917 /packages/frontend/src/lib/components
parent45a4890031192f4e7409443f98e824dad17ba175 (diff)
downloaddispatch-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')
-rw-r--r--packages/frontend/src/lib/components/ModelStatus.svelte49
-rw-r--r--packages/frontend/src/lib/components/SettingsPanel.svelte46
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte4
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"}