diff --git a/furniture-3d-system/src/app/api/supply-chain/script-monitor/agents/[id]/route.ts b/furniture-3d-system/src/app/api/supply-chain/script-monitor/agents/[id]/route.ts new file mode 100644 index 00000000..fb194251 --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/script-monitor/agents/[id]/route.ts @@ -0,0 +1,17 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess } from '@/server/core/response' +import { scriptMonitorService } from '@/server/services/script-monitor.service' + +export async function DELETE( + request: NextRequest, + context: { params: Promise<{ id: string }> }, +) { + return handleRequest( + request, + async (ctx) => { + const { id } = await context.params + return jsonSuccess(await scriptMonitorService.deleteAgent(id), 200, ctx.requestId) + }, + { requireAuth: true }, + ) +} diff --git a/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.test.tsx b/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.test.tsx index 045062df..41156a4e 100644 --- a/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.test.tsx +++ b/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import type { ScriptMonitorOverview } from '@/lib/supply-chain-script-monitor' import { ScriptMonitorView } from './ScriptMonitorView' @@ -132,4 +132,48 @@ describe('ScriptMonitorView', () => { expect(screen.getByText(/不计入已接入机器/)).toBeInTheDocument() expect(screen.getByText('共 0 台已接入')).toBeInTheDocument() }) + + it('confirms the monitoring impact before deleting a machine', async () => { + const originalFetch = global.fetch + const emptyOverview: ScriptMonitorOverview = { + ...overview, + agents: [], + scripts: [], + metrics: { agentTotal: 0, agentConnected: 0, agentOnline: 0, agentPending: 0, scriptTotal: 0, healthy: 0, running: 0, abnormal: 0, unknown: 0 }, + } + global.fetch = jest.fn() + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + success: true, + data: { id: 'agent-1', name: '仓库服务器 01', deletedScripts: 1, deletedRuns: 2, deletedCommands: 0 }, + }), + }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ success: true, data: emptyOverview }), + }) as typeof fetch + + try { + render() + + fireEvent.click(screen.getByRole('button', { name: '删除机器 仓库服务器 01' })) + expect(screen.getByRole('heading', { name: '删除“仓库服务器 01”' })).toBeInTheDocument() + expect(screen.getByText(/这台机器、1 个脚本/)).toBeInTheDocument() + expect(screen.getByText(/真实脚本和文件不会被删除/)).toBeInTheDocument() + expect(screen.getByText(/Agent 当前在线/)).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: '确认删除' })) + + await waitFor(() => expect(global.fetch).toHaveBeenCalledWith( + '/api/supply-chain/script-monitor/agents/agent-1', + { method: 'DELETE' }, + )) + await waitFor(() => expect(screen.getByText(/已删除“仓库服务器 01”及 1 个脚本/)).toBeInTheDocument()) + } finally { + global.fetch = originalFetch + } + }) }) diff --git a/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.tsx b/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.tsx index 8023b5f4..a0a60c8f 100644 --- a/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.tsx +++ b/furniture-3d-system/src/app/supply-chain/script-monitor/ScriptMonitorView.tsx @@ -14,6 +14,7 @@ import { Search, Server, SquareTerminal, + Trash2, TriangleAlert, Wifi, WifiOff, @@ -175,6 +176,8 @@ export function ScriptMonitorView({ const [connectOpen, setConnectOpen] = useState(false) const [confirmRun, setConfirmRun] = useState(null) const [runRequesting, setRunRequesting] = useState(false) + const [confirmDeleteAgent, setConfirmDeleteAgent] = useState(null) + const [agentDeleting, setAgentDeleting] = useState(false) const refresh = useCallback(async (quiet = false) => { if (!databaseReady) return @@ -274,6 +277,30 @@ export function ScriptMonitorView({ } } + async function deleteAgent() { + if (!confirmDeleteAgent) return + const agent = confirmDeleteAgent + setAgentDeleting(true) + setError(null) + try { + const result = await readApi<{ + name: string + deletedScripts: number + }>(await fetch(`/api/supply-chain/script-monitor/agents/${agent.id}`, { + method: 'DELETE', + })) + if (selectedScript?.agent.id === agent.id) setSelectedId(null) + if (agentFilter === agent.id) setAgentFilter('ALL') + setConfirmDeleteAgent(null) + setNotice(`已删除“${result.name}”及 ${result.deletedScripts} 个脚本的监控数据`) + await refresh(true) + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '机器删除失败,请重试') + } finally { + setAgentDeleting(false) + } + } + return ( @@ -384,6 +411,17 @@ export function ScriptMonitorView({ {agent.scriptCount} 个脚本 {agent.lastSeenAt ? `最后心跳 ${formatDateTime(agent.lastSeenAt)}` : '凭据已生成,尚未连接'} + {canEdit ? ( + setConfirmDeleteAgent(agent)} + > + 删除 + + ) : null} @@ -470,6 +508,14 @@ export function ScriptMonitorView({ onConfirm={requestManualRun} /> ) : null} + {confirmDeleteAgent ? ( + setConfirmDeleteAgent(null)} + onConfirm={deleteAgent} + /> + ) : null} + + ) +} + function ConnectAgentDialog({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) { const [name, setName] = useState('') const [networkMode, setNetworkMode] = useState('AUTO') diff --git a/furniture-3d-system/src/server/repositories/script-monitor.repository.ts b/furniture-3d-system/src/server/repositories/script-monitor.repository.ts index 1786fa27..cee14bf2 100644 --- a/furniture-3d-system/src/server/repositories/script-monitor.repository.ts +++ b/furniture-3d-system/src/server/repositories/script-monitor.repository.ts @@ -119,6 +119,13 @@ export interface ScriptMonitorRepository { jobType: ScriptMonitorJobType } } | null> + deleteAgent(id: string): Promise<{ + id: string + name: string + scriptCount: number + runCount: number + commandCount: number + } | null> listOverview(): Promise listRuns(scriptId: string, take: number): Promise { + const agent = await tx.scriptMonitorAgent.findUnique({ + where: { id }, + select: { + id: true, + name: true, + _count: { + select: { + scripts: true, + runs: true, + commands: true, + }, + }, + }, + }) + if (!agent) return null + const deleted = await tx.scriptMonitorAgent.deleteMany({ where: { id } }) + if (deleted.count !== 1) return null + return { + id: agent.id, + name: agent.name, + scriptCount: agent._count.scripts, + runCount: agent._count.runs, + commandCount: agent._count.commands, + } + }) + } + listOverview() { return prisma.scriptMonitorAgent.findMany({ orderBy: [{ isActive: 'desc' }, { name: 'asc' }], diff --git a/furniture-3d-system/src/server/services/script-monitor.service.test.ts b/furniture-3d-system/src/server/services/script-monitor.service.test.ts index f5e02bfb..10ae05dd 100644 --- a/furniture-3d-system/src/server/services/script-monitor.service.test.ts +++ b/furniture-3d-system/src/server/services/script-monitor.service.test.ts @@ -15,6 +15,7 @@ function repositoryMock(): jest.Mocked { findScriptForManualRun: jest.fn(), createRunCommand: jest.fn(), claimPendingCommand: jest.fn(), + deleteAgent: jest.fn(), listOverview: jest.fn(), listRuns: jest.fn(), } @@ -160,4 +161,33 @@ describe('ScriptMonitorService', () => { requestedById: 'user-1', })) }) + + it('deletes one machine and returns the cascaded monitoring record counts', async () => { + const repository = repositoryMock() + repository.deleteAgent.mockResolvedValue({ + id: 'agent-1', + name: '仓库服务器 01', + scriptCount: 3, + runCount: 12, + commandCount: 2, + }) + const service = new ScriptMonitorService(repository) + + await expect(service.deleteAgent(' agent-1 ')).resolves.toEqual({ + id: 'agent-1', + name: '仓库服务器 01', + deletedScripts: 3, + deletedRuns: 12, + deletedCommands: 2, + }) + expect(repository.deleteAgent).toHaveBeenCalledWith('agent-1') + }) + + it('reports a clear error when deleting a missing machine', async () => { + const repository = repositoryMock() + repository.deleteAgent.mockResolvedValue(null) + const service = new ScriptMonitorService(repository) + + await expect(service.deleteAgent('missing-agent')).rejects.toThrow('脚本监控机器不存在或已删除') + }) }) diff --git a/furniture-3d-system/src/server/services/script-monitor.service.ts b/furniture-3d-system/src/server/services/script-monitor.service.ts index df4cec67..5b20f796 100644 --- a/furniture-3d-system/src/server/services/script-monitor.service.ts +++ b/furniture-3d-system/src/server/services/script-monitor.service.ts @@ -198,6 +198,27 @@ export class ScriptMonitorService { } } + async deleteAgent(id: string) { + const agentId = id.trim() + if (!agentId) { + throw new AppError(ErrorCode.VALIDATION_REQUIRED_FIELD, '缺少脚本监控机器 ID') + } + if (agentId.length > 80) { + throw new AppError(ErrorCode.VALIDATION_INVALID_FORMAT, '脚本监控机器 ID 不正确') + } + const deleted = await this.repository.deleteAgent(agentId) + if (!deleted) { + throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '脚本监控机器不存在或已删除') + } + return { + id: deleted.id, + name: deleted.name, + deletedScripts: deleted.scriptCount, + deletedRuns: deleted.runCount, + deletedCommands: deleted.commandCount, + } + } + async getOverview(): Promise { const records = await this.repository.listOverview() const now = Date.now()