From b786320ed2c87c07069916d59c5d5aac28725cdb Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Mon, 18 May 2026 15:19:46 +0200 Subject: [PATCH] fix(cli): use correct dashboard url --- .changeset/dashboard-url-in-response.md | 5 + packages/cli/AGENTS.md | 2 + packages/cli/src/commands/dashboards.test.ts | 173 ++++++++++++++++++ packages/cli/src/commands/dashboards.ts | 4 +- skills/create-dashboard/SKILL.md | 2 +- skills/create-dashboard/rules/workflow.md | 9 +- skills/create-dashboard/tile.json | 2 +- .../rules/workflow-identify-cause.md | 8 +- 8 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 .changeset/dashboard-url-in-response.md create mode 100644 packages/cli/src/commands/dashboards.test.ts diff --git a/.changeset/dashboard-url-in-response.md b/.changeset/dashboard-url-in-response.md new file mode 100644 index 0000000..61c2950 --- /dev/null +++ b/.changeset/dashboard-url-in-response.md @@ -0,0 +1,5 @@ +--- +"@kopai/cli": minor +--- + +`dashboards create` response now includes a `url` field — the fully-resolved dashboard URL based on the active `.kopairc` (or `--url` flag, or default). Callers (notably the `create-dashboard` skill) should display this URL directly instead of constructing it from `id`. diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index eced219..996f395 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -158,6 +158,8 @@ kopai dashboards schema echo '{"uiTree":{"root":"s1","elements":{"s1":{"key":"s1","type":"Stack","props":{"direction":"vertical","gap":"md","align":null},"children":[],"parentKey":""}}},"metadata":{}}' | kopai dashboards create --name "My Dashboard" --tree-version "0.5.0" --json ``` +The response includes a `url` field — the fully-resolved dashboard URL based on the active `.kopairc` (or `--url` flag, or default). Display it directly. + ## Examples for Agents ```bash diff --git a/packages/cli/src/commands/dashboards.test.ts b/packages/cli/src/commands/dashboards.test.ts new file mode 100644 index 0000000..944232b --- /dev/null +++ b/packages/cli/src/commands/dashboards.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { Command } from "commander"; +import { Readable } from "node:stream"; + +vi.mock("../config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: vi.fn().mockReturnValue({}), + }; +}); + +const mockCreateDashboard = vi.fn(); + +vi.mock("@kopai/sdk", () => ({ + KopaiClient: class MockKopaiClient { + createDashboard = mockCreateDashboard; + }, +})); + +import { loadConfig } from "../config.js"; +import { createDashboardsCommand } from "./dashboards.js"; + +const VALID_TREE = { + uiTree: { + root: "s1", + elements: { + s1: { + key: "s1", + type: "Stack", + props: { direction: "vertical", gap: "md", align: null }, + children: [], + parentKey: "", + }, + }, + }, + metadata: {}, +}; + +function pushStdin(payload: string): void { + const stream = Readable.from([Buffer.from(payload, "utf-8")]); + Object.defineProperty(process, "stdin", { + configurable: true, + value: stream, + }); +} + +function runCreate(args: string[], stdin: string): Promise { + return new Promise((resolve, reject) => { + const logs: string[] = []; + vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => + logs.push(a.join(" ")) + ); + vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => + logs.push(a.join(" ")) + ); + + pushStdin(stdin); + + const program = new Command(); + program.exitOverride(); + program.addCommand(createDashboardsCommand()); + + program + .parseAsync([ + "node", + "test", + "dashboards", + "create", + "--name", + "Test", + "--tree-version", + "0.7.0", + "--json", + ...args, + ]) + .then( + () => resolve(logs.join("\n")), + (err) => reject(err) + ); + }); +} + +describe("dashboards create command", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(loadConfig).mockReturnValue({}); + mockCreateDashboard.mockResolvedValue({ + id: "dash-123", + name: "Test", + createdAt: "2026-05-18T00:00:00.000Z", + metadata: {}, + uiTreeVersion: "0.7.0", + uiTree: VALID_TREE.uiTree, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("includes a url built from the .kopairc url in the JSON response", async () => { + vi.mocked(loadConfig).mockReturnValue({ + url: "https://custom.example.com", + }); + + const output = await runCreate([], JSON.stringify(VALID_TREE)); + const parsed = JSON.parse(output) as { url: string; id: string }; + + expect(parsed.id).toBe("dash-123"); + expect(parsed.url).toBe( + "https://custom.example.com/?tab=metrics&dashboardId=dash-123" + ); + }); + + it("uses --url flag value over .kopairc when both present", async () => { + vi.mocked(loadConfig).mockReturnValue({ + url: "https://from-config.example.com", + }); + + const output = await runCreate( + ["--url", "https://from-flag.example.com"], + JSON.stringify(VALID_TREE) + ); + const parsed = JSON.parse(output) as { url: string }; + + expect(parsed.url).toBe( + "https://from-flag.example.com/?tab=metrics&dashboardId=dash-123" + ); + }); + + it("falls back to localhost default when no url is configured", async () => { + vi.mocked(loadConfig).mockReturnValue({}); + + const output = await runCreate([], JSON.stringify(VALID_TREE)); + const parsed = JSON.parse(output) as { url: string }; + + expect(parsed.url).toBe( + "http://localhost:8000/?tab=metrics&dashboardId=dash-123" + ); + }); + + it("strips trailing /signals path from configured url before building dashboard url", async () => { + vi.mocked(loadConfig).mockReturnValue({ + url: "https://hosted.example.com/signals", + }); + + const output = await runCreate([], JSON.stringify(VALID_TREE)); + const parsed = JSON.parse(output) as { url: string }; + + expect(parsed.url).toBe( + "https://hosted.example.com/?tab=metrics&dashboardId=dash-123" + ); + }); + + it("encodes the dashboard id in the url", async () => { + mockCreateDashboard.mockResolvedValue({ + id: "needs/encoding", + name: "Test", + createdAt: "2026-05-18T00:00:00.000Z", + metadata: {}, + uiTreeVersion: "0.7.0", + uiTree: VALID_TREE.uiTree, + }); + + const output = await runCreate([], JSON.stringify(VALID_TREE)); + const parsed = JSON.parse(output) as { url: string }; + + expect(parsed.url).toBe( + "http://localhost:8000/?tab=metrics&dashboardId=needs%2Fencoding" + ); + }); +}); diff --git a/packages/cli/src/commands/dashboards.ts b/packages/cli/src/commands/dashboards.ts index 4c47afe..e255afd 100644 --- a/packages/cli/src/commands/dashboards.ts +++ b/packages/cli/src/commands/dashboards.ts @@ -64,6 +64,7 @@ Example: const isJson = opts.json ?? false; try { const client = createClient(opts); + const { url: baseUrl } = resolveConnectionOpts(opts); const raw = await readStdin(); let body: Record; try { @@ -84,7 +85,8 @@ Example: }); const format = detectFormat(opts.json, opts.table); - output(result, { format }); + const url = `${baseUrl}/?tab=metrics&dashboardId=${encodeURIComponent(result.id)}`; + output({ ...result, url }, { format }); } catch (err) { outputError(err, isJson); process.exit(1); diff --git a/skills/create-dashboard/SKILL.md b/skills/create-dashboard/SKILL.md index e7fe97a..6d95f6b 100644 --- a/skills/create-dashboard/SKILL.md +++ b/skills/create-dashboard/SKILL.md @@ -4,7 +4,7 @@ description: Create observability dashboards from OTEL metrics, logs, and traces license: Apache-2.0 metadata: author: kopai - version: "1.1.0" + version: "1.2.0" --- # Create Dashboard with Kopai diff --git a/skills/create-dashboard/rules/workflow.md b/skills/create-dashboard/rules/workflow.md index 4385ca7..9030af6 100644 --- a/skills/create-dashboard/rules/workflow.md +++ b/skills/create-dashboard/rules/workflow.md @@ -94,14 +94,7 @@ When creation fails, read the error message, fix the tree, and retry. Do not gue ## Post-Creation -After the dashboard is created, display the URL to the user: - -``` -/?tab=metrics&dashboardId= -``` - -- `` — the `id` field from the CLI JSON response -- `` — the URL used for the CLI command: the `--url` flag value, or `http://localhost:8000` if omitted +After the dashboard is created, display the URL to the user. The CLI JSON response includes a `url` field — the fully-resolved dashboard URL based on the active `.kopairc` (or `--url` flag, or default). Show that value directly. Do not construct the URL yourself. Common pitfalls: diff --git a/skills/create-dashboard/tile.json b/skills/create-dashboard/tile.json index a680a19..ccf1282 100644 --- a/skills/create-dashboard/tile.json +++ b/skills/create-dashboard/tile.json @@ -1,6 +1,6 @@ { "name": "kopai/create-dashboard", - "version": "1.1.0", + "version": "1.2.0", "private": false, "summary": "Create observability dashboards from OTEL metrics, logs, and traces using Kopai. Use when building metric visualizations, monitoring views, KPI panels, or when the user wants to see their telemetry data in a dashboard — even if they don't say \"dashboard\" explicitly. Also use when other skills or workflows need to present telemetry data visually (e.g. after root cause analysis).", "skills": { diff --git a/skills/root-cause-analysis/rules/workflow-identify-cause.md b/skills/root-cause-analysis/rules/workflow-identify-cause.md index 54037b2..9a9f5f7 100644 --- a/skills/root-cause-analysis/rules/workflow-identify-cause.md +++ b/skills/root-cause-analysis/rules/workflow-identify-cause.md @@ -36,13 +36,7 @@ The dashboard should include: 2. **Logs** — LogTimeline component filtered to the affected service(s) during the incident timeframe (dataSource method: `searchLogsPage` with `serviceName` param) 3. **Traces** — TraceDetail component showing a representative error trace -After dashboard creation, present the link to the user: - -``` -/?tab=metrics&dashboardId= -``` - -Where `` is from the CLI JSON response and `` is the `--url` flag value or `http://localhost:8000` if omitted. +After dashboard creation, present the link to the user — use the `url` field from the CLI JSON response. It's the fully-resolved dashboard URL based on the active `.kopairc` (or `--url` flag, or default). Do not construct the URL yourself. ### Why create a dashboard?