From fb33aa1b6d164b7206cb04b4f2374a4e5f345a6f Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Tue, 26 May 2026 03:07:57 +0000 Subject: [PATCH] feat: logs endpoint annotation, alignment, descending sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fw/wg events: raw_ip → resolved_name annotation (dim) - fw events: endpoint column with pre-resolved names (two-pass render) - fw events: raw IP:port dim suffix after service name - wg events: endpoint annotation in logs (same as watch) - fw/wg: descending sort default, --ascending/--descending flags - wg events: gap/offline indicator, threshold * 2 for offline label - fw_row: no-endpoint rows show dim — placeholder for alignment - section headers: dynamic width via tput cols --- commands/logs.command.sh | 92 +++++++++++----- core/json_helper.py | 3 +- core/lib/__pycache__/events.cpython-311.pyc | Bin 33035 -> 34012 bytes core/lib/events.py | 29 +++-- modules/ui/logs.module.sh | 112 +++++++++++++++----- 5 files changed, 176 insertions(+), 60 deletions(-) diff --git a/commands/logs.command.sh b/commands/logs.command.sh index de5762d..7957c63 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -225,34 +225,65 @@ function cmd::logs::show_fw_events() { "$filter_dest_ip" "$filter_dest_port" \ "$sort_order" \ 2>/dev/null) - + [[ -z "$data" ]] && return 0 - - # Measure column widths - local w_client=16 w_dest=20 - while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + + # ── Pass 1: resolve endpoints and measure widths ── + local w_client=16 w_dest=20 w_endpoint=0 + local resolved_data="" + + while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do [[ -z "$ts" ]] && continue + + # Measure client (( ${#client} > w_client )) && w_client=${#client} - local dest_display host_name - host_name=$(hosts::resolve_ip "$dest_ip") - if [[ -n "$host_name" ]]; then - dest_display="$host_name" - elif [[ -n "$svc" ]]; then - [[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})" + + # Build svc_display (for w_dest measurement) + local svc_display="" + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && svc_display="${svc}/${proto}" \ + || svc_display="${svc} (${proto})" else - [[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})" + [[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \ + || svc_display="${dest_ip} (${proto})" fi - (( ${#dest_display} > w_dest )) && w_dest=${#dest_display} + + # Build raw_suffix plain (no ANSI) for w_dest measurement + local raw_plain="" + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" \ + || raw_plain=" (${dest_ip})" + fi + + local full_dest_len=$(( ${#svc_display} + ${#raw_plain} )) + (( full_dest_len > w_dest )) && w_dest=$full_dest_len + + # Resolve endpoint once + local src_resolved="" + if [[ -n "$src_endpoint" ]]; then + src_resolved=$(resolve::ip "$src_endpoint" 2>/dev/null || true) + [[ "$src_resolved" == "$src_endpoint" ]] && src_resolved="" + # Measure endpoint column: raw IP + " → resolved" + local ep_display_len=${#src_endpoint} + [[ -n "$src_resolved" ]] && ep_display_len=$(( ep_display_len + 4 + ${#src_resolved} )) + (( ep_display_len > w_endpoint )) && w_endpoint=$ep_display_len + fi + + resolved_data+="${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}|${src_endpoint}|${src_resolved}"$'\n' done <<< "$data" - (( w_client += 2 )) - (( w_dest += 2 )) - + + (( w_client += 2 )) + (( w_dest += 2 )) + [[ "$w_endpoint" -gt 0 ]] && (( w_endpoint += 2 )) + + # ── Pass 2: render ── ui::logs::fw_section_header - while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint src_resolved; do [[ -z "$ts" ]] && continue ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \ - "$proto" "$svc" "$count" "$w_client" "$w_dest" - done <<< "$data" + "$proto" "$svc" "$count" "$w_client" "$w_dest" \ + "$src_endpoint" "$src_resolved" "$w_endpoint" + done <<< "$resolved_data" printf "\n" } @@ -277,21 +308,26 @@ function cmd::logs::show_wg_events() { local resolved_data="" while IFS='|' read -r ts client endpoint event count gap_seconds; do [[ -z "$ts" ]] && continue - local endpoint_display - endpoint_display=$(resolve::ip "$endpoint") - [[ -z "$endpoint_display" ]] && endpoint_display="$endpoint" - resolved_data+="${ts}|${client}|${endpoint_display}|${event}|${count}|${gap_seconds}"$'\n' - (( ${#client} > w_client )) && w_client=${#client} - (( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display} + (( ${#client} > w_client )) && w_client=${#client} + local ep_len=${#endpoint} + [[ -z "$endpoint" ]] && ep_len=1 + (( ep_len > w_endpoint )) && w_endpoint=$ep_len + + # Resolve endpoint + local resolved="" + [[ -n "$endpoint" ]] && resolved=$(resolve::ip "$endpoint" 2>/dev/null || true) + [[ "$resolved" == "$endpoint" ]] && resolved="" + + resolved_data+="${ts}|${client}|${endpoint}|${event}|${count}|${gap_seconds}|${resolved}"$'\n' done <<< "$data" (( w_client += 2 )) - (( w_endpoint += 2 )) + (( w_endpoint += 18 )) ui::logs::wg_section_header - while IFS='|' read -r ts client endpoint event count gap_seconds; do + while IFS='|' read -r ts client endpoint event count gap_seconds resolved; do [[ -z "$ts" ]] && continue ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \ - "$count" "$w_client" "$w_endpoint" "$gap_seconds" + "$count" "$w_client" "$w_endpoint" "$gap_seconds" "$resolved" done <<< "$resolved_data" printf "\n" } diff --git a/core/json_helper.py b/core/json_helper.py index cbe6669..dd084b0 100644 --- a/core/json_helper.py +++ b/core/json_helper.py @@ -1690,7 +1690,8 @@ commands = { args[7] if len(args) > 7 else '', args[8] if len(args) > 8 else '', args[9] if len(args) > 9 else '', - args[10] if len(args) > 10 else 'desc'), + args[10] if len(args) > 10 else 'desc', + args[11] if len(args) > 11 else ''), 'wg_events': lambda args: __import__('lib.events', fromlist=['wg_events']).wg_events( args[0], args[1], args[2], args[3] if len(args) > 3 else '50', diff --git a/core/lib/__pycache__/events.cpython-311.pyc b/core/lib/__pycache__/events.cpython-311.pyc index 0152321633c6cb850ac2c628c73422baacf026d8..98fb0ac9f01e581380555951c72aace5f00d2cd5 100644 GIT binary patch delta 5149 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/dev/null || echo 80) - 4 )) printf " Firewall Drops\n" - printf " %s\n" "$(printf '─%.0s' {1..42})" + printf " %s\n" "$(printf '─%.0s' $(seq 1 $cols))" } function ui::logs::wg_section_header() { + local cols=$(( $(tput cols 2>/dev/null || echo 80) - 4 )) printf " WireGuard Events\n" - printf " %s\n" "$(printf '─%.0s' {1..42})" + printf " %s\n" "$(printf '─%.0s' $(seq 1 $cols))" } function ui::logs::fw_section_header_table() { @@ -35,17 +37,67 @@ function ui::logs::wg_section_header_table() { function ui::logs::fw_row() { local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \ - w_client="${8:-20}" w_dest="${9:-30}" - local dest_display - dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name") + w_client="${8:-20}" w_dest="${9:-30}" \ + src_endpoint="${10:-}" src_resolved="${11:-}" w_endpoint="${12:-0}" + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + # ── Source endpoint — always render at w_endpoint width ── + local src_padded="" + if [[ "$w_endpoint" -gt 0 ]]; then + if [[ -n "$src_endpoint" ]]; then + local src_colored="$src_endpoint" + [[ -n "$src_resolved" ]] && \ + src_colored="${src_endpoint} \033[2m→ ${src_resolved}\033[0m" + src_padded=$(ui::pad_mb "$src_colored" "$w_endpoint") + else + # No endpoint — use dim dash padded to w_endpoint + src_padded=$(ui::pad_mb "\033[2m—\033[0m" "$w_endpoint") + fi + fi + + # ── Destination ── + local svc_display="" + local raw_suffix="" + if [[ -n "$svc_name" ]]; then + [[ -n "$dest_port" ]] && svc_display="${svc_name}/${proto}" \ + || svc_display="${svc_name} (${proto})" + [[ -n "$dest_port" ]] && raw_suffix=" \033[2m(${dest_ip}:${dest_port})\033[0m" \ + || raw_suffix=" \033[2m(${dest_ip})\033[0m" + else + [[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \ + || svc_display="${dest_ip} (${proto})" + fi + + # Pad so count aligns — based on full dest (svc + raw_suffix plain length) + local raw_plain="" + [[ -n "$svc_name" && -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" + [[ -n "$svc_name" && -z "$dest_port" ]] && raw_plain=" (${dest_ip})" + local full_dest_len=$(( ${#svc_display} + ${#raw_plain} )) + local dest_pad_n=$(( w_dest - full_dest_len )) + [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 + + # ── Count ── local count_suffix="" [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - local client_pad dest_pad_n - client_pad=$(printf "%-${w_client}s" "$client") - dest_pad_n=$(( w_dest - ${#dest_display} )) - [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 - printf " %s %s \033[1;31m→\033[0m %s%*s%b\n" \ - "$ts" "$client_pad" "$dest_display" "$dest_pad_n" "" "$count_suffix" + + # ── Render ── + if [[ "$w_endpoint" -gt 0 ]]; then + printf " %s %s %b \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" \ + "$src_padded" \ + "$svc_display" "$raw_suffix" \ + "$dest_pad_n" "" \ + "$count_suffix" + else + printf " %s %s \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" \ + "$svc_display" "$raw_suffix" \ + "$dest_pad_n" "" \ + "$count_suffix" + fi } function ui::logs::fw_row_table() { @@ -58,39 +110,51 @@ function ui::logs::fw_row_table() { function ui::logs::wg_row() { local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ - gap_seconds="${8:-}" - + gap_seconds="${8:-}" resolved="${9:-}" + local event_color case "$event" in handshake) event_color="\033[1;32m" ;; attempt) event_color="\033[1;31m" ;; *) event_color="\033[0;37m" ;; esac - + local count_suffix="" [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - - # Gap suffix — only for handshakes with a meaningful gap + + # Gap suffix — offline label only when gap > threshold * 2 local gap_suffix="" if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then local gap_int="$gap_seconds" local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" local offline_label="" - [[ "$gap_int" -gt "$threshold" ]] && offline_label=" offline" + [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" if (( gap_int >= 3600 )); then gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" elif (( gap_int >= 60 )); then gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" fi fi - - local client_pad endpoint_pad_n + + # Build endpoint display: raw_ip [dim → resolved] + # Use ui::pad_mb so ANSI annotation doesn't affect column alignment + local endpoint_raw="${endpoint:--}" + local endpoint_colored + if [[ -n "$resolved" && -n "$endpoint" ]]; then + endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" + else + endpoint_colored="$endpoint_raw" + fi + local endpoint_padded + endpoint_padded=$(ui::pad_mb "$endpoint_colored" "$w_endpoint") + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") client_pad=$(printf "%-${w_client}s" "$client") - endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) - [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 - - printf " %s %s %s%*s %b%s\033[0m%b%b\n" \ - "$ts" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ + + printf " %s %s %b %b%s\033[0m%b%b\n" \ + "$ts_pad" "$client_pad" \ + "$endpoint_padded" \ "$event_color" "$event" "$count_suffix" "$gap_suffix" }