From a0ae31fe2a1c62d0f7607f944d2aaf17608d5a00 Mon Sep 17 00:00:00 2001
From: dvcdsys
Date: Wed, 3 Jun 2026 11:18:18 +0100
Subject: [PATCH 1/2] feat(dashboard): add copyable "connect CLI" command to
API-key-created popup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
After creating an API key, the success popup now shows a second copyable
field: a ready-to-paste command that registers THIS server in the cix CLI
and makes it the default, so the flow is create → copy → paste → use.
The command writes a single `server.` entry (URL + the new key as
one unit — the key belongs to the server), then sets it as default:
cix config set server..url && \
cix config set server..key && \
cix config set default_server
- comes from window.location.origin (dashboard is served
same-origin from cix-server, so it's always the correct base URL).
- is derived from window.location.host, sanitized to [a-z0-9-]
so it's dot-/whitespace-free as the CLI's parseServerKey requires.
Copy logic refactored into a reusable copyText() helper driving two
independent button states (key + command), preserving the existing
secure-context / execCommand fallback. Frontend-only; no CLI or server
change — the command uses existing `cix config set` commands.
Co-Authored-By: Claude Opus 4.8
---
.../components/CreateApiKeyDialog.tsx | 114 +++++++++++++++---
1 file changed, 99 insertions(+), 15 deletions(-)
diff --git a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
index 4b56de6..633bd85 100644
--- a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
+++ b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
-import { Check, Copy, Loader2, KeyRound, AlertTriangle } from 'lucide-react';
+import { Check, Copy, Loader2, KeyRound, AlertTriangle, Terminal } from 'lucide-react';
import { toast } from 'sonner';
import { ApiError } from '@/api/client';
import { Alert, AlertDescription, AlertTitle } from '@/ui/alert';
@@ -41,6 +41,21 @@ function legacyCopy(text: string): boolean {
return ok;
}
+// Derive a cix CLI server alias from the browser host. The CLI stores each
+// server as one entry under `server.` and parses that config key by
+// splitting on dots, so the alias must be dot-free (and whitespace-free, and
+// non-empty) — see validateServerName / parseServerKey in the CLI. We fold the
+// host (including any non-default port, so distinct ports get distinct aliases)
+// to [a-z0-9-]: "cix.example.com" -> "cix-example-com",
+// "localhost:21847" -> "localhost-21847".
+function cixServerAlias(host: string): string {
+ const alias = host
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ return alias || 'default';
+}
+
// Two-stage dialog: collect a name, then reveal the freshly minted key once.
// Once revealed, the dialog refuses outside-click and Escape — accidental
// dismissal would lose the unrecoverable secret. Only the explicit "I've
@@ -49,7 +64,8 @@ export function CreateApiKeyDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [revealed, setRevealed] = useState(null);
- const [copied, setCopied] = useState(false);
+ const [copiedKey, setCopiedKey] = useState(false);
+ const [copiedCmd, setCopiedCmd] = useState(false);
const inputRef = useRef(null);
const create = useCreateApiKey();
@@ -65,7 +81,8 @@ export function CreateApiKeyDialog() {
function reset() {
setName('');
setRevealed(null);
- setCopied(false);
+ setCopiedKey(false);
+ setCopiedCmd(false);
create.reset();
}
@@ -81,23 +98,50 @@ export function CreateApiKeyDialog() {
}
}
- async function copyToClipboard() {
- if (!revealed) return;
- // navigator.clipboard requires a secure context (HTTPS or localhost). On
- // bare-IP / HTTP deploys it throws — fall back to document.execCommand
- // through a transient textarea so users still get one-click copy.
+ // copyText copies one string to the clipboard, surfacing a toast on failure.
+ // navigator.clipboard requires a secure context (HTTPS or localhost). On
+ // bare-IP / HTTP deploys it throws — fall back to document.execCommand
+ // through a transient textarea so users still get one-click copy.
+ async function copyText(text: string): Promise {
try {
if (window.isSecureContext && navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(revealed);
+ await navigator.clipboard.writeText(text);
} else {
- if (!legacyCopy(revealed)) throw new Error('legacy copy failed');
+ if (!legacyCopy(text)) throw new Error('legacy copy failed');
}
- setCopied(true);
- window.setTimeout(() => setCopied(false), 2000);
+ return true;
} catch {
toast.error('Could not copy automatically.', {
- description: 'Click the field, ⌘A / Ctrl-A to select, then copy.',
+ description: 'Select the text, ⌘A / Ctrl-A, then copy.',
});
+ return false;
+ }
+ }
+
+ // One-paste command that registers THIS server (URL + the freshly minted key,
+ // as a single `server.` entry) in the cix CLI config and makes it the
+ // default. The dashboard is served same-origin from cix-server, so
+ // window.location.origin is exactly the base URL the CLI must talk to. Values
+ // are shell-safe (a URL, a `cix_` key, an [a-z0-9-] alias), so no quoting
+ // is needed — matching the CLI README's unquoted examples.
+ const alias = cixServerAlias(window.location.host);
+ const connectCmd =
+ `cix config set server.${alias}.url ${window.location.origin} && ` +
+ `cix config set server.${alias}.key ${revealed ?? ''} && ` +
+ `cix config set default_server ${alias}`;
+
+ async function copyKey() {
+ if (!revealed) return;
+ if (await copyText(revealed)) {
+ setCopiedKey(true);
+ window.setTimeout(() => setCopiedKey(false), 2000);
+ }
+ }
+
+ async function copyConnectCmd() {
+ if (await copyText(connectCmd)) {
+ setCopiedCmd(true);
+ window.setTimeout(() => setCopiedCmd(false), 2000);
}
}
@@ -165,8 +209,8 @@ export function CreateApiKeyDialog() {
onFocus={(e) => e.currentTarget.select()}
onClick={(e) => e.currentTarget.select()}
/>
-
+
+
+
+ {/* Read-only, wrapping command box: one paste registers this
+ server (URL + key as a single entry) and makes it the
+ default, so `cix search …` works right after. */}
+
+ Paste this in a terminal with the cix CLI installed. It saves the
+ server to ~/.cix/config.yaml as the default, then{' '}
+ cix search "…" works.
+
+
{
From 880346306fd833fa8056aa651f6ff8b7ab446c16 Mon Sep 17 00:00:00 2001
From: dvcdsys
Date: Wed, 3 Jun 2026 11:23:35 +0100
Subject: [PATCH 2/2] fix(dashboard): make the X button close the "API key
created" popup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The onOpenChange handler returned early whenever a key was revealed
(`if (!next && revealed) return;`), which swallowed the close event from
the top-right X button — the dialog could then only be dismissed by
reloading the page. Accidental dismissal (outside-click, Escape) is
already blocked separately at the DialogContent layer via preventDefault,
so that early-return only ever harmed the explicit X click.
Remove the guard so X (and "I've saved it") close and reset the dialog,
while Escape / outside-click stay blocked at the content layer.
Co-Authored-By: Claude Opus 4.8
---
.../api-keys/components/CreateApiKeyDialog.tsx | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
index 633bd85..8ad90fc 100644
--- a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
+++ b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
@@ -149,11 +149,14 @@ export function CreateApiKeyDialog() {