From 1b7d301153aaf8a7a1e674e05b0a4e278640a05e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 23 Jul 2026 19:20:15 +0700 Subject: [PATCH] feat(api, web): add agent stats reset functionality and enhance UI components - Implemented a new API endpoint to reset agent statistics, allowing for better management of agent performance data. - Updated the AgentCard component to display traffic statistics in a consolidated format, improving clarity for users. - Enhanced the AgentDetailView to include a button for resetting agent stats, providing a direct action for users. - Refactored the AgentFleetDataGrid to show combined traffic metrics, streamlining data presentation. - Added a utility function to delete stats samples for agents in the database, ensuring data integrity. These changes improve the user experience by providing more intuitive controls and clearer data representation for agent statistics. --- apps/api/src/routes/control.ts | 22 ++++++++ apps/web/src/components/agents/agent-card.tsx | 33 ++++++++---- .../components/agents/agent-detail-sheet.tsx | 9 +++- .../components/agents/agent-detail-view.tsx | 44 ++++++++++----- .../agents/agent-fleet-data-grid.tsx | 53 +++++++------------ packages/db/src/repositories/index.ts | 7 +++ 6 files changed, 110 insertions(+), 58 deletions(-) diff --git a/apps/api/src/routes/control.ts b/apps/api/src/routes/control.ts index b67a4e3..a8905c7 100644 --- a/apps/api/src/routes/control.ts +++ b/apps/api/src/routes/control.ts @@ -968,6 +968,28 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( })), })) + app.post<{ Params: { id: string } }>( + '/agents/:id/stats/reset', + async (req) => { + const agent = repos.getAgent(app.db, req.params.id) + if (!agent) throw new AppError('NOT_FOUND', 'Agent not found', 404) + const updated = repos.updateAgent(app.db, agent.id, { + lastApplyPacketsDropped: 0, + lastApplyPacketsAccepted: 0, + }) + repos.deleteStatsSamplesForAgent(app.db, agent.id) + auditMutation(app, config, req, { + action: 'agent.stats_reset', + severity: 'info', + targetType: 'app_resource', + targetId: agent.id, + summary: `Сброшена статистика counters агента ${agent.name}`, + details: { agent_id: agent.id }, + }) + return mapAgent(updated!) + }, + ) + app.get('/stats/recent', async () => ({ items: repos.listRecentStats(app.db).map((s) => ({ id: s.id, diff --git a/apps/web/src/components/agents/agent-card.tsx b/apps/web/src/components/agents/agent-card.tsx index 9d202d8..949d030 100644 --- a/apps/web/src/components/agents/agent-card.tsx +++ b/apps/web/src/components/agents/agent-card.tsx @@ -55,6 +55,10 @@ export function AgentCard({ agent, selected, onSelect }: AgentCardProps) { const hasApply = Boolean(agent.last_apply_at || agent.last_apply_status) const dropped = formatPackets(agent.last_apply_packets_dropped, hasApply) const accepted = formatPackets(agent.last_apply_packets_accepted, hasApply) + const traffic = + dropped === '—' && accepted === '—' + ? '—' + : `↓${dropped} · ↑${accepted}` const seen = formatShort(agent.last_seen_at ?? agent.last_apply_at) const defaultAction = agent.default_action === 'drop' ? 'Drop' : 'Accept' @@ -68,19 +72,28 @@ export function AgentCard({ agent, selected, onSelect }: AgentCardProps) { const stats = [ { - label: 'Dropped', - value: dropped, - valueClass: dropped === '—' ? undefined : 'text-warning', - }, - { - label: 'Accepted', - value: accepted, - valueClass: accepted === '—' ? undefined : 'text-success', + label: 'Traffic', + value: traffic, + valueClass: + traffic === '—' + ? undefined + : '[&]:text-foreground [&>span]:tabular-nums', + valueNode: + traffic === '—' ? ( + '—' + ) : ( + <> + ↓{dropped} + · + ↑{accepted} + + ), }, { label: 'Seen', value: seen, valueClass: 'text-muted-foreground', + valueNode: seen, }, ] as const @@ -129,7 +142,7 @@ export function AgentCard({ agent, selected, onSelect }: AgentCardProps) { -
+
{stats.map((stat, index) => (
- {stat.value} + {stat.valueNode} {stat.label} diff --git a/apps/web/src/components/agents/agent-detail-sheet.tsx b/apps/web/src/components/agents/agent-detail-sheet.tsx index b5fdfc4..67fad83 100644 --- a/apps/web/src/components/agents/agent-detail-sheet.tsx +++ b/apps/web/src/components/agents/agent-detail-sheet.tsx @@ -13,6 +13,7 @@ import { SheetHeader, SheetTitle, } from '@evofw/ui/components/sheet' +import { cn } from '@evofw/ui/lib/utils' /** * Wide agent detail Sheet — inventory-9 / CRM-4 shell + SA3 body. @@ -45,7 +46,13 @@ export function AgentDetailSheet({
diff --git a/apps/web/src/components/agents/agent-detail-view.tsx b/apps/web/src/components/agents/agent-detail-view.tsx index 5b6e6c7..4bee41c 100644 --- a/apps/web/src/components/agents/agent-detail-view.tsx +++ b/apps/web/src/components/agents/agent-detail-view.tsx @@ -2,8 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { useRef, useState } from 'react' import { - BanIcon, - CheckCircle2Icon, + ActivityIcon, CircleAlertIcon, ClockIcon, Copy, @@ -88,6 +87,20 @@ export function AgentDetailView({ agentId }: AgentDetailViewProps) { onError: (e: Error) => toast.error(e.message), }) + const resetStats = useMutation({ + mutationFn: () => + apiFetch(`/api/v1/agents/${agentId}/stats/reset`, { method: 'POST' }), + onSuccess: () => { + toast.success('Статистика сброшена') + void qc.invalidateQueries({ queryKey: ['agents'] }) + void qc.invalidateQueries({ queryKey: ['agents', agentId] }) + void qc.invalidateQueries({ queryKey: ['agents', agentId, 'stats'] }) + void qc.invalidateQueries({ queryKey: ['stats'] }) + void qc.invalidateQueries({ queryKey: ['dashboard'] }) + }, + onError: (e: Error) => toast.error(e.message), + }) + const a = agentQ.data if (agentQ.isLoading || !a) { @@ -229,21 +242,24 @@ export function AgentDetailView({ agentId }: AgentDetailViewProps) { , + id: 'traffic', + icon: , iconClassName: 'text-warning', - label: 'Dropped', - description: String(a.last_apply_packets_dropped ?? 0), + label: 'Traffic', + description: `↓${a.last_apply_packets_dropped ?? 0} · ↑${a.last_apply_packets_accepted ?? 0}`, hint: 'сумма counters', variant: 'warning', - }, - { - id: 'accepted', - icon: , - iconClassName: 'text-success', - label: 'Accepted', - description: String(a.last_apply_packets_accepted ?? 0), - hint: 'сумма counters', + footer: ( + + ), }, { id: 'kernel', diff --git a/apps/web/src/components/agents/agent-fleet-data-grid.tsx b/apps/web/src/components/agents/agent-fleet-data-grid.tsx index f53676d..778bed8 100644 --- a/apps/web/src/components/agents/agent-fleet-data-grid.tsx +++ b/apps/web/src/components/agents/agent-fleet-data-grid.tsx @@ -212,47 +212,35 @@ export function AgentFleetDataGrid({ }, }, { - id: 'dropped', - size: 90, - minSize: 80, - maxSize: 110, - accessorFn: (row) => row.last_apply_packets_dropped ?? -1, + id: 'traffic', + size: 130, + minSize: 110, + maxSize: 160, + accessorFn: (row) => + (row.last_apply_packets_dropped ?? 0) + + (row.last_apply_packets_accepted ?? 0), header: ({ column }) => ( - + ), cell: ({ row }) => { const a = row.original const hasApply = Boolean(a.last_apply_at || a.last_apply_status) - const text = formatPackets(a.last_apply_packets_dropped, hasApply) - if (text === '—') { - return - } - return ( - - {text} - + const dropped = formatPackets( + a.last_apply_packets_dropped, + hasApply, ) - }, - }, - { - id: 'accepted', - size: 90, - minSize: 80, - maxSize: 110, - accessorFn: (row) => row.last_apply_packets_accepted ?? -1, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const a = row.original - const hasApply = Boolean(a.last_apply_at || a.last_apply_status) - const text = formatPackets(a.last_apply_packets_accepted, hasApply) - if (text === '—') { + const accepted = formatPackets( + a.last_apply_packets_accepted, + hasApply, + ) + if (dropped === '—' && accepted === '—') { return } return ( - - {text} + + ↓{dropped} + · + ↑{accepted} ) }, @@ -368,7 +356,6 @@ export function AgentFleetDataGrid({ searchPlaceholder="Поиск агентов…" getSearchText={getSearchText} tableLayout={{ width: 'fixed', columnsResizable: true }} - columnPinning={{ right: ['actions'] }} onRowClick={(row) => onSelect(row.id)} tabs={tabs} activeTab={activeTab} diff --git a/packages/db/src/repositories/index.ts b/packages/db/src/repositories/index.ts index fa2d60f..ac25ee7 100644 --- a/packages/db/src/repositories/index.ts +++ b/packages/db/src/repositories/index.ts @@ -431,6 +431,12 @@ export function listRecentStats(db: Db, limit = 500) { .all() } +export function deleteStatsSamplesForAgent(db: Db, agentId: string) { + db.delete(agentStatsSamples) + .where(eq(agentStatsSamples.agentId, agentId)) + .run() +} + export function getSetting(db: Db, key: string): string { const row = db.select().from(settings).where(eq(settings.key, key)).get() return row?.value ?? '' @@ -607,6 +613,7 @@ export const repos = { insertStatsSample, listStatsSamples, listRecentStats, + deleteStatsSamplesForAgent, getSetting, setSetting, listSettings,