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
@@ -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 };
|
||||
|
||||
Reference in new issue
Block a user