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: (