Skip to content

Commit 1d0915f

Browse files
cliffhallclaude
andcommitted
feat(export): wire History + Logs export JSON handlers (#1354)
Replaces the `todoNoop` placeholders in App.tsx with real handlers that serialize the current entries and trigger a browser download via the existing `downloadJsonFile` helper. - Adds `buildExportFilename(kind, serverId, now?)` next to the download helper. Produces sortable, filesystem-safe names like `inspector-history-alpha-2026-03-17T10-00-42.123Z.json` (URL-encodes server ids that contain slashes / spaces / colons, swaps `:` for `-` in the ISO timestamp so the file is safe on Windows). - Wires `onExportHistory` (full MessageEntry list) and `onExportLogs` (the LogEntryData list the screen renders). - Routes the existing Network export through the same filename helper for consistency. Unit tests cover the filename builder, including the no-server-id fallback, special-character encoding, and the default-`now` shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4641339 commit 1d0915f

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

clients/web/src/App.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {
5151
} from "./components/groups/ServerConfigModal/ServerConfigModal";
5252
import { ServerSettingsModal } from "./components/groups/ServerSettingsModal/ServerSettingsModal";
5353
import { ServerRemoveConfirmModal } from "./components/groups/ServerRemoveConfirmModal/ServerRemoveConfirmModal";
54-
import { downloadJsonFile } from "./lib/downloadFile";
54+
import { buildExportFilename, downloadJsonFile } from "./lib/downloadFile";
5555
import { createWebEnvironment } from "./lib/environmentFactory";
5656

5757
// OAuth redirect URL provider — points at the dev backend's `/oauth/callback`
@@ -679,8 +679,27 @@ function App() {
679679

680680
const onExportNetwork = useCallback(() => {
681681
if (fetchRequests.length === 0) return;
682-
downloadJsonFile("network.json", JSON.stringify(fetchRequests, null, 2));
683-
}, [fetchRequests]);
682+
downloadJsonFile(
683+
buildExportFilename("network", activeServerId),
684+
JSON.stringify(fetchRequests, null, 2),
685+
);
686+
}, [fetchRequests, activeServerId]);
687+
688+
const onExportHistory = useCallback(() => {
689+
if (messages.length === 0) return;
690+
downloadJsonFile(
691+
buildExportFilename("history", activeServerId),
692+
JSON.stringify(messages, null, 2),
693+
);
694+
}, [messages, activeServerId]);
695+
696+
const onExportLogs = useCallback(() => {
697+
if (logs.length === 0) return;
698+
downloadJsonFile(
699+
buildExportFilename("logs", activeServerId),
700+
JSON.stringify(logs, null, 2),
701+
);
702+
}, [logs, activeServerId]);
684703

685704
// Action stubs — these UI affordances exist but require additional
686705
// wiring (server CRUD, history pinning, app sandbox round-trip, log
@@ -916,10 +935,10 @@ function App() {
916935
onRefreshTasks={onRefreshTasks}
917936
onSetLogLevel={onSetLogLevel}
918937
onClearLogs={onClearLogs}
919-
onExportLogs={todoNoop}
938+
onExportLogs={onExportLogs}
920939
onCopyAllLogs={todoNoop}
921940
onClearHistory={onClearHistory}
922-
onExportHistory={todoNoop}
941+
onExportHistory={onExportHistory}
923942
onReplayHistory={todoNoop}
924943
onTogglePinHistory={todoNoop}
925944
onClearNetwork={onClearNetwork}

clients/web/src/lib/downloadFile.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,25 @@ export function downloadJsonFile(filename: string, json: string): void {
3030
URL.revokeObjectURL(url);
3131
}
3232
}
33+
34+
/**
35+
* Build a sortable export filename in the shape
36+
* `inspector-<kind>-<server-id>-<ISO timestamp>.json`. The timestamp uses
37+
* the standard ISO-8601 form with `:` swapped for `-` so the result is
38+
* safe on Windows (which disallows `:` in filenames). Server id is
39+
* passed through `encodeURIComponent` for the same reason — config ids
40+
* are user-supplied and may contain slashes / spaces / colons.
41+
*
42+
* When `serverId` is undefined the segment is omitted; the rest of the
43+
* filename still uniquely identifies the export by kind + time.
44+
*/
45+
export function buildExportFilename(
46+
kind: string,
47+
serverId: string | undefined,
48+
now: Date = new Date(),
49+
): string {
50+
const iso = now.toISOString().replace(/:/g, "-");
51+
const id = serverId ? encodeURIComponent(serverId) : undefined;
52+
const segments = ["inspector", kind, ...(id ? [id] : []), iso];
53+
return `${segments.join("-")}.json`;
54+
}

clients/web/src/test/lib/downloadFile.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
8-
import { downloadJsonFile } from "../../lib/downloadFile";
8+
import { downloadJsonFile, buildExportFilename } from "../../lib/downloadFile";
99

1010
describe("downloadJsonFile", () => {
1111
const originalCreate = URL.createObjectURL;
@@ -92,3 +92,32 @@ describe("downloadJsonFile", () => {
9292
expect(text).toBe('{"x":1}');
9393
});
9494
});
95+
96+
describe("buildExportFilename", () => {
97+
const fixedNow = new Date("2026-03-17T10:00:42.123Z");
98+
99+
it("includes kind, server id, and ISO timestamp with `:` swapped for `-`", () => {
100+
expect(buildExportFilename("history", "alpha", fixedNow)).toBe(
101+
"inspector-history-alpha-2026-03-17T10-00-42.123Z.json",
102+
);
103+
});
104+
105+
it("omits the server-id segment when serverId is undefined", () => {
106+
expect(buildExportFilename("logs", undefined, fixedNow)).toBe(
107+
"inspector-logs-2026-03-17T10-00-42.123Z.json",
108+
);
109+
});
110+
111+
it("encodes server ids that contain filesystem-unsafe characters", () => {
112+
expect(buildExportFilename("network", "my server/v2", fixedNow)).toBe(
113+
"inspector-network-my%20server%2Fv2-2026-03-17T10-00-42.123Z.json",
114+
);
115+
});
116+
117+
it("defaults `now` to the current time when not provided", () => {
118+
const name = buildExportFilename("history", "alpha");
119+
expect(name).toMatch(
120+
/^inspector-history-alpha-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z\.json$/,
121+
);
122+
});
123+
});

0 commit comments

Comments
 (0)