cleanup: ui::pad_mb removal, watch alignment fixes, endpoint cache fallback

- ui::rule::list_row: inline padding math replaces ui::pad_mb (major perf gain)
- ui::fw_row/wg_row: drop ui::pad_mb for fw/wg labels (always 2 chars)
- watch: endpoint fallback via monitor::get_cached_endpoint
- watch: _poll_handshakes sorts by ts descending (most recent first)
- watch: empty endpoint uses - not — (avoids multi-byte padding issues)
- ui.sh: UTF-8 extra byte constants (_UI_EMDASH_EXTRA, _UI_ARROW_EXTRA, _UI_BULLET_EXTRA)
This commit is contained in:
Nuno Duque Nunes 2026-05-25 15:09:13 +00:00
parent 5c2e16e358
commit 3058750c3d
9 changed files with 98 additions and 76 deletions

View file

@ -107,13 +107,16 @@ function cmd::watch::run() {
function cmd::watch::_poll_handshakes() { function cmd::watch::_poll_handshakes() {
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
local w_client="${4:-20}" w_dest="${5:-18}" local w_client="${4:-20}" w_dest="${5:-18}"
# Collect rows with sort key before printing
local -a rows=()
while IFS= read -r line; do while IFS= read -r line; do
local public_key ts local public_key ts
public_key=$(echo "$line" | awk '{print $1}') public_key=$(echo "$line" | awk '{print $1}')
ts=$(echo "$line" | awk '{print $2}') ts=$(echo "$line" | awk '{print $2}')
[[ -z "$ts" || "$ts" == "0" ]] && continue [[ -z "$ts" || "$ts" == "0" ]] && continue
local client_name="" local client_name=""
for conf in "$(ctx::clients)"/*.conf; do for conf in "$(ctx::clients)"/*.conf; do
[[ -f "$conf" ]] || continue [[ -f "$conf" ]] || continue
@ -128,34 +131,48 @@ function cmd::watch::_poll_handshakes() {
done done
[[ -z "$client_name" ]] && continue [[ -z "$client_name" ]] && continue
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
local safe_key local safe_key
safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1) safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1)
local prev_ts_file="/tmp/wgctl_hs_${safe_key}" local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
local prev_ts="0" local prev_ts="0"
[[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
[[ "$ts" == "$prev_ts" ]] && continue [[ "$ts" == "$prev_ts" ]] && continue
local gap=$(( ts - ${prev_ts:-0} )) local gap=$(( ts - ${prev_ts:-0} ))
echo "$ts" > "$prev_ts_file" echo "$ts" > "$prev_ts_file"
(( gap < ${WG_HANDSHAKE_CHECK_TIME_SEC:-300} )) && continue (( gap < ${WG_HANDSHAKE_CHECK_TIME_SEC:-300} )) && continue
local ts_fmt local ts_fmt
ts_fmt=$(fmt::datetime_short "$ts") ts_fmt=$(fmt::datetime_short "$ts")
# Resolve endpoint — try wg show first, fall back to endpoint cache
local endpoint local endpoint
endpoint=$(monitor::endpoint_for_key "$public_key") endpoint=$(monitor::endpoint_for_key "$public_key")
# Resolve endpoint if [[ -z "$endpoint" ]]; then
endpoint=$(monitor::get_cached_endpoint "$client_name")
fi
local endpoint_display local endpoint_display
endpoint_display=$(resolve::ip "${endpoint:-}") endpoint_display=$(resolve::ip "${endpoint:-}")
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:-}" [[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:--}"
ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \ # Build row with ts prefix for sorting
"$w_client" "$w_dest" local row
row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \
"$w_client" "$w_dest")
rows+=("${ts}|${row}")
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
# Sort by ts descending (most recent first) and print
if [[ ${#rows[@]} -gt 0 ]]; then
printf '%s\n' "${rows[@]}" | sort -t'|' -k1,1rn | while IFS= read -r entry; do
printf "%s\n" "${entry#*|}"
done
fi
} }
# ============================================ # ============================================
# Event Tailer # Event Tailer
# ============================================ # ============================================
@ -164,7 +181,7 @@ function cmd::watch::_tail_events() {
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}" local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}"
local w_client="${7:-20}" w_dest="${8:-18}" local w_client="${7:-20}" w_dest="${8:-18}"
# Build ip->name map # Build ip->name map
declare -A ip_to_name=() declare -A ip_to_name=()
while IFS= read -r conf; do while IFS= read -r conf; do
@ -174,41 +191,41 @@ function cmd::watch::_tail_events() {
ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
[[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname" [[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname"
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null) done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
declare -A _WATCH_LAST_FW=() declare -A _WATCH_LAST_FW=()
declare -A _WATCH_LAST_WG=() declare -A _WATCH_LAST_WG=()
local source_file local source_file
source_file=$(mktemp) source_file=$(mktemp)
echo "wg" > "$source_file" echo "wg" > "$source_file"
trap "rm -f '$source_file'" EXIT trap "rm -f '$source_file'" EXIT
tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \ tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \
| while IFS= read -r line; do | while IFS= read -r line; do
[[ -z "$line" ]] && continue [[ -z "$line" ]] && continue
if [[ "$line" == "==> "* ]]; then if [[ "$line" == "==> "* ]]; then
[[ "$line" == *"fw_events"* ]] && echo "fw" > "$source_file" || echo "wg" > "$source_file" [[ "$line" == *"fw_events"* ]] && echo "fw" > "$source_file" || echo "wg" > "$source_file"
continue continue
fi fi
local source local source
source=$(cat "$source_file") source=$(cat "$source_file")
if [[ "$source" == "fw" ]]; then if [[ "$source" == "fw" ]]; then
$allowed_only && continue $allowed_only && continue
local fw_data local fw_data
fw_data=$(python3 "$(ctx::json_helper)" parse_fw_event "$line" 2>/dev/null) || continue fw_data=$(python3 "$(ctx::json_helper)" parse_fw_event "$line" 2>/dev/null) || continue
[[ -z "$fw_data" ]] && continue [[ -z "$fw_data" ]] && continue
local ts src_ip dest_ip dest_port proto local ts src_ip dest_ip dest_port proto
IFS='|' read -r ts src_ip dest_ip dest_port proto <<< "$fw_data" IFS='|' read -r ts src_ip dest_ip dest_port proto <<< "$fw_data"
[[ -z "$src_ip" ]] && continue [[ -z "$src_ip" ]] && continue
local client="${ip_to_name[$src_ip]:-$src_ip}" local client="${ip_to_name[$src_ip]:-$src_ip}"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}" local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}"
local now; now=$(date +%s) local now; now=$(date +%s)
local window=30 local window=30
@ -217,52 +234,53 @@ function cmd::watch::_tail_events() {
local last="${_WATCH_LAST_FW[$fw_key]:-0}" local last="${_WATCH_LAST_FW[$fw_key]:-0}"
(( now - last < window )) && continue (( now - last < window )) && continue
_WATCH_LAST_FW["$fw_key"]="$now" _WATCH_LAST_FW["$fw_key"]="$now"
local ts_fmt local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
local dest_display local dest_display
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto") dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto")
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest" ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
else else
$restricted_only && continue $restricted_only && continue
local ev_data local ev_data
ev_data=$(python3 "$(ctx::json_helper)" parse_event "$line" 2>/dev/null) || continue ev_data=$(python3 "$(ctx::json_helper)" parse_event "$line" 2>/dev/null) || continue
[[ -z "$ev_data" ]] && continue [[ -z "$ev_data" ]] && continue
local ts client endpoint event local ts client endpoint event
IFS='|' read -r ts client endpoint event <<< "$ev_data" IFS='|' read -r ts client endpoint event <<< "$ev_data"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
$blocked_only && [[ "$event" != "attempt" ]] && continue $blocked_only && [[ "$event" != "attempt" ]] && continue
$allowed_only && [[ "$event" != "handshake" ]] && continue $allowed_only && [[ "$event" != "handshake" ]] && continue
local wg_key="${client}:${endpoint}:${event}" local wg_key="${client}:${endpoint}:${event}"
local now; now=$(date +%s) local now; now=$(date +%s)
local last="${_WATCH_LAST_WG[$wg_key]:-0}" local last="${_WATCH_LAST_WG[$wg_key]:-0}"
# Handshakes — only show if gap > 5min (new session)
# Attempts — shorter window (30s) since each attempt is meaningful
local window=30 local window=30
[[ "$event" == "handshake" ]] && window="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" [[ "$event" == "handshake" ]] && window="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}"
(( now - last < window )) && continue (( now - last < window )) && continue
_WATCH_LAST_WG["$wg_key"]="$now" _WATCH_LAST_WG["$wg_key"]="$now"
local ts_fmt local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
# Resolve endpoint # Resolve endpoint — fall back to endpoint cache if empty
local endpoint_display local endpoint_resolved
endpoint_display=$(resolve::ip "${endpoint:-}") endpoint_resolved=$(resolve::ip "${endpoint:-}")
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:-}" if [[ -z "$endpoint_resolved" && -n "$endpoint" ]]; then
endpoint_resolved="$endpoint"
ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_display" "$event" \ fi
[[ -z "$endpoint_resolved" ]] && endpoint_resolved="-"
ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_resolved" "$event" \
"$w_client" "$w_dest" "$w_client" "$w_dest"
fi fi
done done
rm -f "$source_file" rm -f "$source_file"
} }

View file

@ -3,6 +3,12 @@
UI_ROW_WIDTH=${UI_ROW_WIDTH:-20} UI_ROW_WIDTH=${UI_ROW_WIDTH:-20}
UI_SECTION_WIDTH=${UI_SECTION_WIDTH:-44} UI_SECTION_WIDTH=${UI_SECTION_WIDTH:-44}
# UTF-8 multi-byte character extras (bash ${#} counts bytes, not chars)
# extra = byte_length - visible_char_length
_UI_EMDASH_EXTRA=2 # — (em dash) 3 bytes, 1 visible
_UI_ARROW_EXTRA=2 # → (right arrow) 3 bytes, 1 visible
_UI_BULLET_EXTRA=1 # · (middle dot) 2 bytes, 1 visible
function ui::row() { function ui::row() {
local label="$1" value="$2" width="${3:-$UI_ROW_WIDTH}" local label="$1" value="$2" width="${3:-$UI_ROW_WIDTH}"
printf " %-${width}s %s\n" "${label}:" "$value" printf " %-${width}s %s\n" "${label}:" "$value"

View file

@ -89,7 +89,7 @@ function monitor::cache_endpoint() {
} }
function monitor::get_cached_endpoint() { function monitor::get_cached_endpoint() {
local client="$1" local client="${1:-}"
json::get "$ENDPOINT_CACHE" "$client" json::get "$ENDPOINT_CACHE" "$client"
} }

View file

@ -94,17 +94,16 @@ _UI_WATCH_WG_COLOR="\033[1;32m"
function ui::watch::fw_row() { function ui::watch::fw_row() {
local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \ local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \
w_client="${4:-20}" w_dest="${5:-18}" w_client="${4:-20}" w_dest="${5:-18}"
local ts_pad # "fw" is always 2 visible chars — no padding needed
local src="${_UI_WATCH_FW_COLOR}fw\033[0m"
local ts_pad client_pad dest_pad_n
ts_pad=$(printf "%-11s" "$ts") ts_pad=$(printf "%-11s" "$ts")
local src
src=$(ui::pad_mb "${_UI_WATCH_FW_COLOR}fw\033[0m" 2)
local client_pad dest_pad_n
client_pad=$(printf "%-${w_client}s" "$client") client_pad=$(printf "%-${w_client}s" "$client")
dest_pad_n=$(( w_dest - ${#dest_display} )) dest_pad_n=$(( w_dest - ${#dest_display} ))
[[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0
# echo "DEBUG fw: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2
printf " %s %b %s \033[1;31m→\033[0m %s%*s \033[1;31mdrop\033[0m\n" \ printf " %s %b %s \033[1;31m→\033[0m %s%*s \033[1;31mdrop\033[0m\n" \
"$ts_pad" "$src" "$client_pad" "$dest_display" "$dest_pad_n" "" "$ts_pad" "$src" "$client_pad" "$dest_display" "$dest_pad_n" ""
} }
@ -112,35 +111,31 @@ function ui::watch::fw_row() {
function ui::watch::wg_row() { function ui::watch::wg_row() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
w_client="${5:-20}" w_endpoint="${6:-18}" w_client="${5:-20}" w_endpoint="${6:-18}"
local ts_pad
ts_pad=$(printf "%-11s" "$ts")
local event_color local event_color
case "$event" in case "$event" in
handshake) event_color="\033[1;32m" ;; handshake) event_color="\033[1;32m" ;;
attempt) event_color="\033[1;31m" ;; attempt) event_color="\033[1;31m" ;;
*) event_color="\033[0;37m" ;; *) event_color="\033[0;37m" ;;
esac esac
local src
src=$(ui::pad_mb "${_UI_WATCH_WG_COLOR}wg\033[0m" 2) # "wg" is always 2 visible chars — no padding needed
local src="${_UI_WATCH_WG_COLOR}wg\033[0m"
case "$event" in
handshake) src="\033[1;32m" ;; # green # Use "-" not "—" to avoid multi-byte padding issues
attempt) src="\033[1;31m" ;; # red local endpoint_display="${endpoint:--}"
*) src="\033[0;37m" ;; # gray
esac local ts_pad client_pad endpoint_pad_n
local src_colored="${src}wg\033[0m" ts_pad=$(printf "%-11s" "$ts")
local client_pad endpoint_pad_n
client_pad=$(printf "%-${w_client}s" "$client") client_pad=$(printf "%-${w_client}s" "$client")
endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) endpoint_pad_n=$(( w_endpoint - ${#endpoint_display} ))
[[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0
# echo "DEBUG wg: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2
printf " %s %b %s %s%*s %b%s\033[0m\n" \ printf " %s %b %s %s%*s %b%s\033[0m\n" \
"$ts_pad" "$src_colored" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ "$ts_pad" "$src" "$client_pad" "$endpoint_display" "$endpoint_pad_n" "" \
"$event_color" "$event" "$event_color" "$event"
} }
function ui::watch::header_table() { function ui::watch::header_table() {
printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \

View file

@ -331,20 +331,23 @@ function ui::rule::list_row() {
extends_indicator=" \033[2m↳ ${extends_display}\033[0m" extends_indicator=" \033[2m↳ ${extends_display}\033[0m"
fi fi
# Allows column — green +N if >0, dim +0 if zero # Allows — green +N, padded to 5 visible chars
# Append spaces after reset code so printf doesn't miscount
local allows_str local allows_str
if [[ "$n_allows" -gt 0 ]]; then if [[ "$n_allows" -gt 0 ]]; then
allows_str=$(ui::pad_mb "\033[1;32m+${n_allows}\033[0m" 5) printf -v allows_str "\033[1;32m+%s\033[0m" "$n_allows"
allows_str="${allows_str}$(printf '%*s' "$(( 4 - ${#n_allows} ))" '')"
else else
allows_str=$(ui::pad_mb "\033[2m+0\033[0m" 5) printf -v allows_str "\033[2m+0\033[0m " # "+0" = 2 visible + 3 spaces = 5
fi fi
# Blocks column — red -N if >0, dim -0 if zero # Blocks — red -N, padded to 5 visible chars
local blocks_str local blocks_str
if [[ "$n_blocks" -gt 0 ]]; then if [[ "$n_blocks" -gt 0 ]]; then
blocks_str=$(ui::pad_mb "\033[1;31m-${n_blocks}\033[0m" 5) printf -v blocks_str "\033[1;31m-%s\033[0m" "$n_blocks"
blocks_str="${blocks_str}$(printf '%*s' "$(( 4 - ${#n_blocks} ))" '')"
else else
blocks_str=$(ui::pad_mb "\033[2m-0\033[0m" 5) printf -v blocks_str "\033[2m-0\033[0m " # "-0" = 2 visible + 3 spaces = 5
fi fi
# Peers — dim if zero # Peers — dim if zero