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,