tr200 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,683 @@
1
+ #!/bin/bash
2
+ # TR-200 Machine Report
3
+ # Copyright 2026, ES Development LLC (https://emmetts.dev)
4
+ # Based on original work by U.S. Graphics, LLC (BSD-3-Clause)
5
+ #
6
+ # Cross-platform system information tool
7
+ # Supports: Linux (all major distros), macOS 10.13+, partial BSD support
8
+ # Requires: Bash 4.0+ (macOS users: brew install bash)
9
+
10
+ # Global variables
11
+ MIN_NAME_LEN=5
12
+ MAX_NAME_LEN=13
13
+
14
+ MIN_DATA_LEN=20
15
+ MAX_DATA_LEN=32
16
+
17
+ BORDERS_AND_PADDING=7
18
+
19
+ # Basic configuration, change as needed
20
+ report_title="SHAUGHNESSY V DEVELOPMENT INC."
21
+ last_login_ip_present=0
22
+ zfs_present=0
23
+ zfs_filesystem="zroot/ROOT/os"
24
+
25
+ # ============================================================================
26
+ # CROSS-PLATFORM COMPATIBILITY FRAMEWORK
27
+ # ============================================================================
28
+
29
+ # Detect operating system type
30
+ detect_os() {
31
+ if [[ "$OSTYPE" == "darwin"* ]]; then
32
+ echo "macos"
33
+ elif [[ "$OSTYPE" == "linux"* ]] || [[ "$OSTYPE" == "linux-gnu"* ]]; then
34
+ echo "linux"
35
+ elif [[ "$OSTYPE" == "freebsd"* ]] || [[ "$OSTYPE" == "openbsd"* ]] || [[ "$OSTYPE" == "netbsd"* ]]; then
36
+ echo "bsd"
37
+ else
38
+ echo "unknown"
39
+ fi
40
+ }
41
+
42
+ # Check if command exists
43
+ command_exists() {
44
+ command -v "$1" &> /dev/null
45
+ }
46
+
47
+ # Check if file exists and is readable
48
+ file_readable() {
49
+ [ -f "$1" ] && [ -r "$1" ]
50
+ }
51
+
52
+ # Validate that a value looks like an IPv4 address (basic check)
53
+ is_ipv4() {
54
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]
55
+ }
56
+
57
+ # Set OS type
58
+ OS_TYPE=$(detect_os)
59
+
60
+ # Check Bash version (warn but don't exit)
61
+ if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
62
+ echo "⚠ Warning: Bash 4.0+ recommended for best compatibility" >&2
63
+ echo " Current version: ${BASH_VERSION}" >&2
64
+ if [ "$OS_TYPE" = "macos" ]; then
65
+ echo " macOS users: Install with 'brew install bash'" >&2
66
+ fi
67
+ echo "" >&2
68
+ fi
69
+
70
+ # ============================================================================
71
+ # UTILITIES
72
+ # ============================================================================
73
+ max_length() {
74
+ local max_len=0
75
+ local len
76
+
77
+ for str in "$@"; do
78
+ len=${#str}
79
+ if (( len > max_len )); then
80
+ max_len=$len
81
+ fi
82
+ done
83
+
84
+ if [ $max_len -lt $MAX_DATA_LEN ]; then
85
+ printf '%s' "$max_len"
86
+ else
87
+ printf '%s' "$MAX_DATA_LEN"
88
+ fi
89
+ }
90
+
91
+ # All data strings must go here
92
+ set_current_len() {
93
+ CURRENT_LEN=$(max_length \
94
+ "$report_title" \
95
+ "$os_name" \
96
+ "$os_kernel" \
97
+ "$net_hostname" \
98
+ "$net_machine_ip" \
99
+ "$net_client_ip" \
100
+ "$net_current_user" \
101
+ "$cpu_model" \
102
+ "$cpu_cores_per_socket vCPU(s) / $cpu_sockets Socket(s)" \
103
+ "$cpu_hypervisor" \
104
+ "$cpu_freq GHz" \
105
+ "$cpu_1min_bar_graph" \
106
+ "$cpu_5min_bar_graph" \
107
+ "$cpu_15min_bar_graph" \
108
+ "$zfs_used_gb/$zfs_available_gb GB [$disk_percent%]" \
109
+ "$disk_bar_graph" \
110
+ "$zfs_health" \
111
+ "$root_used_gb/$root_total_gb GB [$disk_percent%]" \
112
+ "${mem_used_gb}/${mem_total_gb} GiB [${mem_percent}%]" \
113
+ "${mem_bar_graph}" \
114
+ "$last_login_time" \
115
+ "$last_login_ip" \
116
+ "$last_login_ip" \
117
+ "$sys_uptime" \
118
+ )
119
+ }
120
+
121
+ PRINT_HEADER() {
122
+ local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING))
123
+
124
+ local top="┌"
125
+ local bottom="├"
126
+ for (( i = 0; i < length - 2; i++ )); do
127
+ top+="┬"
128
+ bottom+="┴"
129
+ done
130
+ top+="┐"
131
+ bottom+="┤"
132
+
133
+ printf '%s\n' "$top"
134
+ printf '%s\n' "$bottom"
135
+ }
136
+
137
+ PRINT_CENTERED_DATA() {
138
+ local max_len=$((CURRENT_LEN+MAX_NAME_LEN-BORDERS_AND_PADDING))
139
+ local text="$1"
140
+ local total_width=$((max_len + 12))
141
+
142
+ local text_len=${#text}
143
+ local padding_left=$(( (total_width - text_len) / 2 ))
144
+ local padding_right=$(( total_width - text_len - padding_left ))
145
+
146
+ printf "│%${padding_left}s%s%${padding_right}s│\n" "" "$text" ""
147
+ }
148
+
149
+ PRINT_DIVIDER() {
150
+ # either "top" or "bottom", no argument means middle divider
151
+ local side="$1"
152
+ case "$side" in
153
+ "top")
154
+ local left_symbol="├"
155
+ local middle_symbol="┬"
156
+ local right_symbol="┤"
157
+ ;;
158
+ "bottom")
159
+ local left_symbol="└"
160
+ local middle_symbol="┴"
161
+ local right_symbol="┘"
162
+ ;;
163
+ *)
164
+ local left_symbol="├"
165
+ local middle_symbol="┼"
166
+ local right_symbol="┤"
167
+ esac
168
+
169
+ local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING))
170
+ local divider="$left_symbol"
171
+ for (( i = 0; i < length - 3; i++ )); do
172
+ divider+="─"
173
+ if [ "$i" -eq 14 ]; then
174
+ divider+="$middle_symbol"
175
+ fi
176
+ done
177
+ divider+="$right_symbol"
178
+ printf '%s\n' "$divider"
179
+ }
180
+
181
+ PRINT_DATA() {
182
+ local name="$1"
183
+ local data="$2"
184
+ local max_data_len=$CURRENT_LEN
185
+
186
+ # Pad name
187
+ local name_len=${#name}
188
+ if (( name_len < MIN_NAME_LEN )); then
189
+ name=$(printf "%-${MIN_NAME_LEN}s" "$name")
190
+ elif (( name_len > MAX_NAME_LEN )); then
191
+ name=$(echo "$name" | cut -c 1-$((MAX_NAME_LEN-3)))...
192
+ else
193
+ name=$(printf "%-${MAX_NAME_LEN}s" "$name")
194
+ fi
195
+
196
+ # Truncate or pad data
197
+ local data_len=${#data}
198
+ if (( data_len >= MAX_DATA_LEN || data_len == MAX_DATA_LEN-1 )); then
199
+ data=$(echo "$data" | cut -c 1-$((MAX_DATA_LEN-3-2)))...
200
+ else
201
+ data=$(printf "%-${max_data_len}s" "$data")
202
+ fi
203
+
204
+ printf "│ %-${MAX_NAME_LEN}s │ %s │\n" "$name" "$data"
205
+ }
206
+
207
+ PRINT_FOOTER() {
208
+ local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING))
209
+ local footer="└"
210
+ for (( i = 0; i < length - 3; i++ )); do
211
+ footer+="─"
212
+ if [ "$i" -eq 14 ]; then
213
+ footer+="┴"
214
+ fi
215
+ done
216
+ footer+="┘"
217
+ printf '%s\n' "$footer"
218
+ }
219
+
220
+ bar_graph() {
221
+ local percent
222
+ local num_blocks
223
+ local width=$CURRENT_LEN
224
+ local graph=""
225
+ local used=$1
226
+ local total=$2
227
+
228
+ if (( total == 0 )); then
229
+ percent=0
230
+ else
231
+ percent=$(awk -v used="$used" -v total="$total" 'BEGIN { printf "%.2f", (used / total) * 100 }')
232
+ fi
233
+
234
+ num_blocks=$(awk -v percent="$percent" -v width="$width" 'BEGIN { printf "%d", (percent / 100) * width }')
235
+
236
+ for (( i = 0; i < num_blocks; i++ )); do
237
+ graph+="█"
238
+ done
239
+ for (( i = num_blocks; i < width; i++ )); do
240
+ graph+="░"
241
+ done
242
+ printf "%s" "${graph}"
243
+ }
244
+
245
+ get_ip_addr() {
246
+ # Initialize variables
247
+ ipv4_address=""
248
+ ipv6_address=""
249
+
250
+ # Check if ifconfig command exists
251
+ if command -v ifconfig &> /dev/null; then
252
+ # Try to get IPv4 address using ifconfig
253
+ ipv4_address=$(ifconfig | awk '
254
+ /^[a-z]/ {iface=$1}
255
+ iface != "lo:" && iface !~ /^docker/ && /inet / && !found_ipv4 {found_ipv4=1; print $2}')
256
+
257
+ # If IPv4 address not available, try IPv6 using ifconfig
258
+ if [ -z "$ipv4_address" ]; then
259
+ ipv6_address=$(ifconfig | awk '
260
+ /^[a-z]/ {iface=$1}
261
+ iface != "lo:" && iface !~ /^docker/ && /inet6 / && !found_ipv6 {found_ipv6=1; print $2}')
262
+ fi
263
+ elif command -v ip &> /dev/null; then
264
+ # Try to get IPv4 address using ip addr
265
+ ipv4_address=$(ip -o -4 addr show | awk '
266
+ $2 != "lo" && $2 !~ /^docker/ {split($4, a, "/"); if (!found_ipv4++) print a[1]}')
267
+
268
+ # If IPv4 address not available, try IPv6 using ip addr
269
+ if [ -z "$ipv4_address" ]; then
270
+ ipv6_address=$(ip -o -6 addr show | awk '
271
+ $2 != "lo" && $2 !~ /^docker/ {split($4, a, "/"); if (!found_ipv6++) print a[1]}')
272
+ fi
273
+ fi
274
+
275
+ # If neither IPv4 nor IPv6 address is available, assign "No IP found"
276
+ if [ -z "$ipv4_address" ] && [ -z "$ipv6_address" ]; then
277
+ ip_address="No IP found"
278
+ else
279
+ # Prioritize IPv4 if available, otherwise use IPv6
280
+ ip_address="${ipv4_address:-$ipv6_address}"
281
+ fi
282
+
283
+ printf '%s' "$ip_address"
284
+ }
285
+
286
+ # ============================================================================
287
+ # OPERATING SYSTEM INFORMATION
288
+ # ============================================================================
289
+
290
+ if [ "$OS_TYPE" = "macos" ]; then
291
+ # macOS detection using sw_vers
292
+ if command_exists sw_vers; then
293
+ os_name="macOS $(sw_vers -productVersion 2>/dev/null || echo 'Unknown')"
294
+ else
295
+ os_name="macOS (Unknown Version)"
296
+ fi
297
+ elif file_readable /etc/os-release; then
298
+ # Linux detection using os-release
299
+ source /etc/os-release
300
+ if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
301
+ # Bash 4+: Use capitalize
302
+ os_name="${ID^} ${VERSION} ${VERSION_CODENAME^}"
303
+ else
304
+ # Bash 3: Plain format
305
+ os_name="${ID} ${VERSION} ${VERSION_CODENAME}"
306
+ fi
307
+ else
308
+ # Fallback for systems without os-release
309
+ os_name="$(uname -s) (Unknown Version)"
310
+ fi
311
+
312
+ # Kernel information - portable format
313
+ os_kernel="$(uname -s) $(uname -r)"
314
+
315
+ # ============================================================================
316
+ # NETWORK INFORMATION
317
+ # ============================================================================
318
+
319
+ net_current_user=$(whoami)
320
+
321
+ # Hostname detection with fallbacks
322
+ if command_exists hostname; then
323
+ # Try hostname -f first (FQDN), fallback to plain hostname
324
+ if hostname -f &> /dev/null 2>&1; then
325
+ net_hostname=$(hostname -f 2>/dev/null)
326
+ else
327
+ net_hostname=$(hostname 2>/dev/null)
328
+ fi
329
+ elif file_readable /etc/hosts; then
330
+ # Fallback: try to find hostname in /etc/hosts
331
+ net_hostname=$(grep -w "$(uname -n)" /etc/hosts 2>/dev/null | awk '{print $2}' | head -n 1)
332
+ else
333
+ # Last resort: use uname
334
+ net_hostname=$(uname -n 2>/dev/null)
335
+ fi
336
+
337
+ # Ensure we have something
338
+ [ -z "$net_hostname" ] && net_hostname="Not Defined"
339
+
340
+ # Get machine IP address (uses get_ip_addr function defined earlier)
341
+ net_machine_ip=$(get_ip_addr)
342
+
343
+ # Get client IP (for SSH sessions)
344
+ net_client_ip=$(who am i 2>/dev/null | awk '{print $5}' | tr -d '()')
345
+ [ -z "$net_client_ip" ] && net_client_ip="Not connected"
346
+
347
+ # DNS servers detection
348
+ if [ "$OS_TYPE" = "macos" ] && command_exists scutil; then
349
+ # macOS: use scutil to get DNS servers
350
+ net_dns_ip=($(scutil --dns 2>/dev/null | grep 'nameserver\[[0-9]*\]' | awk '{print $3}' | head -5))
351
+ elif file_readable /etc/resolv.conf; then
352
+ # Linux/Unix: parse resolv.conf
353
+ net_dns_ip=($(grep '^nameserver [0-9.]' /etc/resolv.conf 2>/dev/null | awk '{print $2}'))
354
+ else
355
+ # No DNS info available
356
+ net_dns_ip=()
357
+ fi
358
+
359
+ # ============================================================================
360
+ # CPU INFORMATION
361
+ # ============================================================================
362
+
363
+ if [ "$OS_TYPE" = "macos" ]; then
364
+ # macOS CPU detection using sysctl
365
+ cpu_model="$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo 'Unknown CPU')"
366
+ cpu_cores="$(sysctl -n hw.ncpu 2>/dev/null || echo '0')"
367
+ cpu_cores_per_socket="$(sysctl -n machdep.cpu.core_count 2>/dev/null || echo '')"
368
+ cpu_sockets="$(sysctl -n hw.packages 2>/dev/null || sysctl -n hw.physicalcpu 2>/dev/null || echo '-')"
369
+ cpu_hypervisor="Bare Metal" # macOS doesn't expose hypervisor easily
370
+
371
+ # CPU frequency on macOS (may not always be available)
372
+ cpu_freq_hz="$(sysctl -n hw.cpufrequency_max 2>/dev/null || sysctl -n hw.cpufrequency 2>/dev/null)"
373
+ if [ -n "$cpu_freq_hz" ] && [ "$cpu_freq_hz" != "0" ]; then
374
+ cpu_freq=$(awk -v freq="$cpu_freq_hz" 'BEGIN { printf "%.2f", freq / 1000000000 }')
375
+ else
376
+ cpu_freq="" # Will show blank like on some ARM systems
377
+ fi
378
+
379
+ elif command_exists lscpu; then
380
+ # Linux with lscpu
381
+ cpu_model="$(lscpu | grep -E 'Model name:|^Model:' | grep -v 'BIOS' | cut -f 2 -d ':' | awk '{$1=$1; print $1 " " $2 " " $3 " " $4}')"
382
+ cpu_hypervisor="$(lscpu | grep 'Hypervisor vendor' | cut -f 2 -d ':' | awk '{$1=$1}1')"
383
+ [ -z "$cpu_hypervisor" ] && cpu_hypervisor="Bare Metal"
384
+
385
+ cpu_cores="$(lscpu | grep -E '^CPU\(s\):' | awk '{print $2}' | head -1)"
386
+ cpu_cores_per_socket="$(lscpu | grep 'Core(s) per socket' | cut -f 2 -d ':' | awk '{$1=$1}1')"
387
+ cpu_sockets="$(lscpu | grep 'Socket(s)' | cut -f 2 -d ':' | awk '{$1=$1}1')"
388
+
389
+ # CPU frequency - try multiple sources
390
+ if file_readable /proc/cpuinfo; then
391
+ cpu_freq="$(grep 'cpu MHz' /proc/cpuinfo | cut -f 2 -d ':' | awk 'NR==1 { printf "%.2f", $1 / 1000 }')"
392
+ fi
393
+ # Fallback for ARM: try sysfs
394
+ if [ -z "$cpu_freq" ] && [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq ]; then
395
+ cpu_freq_khz="$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null)"
396
+ [ -n "$cpu_freq_khz" ] && cpu_freq=$(awk -v freq="$cpu_freq_khz" 'BEGIN { printf "%.2f", freq / 1000000 }')
397
+ fi
398
+
399
+ else
400
+ # Fallback: No lscpu available
401
+ cpu_model="Unknown CPU"
402
+ cpu_hypervisor="Unknown"
403
+
404
+ # Try nproc or fallback to getconf
405
+ if command_exists nproc; then
406
+ cpu_cores="$(nproc --all 2>/dev/null || nproc 2>/dev/null)"
407
+ elif command_exists getconf; then
408
+ cpu_cores="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '0')"
409
+ else
410
+ cpu_cores="0"
411
+ fi
412
+
413
+ cpu_cores_per_socket=""
414
+ cpu_sockets="-"
415
+ cpu_freq=""
416
+ fi
417
+
418
+ # Normalize cpu_cores if using nproc as fallback
419
+ if [ -z "$cpu_cores" ] || [ "$cpu_cores" = "0" ]; then
420
+ if command_exists nproc; then
421
+ cpu_cores="$(nproc --all 2>/dev/null || echo '0')"
422
+ fi
423
+ fi
424
+
425
+ # Load averages - prefer /proc/loadavg on Linux, use uptime elsewhere
426
+ if file_readable /proc/loadavg; then
427
+ read load_avg_1min load_avg_5min load_avg_15min rest < /proc/loadavg
428
+ elif [ "$OS_TYPE" = "macos" ] && command_exists sysctl; then
429
+ # macOS: use sysctl
430
+ load_avg="$(sysctl -n vm.loadavg 2>/dev/null | tr -d '{}')"
431
+ load_avg_1min="$(echo "$load_avg" | awk '{print $1}')"
432
+ load_avg_5min="$(echo "$load_avg" | awk '{print $2}')"
433
+ load_avg_15min="$(echo "$load_avg" | awk '{print $3}')"
434
+ else
435
+ # Fallback: parse uptime (less reliable with different locales)
436
+ load_avg_1min=$(uptime | awk -F'load average: ' '{print $2}' | cut -d ',' -f1 | tr -d ' ')
437
+ load_avg_5min=$(uptime | awk -F'load average: ' '{print $2}' | cut -d ',' -f2 | tr -d ' ')
438
+ load_avg_15min=$(uptime | awk -F'load average: ' '{print $2}' | cut -d ',' -f3 | tr -d ' ')
439
+ fi
440
+
441
+ # ============================================================================
442
+ # MEMORY INFORMATION
443
+ # ============================================================================
444
+
445
+ if [ "$OS_TYPE" = "macos" ]; then
446
+ # macOS memory detection using sysctl and vm_stat
447
+ mem_total_bytes="$(sysctl -n hw.memsize 2>/dev/null || echo '0')"
448
+ mem_total=$((mem_total_bytes / 1024)) # Convert to KB to match Linux format
449
+
450
+ if command_exists vm_stat; then
451
+ # Parse vm_stat for memory usage
452
+ vm_stat_output="$(vm_stat)"
453
+ page_size=$(echo "$vm_stat_output" | grep 'page size' | awk '{print $8}' | tr -d '.')
454
+ [ -z "$page_size" ] && page_size=4096 # Default page size
455
+
456
+ pages_free=$(echo "$vm_stat_output" | grep 'Pages free' | awk '{print $3}' | tr -d '.')
457
+ pages_inactive=$(echo "$vm_stat_output" | grep 'Pages inactive' | awk '{print $3}' | tr -d '.')
458
+ pages_speculative=$(echo "$vm_stat_output" | grep 'Pages speculative' | awk '{print $3}' | tr -d '.')
459
+
460
+ # Calculate available memory in KB
461
+ mem_available=$((((pages_free + pages_inactive + pages_speculative) * page_size) / 1024))
462
+ else
463
+ # Rough estimate if vm_stat unavailable
464
+ mem_available=$((mem_total / 4))
465
+ fi
466
+
467
+ mem_used=$((mem_total - mem_available))
468
+
469
+ elif file_readable /proc/meminfo; then
470
+ # Linux memory detection
471
+ mem_total=$(grep 'MemTotal' /proc/meminfo | awk '{print $2}')
472
+ mem_available=$(grep 'MemAvailable' /proc/meminfo | awk '{print $2}')
473
+ mem_used=$((mem_total - mem_available))
474
+
475
+ else
476
+ # Fallback for unknown systems
477
+ mem_total=0
478
+ mem_available=0
479
+ mem_used=0
480
+ fi
481
+
482
+ # Calculate percentages and convert to GB
483
+ if [ "$mem_total" -gt 0 ]; then
484
+ mem_percent=$(awk -v used="$mem_used" -v total="$mem_total" 'BEGIN { printf "%.2f", (used / total) * 100 }')
485
+ mem_total_gb=$(echo "$mem_total" | awk '{ printf "%.2f", $1 / (1024 * 1024) }')
486
+ mem_available_gb=$(echo "$mem_available" | awk '{ printf "%.2f", $1 / (1024 * 1024) }')
487
+ mem_used_gb=$(echo "$mem_used" | awk '{ printf "%.2f", $1 / (1024 * 1024) }')
488
+ else
489
+ mem_percent="0.00"
490
+ mem_total_gb="0.00"
491
+ mem_available_gb="0.00"
492
+ mem_used_gb="0.00"
493
+ fi
494
+
495
+ # ============================================================================
496
+ # DISK INFORMATION
497
+ # ============================================================================
498
+
499
+ # Check for ZFS (Linux only)
500
+ if [ "$OS_TYPE" = "linux" ] && command_exists zfs && grep -q "zfs" /proc/mounts 2>/dev/null; then
501
+ zfs_present=1
502
+
503
+ # ZFS health check - handle different ZFS versions
504
+ zfs_health_output="$(zpool status -x zroot 2>/dev/null | head -1)"
505
+ if [[ "$zfs_health_output" == "all pools are healthy" ]] || [[ "$zfs_health_output" == *"is healthy"* ]]; then
506
+ zfs_health="HEALTH O.K."
507
+ elif [ -z "$zfs_health_output" ]; then
508
+ zfs_health="Unknown"
509
+ else
510
+ zfs_health="CHECK REQUIRED"
511
+ fi
512
+
513
+ # Get ZFS usage
514
+ zfs_available=$(zfs get -o value -Hp available "$zfs_filesystem" 2>/dev/null || echo "0")
515
+ zfs_used=$(zfs get -o value -Hp used "$zfs_filesystem" 2>/dev/null || echo "0")
516
+ zfs_available_gb=$(echo "$zfs_available" | awk '{ printf "%.2f", $1 / (1024 * 1024 * 1024) }')
517
+ zfs_used_gb=$(echo "$zfs_used" | awk '{ printf "%.2f", $1 / (1024 * 1024 * 1024) }')
518
+
519
+ # FIX: Correct percentage calculation - used / (used + available)
520
+ disk_percent=$(awk -v used="$zfs_used" -v available="$zfs_available" \
521
+ 'BEGIN { total = used + available; if (total > 0) printf "%.2f", (used / total) * 100; else print "0.00" }')
522
+ else
523
+ # Standard filesystem detection (Linux, macOS, BSD)
524
+ root_partition="/"
525
+
526
+ # Try df -m first (MB), fallback to df -k (KB) if unsupported
527
+ if df -m "$root_partition" &> /dev/null 2>&1; then
528
+ root_used=$(df -m "$root_partition" 2>/dev/null | awk 'NR==2 {print $3}')
529
+ root_total=$(df -m "$root_partition" 2>/dev/null | awk 'NR==2 {print $2}')
530
+ root_total_gb=$(awk -v total="$root_total" 'BEGIN { printf "%.2f", total / 1024 }')
531
+ root_used_gb=$(awk -v used="$root_used" 'BEGIN { printf "%.2f", used / 1024 }')
532
+ else
533
+ # Fallback to KB and convert
534
+ root_used=$(df -k "$root_partition" 2>/dev/null | awk 'NR==2 {print $3}')
535
+ root_total=$(df -k "$root_partition" 2>/dev/null | awk 'NR==2 {print $2}')
536
+ root_total_gb=$(awk -v total="$root_total" 'BEGIN { printf "%.2f", total / 1024 / 1024 }')
537
+ root_used_gb=$(awk -v used="$root_used" 'BEGIN { printf "%.2f", used / 1024 / 1024 }')
538
+ fi
539
+
540
+ # Calculate percentage with safety check
541
+ if [ -n "$root_total" ] && [ "$root_total" -gt 0 ]; then
542
+ disk_percent=$(awk -v used="$root_used" -v total="$root_total" \
543
+ 'BEGIN { printf "%.2f", (used / total) * 100 }')
544
+ else
545
+ disk_percent="0.00"
546
+ root_total_gb="0.00"
547
+ root_used_gb="0.00"
548
+ fi
549
+ fi
550
+
551
+ # Last login and Uptime
552
+ # Try lastlog2 first (modern Debian), fall back to lastlog if available
553
+ if command -v lastlog2 &> /dev/null; then
554
+ last_login=$(lastlog2 -u "$USER" 2>/dev/null)
555
+ last_login_ip=$(echo "$last_login" | awk 'NR==2 {print $3}')
556
+
557
+ # Check if last_login_ip is an IP address
558
+ if [[ "$last_login_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
559
+ last_login_ip_present=1
560
+ last_login_time=$(echo "$last_login" | awk 'NR==2 {print $4, $5, $6, $7, $8}' | sed 's/ */ /g')
561
+ else
562
+ last_login_time=$(echo "$last_login" | awk 'NR==2 {print $4, $5, $6, $7, $8}' | sed 's/ */ /g')
563
+ # Check for never logged in edge case
564
+ if [ -z "$last_login_time" ] || [ "$last_login_time" = "in**" ]; then
565
+ last_login_time="Never logged in"
566
+ fi
567
+ fi
568
+ elif command -v lastlog &> /dev/null; then
569
+ last_login=$(lastlog -u "$USER" 2>/dev/null)
570
+ last_login_ip=$(echo "$last_login" | awk 'NR==2 {print $3}')
571
+
572
+ # Check if last_login_ip is an IP address
573
+ if [[ "$last_login_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
574
+ last_login_ip_present=1
575
+ last_login_time=$(echo "$last_login" | awk 'NR==2 {print $6, $7, $10, $8}')
576
+ else
577
+ last_login_time=$(echo "$last_login" | awk 'NR==2 {print $4, $5, $8, $6}')
578
+ # Check for **Never logged in** edge case
579
+ if [ "$last_login_time" = "in**" ]; then
580
+ last_login_time="Never logged in"
581
+ fi
582
+ fi
583
+ else
584
+ # Neither command available
585
+ last_login_time="Login tracking unavailable"
586
+ last_login_ip=""
587
+ last_login_ip_present=0
588
+ fi
589
+
590
+ # System uptime - use uptime -p if available, otherwise calculate from boot time
591
+ if uptime -p &> /dev/null 2>&1; then
592
+ # Linux with uptime -p
593
+ sys_uptime=$(uptime -p 2>/dev/null | sed 's/up\s*//; s/\s*day\(s*\)/d/; s/\s*hour\(s*\)/h/; s/\s*minute\(s*\)/m/')
594
+ elif [ "$OS_TYPE" = "macos" ] && command_exists sysctl; then
595
+ # macOS: calculate from boot time
596
+ boot_time=$(sysctl -n kern.boottime 2>/dev/null | awk '{print $4}' | tr -d ',')
597
+ if [ -n "$boot_time" ]; then
598
+ current_time=$(date +%s)
599
+ uptime_seconds=$((current_time - boot_time))
600
+ uptime_days=$((uptime_seconds / 86400))
601
+ uptime_hours=$(( (uptime_seconds % 86400) / 3600 ))
602
+ uptime_mins=$(( (uptime_seconds % 3600) / 60 ))
603
+
604
+ # Format similar to Linux uptime -p
605
+ sys_uptime=""
606
+ [ "$uptime_days" -gt 0 ] && sys_uptime="${uptime_days}d "
607
+ [ "$uptime_hours" -gt 0 ] && sys_uptime="${sys_uptime}${uptime_hours}h "
608
+ [ "$uptime_mins" -gt 0 ] && sys_uptime="${sys_uptime}${uptime_mins}m"
609
+ sys_uptime=$(echo "$sys_uptime" | sed 's/ */ /g; s/ $//') # Clean up spacing
610
+ else
611
+ sys_uptime="Unknown"
612
+ fi
613
+ else
614
+ # Fallback: parse uptime command output (less reliable)
615
+ sys_uptime=$(uptime 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="up") {for(j=i+1;j<=NF;j++) {if($j ~ /user/) break; printf "%s ", $j}}}' | sed 's/,//g')
616
+ [ -z "$sys_uptime" ] && sys_uptime="Unknown"
617
+ fi
618
+
619
+ # Set current length before graphs get calculated
620
+ set_current_len
621
+
622
+ # Create graphs
623
+ cpu_1min_bar_graph=$(bar_graph "$load_avg_1min" "$cpu_cores")
624
+ cpu_5min_bar_graph=$(bar_graph "$load_avg_5min" "$cpu_cores")
625
+ cpu_15min_bar_graph=$(bar_graph "$load_avg_15min" "$cpu_cores")
626
+
627
+ mem_bar_graph=$(bar_graph "$mem_used" "$mem_total")
628
+
629
+ if [ $zfs_present -eq 1 ]; then
630
+ disk_bar_graph=$(bar_graph "$zfs_used" "$zfs_available")
631
+ else
632
+ disk_bar_graph=$(bar_graph "$root_used" "$root_total")
633
+ fi
634
+
635
+ # Machine Report
636
+ PRINT_HEADER
637
+ PRINT_CENTERED_DATA "$report_title"
638
+ PRINT_CENTERED_DATA "TR-200 MACHINE REPORT"
639
+ PRINT_DIVIDER "top"
640
+ PRINT_DATA "OS" "$os_name"
641
+ PRINT_DATA "KERNEL" "$os_kernel"
642
+ PRINT_DIVIDER
643
+ PRINT_DATA "HOSTNAME" "$net_hostname"
644
+ PRINT_DATA "MACHINE IP" "$net_machine_ip"
645
+ PRINT_DATA "CLIENT IP" "$net_client_ip"
646
+
647
+ for dns_num in "${!net_dns_ip[@]}"; do
648
+ PRINT_DATA "DNS IP $(($dns_num + 1))" "${net_dns_ip[dns_num]}"
649
+ done
650
+
651
+ PRINT_DATA "USER" "$net_current_user"
652
+ PRINT_DIVIDER
653
+ PRINT_DATA "PROCESSOR" "$cpu_model"
654
+ PRINT_DATA "CORES" "$cpu_cores_per_socket vCPU(s) / $cpu_sockets Socket(s)"
655
+ PRINT_DATA "HYPERVISOR" "$cpu_hypervisor"
656
+ PRINT_DATA "CPU FREQ" "$cpu_freq GHz"
657
+ PRINT_DATA "LOAD 1m" "$cpu_1min_bar_graph"
658
+ PRINT_DATA "LOAD 5m" "$cpu_5min_bar_graph"
659
+ PRINT_DATA "LOAD 15m" "$cpu_15min_bar_graph"
660
+
661
+ if [ $zfs_present -eq 1 ]; then
662
+ PRINT_DIVIDER
663
+ PRINT_DATA "VOLUME" "$zfs_used_gb/$zfs_available_gb GB [$disk_percent%]"
664
+ PRINT_DATA "DISK USAGE" "$disk_bar_graph"
665
+ PRINT_DATA "ZFS HEALTH" "$zfs_health"
666
+ else
667
+ PRINT_DIVIDER
668
+ PRINT_DATA "VOLUME" "$root_used_gb/$root_total_gb GB [$disk_percent%]"
669
+ PRINT_DATA "DISK USAGE" "$disk_bar_graph"
670
+ fi
671
+
672
+ PRINT_DIVIDER
673
+ PRINT_DATA "MEMORY" "${mem_used_gb}/${mem_total_gb} GiB [${mem_percent}%]"
674
+ PRINT_DATA "USAGE" "${mem_bar_graph}"
675
+ PRINT_DIVIDER
676
+ PRINT_DATA "LAST LOGIN" "$last_login_time"
677
+
678
+ if [ $last_login_ip_present -eq 1 ]; then
679
+ PRINT_DATA "" "$last_login_ip"
680
+ fi
681
+
682
+ PRINT_DATA "UPTIME" "$sys_uptime"
683
+ PRINT_DIVIDER "bottom"