From 69e903aa1b1dd7e9f9658ce3ebff4a83598280e5 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 23 Jul 2026 19:47:17 +0700 Subject: [PATCH] feat(api, web): enhance agent traffic statistics and update installation scripts - Improved the collection and reporting of agent traffic statistics, including total packets dropped and accepted, to provide a more comprehensive view of agent performance. - Updated the `evofw-firewall.sh` script to capture and report traffic statistics before chain recreation, ensuring accurate data retention. - Enhanced the installation script to support updates on already-installed agents, allowing for script and timer refresh without re-enrollment, while preserving existing credentials. - Refactored UI components to utilize new traffic statistics, improving clarity and user experience in displaying agent performance metrics. These changes enhance the overall functionality and usability of the agent management system, providing better insights and easier updates for users. --- apps/api/src/agent-scripts/evofw-firewall.sh | 30 ++- apps/api/src/agent-scripts/install.sh | 230 +++++++++++------- .../src/agent-scripts/mikrotik-install.rsc | 57 +++-- apps/api/src/routes/agent.ts | 28 ++- apps/api/src/routes/control.ts | 8 +- apps/web/src/components/agents/agent-card.tsx | 11 +- .../components/agents/agent-detail-view.tsx | 8 +- .../agents/agent-fleet-data-grid.tsx | 20 +- .../src/components/agents/agent-traffic.ts | 19 ++ apps/web/src/routes/_auth/index.tsx | 3 +- docs/agents.md | 4 + packages/db/migrations/008_total_packets.sql | 9 + packages/db/src/schema.ts | 3 + packages/shared/src/contracts.ts | 3 + 14 files changed, 293 insertions(+), 140 deletions(-) create mode 100644 apps/web/src/components/agents/agent-traffic.ts create mode 100644 packages/db/migrations/008_total_packets.sql diff --git a/apps/api/src/agent-scripts/evofw-firewall.sh b/apps/api/src/agent-scripts/evofw-firewall.sh index 911c149..c8028f5 100644 --- a/apps/api/src/agent-scripts/evofw-firewall.sh +++ b/apps/api/src/agent-scripts/evofw-firewall.sh @@ -118,12 +118,19 @@ nft_add_chunk() { collect_nft_stats() { PACKETS_DROPPED=0; PACKETS_ACCEPTED=0 - local line + local line n while IFS= read -r line; do - if [[ "$line" == *drop* && "$line" =~ packets[[:space:]]+([0-9]+) ]]; then - PACKETS_DROPPED="${BASH_REMATCH[1]}" - elif [[ "$line" == *accept* && "$line" =~ packets[[:space:]]+([0-9]+) ]]; then - PACKETS_ACCEPTED="${BASH_REMATCH[1]}" + [[ "$line" =~ packets[[:space:]]+([0-9]+) ]] || continue + n="${BASH_REMATCH[1]}" + # Policy set hits only (ignore lo / established noise) + if [[ "$line" == *@deny_v4* ]]; then + PACKETS_DROPPED=$((PACKETS_DROPPED + n)) + elif [[ "$line" == *@allow_v4* ]]; then + PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n)) + elif [[ "$line" == *" counter drop"* && "$line" != *@* ]]; then + PACKETS_DROPPED=$((PACKETS_DROPPED + n)) + elif [[ "$line" == *" counter accept"* && "$line" != *@* && "$line" != *established* && "$line" != *"iif \"lo\""* && "$line" != *"iif lo"* ]]; then + PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n)) fi done < <(nft list chain inet evofw input 2>/dev/null || true) } @@ -197,8 +204,11 @@ apply_ipset() { } send_report() { - if [[ "$KERNEL_METHOD" == "nft" ]] || command -v nft >/dev/null 2>&1; then - collect_nft_stats + # If caller already collected (pre-apply), keep those values. + if [[ -z "${STATS_CAPTURED:-}" ]]; then + if [[ "$KERNEL_METHOD" == "nft" ]] || command -v nft >/dev/null 2>&1; then + collect_nft_stats + fi fi local report report=$(printf '{"status":"ok","prefix_count":%s,"packets_dropped":%s,"packets_accepted":%s,"kernel_method":"%s","source":"agent"}' \ @@ -220,6 +230,12 @@ if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH exit 0 fi +# Capture counters BEFORE recreate (nft delete chain zeroes them). +if command -v nft >/dev/null 2>&1; then + collect_nft_stats + STATS_CAPTURED=1 +fi + case "$BACKEND" in nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft diff --git a/apps/api/src/agent-scripts/install.sh b/apps/api/src/agent-scripts/install.sh index a96afe5..36c1e8c 100644 --- a/apps/api/src/agent-scripts/install.sh +++ b/apps/api/src/agent-scripts/install.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # EvoFirewall Linux install one-liner +# Re-run on an already-installed host updates scripts/timer and keeps credentials +# (unless EVOFW_INSTALL_FORCE=1 → full re-enroll). set -euo pipefail if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then @@ -114,12 +116,19 @@ fi CONF_DIR=/etc/evofw CONF_FILE="${CONF_DIR}/agent.conf" SYNC_SCRIPT=/usr/local/sbin/evofw-firewall.sh +UNINSTALL_SCRIPT=/usr/local/sbin/evofw-uninstall.sh PLATFORM="${EVOFW_PLATFORM:-linux}" +# Capture CP from install-link / env before any `source` of agent.conf. +CP_URL="${EVOFW_CP_URL%/}" +LINK_CP_URL="$CP_URL" +CLIENT_NAME_FROM_LINK="$EVOFW_CLIENT_NAME" -if [[ -f "$CONF_FILE" && "${EVOFW_INSTALL_FORCE:-}" != "1" ]]; then - echo "Already installed ($CONF_FILE). Set EVOFW_INSTALL_FORCE=1 to reinstall." >&2 - exit 1 -fi +# Always single-quote values so names with spaces are safe under `source`. +shell_quote() { + local s=$1 + s=${s//\'/\'\\\'\'} + printf "'%s'" "$s" +} gen_token() { if command -v openssl >/dev/null 2>&1; then @@ -129,23 +138,132 @@ gen_token() { fi } -CLIENT_TOKEN="$(gen_token)" -HOSTNAME="$(hostname -f 2>/dev/null || hostname)" -CP_URL="${EVOFW_CP_URL%/}" +detect_backend() { + if command -v nft >/dev/null 2>&1; then + echo nft + elif command -v ipset >/dev/null 2>&1 && command -v iptables >/dev/null 2>&1; then + echo ipset + elif command -v iptables >/dev/null 2>&1; then + echo iptables + else + echo "" + fi +} + +write_conf() { + local client_id=$1 client_token=$2 client_name=$3 backend=$4 + mkdir -p "$CONF_DIR" + chmod 700 "$CONF_DIR" + { + printf 'EVOFW_CP_URL=%s\n' "$(shell_quote "$CP_URL")" + printf 'CLIENT_ID=%s\n' "$(shell_quote "$client_id")" + printf 'CLIENT_TOKEN=%s\n' "$(shell_quote "$client_token")" + printf 'CLIENT_NAME=%s\n' "$(shell_quote "$client_name")" + printf 'KERNEL_BACKEND=%s\n' "$(shell_quote "$backend")" + } >"$CONF_FILE" + chmod 600 "$CONF_FILE" +} + +install_sync_and_uninstall() { + local sync_tmp=$1 + install -m 755 "$sync_tmp" "$SYNC_SCRIPT" + if curl -fsSL "${CP_URL}/v1/agent/uninstall.sh" -o "$UNINSTALL_SCRIPT" 2>/dev/null; then + chmod 755 "$UNINSTALL_SCRIPT" + else + echo "evofw install: warning — could not download uninstall.sh (optional)" >&2 + fi +} + +enable_scheduler_and_run() { + local interval="${EVOFW_SYNC_INTERVAL:-1min}" + if [[ "$HAS_SYSTEMD" -eq 1 ]]; then + cat >/etc/systemd/system/evofw-firewall.service <<'UNIT' +[Unit] +Description=EvoFirewall sync +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/evofw-firewall.sh +UNIT + cat >/etc/systemd/system/evofw-firewall.timer </dev/null | grep -v evofw-firewall || true; echo "*/1 * * * * $SYNC_SCRIPT") | crontab - + "$SYNC_SCRIPT" || true + fi +} + +download_sync_script() { + local out=$1 + if ! curl -fsSL "${CP_URL}/v1/agent/sync-script" -o "$out"; then + echo "failed to download sync script from ${CP_URL}/v1/agent/sync-script" >&2 + return 1 + fi + if ! head -n1 "$out" | grep -q '^#!'; then + echo "sync script is not a shell script (CP returned unexpected body)" >&2 + return 1 + fi +} -# Fail fast: pull sync script before enroll so we never leave a DB agent without a local agent. SYNC_TMP=$(mktemp) ENROLL_TMP=$(mktemp) trap 'rm -f "$SYNC_TMP" "$ENROLL_TMP"' EXIT -if ! curl -fsSL "${CP_URL}/v1/agent/sync-script" -o "$SYNC_TMP"; then - echo "failed to download sync script from ${CP_URL}/v1/agent/sync-script" >&2 - exit 1 -fi -if ! head -n1 "$SYNC_TMP" | grep -q '^#!'; then - echo "sync script is not a shell script (CP returned unexpected body)" >&2 - exit 1 + +# --- Update path: agent already installed --- +if [[ -f "$CONF_FILE" && "${EVOFW_INSTALL_FORCE:-}" != "1" ]]; then + echo "evofw update: existing install at $CONF_FILE — refreshing scripts (credentials kept)" + # shellcheck disable=SC1090 + set -a + # shellcheck source=/dev/null + source "$CONF_FILE" + set +a + if [[ -z "${CLIENT_ID:-}" || -z "${CLIENT_TOKEN:-}" ]]; then + echo "evofw update: $CONF_FILE incomplete (need CLIENT_ID + CLIENT_TOKEN). Set EVOFW_INSTALL_FORCE=1 to re-enroll." >&2 + exit 1 + fi + # Prefer CP URL from this install link / env (stashed before source). + CP_URL="${LINK_CP_URL%/}" + CLIENT_NAME="${CLIENT_NAME_FROM_LINK:-${CLIENT_NAME:-unknown}}" + BACKEND="$(detect_backend)" + if [[ -z "$BACKEND" ]]; then + echo "no supported firewall backend" >&2 + exit 1 + fi + download_sync_script "$SYNC_TMP" || exit 1 + write_conf "$CLIENT_ID" "$CLIENT_TOKEN" "$CLIENT_NAME" "$BACKEND" + install_sync_and_uninstall "$SYNC_TMP" + enable_scheduler_and_run + echo "Updated. Client id=${CLIENT_ID}. Sync script + timer refreshed." + echo "Force sync: $SYNC_SCRIPT" + echo "Uninstall: $UNINSTALL_SCRIPT (or: curl -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)" + exit 0 fi +# --- Fresh install (or EVOFW_INSTALL_FORCE=1) --- +CLIENT_TOKEN="$(gen_token)" +HOSTNAME="$(hostname -f 2>/dev/null || hostname)" + +# Fail fast: pull sync script before enroll so we never leave a DB agent without a local agent. +download_sync_script "$SYNC_TMP" || exit 1 + if [[ -n "${EVOFW_INSTALL_LINK_ID:-}" ]]; then ENROLL_BODY=$(printf '{"name":"%s","hostname":"%s","platform":"%s","token":"%s","client_version":"install.sh/1","install_link_id":"%s"}' \ "$EVOFW_CLIENT_NAME" "$HOSTNAME" "$PLATFORM" "$CLIENT_TOKEN" "$EVOFW_INSTALL_LINK_ID") @@ -176,87 +294,17 @@ if [[ -z "$CLIENT_ID" || "$CLIENT_ID" == "null" ]]; then exit 1 fi -mkdir -p "$CONF_DIR" -chmod 700 "$CONF_DIR" -# Always single-quote values so names with spaces are safe under `source`. -shell_quote() { - local s=$1 - s=${s//\'/\'\\\'\'} - printf "'%s'" "$s" -} -{ - printf 'EVOFW_CP_URL=%s\n' "$(shell_quote "$CP_URL")" - printf 'CLIENT_ID=%s\n' "$(shell_quote "$CLIENT_ID")" - printf 'CLIENT_TOKEN=%s\n' "$(shell_quote "$CLIENT_TOKEN")" - printf 'CLIENT_NAME=%s\n' "$(shell_quote "$EVOFW_CLIENT_NAME")" - printf 'KERNEL_BACKEND=%s\n' "$(shell_quote "auto")" -} >"$CONF_FILE" -chmod 600 "$CONF_FILE" - -install -m 755 "$SYNC_TMP" "$SYNC_SCRIPT" - -UNINSTALL_SCRIPT=/usr/local/sbin/evofw-uninstall.sh -if curl -fsSL "${CP_URL}/v1/agent/uninstall.sh" -o "$UNINSTALL_SCRIPT" 2>/dev/null; then - chmod 755 "$UNINSTALL_SCRIPT" -else - echo "evofw install: warning — could not download uninstall.sh (optional)" >&2 -fi - -if command -v nft >/dev/null 2>&1; then - BACKEND=nft -elif command -v ipset >/dev/null 2>&1 && command -v iptables >/dev/null 2>&1; then - BACKEND=ipset -elif command -v iptables >/dev/null 2>&1; then - BACKEND=iptables -else +BACKEND="$(detect_backend)" +if [[ -z "$BACKEND" ]]; then echo "no supported firewall backend" >&2 exit 1 fi -# Replace KERNEL_BACKEND line without breaking other quoted values. -if grep -q '^KERNEL_BACKEND=' "$CONF_FILE"; then - grep -v '^KERNEL_BACKEND=' "$CONF_FILE" >"${CONF_FILE}.tmp" - printf 'KERNEL_BACKEND=%s\n' "$(shell_quote "$BACKEND")" >>"${CONF_FILE}.tmp" - mv "${CONF_FILE}.tmp" "$CONF_FILE" - chmod 600 "$CONF_FILE" -else - printf 'KERNEL_BACKEND=%s\n' "$(shell_quote "$BACKEND")" >>"$CONF_FILE" -fi -INTERVAL="${EVOFW_SYNC_INTERVAL:-1min}" -if [[ "$HAS_SYSTEMD" -eq 1 ]]; then - cat >/etc/systemd/system/evofw-firewall.service <<'UNIT' -[Unit] -Description=EvoFirewall sync -After=network-online.target -Wants=network-online.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/evofw-firewall.sh -UNIT - cat >/etc/systemd/system/evofw-firewall.timer </dev/null | grep -v evofw-firewall || true; echo "*/1 * * * * $SYNC_SCRIPT") | crontab - - "$SYNC_SCRIPT" || true -fi +write_conf "$CLIENT_ID" "$CLIENT_TOKEN" "$EVOFW_CLIENT_NAME" "$BACKEND" +install_sync_and_uninstall "$SYNC_TMP" +enable_scheduler_and_run echo "Installed. Client id=${CLIENT_ID}. Approve in EvoFirewall UI (rules optional — can assign later)." echo "If still offline after Approve, run: $SYNC_SCRIPT" +echo "Re-run the same install URL to update scripts without re-enroll." echo "Uninstall: $UNINSTALL_SCRIPT (or: curl -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)" diff --git a/apps/api/src/agent-scripts/mikrotik-install.rsc b/apps/api/src/agent-scripts/mikrotik-install.rsc index 62bfd2e..4199e97 100644 --- a/apps/api/src/agent-scripts/mikrotik-install.rsc +++ b/apps/api/src/agent-scripts/mikrotik-install.rsc @@ -2,6 +2,8 @@ # Short-link sets EvofwCpUrl / EvofwSeed / EvofwName / EvofwInstallLinkId before body. # Legacy: set globals, then /import file-name=mikrotik-install.rsc # +# Re-import on an already-enrolled router: skips enroll, refreshes sync + scheduler (token kept). +# # Blacklist: drop EVOFW_DENY on input+forward # Whitelist: accept EVOFW_ALLOW + drop others on forward only (input stays open for Winbox/SSH) @@ -9,31 +11,44 @@ :global EvofwSeed :global EvofwName :global EvofwInstallLinkId +:global EvofwToken :if ([:typeof $EvofwCpUrl] = "nothing" || [:len $EvofwCpUrl] = 0) do={ :error "EvofwCpUrl required" } :if ([:typeof $EvofwSeed] = "nothing" || [:len $EvofwSeed] = 0) do={ :error "EvofwSeed required" } :if ([:typeof $EvofwName] = "nothing" || [:len $EvofwName] = 0) do={ :set EvofwName [/system identity get name] } -:local token ("evofw_" . [/certificate scep-server nonce generate]) -:if ([:len $token] < 20) do={ - :set token ("evofw_" . [:tostr [/system clock get time]] . [:tostr [/system resource get cpu-load]] . [:tostr [/system resource get free-memory]]) -} - -:local body ("{\"name\":\"" . $EvofwName . "\",\"hostname\":\"" . [/system identity get name] . "\",\"platform\":\"mikrotik\",\"token\":\"" . $token . "\",\"client_version\":\"rsc/1\"") -:if ([:typeof $EvofwInstallLinkId] != "nothing" && [:len $EvofwInstallLinkId] > 0) do={ - :set body ($body . ",\"install_link_id\":\"" . $EvofwInstallLinkId . "\"") -} -:set body ($body . "}") - +:local already 0 :do { - /tool fetch url=($EvofwCpUrl . "/v1/agent/enroll") http-method=post http-header-field=("Content-Type: application/json,X-EvoFW-Seed: " . $EvofwSeed) http-data=$body keep-result=no -} on-error={ - :error "evofw: enroll failed — check EvofwCpUrl / EvofwSeed / connectivity" -} + /system script run evofw-env + :if ([:typeof $EvofwToken] != "nothing" && [:len $EvofwToken] > 0) do={ :set already 1 } +} on-error={} -# Persist credentials -:do { /system script remove [find name="evofw-env"] } on-error={} -/system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $token . "\" ") +:if ($already = 0) do={ + :local token ("evofw_" . [/certificate scep-server nonce generate]) + :if ([:len $token] < 20) do={ + :set token ("evofw_" . [:tostr [/system clock get time]] . [:tostr [/system resource get cpu-load]] . [:tostr [/system resource get free-memory]]) + } + + :local body ("{\"name\":\"" . $EvofwName . "\",\"hostname\":\"" . [/system identity get name] . "\",\"platform\":\"mikrotik\",\"token\":\"" . $token . "\",\"client_version\":\"rsc/1\"") + :if ([:typeof $EvofwInstallLinkId] != "nothing" && [:len $EvofwInstallLinkId] > 0) do={ + :set body ($body . ",\"install_link_id\":\"" . $EvofwInstallLinkId . "\"") + } + :set body ($body . "}") + + :do { + /tool fetch url=($EvofwCpUrl . "/v1/agent/enroll") http-method=post http-header-field=("Content-Type: application/json,X-EvoFW-Seed: " . $EvofwSeed) http-data=$body keep-result=no + } on-error={ + :error "evofw: enroll failed — check EvofwCpUrl / EvofwSeed / connectivity" + } + + :do { /system script remove [find name="evofw-env"] } on-error={} + /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $token . "\" ") + :set EvofwToken $token +} else={ + :put "evofw: already enrolled — updating sync script (token kept)" + :do { /system script remove [find name="evofw-env"] } on-error={} + /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $EvofwToken . "\" ") +} # Filter rules (idempotent by comment) :do { /ip firewall filter remove [find comment~"^evofw-"] } on-error={} @@ -81,4 +96,8 @@ :log info "evofw: initial sync skipped (approve agent in UI)" } -:put ("EvoFirewall enrolled as " . $EvofwName . " — approve in UI; scheduler evofw-sync every 1m") +:if ($already = 1) do={ + :put ("EvoFirewall updated for " . $EvofwName . " — sync script + scheduler refreshed") +} else={ + :put ("EvoFirewall enrolled as " . $EvofwName . " — approve in UI; scheduler evofw-sync every 1m") +} diff --git a/apps/api/src/routes/agent.ts b/apps/api/src/routes/agent.ts index 2494f82..5564962 100644 --- a/apps/api/src/routes/agent.ts +++ b/apps/api/src/routes/agent.ts @@ -181,13 +181,33 @@ export const agentRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( const agentId = req.agentId! const body = applyReportBodySchema.parse(req.body) const now = new Date().toISOString() + const prev = repos.getAgent(app.db, agentId) + const reportedDropped = body.packets_dropped ?? 0 + const reportedAccepted = body.packets_accepted ?? 0 + const prevDropped = prev?.lastApplyPacketsDropped ?? 0 + const prevAccepted = prev?.lastApplyPacketsAccepted ?? 0 + // Absolute-since-chain-create from agent. If counters reset (re-apply), + // treat the new absolute as the delta; else add the increase. + const deltaDropped = + reportedDropped >= prevDropped + ? reportedDropped - prevDropped + : reportedDropped + const deltaAccepted = + reportedAccepted >= prevAccepted + ? reportedAccepted - prevAccepted + : reportedAccepted + const totalDropped = (prev?.totalPacketsDropped ?? 0) + deltaDropped + const totalAccepted = (prev?.totalPacketsAccepted ?? 0) + deltaAccepted + repos.updateAgent(app.db, agentId, { lastApplyAt: now, lastApplyStatus: body.status, lastApplyError: body.error ?? null, lastApplyPrefixCount: body.prefix_count ?? 0, - lastApplyPacketsDropped: body.packets_dropped ?? 0, - lastApplyPacketsAccepted: body.packets_accepted ?? 0, + lastApplyPacketsDropped: reportedDropped, + lastApplyPacketsAccepted: reportedAccepted, + totalPacketsDropped: totalDropped, + totalPacketsAccepted: totalAccepted, lastApplyKernelMethod: body.kernel_method ?? null, lastSeenAt: now, lastSeenIp: req.ip, @@ -195,8 +215,8 @@ export const agentRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( repos.insertStatsSample(app.db, { id: crypto.randomUUID(), agentId, - packetsDropped: body.packets_dropped ?? 0, - packetsAccepted: body.packets_accepted ?? 0, + packetsDropped: reportedDropped, + packetsAccepted: reportedAccepted, prefixCount: body.prefix_count ?? 0, kernelMethod: body.kernel_method ?? null, recordedAt: now, diff --git a/apps/api/src/routes/control.ts b/apps/api/src/routes/control.ts index a8905c7..9b86ed3 100644 --- a/apps/api/src/routes/control.ts +++ b/apps/api/src/routes/control.ts @@ -61,6 +61,8 @@ function mapAgent( last_apply_prefix_count: a.lastApplyPrefixCount, last_apply_packets_dropped: a.lastApplyPacketsDropped, last_apply_packets_accepted: a.lastApplyPacketsAccepted, + total_packets_dropped: a.totalPacketsDropped ?? 0, + total_packets_accepted: a.totalPacketsAccepted ?? 0, last_apply_kernel_method: a.lastApplyKernelMethod, client_version: a.clientVersion, created_at: a.createdAt, @@ -128,11 +130,11 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( agents_online: online.length, agents_pending: all.filter((a) => a.status === 'pending').length, packets_dropped: all.reduce( - (s, a) => s + (a.lastApplyPacketsDropped ?? 0), + (s, a) => s + (a.totalPacketsDropped ?? a.lastApplyPacketsDropped ?? 0), 0, ), packets_accepted: all.reduce( - (s, a) => s + (a.lastApplyPacketsAccepted ?? 0), + (s, a) => s + (a.totalPacketsAccepted ?? a.lastApplyPacketsAccepted ?? 0), 0, ), lists_total: repos.listIpLists(app.db).length, @@ -976,6 +978,8 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( const updated = repos.updateAgent(app.db, agent.id, { lastApplyPacketsDropped: 0, lastApplyPacketsAccepted: 0, + totalPacketsDropped: 0, + totalPacketsAccepted: 0, }) repos.deleteStatsSamplesForAgent(app.db, agent.id) auditMutation(app, config, req, { diff --git a/apps/web/src/components/agents/agent-card.tsx b/apps/web/src/components/agents/agent-card.tsx index 949d030..c4f60d7 100644 --- a/apps/web/src/components/agents/agent-card.tsx +++ b/apps/web/src/components/agents/agent-card.tsx @@ -3,6 +3,11 @@ import { AgentPlatformIcon, platformLabel, } from '@/components/agents/agent-platform-icon' +import { + agentHasTrafficSample, + agentTrafficAccepted, + agentTrafficDropped, +} from '@/components/agents/agent-traffic' import { StatusBadge } from '@/components/status-badge' import { Badge } from '@/components/reui/badge' import { Frame, FramePanel } from '@/components/reui/frame' @@ -52,9 +57,9 @@ type AgentCardProps = { } 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 hasApply = agentHasTrafficSample(agent) + const dropped = formatPackets(agentTrafficDropped(agent), hasApply) + const accepted = formatPackets(agentTrafficAccepted(agent), hasApply) const traffic = dropped === '—' && accepted === '—' ? '—' diff --git a/apps/web/src/components/agents/agent-detail-view.tsx b/apps/web/src/components/agents/agent-detail-view.tsx index 4bee41c..1d82534 100644 --- a/apps/web/src/components/agents/agent-detail-view.tsx +++ b/apps/web/src/components/agents/agent-detail-view.tsx @@ -24,6 +24,10 @@ import { AgentPlatformIcon, platformLabel, } from '@/components/agents/agent-platform-icon' +import { + agentTrafficAccepted, + agentTrafficDropped, +} from '@/components/agents/agent-traffic' import { AgentPolicySetsSortable } from '@/components/agents/agent-policy-sets-sortable' import { AgentPolicyTrace } from '@/components/agents/agent-policy-trace' import { AgentFactsPanel } from '@/components/agents/agent-facts-panel' @@ -246,8 +250,8 @@ export function AgentDetailView({ agentId }: AgentDetailViewProps) { icon: , iconClassName: 'text-warning', label: 'Traffic', - description: `↓${a.last_apply_packets_dropped ?? 0} · ↑${a.last_apply_packets_accepted ?? 0}`, - hint: 'сумма counters', + description: `↓${agentTrafficDropped(a)} · ↑${agentTrafficAccepted(a)}`, + hint: 'накопительно', variant: 'warning', footer: (