Zuletzt aktiv 1 month ago

nuno hat die Gist bearbeitet 1 month ago. Zu Änderung gehen

1 file changed, 655 insertions

gistfile1.txt(Datei erstellt)

@@ -0,0 +1,655 @@
1 + #!/usr/bin/env bash
2 +
3 + # ============================================
4 + # Lifecycle
5 + # ============================================
6 +
7 + function cmd::list::on_load() {
8 + load_module identity
9 + load_module ui
10 +
11 + flag::register --type
12 + flag::register --rule
13 + flag::register --group
14 + flag::register --identity
15 + flag::register --online
16 + flag::register --offline
17 + flag::register --restricted
18 + flag::register --blocked
19 + flag::register --allowed
20 + flag::register --detailed
21 + flag::register --name
22 + }
23 +
24 + # ============================================
25 + # Help
26 + # ============================================
27 +
28 + function cmd::list::help() {
29 + cat <<EOF
30 + Usage: wgctl list [options]
31 +
32 + List all WireGuard clients.
33 +
34 + Options:
35 + --type <type> Filter by device type
36 + --rule <rule> Filter by assigned rule
37 + --group <group> Filter by group membership
38 + --identity <name> Filter by identity
39 + --online Show only connected clients
40 + --offline Show only disconnected clients
41 + --blocked Show only fully blocked clients
42 + --restricted Show only restricted clients
43 + --allowed Show only unrestricted clients
44 + --detailed Show detailed view grouped by identity
45 + --name <name> Show detail card for a single client
46 +
47 + Examples:
48 + wgctl list
49 + wgctl list --type phone
50 + wgctl list --identity nuno
51 + wgctl list --online
52 + wgctl list --blocked
53 + wgctl list --detailed
54 + wgctl list --name phone-nuno
55 + EOF
56 + }
57 +
58 + # ============================================
59 + # Detail Card
60 + # ============================================
61 +
62 + function cmd::list::show_client() {
63 + local name="${1:-}"
64 + local conf
65 + conf="$(ctx::clients)/${name}.conf"
66 +
67 + if [[ ! -f "$conf" ]]; then
68 + log::error "Client not found: ${name}"
69 + return 1
70 + fi
71 +
72 + local ip
73 + ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
74 +
75 + local allowed_ips
76 + allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
77 +
78 + local public_key
79 + public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
80 +
81 + local type
82 + type=$(peers::get_meta "$name" "type" 2>/dev/null)
83 + [[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
84 +
85 + local endpoint="—"
86 + local ep
87 + ep=$(monitor::endpoint_for_key "$public_key")
88 + [[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name")
89 + [[ -n "$ep" ]] && endpoint="$ep"
90 +
91 + local is_blocked="false"
92 + peers::is_blocked "$name" && is_blocked="true"
93 +
94 + local is_restricted="false"
95 + peers::is_restricted "$name" && is_restricted="true"
96 +
97 + local handshake_ts
98 + handshake_ts=$(monitor::get_handshake_ts "$public_key")
99 +
100 + local last_ts
101 + last_ts=$(monitor::last_attempt "$name")
102 +
103 + local status
104 + status=$(peers::format_status_verbose "$name" "$public_key" \
105 + "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
106 +
107 + local last_seen
108 + last_seen=$(peers::format_last_seen "$name" "$public_key" \
109 + "$is_blocked" "$last_ts" "" "$handshake_ts")
110 +
111 + local blocks=""
112 + if block::has_file "$name" && block::is_blocked "$name"; then
113 + if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
114 + blocks="all traffic blocked"
115 + fi
116 + local rule_lines
117 + rule_lines=$(block::format_rules "$name")
118 + [[ -n "$rule_lines" ]] && blocks+="$rule_lines"
119 + fi
120 +
121 + ui::section "Client: ${name}"
122 + ui::row "IP" "$ip"
123 + ui::row "Type" "$(peers::display_type "$type")"
124 + ui::row "Status" "$(echo -e "$status")"
125 + ui::row "Endpoint" "$endpoint"
126 + ui::row "Last seen" "$last_seen"
127 + ui::row "AllowedIPs" "$allowed_ips"
128 + ui::row "Public key" "$public_key"
129 +
130 + if [[ -z "$blocks" ]]; then
131 + ui::row "Blocks" "none"
132 + elif [[ "$blocks" == *"all traffic blocked"* ]]; then
133 + ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")"
134 + else
135 + printf " %-20s\n" "Blocks:"
136 + echo -e "$blocks"
137 + fi
138 + printf "\n"
139 + }
140 +
141 + # ============================================
142 + # Run
143 + # ============================================
144 +
145 + function cmd::list::run() {
146 + local filter_type="" filter_rule="" filter_group="" filter_identity=""
147 + local online_only=false offline_only=false
148 + local restricted_only=false blocked_only=false allowed_only=false
149 + local detailed=false single_name=""
150 +
151 + while [[ $# -gt 0 ]]; do
152 + case "$1" in
153 + --type) filter_type="$2"; shift 2 ;;
154 + --rule) filter_rule="$2"; shift 2 ;;
155 + --group) filter_group="$2"; shift 2 ;;
156 + --identity) filter_identity="$2"; shift 2 ;;
157 + --online) online_only=true; shift ;;
158 + --offline) offline_only=true; shift ;;
159 + --restricted) restricted_only=true; shift ;;
160 + --blocked) blocked_only=true; shift ;;
161 + --allowed) allowed_only=true; shift ;;
162 + --detailed) detailed=true; shift ;;
163 + --name) single_name="$2"; shift 2 ;;
164 + --help) cmd::list::help; return ;;
165 + *)
166 + log::error "Unknown flag: $1"
167 + cmd::list::help
168 + return 1
169 + ;;
170 + esac
171 + done
172 +
173 + if [[ -n "$single_name" ]]; then
174 + cmd::list::show_client "$single_name"
175 + return 0
176 + fi
177 +
178 + local dir
179 + dir="$(ctx::clients)"
180 + local confs=("${dir}"/*.conf)
181 + if [[ ! -f "${confs[0]}" ]]; then
182 + log::wg_warning "No clients configured"
183 + return 0
184 + fi
185 +
186 + cmd::list::_precompute_all
187 +
188 + # Resolve identity filter
189 + declare -gA p_identity_filter=()
190 + if [[ -n "$filter_identity" ]]; then
191 + identity::require_exists "$filter_identity" || return 1
192 + while IFS= read -r peer_name; do
193 + [[ -n "$peer_name" ]] && p_identity_filter["$peer_name"]=1
194 + done < <(identity::peers "$filter_identity")
195 + if [[ ${#p_identity_filter[@]} -eq 0 ]]; then
196 + log::wg_warning "Identity '${filter_identity}' has no peers"
197 + return 0
198 + fi
199 + fi
200 +
201 + log::section "WireGuard Clients"
202 +
203 + # Collect all filtered rows first (needed for dynamic column widths)
204 + local collected_rows=""
205 + collected_rows=$(cmd::list::_collect_all_rows | ui::sort_rows)
206 +
207 + if [[ -z "$collected_rows" ]]; then
208 + log::wg_warning "No results found"
209 + return 0
210 + fi
211 +
212 + if $detailed; then
213 + cmd::list::_render_detailed "$collected_rows"
214 + cmd::list::_render_summary_from_rows "$collected_rows"
215 + return 0
216 + fi
217 +
218 + local style
219 + style=$(ui::peer::list_style)
220 +
221 + case "$style" in
222 + table) cmd::list::_render_table ;;
223 + compact) cmd::list::_render_compact "$collected_rows" ;;
224 + *) cmd::list::_render_compact "$collected_rows" ;;
225 + esac
226 + }
227 +
228 + # ============================================
229 + # Row collection (single pass, all filters)
230 + # ============================================
231 +
232 + function cmd::list::_collect_all_rows() {
233 + # Outputs pipe-delimited rows for peers that pass all filters
234 + # Fields: name|ip|type|rule|group|status|last_seen|is_blocked|is_restricted
235 + local dir
236 + dir="$(ctx::clients)"
237 +
238 + for conf in "${dir}"/*.conf; do
239 + [[ -f "$conf" ]] || continue
240 + local client_name
241 + client_name=$(basename "$conf" .conf)
242 + [[ -z "$client_name" ]] && continue
243 +
244 + # Identity filter
245 + if [[ ${#p_identity_filter[@]} -gt 0 && \
246 + -z "${p_identity_filter[$client_name]:-}" ]]; then
247 + continue
248 + fi
249 +
250 + local ip="${p_ips[$client_name]:-}"
251 + [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
252 + [[ -z "$ip" ]] && continue
253 +
254 + local type="${p_types[$client_name]:-unknown}"
255 + [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
256 +
257 + local pubkey="${p_pubkeys[$client_name]:-}"
258 + local handshake_ts="${wg_handshakes[$pubkey]:-0}"
259 + local is_blocked="${p_blocked[$client_name]:-false}"
260 + local is_restricted="${p_restricted[$client_name]:-false}"
261 + local last_ts="${p_last_ts[$client_name]:-}"
262 + local rule="${p_rules[$client_name]:-}"
263 + local group="${p_main_groups[$client_name]:-}"
264 +
265 + # Apply status filters
266 + if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || continue; fi
267 + if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || continue; fi
268 + if $restricted_only && [[ "$is_restricted" != "true" ]]; then continue; fi
269 + if $blocked_only && [[ "$is_blocked" != "true" ]]; then continue; fi
270 + if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
271 + [[ "$is_restricted" == "true" ]]; }; then continue; fi
272 +
273 + # Apply rule/group filters
274 + if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then continue; fi
275 + if [[ -n "$filter_group" ]]; then
276 + local all_groups="${peer_group_map[$client_name]:-}"
277 + [[ "$all_groups" != *"$filter_group"* ]] && continue
278 + fi
279 +
280 + # Resolve status
281 + local state
282 + state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
283 + local status="${state%%|*}"
284 +
285 + # Resolve last seen
286 + local last_seen="-"
287 + if [[ "$is_blocked" == "true" && -n "$last_ts" && "$last_ts" != "0" ]]; then
288 + local attempt_ts
289 + attempt_ts=$(json::iso_to_ts "$last_ts")
290 + last_seen="$(fmt::datetime_short "$attempt_ts") (dropped)"
291 + elif [[ -n "$handshake_ts" && "$handshake_ts" != "0" ]]; then
292 + local ts_display
293 + ts_display=$(fmt::datetime_short "$handshake_ts")
294 + if [[ "$status" == "online" ]]; then
295 + last_seen="${ts_display} (handshake)"
296 + else
297 + last_seen="$ts_display"
298 + fi
299 + fi
300 +
301 + printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \
302 + "$client_name" "$ip" "$type" \
303 + "${rule:--}" "${group:--}" \
304 + "$status" "$last_seen" \
305 + "$is_blocked" "$is_restricted"
306 + done
307 + }
308 +
309 + # ============================================
310 + # Compact render
311 + # ============================================
312 +
313 + function cmd::list::_render_compact() {
314 + local rows="${1:-}"
315 +
316 + # Measure column widths from pure values (fields 1-5, no labels)
317 + local w_name w_ip w_type w_rule w_group
318 + w_name=$(ui::measure_col "$rows" 1 14)
319 + w_ip=$(ui::measure_col "$rows" 2 13)
320 + w_type=$(ui::measure_col "$rows" 3 7)
321 + w_rule=$(ui::measure_col "$rows" 4 4)
322 + w_group=$(ui::measure_col "$rows" 5 4)
323 +
324 + echo ""
325 + while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
326 + [[ -z "$name" ]] && continue
327 + ui::peer::list_row_compact \
328 + "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" \
329 + "$name" "$ip" "$type" "$rule" "$group" \
330 + "$status" "$last_seen" "$is_blocked" "$is_restricted"
331 + done <<< "$rows"
332 + echo ""
333 +
334 + cmd::list::_render_summary_from_rows "$rows"
335 + }
336 +
337 + # ============================================
338 + # Table render (kept for config switching)
339 + # ============================================
340 +
341 + function cmd::list::_render_table() {
342 + declare -A rule_counts=() group_counts=()
343 + _list_header_printed=false
344 +
345 + cmd::list::_iter_confs_table
346 +
347 + if [[ "$_list_header_printed" == "true" ]]; then
348 + cmd::list::_render_footer $has_groups
349 + local group_summary=""
350 + cmd::list::_build_group_summary
351 + printf "\n Showing peers\n\n"
352 + else
353 + log::wg_warning "No results found"
354 + fi
355 + }
356 +
357 + function cmd::list::_iter_confs_table() {
358 + local dir
359 + dir="$(ctx::clients)"
360 + for conf in "${dir}"/*.conf; do
361 + [[ -f "$conf" ]] || continue
362 + local client_name
363 + client_name=$(basename "$conf" .conf)
364 + [[ -z "$client_name" ]] && continue
365 +
366 + if [[ ${#p_identity_filter[@]} -gt 0 && \
367 + -z "${p_identity_filter[$client_name]:-}" ]]; then
368 + continue
369 + fi
370 +
371 + local ip="${p_ips[$client_name]:-}"
372 + [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
373 +
374 + local type="${p_types[$client_name]:-unknown}"
375 + [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
376 +
377 + cmd::list::_render_row "$client_name" "$ip" "$type"
378 + done
379 + }
380 +
381 + # ============================================
382 + # Detailed render (grouped by identity)
383 + # ============================================
384 +
385 + function cmd::list::_render_detailed() {
386 + local rows="${1:-}"
387 +
388 + # Measure widths
389 + local w_name w_ip w_type w_rule w_group w_subnet
390 + w_name=$(ui::measure_col "$rows" 1 14)
391 + w_ip=$(ui::measure_col "$rows" 2 13)
392 + w_type=$(ui::measure_col "$rows" 3 7)
393 + w_rule=$(ui::measure_col "$rows" 4 4)
394 + w_group=$(ui::measure_col "$rows" 5 4)
395 + # subnet not in rows — use fixed width
396 + w_subnet=10
397 +
398 + # Group by identity
399 + declare -A identity_rows=()
400 + local no_identity_rows=""
401 +
402 + while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
403 + [[ -z "$name" ]] && continue
404 + local id_name
405 + id_name=$(identity::get_name "$name")
406 + local row="${name}|${ip}|${type}|${rule}|${group}|${status}|${last_seen}|${is_blocked}|${is_restricted}"
407 + if [[ -n "$id_name" ]]; then
408 + identity_rows["$id_name"]+="${row}"$'\n'
409 + else
410 + no_identity_rows+="${row}"$'\n'
411 + fi
412 + done <<< "$rows"
413 +
414 + echo ""
415 +
416 + # Render identity groups (sorted)
417 + for id_name in $(echo "${!identity_rows[@]}" | tr ' ' '\n' | sort); do
418 + ui::peer::list_identity_header "$id_name"
419 + while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
420 + [[ -z "$name" ]] && continue
421 + local subnet
422 + subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null)
423 + if [[ -z "$subnet" ]]; then
424 + local peer_type="${p_types[$name]:-}"
425 + [[ -n "$peer_type" ]] && subnet="$peer_type"
426 + fi
427 + [[ -z "$subnet" ]] && subnet="-"
428 + ui::peer::list_row_detailed \
429 + "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
430 + "$name" "$ip" "$type" "$rule" "$group" "$subnet" \
431 + "$status" "$last_seen" "$is_blocked" "$is_restricted"
432 + done < <(echo "${identity_rows[$id_name]}" | ui::sort_rows)
433 + done
434 +
435 + # Render peers without identity (no "other" header if empty)
436 + if [[ -n "$no_identity_rows" ]]; then
437 + local trimmed
438 + trimmed=$(echo "$no_identity_rows" | grep -v '^$')
439 + if [[ -n "$trimmed" ]]; then
440 + ui::peer::list_identity_header "other"
441 + while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
442 + [[ -z "$name" ]] && continue
443 + local subnet
444 + subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null)
445 + if [[ -z "$subnet" ]]; then
446 + local peer_type="${p_types[$name]:-}"
447 + [[ -n "$peer_type" ]] && subnet="$peer_type"
448 + fi
449 + [[ -z "$subnet" ]] && subnet="-"
450 + ui::peer::list_row_detailed \
451 + "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
452 + "$name" "$ip" "$type" "$rule" "$group" "$subnet" \
453 + "$status" "$last_seen" "$is_blocked" "$is_restricted"
454 + done <<< "$trimmed"
455 + fi
456 + fi
457 +
458 + echo ""
459 + }
460 +
461 + # ============================================
462 + # Summary
463 + # ============================================
464 +
465 + function cmd::list::_render_summary_from_rows() {
466 + local rows="${1:-}"
467 + declare -A rule_counts=()
468 + local total=0
469 +
470 + while IFS='|' read -r name ip type rule rest; do
471 + [[ -z "$name" ]] && continue
472 + (( total++ )) || true
473 + rule_counts["${rule:-—}"]=$(( ${rule_counts[${rule:-—}]:-0} + 1 )) || true
474 + done <<< "$rows"
475 +
476 + local summary=""
477 + for r in "${!rule_counts[@]}"; do
478 + summary+="${rule_counts[$r]} ${r}, "
479 + done
480 + summary="${summary%, }"
481 +
482 + printf " Showing %s peers [%s]\n\n" "$total" "$summary"
483 + }
484 +
485 + # ============================================
486 + # Table row rendering
487 + # ============================================
488 +
489 + function cmd::list::_render_row() {
490 + local client_name="$1" ip="$2" type="$3"
491 +
492 + local pubkey="${p_pubkeys[$client_name]:-}"
493 + local handshake_ts="${wg_handshakes[$pubkey]:-0}"
494 + local is_blocked="${p_blocked[$client_name]:-false}"
495 + local is_restricted="${p_restricted[$client_name]:-false}"
496 + local last_ts="${p_last_ts[$client_name]:-}"
497 +
498 + if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
499 + if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
500 + if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
501 + if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
502 + if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
503 + [[ "$is_restricted" == "true" ]]; }; then return 0; fi
504 +
505 + if [[ -n "$filter_group" ]]; then
506 + local all_groups="${peer_group_map[$client_name]:-}"
507 + [[ "$all_groups" != *"$filter_group"* ]] && return 0
508 + fi
509 +
510 + local status last_seen display_type rule
511 + status=$(peers::format_status_verbose "$client_name" "$pubkey" \
512 + "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
513 + last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
514 + "$is_blocked" "$last_ts" "" "$handshake_ts")
515 + display_type=$(peers::display_type "$type")
516 + rule="${p_rules[$client_name]:-—}"
517 +
518 + if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
519 +
520 + if [[ "${_list_header_printed:-false}" == "false" ]]; then
521 + cmd::list::_render_header $has_groups
522 + _list_header_printed=true
523 + fi
524 +
525 + rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
526 +
527 + local padded_status
528 + padded_status=$(ui::pad_status "$status" 25)
529 +
530 + if $has_groups; then
531 + local main_group="${p_main_groups[$client_name]:-}"
532 + local group_display="${main_group:-${peer_group_map[$client_name]:-—}}"
533 + printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
534 + "$client_name" "$ip" "$display_type" "$rule" \
535 + "$group_display" "$padded_status" "$last_seen"
536 + else
537 + printf " %-28s %-15s %-13s %-12s %s %s\n" \
538 + "$client_name" "$ip" "$display_type" "$rule" \
539 + "$padded_status" "$last_seen"
540 + fi
541 + }
542 +
543 + # ============================================
544 + # Precompute
545 + # ============================================
546 +
547 + function cmd::list::_precompute_all() {
548 + declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
549 + while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
550 + [[ -z "$name" ]] && continue
551 + p_ips["$name"]="$ip"
552 + p_rules["$name"]="${rule:-}"
553 + p_types["$name"]="${type:-}"
554 + p_last_ts["$name"]="$last_ts"
555 + p_last_evt["$name"]="$last_evt"
556 + p_main_groups["$name"]="${main_group:-}"
557 + done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
558 +
559 + for name in "${!p_ips[@]}"; do
560 + [[ -n "${p_types[$name]:-}" ]] && continue
561 + p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
562 + done
563 +
564 + declare -gA wg_handshakes=() wg_endpoints=()
565 + while IFS=$'\t' read -r pubkey ts; do
566 + [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
567 + done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
568 + while IFS=$'\t' read -r pubkey endpoint; do
569 + [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
570 + done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
571 +
572 + declare -gA p_blocked=() p_restricted=()
573 + cmd::list::_precompute_block_status p_blocked p_restricted
574 +
575 + declare -gA p_pubkeys=()
576 + local dir
577 + dir="$(ctx::clients)"
578 + for kf in "${dir}"/*_public.key; do
579 + [[ -f "$kf" ]] || continue
580 + local kname
581 + kname=$(basename "$kf" _public.key)
582 + p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
583 + done
584 +
585 + has_groups=false
586 + declare -gA peer_group_map=()
587 + local groups_dir
588 + groups_dir="$(ctx::groups)"
589 + local group_files=("${groups_dir}"/*.group)
590 + if [[ -f "${group_files[0]}" ]]; then
591 + has_groups=true
592 + while IFS=":" read -r peer_name group_name; do
593 + [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
594 + done < <(json::peer_group_map "$groups_dir")
595 + fi
596 +
597 + # Identity precompute (for --identity filter)
598 + declare -gA p_identity_filter=()
599 + }
600 +
601 + function cmd::list::_precompute_block_status() {
602 + local -n _blocked="$1"
603 + local -n _restricted="$2"
604 +
605 + local wg_peers
606 + wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
607 +
608 + while IFS= read -r name; do
609 + if block::has_specific_rules "$name" 2>/dev/null; then
610 + _restricted["$name"]=true
611 + else
612 + _restricted["$name"]=false
613 + fi
614 +
615 + local pubkey
616 + pubkey=$(keys::public "$name" 2>/dev/null || echo "")
617 + if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
618 + _blocked["$name"]=true
619 + else
620 + _blocked["$name"]=false
621 + fi
622 + done < <(peers::all)
623 + }
624 +
625 + # ============================================
626 + # Header / Footer (table layout)
627 + # ============================================
628 +
629 + function cmd::list::_render_header() {
630 + ui::peer::list_header_table "$1"
631 + }
632 +
633 + function cmd::list::_render_footer() {
634 + ui::peer::list_footer_table "$1"
635 + }
636 +
637 + function cmd::list::_build_group_summary() {
638 + group_summary=""
639 + if $has_groups; then
640 + declare -A _gs=()
641 + for peer in "${!peer_group_map[@]}"; do
642 + local g="${peer_group_map[$peer]}"
643 + _gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
644 + done
645 + for g in "${!_gs[@]}"; do
646 + group_summary+="${_gs[$g]} in ${g}, "
647 + done
648 + group_summary="${group_summary%, }"
649 + fi
650 + }
651 +
652 + function cmd::list::_show_client_safe() {
653 + local name="$1"
654 + cmd::list::show_client "$name" || true
655 + }
Neuer Älter