feat: watch/logs endpoint annotation, shared row primitives

- ui::_render_endpoint_col: shared endpoint padding primitive
- ui::_build_dest: shared destination display primitive
- ui::wg_row/fw_row: endpoint annotation (raw_ip → resolved)
- resolve::endpoint_parts: fresh resolution, no stale cache
- resolve::service_name: returns service name or empty (no raw fallback)
- monitor::live: pre-measure w_client from peer names
- watch: fixed w_endpoint=30 for consistent live alignment
- shell: add peer/hosts/identity/subnet/policy/activity to known commands
- shell: updated banner with new commands
- identity/rule help: updated with new features
This commit is contained in:
Nuno Duque Nunes 2026-05-26 15:16:33 +00:00
parent c6883c6801
commit c3cf5bc572
7 changed files with 477 additions and 187 deletions

View file

@ -41,33 +41,32 @@ function cmd::identity::help() {
cat <<EOF
Usage: wgctl identity <subcommand> [options]
Manage peer identities.
Manage peer identities — group peers by person/device owner.
Subcommands:
list List all identities
show --name <name> Show identity details and device status
add --name <name> Manually attach a peer to an identity
--peer <peer>
remove --name <name> Remove identity and all associated peers
migrate [--dry-run] Create identities from existing peer names
show --name <n> Show identity details with peers and rule tree
add --name <n> Create a new identity
remove --name <n> Remove an identity
migrate Migrate peers to identities
rule assign --name <name> Assign a rule to an identity
--rule <rule>
rule unassign --name <name> Remove rule from an identity
rule show --name <name> Show current identity rule
rule assign --name <n> --rule <r> Assign rule to identity
Blocked if peer already has rule directly
[--migrate] Remove conflicting direct peer rules first
rule unassign --name <n> --rule <r> Remove rule from identity
rule unassign --name <n> --all Remove all rules from identity
options --name <name> Set identity options
[--policy <policy>]
[--set-strict-rule | --unset-strict-rule]
[--set-auto-apply | --unset-auto-apply]
options --name <n> --strict-rule <bool> Set strict rule mode
options --name <n> --auto-apply <bool> Set auto apply
Examples:
wgctl identity list
wgctl identity show --name nuno
wgctl identity add --name alice
wgctl identity rule assign --name nuno --rule admin
wgctl identity rule unassign --name nuno
wgctl identity options --name guests-identity --policy guest
wgctl identity options --name nuno --set-strict-rule
wgctl identity rule assign --name nuno --rule user --migrate
wgctl identity rule unassign --name nuno --rule admin
wgctl identity options --name nuno --strict-rule true
EOF
}

View file

