diff --git a/Readme.md b/Readme.md index 8e137e6..160b7fd 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Ubuntu Based Kiosk -**Current Version:** 2.3.0 (check script header for latest version) +**Current Version:** 2.4.0 (check script header for latest version) **Built with Claude Sonnet 4.6 AI assistance** **License:** GPL v3 - Keep derivatives open source **Repository:** https://github.com/outis1one/ubuntu-based-kiosk/ @@ -1202,6 +1202,9 @@ terminal menu and the web UI, so they can't drift apart). shutdown (+ RTC wake where available), display on/off, quiet-hours audio muting, and an Electron reload timer, each as systemd timers. Can power the physical machine off and on a schedule. +- `menus/diagnostics.sh` — **Diagnostics**: system status, log viewing, + audio diagnostics, network test — 4 of the legacy Advanced menu's 12 + items, all read-only. - `install.sh` — entry point for the modular tool. Run it against an *already-installed* kiosk: ```bash @@ -1213,8 +1216,9 @@ terminal menu and the web UI, so they can't drift apart). **Honest status:** this does not yet replace first-time installation, or most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000 lines and still contains its own unremoved, unmodified copies of every -menu above (plus Upgrade, Reinstall, Uninstall, all Addons, and all of -Advanced — none of that has moved yet). Both copies coexist deliberately: the old ones stay until enough +menu above (plus Upgrade, Reinstall, Uninstall, all Addons, and the +other 8 Advanced items — none of that has moved yet). Both copies +coexist deliberately: the old ones stay until enough of Core Settings/Addons/Advanced is migrated to retire them in one pass, rather than leaving the legacy menu half-wired. Migration continues one `menus/*.sh` file at a time; first-time @@ -1225,9 +1229,14 @@ at all. ## Project Status & Future Plans -**Current Version:** 2.3.0 +**Current Version:** 2.4.0 -**Recent Updates (v2.3.0):** +**Recent Updates (v2.4.0):** +- **Diagnostics migrated** — system status, log viewing (Electron/LightDM/journal), an 8-step audio diagnostic, and a ping+DNS network test, from the legacy Advanced menu. A change of pace: everything here is read-only, no destructive-action risk to manage. +- **Bug fix (set -e safety):** every diagnostic whose failure is the expected case — no lightdm running, no audio hardware, no network, missing logs, `ping`/`nslookup` not even installed — was a bare unguarded statement that would have crashed the whole session instead of reporting "not found" and moving on. Fixed throughout; a diagnostics tool has to survive exactly the broken states it exists to diagnose. +- Manual Electron Update, Factory Reset, Export/Import Settings, Emergency Hotspot, and Fix Blank Screen are staying in the legacy script for now — destructive/mutating, and some share Upgrade's coupling to the legacy script's self-extraction mechanism (see v2.3.0 notes). + +**Previous (v2.3.0):** - **WiFi and Power/Display/Quiet Hours migrated** — by far the riskiest menus tackled so far. WiFi rewrites live netplan config and, over SSH, can disconnect the session configuring it; power scheduling can shut the physical machine down and wake it via RTC. Every legacy safety mechanism is preserved exactly: netplan backup, 60-second SSH watchdog, restore-on-failure for WiFi; RTC availability detection for power scheduling. - **Bug fix:** the legacy menu refused to open "Configure power schedule" at all without RTC hardware, even though shutdown-only scheduling never needed it. - **Bug fix:** none of the six HH:MM time prompts across these menus were format-validated before — a typo silently produced a broken schedule. All now go through the same `ask_time` validator as everywhere else. diff --git a/install.sh b/install.sh index fbf9eb4..8bc4ee1 100755 --- a/install.sh +++ b/install.sh @@ -16,7 +16,8 @@ # Interaction (menus/display.sh), Timezone (menus/timezone.sh), Hidden # Site PIN (menus/hidden_pin.sh), Password Protection & Lockout # (menus/lockout.sh), WiFi (menus/wifi.sh), Power/Display/Quiet Hours -# (menus/power_schedule.sh). +# (menus/power_schedule.sh), Diagnostics (menus/diagnostics.sh - system +# status/logs/audio/network from the legacy Advanced menu). # # Usage (once the kiosk has already been installed): # git clone @@ -46,6 +47,8 @@ source "$SCRIPT_DIR/menus/lockout.sh" source "$SCRIPT_DIR/menus/wifi.sh" # shellcheck source=menus/power_schedule.sh source "$SCRIPT_DIR/menus/power_schedule.sh" +# shellcheck source=menus/diagnostics.sh +source "$SCRIPT_DIR/menus/diagnostics.sh" ################################################################################ # Preflight @@ -90,6 +93,7 @@ main_menu_builder() { "Password Protection & Lockout" "WiFi" "Power/Display/Quiet Hours" + "Diagnostics" ) MENU_HANDLERS=( sites_menu @@ -99,6 +103,7 @@ main_menu_builder() { lockout_menu wifi_menu power_schedule_menu + diagnostics_menu ) } diff --git a/lib/menu.sh b/lib/menu.sh index 95c6177..34ad654 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -57,6 +57,33 @@ get_ip_address() { fi } +# "WireGuard: 10.x.x.x | Tailscale: 100.x.x.x" for whichever VPN clients +# are installed and connected, or "None" if none are. +get_vpn_ips() { + local vpn_info="" + + if command -v wg &>/dev/null && sudo wg show 2>/dev/null | grep -q interface; then + local wg_ip + wg_ip=$(sudo wg show all | grep "allowed ips" | head -1 | awk '{print $3}' | cut -d'/' -f1) + [[ -n "$wg_ip" ]] && vpn_info="${vpn_info}WireGuard: $wg_ip | " + fi + + if command -v tailscale &>/dev/null; then + local ts_ip + ts_ip=$(tailscale ip -4 2>/dev/null) + [[ -n "$ts_ip" ]] && vpn_info="${vpn_info}Tailscale: $ts_ip | " + fi + + if command -v netbird &>/dev/null; then + local nb_ip + nb_ip=$(netbird status 2>/dev/null | grep "NetBird IP:" | awk '{print $3}') + [[ -n "$nb_ip" ]] && vpn_info="${vpn_info}Netbird: $nb_ip | " + fi + + vpn_info="${vpn_info% | }" + [[ -n "$vpn_info" ]] && echo "$vpn_info" || echo "None" +} + pause() { read -r -p "Press Enter to continue..." } diff --git a/menus/diagnostics.sh b/menus/diagnostics.sh new file mode 100644 index 0000000..34db7e4 --- /dev/null +++ b/menus/diagnostics.sh @@ -0,0 +1,244 @@ +#!/bin/bash +################################################################################ +# menus/diagnostics.sh - "Diagnostics" menu (from the legacy Advanced menu). +# +# A deliberate change of pace after Sites/WiFi/Power: everything here is +# read-only (system/audio status, log tailing, ping+DNS) except one +# optional "play a test sound?" prompt, so there's no destructive-action +# risk profile to design around. Straight port, using $KIOSK_USER/ +# $KIOSK_HOME instead of the legacy code's mix of the variable and a +# hardcoded "kiosk" literal. +# +# Only 4 of the legacy Advanced menu's 12 items are here (System +# Diagnostics, View Logs, Audio Diagnostics, Network Test) - Manual +# Electron Update, Factory Reset, Export/Import Settings, Emergency +# Hotspot, and Fix Blank Screen are mutating/destructive and belong with +# a later, more careful pass (some, like Manual Electron Update, share +# Upgrade's issue of being coupled to the legacy script's own +# self-extraction mechanism - see ubuntu-based-kiosk.sh's changelog for +# why Upgrade/Reinstall/Uninstall aren't migrated yet either). +# +# Depends on: lib/menu.sh, lib/config.sh being sourced first. +################################################################################ + +diagnostics_menu_builder() { + MENU_LABELS=("System status" "View logs" "Audio diagnostics" "Network test") + MENU_HANDLERS=(action_system_diagnostics view_logs_menu action_audio_diagnostics action_network_test) +} + +diagnostics_menu() { + run_menu "DIAGNOSTICS" diagnostics_menu_builder +} + +################################################################################ +# System status +################################################################################ + +action_system_diagnostics() { + clear + echo " ═══ SYSTEM DIAGNOSTICS ═══" + echo + + echo "=== Kiosk Status ===" + systemctl status lightdm --no-pager -l 2>&1 | head -20 || true + + echo + echo "=== Audio Status ===" + sudo -u "$KIOSK_USER" pactl info 2>/dev/null | grep -E "Server|User" || echo "Not running" + + echo + echo "=== Network ===" + echo "IP: $(get_ip_address)" + echo "VPN: $(get_vpn_ips)" + echo + + pause +} + +################################################################################ +# Logs +################################################################################ + +view_logs_menu_builder() { + MENU_LABELS=("Electron log (last 50 lines)" "LightDM log (last 50 lines)" "System journal (last 100 lines)") + MENU_HANDLERS=(action_view_electron_log action_view_lightdm_log action_view_journal) +} + +view_logs_menu() { + run_menu "VIEW LOGS" view_logs_menu_builder +} + +action_view_electron_log() { + echo + if sudo test -f "$KIOSK_HOME/electron.log"; then + sudo tail -50 "$KIOSK_HOME/electron.log" || true + else + echo "No electron log found yet" + fi + pause +} + +action_view_lightdm_log() { + echo + sudo tail -50 /var/log/lightdm/lightdm.log 2>&1 || echo "No lightdm log found" + pause +} + +action_view_journal() { + echo + sudo journalctl -n 100 || log_error "Could not read the system journal" + pause +} + +################################################################################ +# Audio diagnostics +################################################################################ + +audio_diagnostics_pactl() { + sudo -u "$KIOSK_USER" XDG_RUNTIME_DIR="/run/user/$(id -u "$KIOSK_USER")" pactl "$@" +} + +action_audio_diagnostics() { + clear + echo "═══ AUDIO DIAGNOSTICS ═══" + echo + + local issue_found=false + + echo "[1/8] Checking audio hardware..." + if lspci 2>/dev/null | grep -i audio || lsusb 2>/dev/null | grep -i audio; then + log_success "Audio hardware detected" + lspci 2>/dev/null | grep -i audio || true + lsusb 2>/dev/null | grep -i audio | head -3 || true + else + log_error "No audio hardware detected" + issue_found=true + fi + echo + + echo "[2/8] Checking ALSA devices..." + if aplay -l &>/dev/null; then + log_success "ALSA devices found" + aplay -l 2>/dev/null | grep -E "^card|device" || true + else + log_error "No ALSA devices" + issue_found=true + fi + echo + + echo "[3/8] Checking PipeWire status..." + local pipewire_running=false + if audio_diagnostics_pactl info &>/dev/null; then + log_success "PipeWire accessible" + pipewire_running=true + audio_diagnostics_pactl info 2>/dev/null | grep -E "Server|User|Host" || true + else + log_error "PipeWire not accessible to kiosk user" + issue_found=true + echo " Try: sudo -u ${KIOSK_USER} systemctl --user start pipewire pipewire-pulse" + fi + echo + + if $pipewire_running; then + echo "[4/8] Checking audio sinks..." + local sinks + sinks=$(audio_diagnostics_pactl list sinks short 2>/dev/null) || true + if [[ -n "$sinks" ]]; then + echo "$sinks" + local default_sink + default_sink=$(audio_diagnostics_pactl get-default-sink 2>/dev/null || echo "none") + echo "Default: $default_sink" + else + log_error "No audio sinks found" + issue_found=true + fi + echo + + echo "[5/8] Checking active streams..." + local sink_inputs + sink_inputs=$(audio_diagnostics_pactl list sink-inputs short 2>/dev/null) || true + if [[ -n "$sink_inputs" ]]; then + echo "Active streams:" + echo "$sink_inputs" + else + echo "No active streams" + fi + echo + else + echo "[4/8] Skipped - PipeWire not running" + echo "[5/8] Skipped - PipeWire not running" + echo + fi + + echo "[6/8] Checking Squeezelite..." + if systemctl is-active --quiet squeezelite; then + log_success "Squeezelite running" + + if $pipewire_running; then + local sq_pid + sq_pid=$(pgrep -f squeezelite | head -1) || true + if [[ -n "$sq_pid" ]]; then + if audio_diagnostics_pactl list sink-inputs 2>/dev/null | grep -q "application.process.id = \"$sq_pid\""; then + log_success "Squeezelite connected to audio" + else + log_warning "Squeezelite NOT connected to audio sink" + issue_found=true + fi + fi + fi + else + echo "Squeezelite not running" + fi + echo + + if $pipewire_running; then + echo "[7/8] Checking volume..." + local volume muted + volume=$(audio_diagnostics_pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | grep -oE '[0-9]+%' | head -1 || echo "unknown") + muted=$(audio_diagnostics_pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null || echo "unknown") + echo "Volume: $volume" + echo "Muted: $muted" + else + echo "[7/8] Skipped - PipeWire not running" + fi + echo + + echo "[8/8] Audio test..." + if ask_yes_no "Play test sound?" "n" && $pipewire_running; then + echo "Playing beep..." + audio_diagnostics_pactl_play_test + fi + echo + + echo "═══════════════════════════════" + if $issue_found; then + echo "⚠️ ISSUES DETECTED - See above" + else + echo "✓ All checks passed" + fi + echo "═══════════════════════════════" + + pause +} + +audio_diagnostics_pactl_play_test() { + sudo -u "$KIOSK_USER" XDG_RUNTIME_DIR="/run/user/$(id -u "$KIOSK_USER")" paplay /usr/share/sounds/alsa/Front_Center.wav 2>/dev/null || \ + sudo -u "$KIOSK_USER" XDG_RUNTIME_DIR="/run/user/$(id -u "$KIOSK_USER")" speaker-test -t sine -f 1000 -l 1 2>/dev/null || \ + echo "No test available" +} + +################################################################################ +# Network test +################################################################################ + +action_network_test() { + echo + echo " ═══ NETWORK TEST ═══" + echo + echo "Ping test..." + ping -c 4 8.8.8.8 || log_error "Ping failed" + echo + echo "DNS test..." + nslookup google.com || log_error "DNS lookup failed" + pause +} diff --git a/ubuntu-based-kiosk.sh b/ubuntu-based-kiosk.sh index 563a768..433d4a5 100644 --- a/ubuntu-based-kiosk.sh +++ b/ubuntu-based-kiosk.sh @@ -1,8 +1,39 @@ #!/bin/bash ################################################################################ -### Ubuntu Based Kiosk v2.3.0 ### +### Ubuntu Based Kiosk v2.4.0 ### ################################################################################ # +# RELEASE v2.4.0 - Diagnostics Migrated +# - New in ./install.sh: Diagnostics (menus/diagnostics.sh) - system +# status, log viewing (Electron/LightDM/journal), an 8-step audio +# diagnostic, and a ping+DNS network test, pulled from the legacy +# Advanced menu. Everything here is read-only except one optional +# "play a test sound?" prompt - a deliberate change of pace after +# Sites/WiFi/Power, with no destructive-action risk to design around. +# Manual Electron Update, Factory Reset, Export/Import Settings, +# Emergency Hotspot, and Fix Blank Screen are staying in the legacy +# script for now - they're mutating/destructive, and some share +# Upgrade's coupling to the legacy script's own self-extraction +# mechanism (see v2.3.0 below for why Upgrade/Reinstall/Uninstall +# aren't migrated either). +# - Fixed (set -e safety, same class as v2.1.0/v2.3.0): every diagnostic +# command whose failure is actually the expected, common case - no +# lightdm running, no audio hardware, no network, missing log files, +# `ping`/`nslookup` not even installed - was a bare unguarded +# statement that would have crashed the whole session instead of +# reporting "not found" and moving on. A diagnostics tool has to be +# the most crash-proof code in the project, since it exists to run +# *when something is already broken*; every one of these now reports +# and continues instead. Also worth noting for future menus: writing +# `local var;` and `var=$(cmd)` as separate statements (good practice, +# and how earlier real bugs in this migration were caught) removes an +# accidental safety net bash's `local x=$(cmd)` has on one line - that +# form masks the substitution's exit code with `local`'s own +# always-success status. Splitting them is correct, but each split +# assignment needs its own explicit `|| true` (or real fallback) where +# a failure is expected and non-fatal, rather than relying on that +# quirk by accident. +# # RELEASE v2.3.0 - WiFi and Power/Display/Quiet Hours Migrated # - New in ./install.sh: WiFi (menus/wifi.sh) and Power/Display/Quiet # Hours (menus/power_schedule.sh) - by far the biggest and riskiest @@ -157,7 +188,7 @@ set -euo pipefail ### SECTION 1: CONSTANTS & GLOBALS ################################################################################ -SCRIPT_VERSION="2.3.0" +SCRIPT_VERSION="2.4.0" # Resolve the real path to this script file. # When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,