feat: delete traffic targets and LLM task conversations from UI
Add delete controls to the Traffic and LLM Records pages so users can purge recorded data without hand-editing the data volume. Traffic page: - Host target picker (Popover with checkboxes + select-all toggle) - Exact-match batch delete for multiple hosts (DELETE /api/traffic/hosts) - Substring delete for the host-filter input (DELETE /api/traffic?host=) - Orphaned content-addressed blob garbage collection after deletes - Distinct-hosts endpoint (GET /api/traffic/hosts) for the picker LLM Records page: - Task picker dropdown (distinct task_id + record counts) - Per-task delete (DELETE /api/llm/records?task=) Other: - Add /dist/ to .gitignore (build artifact) - Extend traffic_test.go with DeleteHost, DeleteHostsExact, Hosts tests - Fix Popover render bug (replace ScrollArea that broke height calc) - Remove fade/zoom animation on the host picker Popover
This commit is contained in:
1 parent
5962826f2d
commit
c76d0f4c93
12 files changed
+883
-20
No files matched your search
+14
-5
@@ -7,13 +7,13 @@ import { MOCK } from "@/lib/mock/enabled";
|
||||
import { mockHandle } from "@/lib/mock/handler";
|
||||
import type {
|
||||
Task, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, Activity,
|
||||
Audit, TrafficResp, TrafficDetail, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
|
||||
Audit, TrafficResp, TrafficDetail, TrafficHost, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
|
||||
PromptVersion, MCPServer, MCPTool, SkillItem, TokenUsage, TokenTotal, DailyTokenBucket,
|
||||
Tool, Conversation, AgentTrigger,
|
||||
InterceptRule, InterceptPending, InterceptApprovalRow,
|
||||
TaskAssetView, SSProject, SSTask, ConvTokenSummary, CoverageGraphData, CoverageAssetRefs,
|
||||
WorkspaceListing, WorkspaceFile,
|
||||
CommandRecord, LLMRecordItem, LLMRecordDetail,
|
||||
CommandRecord, LLMRecordItem, LLMRecordDetail, LLMTask,
|
||||
} from "@/lib/types";
|
||||
|
||||
function getToken(): string | null {
|
||||
@@ -91,7 +91,7 @@ const get = <T>(p: string) => http<T>(p);
|
||||
const post = <T>(p: string, body?: unknown) => http<T>(p, { method: "POST", body: body ? JSON.stringify(body) : undefined });
|
||||
const put = <T>(p: string, body?: unknown) => http<T>(p, { method: "PUT", body: body ? JSON.stringify(body) : undefined });
|
||||
const patch = <T>(p: string, body?: unknown) => http<T>(p, { method: "PATCH", body: body ? JSON.stringify(body) : undefined });
|
||||
const del = <T>(p: string) => http<T>(p, { method: "DELETE" });
|
||||
const del = <T>(p: string, body?: unknown) => http<T>(p, { method: "DELETE", body: body ? JSON.stringify(body) : undefined });
|
||||
|
||||
// Go serializes nil slices as JSON null — coerce to [].
|
||||
const arr = <T>(x: T[] | null | undefined): T[] => x ?? [];
|
||||
@@ -253,6 +253,11 @@ export const api = {
|
||||
),
|
||||
trafficExchange: (id: string) =>
|
||||
get<TrafficDetail>(`/traffic/exchange?id=${encodeURIComponent(id)}`),
|
||||
trafficHosts: () => get<{ hosts: TrafficHost[] }>(`/traffic/hosts`),
|
||||
trafficDeleteHost: (host: string) =>
|
||||
del<{ deleted: number }>(`/traffic?host=${encodeURIComponent(host)}`),
|
||||
trafficDeleteHosts: (hosts: string[]) =>
|
||||
del<{ deleted: number }>(`/traffic/hosts`, { hosts }),
|
||||
|
||||
// ---- app settings (runtime toggles) ----
|
||||
settings: () => get<Settings>(`/settings`),
|
||||
@@ -543,13 +548,17 @@ export const api = {
|
||||
},
|
||||
|
||||
// ---- LLM records ----
|
||||
llmRecords: (params?: { model?: string; session?: string; page?: number; size?: number }) => {
|
||||
llmRecords: (params?: { model?: string; session?: string; task?: string; page?: number; size?: number }) => {
|
||||
const sp = new URLSearchParams();
|
||||
if (params?.model) sp.set("model", params.model);
|
||||
if (params?.session) sp.set("session", params.session);
|
||||
sp.set("page", String(params?.page ?? 0));
|
||||
if (params?.task) sp.set("task", params.task);
|
||||
if (params?.page !== undefined) sp.set("page", String(params.page));
|
||||
sp.set("size", String(params?.size ?? 50));
|
||||
return get<{ records: LLMRecordItem[]; total: number }>(`/llm/records?${sp}`);
|
||||
},
|
||||
llmRecordDetail: (id: number) => get<LLMRecordDetail>(`/llm/records/${id}`),
|
||||
llmTasks: () => get<{ tasks: LLMTask[] }>(`/llm/records/tasks`),
|
||||
llmRecordsDeleteTask: (task: string) =>
|
||||
del<{ deleted: number }>(`/llm/records?task=${encodeURIComponent(task)}`),
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ConvTokenSummary, DailyTokenBucket, Edge, Finding, InterceptApprovalRow,
|
||||
InterceptPending, InterceptRule, LLMProfile, MCPServer, MCPTool, PromptVar,
|
||||
PromptVersion, Settings, SkillItem, Stats, TaskNode, Task, TokenTotal,
|
||||
TokenUsage, Tool, TrafficResp, TrafficDetail,
|
||||
TokenUsage, Tool, TrafficResp, TrafficDetail, TrafficHost, LLMTask,
|
||||
} from "@/lib/types";
|
||||
|
||||
const T = (iso: string) => iso; // readability helper for timestamps
|
||||
@@ -341,6 +341,11 @@ export const traffic: TrafficResp = {
|
||||
})),
|
||||
};
|
||||
|
||||
// Distinct hosts with counts, derived from the exchanges above (target picker).
|
||||
const hostCounts: Record<string, number> = {};
|
||||
for (const [, host] of exchanges) hostCounts[host] = (hostCounts[host] ?? 0) + 1;
|
||||
export const trafficHosts: TrafficHost[] = Object.entries(hostCounts).map(([host, count]) => ({ host, count }));
|
||||
|
||||
export const trafficDetail: TrafficDetail = {
|
||||
req: `GET /v1/orders?id=1002 HTTP/1.1
|
||||
Host: api.acme.com
|
||||
@@ -641,6 +646,11 @@ export const llmRecords = [
|
||||
{ id: 4, ts: T("2026-08-09T02:16:05Z"), model: "claude-opus-4-8", profile_name: "默认", session_id: "exp1-worker-i18", task_id: "t-001", worker: "worker-2", latency_ms: 900, input_tokens: 7600, output_tokens: 0, cache_read: 5200, cache_write: 0, status: "error", error: "429 Too Many Requests(已退避重试)" },
|
||||
];
|
||||
|
||||
// Distinct tasks with counts, derived from llmRecords above (task picker).
|
||||
const llmTaskCounts: Record<string, number> = {};
|
||||
for (const r of llmRecords) if (r.task_id) llmTaskCounts[r.task_id] = (llmTaskCounts[r.task_id] ?? 0) + 1;
|
||||
export const llmTasks: LLMTask[] = Object.entries(llmTaskCounts).map(([task_id, count]) => ({ task_id, count }));
|
||||
|
||||
export function llmRecordDetail(id: number) {
|
||||
const item = llmRecords.find((r) => r.id === id) ?? llmRecords[0];
|
||||
return {
|
||||
|
||||
@@ -90,6 +90,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
|
||||
// ── traffic / audit / settings ──
|
||||
if (path === "/audit") return D.audit;
|
||||
if (path === "/traffic" && m === "DELETE") return { deleted: 0 };
|
||||
if (path === "/traffic/hosts" && m === "DELETE") return { deleted: (b.hosts as unknown[])?.length ?? 0 };
|
||||
if (path === "/traffic/hosts") return { hosts: D.trafficHosts };
|
||||
if (path === "/traffic") return D.traffic;
|
||||
if (path === "/traffic/exchange") return D.trafficDetail;
|
||||
if (path === "/settings" && m === "GET") return D.settings;
|
||||
@@ -104,6 +107,8 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
|
||||
// ── LLM ──
|
||||
if (path === "/llm/records" && m === "GET") return { records: D.llmRecords, total: D.llmRecords.length };
|
||||
if (path === "/llm/records" && m === "DELETE") return { deleted: 0 };
|
||||
if (path === "/llm/records/tasks") return { tasks: D.llmTasks };
|
||||
if (seg[0] === "llm" && seg[1] === "records" && seg.length === 3 && m === "GET") return D.llmRecordDetail(Number(seg[2]));
|
||||
if (path === "/llm" && m === "GET") return D.llmConfig;
|
||||
if (path === "/llm" && m === "POST") return { ok: true };
|
||||
|
||||
@@ -403,6 +403,12 @@ export interface TrafficDetail {
|
||||
resp: string;
|
||||
}
|
||||
|
||||
// One distinct recorded host with its exchange count (target picker).
|
||||
export interface TrafficHost {
|
||||
host: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ---- App settings (runtime toggles) ----
|
||||
export interface Settings {
|
||||
traffic_capture: boolean;
|
||||
@@ -657,3 +663,9 @@ export interface LLMRecordDetail extends LLMRecordItem {
|
||||
request_body: string;
|
||||
response_body: string;
|
||||
}
|
||||
|
||||
// One distinct task with its LLM-record count (task picker on the records page).
|
||||
export interface LLMTask {
|
||||
task_id: string;
|
||||
count: number;
|
||||
}
|
||||
Reference in new issue
Block a user