@ -47,12 +47,13 @@ Service names from 'wgctl net' can be used instead of raw IPs/ports.
Subcommands:
list, ls List all rules
show, inspect Show rule details and inheritance
add, new, create Create a new rule
update, edit Update a rule and re-apply to peers
remove, rm, del Remove a rule
assign Assign a rule to a peer
unassign Remove rule from a peer
list --detailed Show inheritance tree
show, inspect --name <r> Show rule details and inheritance
add, new, create --name <r> Create a new rule
update, edit --name <r> Update a rule and re-apply to peers
remove, rm, del --name <r> Remove a rule
assign --name <r> Assign a rule to a peer
unassign --name <r> --peer <p> Remove rule from a peer
reapply Re-apply rule to all assigned peers
migrate Apply default rules to unassigned peers
@ -312,10 +313,9 @@ function cmd::rule::show() {
local peer_count=${#peer_list[@]}
ui::empty "$peer_count" && return 0
local peer_word="peers"
[[ "$peer_count" -eq 1 ]] && peer_word="peer"
printf "\n \033[0;37m── Peers (%s %s) \033[0m%s\n\n" \
"$peer_count" "$peer_word" "$(printf '\033[0;37m─%.0s' {1..30})"
[[ "$peer_count" -eq 1 ]]
printf "\n \033[0;37m── Peers (%s) \033[0m%s\n\n" \
"$peer_count" "$(printf '\033[0;37m─%.0s' {1..30})"
for peer_name in "${peer_list[@]}"; do
local ip

View file

@ -19,6 +19,7 @@ function cmd::shell::_is_wgctl_command() {
list add remove rm inspect block unblock
rule group audit logs watch fw config qr
rename keys ip net service shell help test
peer hosts identity subnet policy activity
)
local c
for c in "${known[@]}"; do
@ -82,28 +83,44 @@ function cmd::shell::_banner() {
printf "\n"
printf " Type wgctl commands directly (no 'wgctl' prefix).\n"
printf " Bash commands work too: ls, cat, systemctl, vim...\n\n"
printf " \033[1;37mCommon commands:\033[0m\n"
printf " \033[1;37mPeer management:\033[0m\n"
printf " list List all peers\n"
printf " list --blocked Show blocked peers\n"
printf " list --restricted Show restricted peers\n"
printf " list --rule user Filter by rule\n"
printf " inspect --name <peer> Full peer details\n"
printf " block --name <peer> Block a peer entirely\n"
printf " block --name <peer> --service proxmox Restrict service\n"
printf " unblock --name <peer> Restore full access\n"
printf " add --identity <id> --type phone Add a peer\n"
printf " block --name <peer> Block a peer\n"
printf " unblock --name <peer> Restore access\n"
printf " peer update-dns --all Update DNS on all peers\n"
printf " peer update-tunnel --name <p> --mode split\n\n"
printf " \033[1;37mRules & access:\033[0m\n"
printf " rule list Show firewall rules\n"
printf " rule list --tree Show with inheritance\n"
printf " rule show --name <rule> Rule details\n"
printf " rule assign --name <r> --peer <p> Assign rule to peer\n"
printf " identity list Show identities\n"
printf " identity show --name <id> Identity details + rule tree\n"
printf " identity rule assign --name <id> --rule <r>\n\n"
printf " \033[1;37mNetwork & services:\033[0m\n"
printf " net list Show network services\n"
printf " net list --detailed Show services with ports\n"
printf " net list --detailed Show with ports\n"
printf " hosts list Show host annotations\n"
printf " subnet list Show subnets\n"
printf " group list Show groups\n"
printf " group block --name <group> Block all peers in group\n"
printf " group block --name <group> Block all in group\n\n"
printf " \033[1;37mMonitoring:\033[0m\n"
printf " logs Activity logs\n"
printf " logs --since 2h Logs from last 2h\n"
printf " logs --fw --service pihole FW drops to service\n"
printf " logs --wg --event handshake WG handshake events\n"
printf " logs --resolved Show resolved names only\n"
printf " logs --follow Live activity log\n"
printf " logs clean Remove keepalive entries\n"
printf " logs rotate Clean old log entries\n"
printf " watch Live WG + firewall monitor\n"
printf " activity Transfer + drop summary\n"
printf " fw list Show iptables rules\n"
printf " audit Verify firewall state\n"
printf " audit --fix Auto-repair firewall rules\n\n"
printf " audit --fix Auto-repair firewall\n\n"
printf " \033[1mexit\033[0m or \033[1mquit\033[0m to leave · \033[1mhelp\033[0m for full command list\n\n"
}
@ -143,7 +160,9 @@ EOF
# ============================================
function cmd::shell::_setup_completion() {
local commands="list add remove rm inspect block unblock rule group audit logs watch fw config qr rename service shell help test"
local commands="list add remove rm inspect block unblock rule group audit \
logs watch fw config qr rename service shell help test \
peer hosts identity subnet policy activity"
function _wgctl_shell_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"

View file

@ -133,14 +133,15 @@ function cmd::watch::_poll_handshakes() {
endpoint=$(monitor::get_cached_endpoint "$client_name")
fi
local endpoint_display
endpoint_display=$(resolve::ip "${endpoint:-}")
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:--}"
# Build row with ts prefix for sorting
local row
row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \
"$w_client" "$w_dest")
local ep_raw ep_resolved=""
ep_raw="${endpoint:-}"
if [[ -n "$ep_raw" ]]; then
local ep_parts
ep_parts=$(resolve::endpoint_parts "$ep_raw")
ep_resolved="${ep_parts#*|}"
fi
row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$ep_raw" "handshake" \
"$ep_resolved" "$w_client" "30")
rows+=("${ts}|${row}")
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
@ -217,10 +218,18 @@ function cmd::watch::_tail_events() {
local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
local dest_display
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto")
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
local fw_svc_name
fw_svc_name=$(resolve::service_name "$dest_ip" "$dest_port" "$proto")
local fw_src_ep fw_src_resolved=""
fw_src_ep=$(monitor::get_cached_endpoint "$client")
if [[ -n "$fw_src_ep" ]]; then
local fw_ep_parts
fw_ep_parts=$(resolve::endpoint_parts "$fw_src_ep")
fw_src_resolved="${fw_ep_parts#*|}"
fi
ui::watch::fw_row "$ts_fmt" "$client" "$dest_ip" "$dest_port" "$proto" \
"$fw_svc_name" "$fw_src_ep" "$fw_src_resolved" \
"$w_client" "$w_dest" "30"
else
$restricted_only && continue
@ -249,15 +258,15 @@ function cmd::watch::_tail_events() {
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
# Resolve endpoint — fall back to endpoint cache if empty
local endpoint_resolved
endpoint_resolved=$(resolve::ip "${endpoint:-}")
if [[ -z "$endpoint_resolved" && -n "$endpoint" ]]; then
endpoint_resolved="$endpoint"
local wg_ep_raw wg_ep_resolved=""
wg_ep_raw="${endpoint:-}"
if [[ -n "$wg_ep_raw" ]]; then
local wg_ep_parts
wg_ep_parts=$(resolve::endpoint_parts "$wg_ep_raw")
wg_ep_resolved="${wg_ep_parts#*|}"
fi
[[ -z "$endpoint_resolved" ]] && endpoint_resolved="-"
ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_resolved" "$event" \
"$w_client" "$w_dest"
ui::watch::wg_row "$ts_fmt" "$client" "$wg_ep_raw" "$event" \
"$wg_ep_resolved" "$w_client" "30"
fi
done

View file

@ -149,6 +149,12 @@ function monitor::live() {
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true
local w_client=20 w_dest=18
while IFS= read -r conf; do
local name
name=$(basename "$conf" .conf)
(( ${#name} > w_client )) && w_client=${#name}
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
(( w_client += 2 ))
if ! $blocked_only && ! $restricted_only; then
(

View file

@ -60,6 +60,49 @@ function resolve::dest() {
fi
}
# resolve::service_name <ip> [port] [proto]
# Returns just the service/host name, empty string if no match (not raw IP).
# Use when you need to know IF something resolved, not what the raw fallback is.
function resolve::service_name() {
local ip="${1:-}" port="${2:-}" proto="${3:-}"
[[ -z "$ip" ]] && echo "" && return 0
[[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "" && return 0
local result=""
# 1. hosts.json exact IP match
if [[ -f "$(ctx::hosts)" ]]; then
result=$(hosts::resolve_ip "$ip")
fi
# 2. services.json match
if [[ -z "$result" ]]; then
result=$(net::reverse_lookup "$ip" "$port" "$proto" 2>/dev/null) || result=""
fi
# Return empty if no match (caller handles raw fallback)
[[ "$result" == "$ip" ]] && result=""
echo "$result"
}
# resolve::endpoint_parts <endpoint_ip>
# Returns "raw_ip|resolved_name" — empty resolved if no match.
# Used by watch/logs rows to build "raw_ip → resolved" display.
function resolve::endpoint_parts() {
local ip="${1:-}"
[[ -z "$ip" ]] && echo "|" && return 0
[[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "${ip}|" && return 0
# Don't use cache for endpoint_parts — always resolve fresh
local resolved=""
[[ -f "$(ctx::hosts)" ]] && resolved=$(hosts::resolve_ip "$ip" 2>/dev/null || true)
if [[ -z "$resolved" ]]; then
resolved=$(net::reverse_lookup "$ip" "" "" 2>/dev/null) || resolved=""
fi
[[ "$resolved" == "$ip" ]] && resolved=""
echo "${ip}|${resolved}"
}
# resolve::clear_cache
# Clears the resolution cache — call between commands if needed.
function resolve::clear_cache() {

View file

@ -126,61 +126,61 @@ function ui::logs::fw_row_table() {
printf " %-20s %-18s %-25s %s%s\n" "$ts" "$client" "$dst" "$proto" "$count_str"
}
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:-}" resolved="${9:-}"
# 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:-}" 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 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"
# local count_suffix=""
# [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
# 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 * 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
# # 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 * 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
# ── Endpoint — native padding, no ui::pad_mb ──
local endpoint_colored endpoint_visible_len
local endpoint_raw="${endpoint:--}"
# # ── Endpoint — native padding, no ui::pad_mb ──
# local endpoint_colored endpoint_visible_len
# local endpoint_raw="${endpoint:--}"
if [[ -n "$resolved" && -n "$endpoint" ]]; then
endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m"
endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA ))
else
endpoint_colored="$endpoint_raw"
# "-" is 1 char; endpoint may be empty
[[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \
|| endpoint_visible_len=1
fi
# if [[ -n "$resolved" && -n "$endpoint" ]]; then
# endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m"
# endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA ))
# else
# endpoint_colored="$endpoint_raw"
# # "-" is 1 char; endpoint may be empty
# [[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \
# || endpoint_visible_len=1
# fi
local ep_pad_n=$(( w_endpoint - endpoint_visible_len ))
[[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "")
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
# local ep_pad_n=$(( w_endpoint - endpoint_visible_len ))
# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
# local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "")
# local ts_pad client_pad
# ts_pad=$(printf "%-11s" "$ts")
# client_pad=$(printf "%-${w_client}s" "$client")
printf " %s %s %b %b%s\033[0m%b%b\n" \
"$ts_pad" "$client_pad" \
"$endpoint_padded" \
"$event_color" "$event" "$count_suffix" "$gap_suffix"
}
# printf " %s %s %b %b%s\033[0m%b%b\n" \
# "$ts_pad" "$client_pad" \
# "$endpoint_padded" \
# "$event_color" "$event" "$count_suffix" "$gap_suffix"
# }
function ui::logs::wg_row_table() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}"
@ -198,79 +198,106 @@ function ui::logs::wg_row_table() {
_UI_WATCH_FW_COLOR="\033[1;31m"
_UI_WATCH_WG_COLOR="\033[1;32m"
function ui::watch::fw_row() {
local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \
w_client="${4:-20}" w_dest="${5:-18}"
# function ui::watch::fw_row() {
# local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \
# w_client="${4:-20}" w_dest="${5:-18}"
# "fw" is always 2 visible chars — no padding needed
local src="${_UI_WATCH_FW_COLOR}fw\033[0m"
# # "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")
client_pad=$(printf "%-${w_client}s" "$client")
dest_pad_n=$(( w_dest - ${#dest_display} ))
[[ $dest_pad_n -lt 0 ]] && dest_pad_n=0
# local ts_pad client_pad dest_pad_n
# ts_pad=$(printf "%-11s" "$ts")
# 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 %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" ""
}
# 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" ""
# }
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:-}" 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
# function ui::watch::wg_row() {
# local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
# w_client="${5:-20}" w_endpoint="${6:-18}"
local count_suffix=""
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
# 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 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 * 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 endpoint_display="${endpoint:--}"
# ── Endpoint padding ──
# " → " = 5 bytes, 3 visible → visible = endpoint + 3 + resolved
local endpoint_colored endpoint_visible_len
if [[ -n "$resolved" && -n "$endpoint" ]]; then
endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m"
endpoint_visible_len=$(( ${#endpoint} + 3 + ${#resolved} ))
elif [[ -n "$endpoint" ]]; then
endpoint_colored="${endpoint}"
endpoint_visible_len=${#endpoint}
else
endpoint_colored="-"
endpoint_visible_len=1
fi
# local ts_pad client_pad ep_pad_n
# ts_pad=$(printf "%-11s" "$ts")
# client_pad=$(printf "%-${w_client}s" "$client")
# ep_pad_n=$(( w_endpoint - ${#endpoint_display} ))
# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
local ep_pad_n=$(( w_endpoint - endpoint_visible_len ))
[[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
local endpoint_padded
endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "")
# printf " %s %s %s%*s %b%s\033[0m\n" \
# "$ts_pad" "$client_pad" \
# "$endpoint_display" "$ep_pad_n" "" \
# "$event_color" "$event"
# }
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
printf " %s %s %b %b%s\033[0m%b%b\n" \
"$ts_pad" "$client_pad" \
"$endpoint_padded" \
"$event_color" "$event" "$count_suffix" "$gap_suffix"
}
# 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:-}" 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"
# 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 * 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
# # ── Endpoint padding ──
# # " → " = 5 bytes, 3 visible → visible = endpoint + 3 + resolved
# local endpoint_colored endpoint_visible_len
# if [[ -n "$resolved" && -n "$endpoint" ]]; then
# endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m"
# endpoint_visible_len=$(( ${#endpoint} + 3 + ${#resolved} ))
# elif [[ -n "$endpoint" ]]; then
# endpoint_colored="${endpoint}"
# endpoint_visible_len=${#endpoint}
# else
# endpoint_colored="-"
# endpoint_visible_len=1
# fi
# local ep_pad_n=$(( w_endpoint - endpoint_visible_len ))
# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
# local endpoint_padded
# endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "")
# local ts_pad client_pad
# ts_pad=$(printf "%-11s" "$ts")
# client_pad=$(printf "%-${w_client}s" "$client")
# printf " %s %s %b %b%s\033[0m%b%b\n" \
# "$ts_pad" "$client_pad" \
# "$endpoint_padded" \
# "$event_color" "$event" "$count_suffix" "$gap_suffix"
# }
function ui::watch::header_table() {
printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \
@ -295,3 +322,190 @@ function ui::watch::wg_row_table() {
printf " %-20s %-8s %-22s %-28s %b%-14s\033[0m %s\n" \
"$ts" "wireguard" "$client" "$endpoint" "$event_color" "$event" "$status"
}
# ui::_render_endpoint_col
# Builds padded endpoint string: "raw_ip → resolved" or "raw_ip" or "-"
# Args: endpoint resolved w_endpoint
# Returns: padded colored string via stdout
function ui::_render_endpoint_col() {
local endpoint="${1:-}" resolved="${2:-}" w_endpoint="${3:-20}"
local colored visible_len pad_n
if [[ -n "$endpoint" ]]; then
if [[ -n "$resolved" ]]; then
colored="${endpoint} \033[2m→ ${resolved}\033[0m"
visible_len=$(( ${#endpoint} + 3 + ${#resolved} ))
else
colored="$endpoint"
visible_len=${#endpoint}
fi
else
colored="\033[2m—\033[0m"
visible_len=1
fi
pad_n=$(( w_endpoint - visible_len ))
[[ $pad_n -lt 0 ]] && pad_n=0
printf "%b%*s" "$colored" "$pad_n" ""
}
# ui::_render_dest_col
# Builds padded destination string: "svc/proto (ip:port)" or raw
# Args: dest_ip dest_port proto svc_name w_dest resolved_only
# Returns: two vars via nameref — dest_colored dest_pad_n
function ui::_build_dest() {
local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}" \
svc_name="${4:-}" w_dest="${5:-20}" resolved_only="${6:-false}"
local svc_display raw_suffix="" raw_plain=""
if [[ -n "$svc_name" ]]; then
[[ -n "$dest_port" ]] && svc_display="${svc_name}/${proto}" \
|| svc_display="${svc_name} (${proto})"
if ! $resolved_only; then
[[ -n "$dest_port" ]] && raw_suffix=" \033[2m(${dest_ip}:${dest_port})\033[0m" \
|| raw_suffix=" \033[2m(${dest_ip})\033[0m"
[[ -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" \
|| raw_plain=" (${dest_ip})"
fi
else
[[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \
|| svc_display="${dest_ip} (${proto})"
fi
local full_len=$(( ${#svc_display} + ${#raw_plain} ))
local dest_pad_n=$(( w_dest - full_len ))
[[ $dest_pad_n -lt 0 ]] && dest_pad_n=0
# Output: svc_display|raw_suffix|dest_pad_n
printf "%s\t%s\t%s" "$svc_display" "$raw_suffix" "$dest_pad_n"
}
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}" \
src_endpoint="${10:-}" src_resolved="${11:-}" \
w_endpoint="${12:-0}" resolved_only="${13:-false}"
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
local count_suffix=""
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
# Dest
local dest_parts
dest_parts=$(ui::_build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name" "$w_dest" "$resolved_only")
local svc_display raw_suffix dest_pad_n
IFS=$'\t' read -r svc_display raw_suffix dest_pad_n <<< "$dest_parts"
if [[ "$w_endpoint" -gt 0 ]]; then
local src_padded
src_padded=$(ui::_render_endpoint_col "$src_endpoint" "$src_resolved" "$w_endpoint")
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::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:-}" 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"
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 * 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 ep_padded
ep_padded=$(ui::_render_endpoint_col "$endpoint" "$resolved" "$w_endpoint")
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
printf " %s %s %b %b%s\033[0m%b%b\n" \
"$ts_pad" "$client_pad" "$ep_padded" \
"$event_color" "$event" "$count_suffix" "$gap_suffix"
}
function ui::watch::fw_row() {
local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \
proto="${5:-}" svc_name="${6:-}" \
src_endpoint="${7:-}" src_resolved="${8:-}" \
w_client="${9:-20}" w_dest="${10:-18}" w_endpoint="${11:-0}"
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
local dest_parts
dest_parts=$(ui::_build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name" "$w_dest" "false")
local svc_display raw_suffix dest_pad_n
IFS=$'\t' read -r svc_display raw_suffix dest_pad_n <<< "$dest_parts"
if [[ "$w_endpoint" -gt 0 ]]; then
local src_padded
src_padded=$(ui::_render_endpoint_col "$src_endpoint" "$src_resolved" "$w_endpoint")
printf " %s %s %s \033[1;31m→\033[0m %s%b%*s \033[1;31mdrop\033[0m\n" \
"$ts_pad" "$client_pad" "$src_padded" \
"$svc_display" "$raw_suffix" "$dest_pad_n" ""
else
printf " %s %s \033[1;31m→\033[0m %s%b%*s \033[1;31mdrop\033[0m\n" \
"$ts_pad" "$client_pad" \
"$svc_display" "$raw_suffix" "$dest_pad_n" ""
fi
}
function ui::watch::wg_row() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
src_resolved="${5:-}" \
w_client="${6:-20}" w_endpoint="${7:-18}"
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 ep_padded
ep_padded=$(ui::_render_endpoint_col "$endpoint" "$src_resolved" "$w_endpoint")
local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
# echo "DEBUG: ts='$ts_pad' client='$client_pad'(${#client_pad}) ep='$ep_padded'(${#ep_padded}) event='$event'" >&2
printf " %s %s %s %b%s\033[0m\n" \
"$ts_pad" "$client_pad" "$ep_padded" \
"$event_color" "$event"
}