Migrate Diagnostics menu; bump to v2.4.0

Deliberately skipped Upgrade/Full Reinstall/Complete Uninstall for now:
all three are large (130-250 lines), genuinely destructive (wipe/
reinstall the kiosk), and Upgrade specifically is coupled to the legacy
script's own self-extraction mechanism (it greps its own running source
for embedded heredocs to pull out main.js/preload.js) - there's no
modular equivalent to migrate it to yet, since those files don't exist
as separate assets outside the monolith. Migrated Diagnostics instead:
4 of the legacy Advanced menu's 12 items (System Status, View Logs,
Audio Diagnostics, Network Test), all read-only except one optional
"play a test sound?" prompt - a deliberate change of pace with no
destructive-action risk to design around, after Sites/WiFi/Power.

- lib/menu.sh: ported get_vpn_ips alongside get_ip_address.
- menus/diagnostics.sh: straight port, using $KIOSK_USER/$KIOSK_HOME
  throughout instead of the legacy code's mix of the variable and a
  hardcoded "kiosk" literal.

Bug fixed, same set -e-safety class as v2.1.0's run_menu fix and
v2.3.0's netplan/systemctl fixes, but a bigger batch this time: nearly
every diagnostic command here was a bare unguarded statement whose
*expected, common* failure - no lightdm running, no audio hardware, no
network, missing log files, ping/nslookup not even installed - would
have crashed the entire session instead of reporting "not found" and
continuing. A diagnostics tool has to be the most crash-proof code in
the project, since it exists to run when something is already broken.
Fixed at every call site: systemctl status | head, tail on lightdm's
log, journalctl, ping, nslookup, and three pactl-backed variable
assignments.

Also noted for future menus in this migration: writing `local var;` and
`var=$(cmd)` as separate statements (good practice, and how several
earlier real bugs were caught) removes an accidental safety net -
`local x=$(cmd)` on one line masks the substitution's exit code with
`local`'s own always-success status. Splitting them is correct, but
each split assignment needs an explicit `|| true` (or real fallback)
where failure is expected and non-fatal, rather than relying on that
masking by accident. Caught three instances of exactly this while
writing this file fresh, not just porting old bugs.

Verified:
- Full regression: re-ran every existing scratch-config/stub test suite
  (sites, display, timezone/pin, lockout, power schedule + RTC, wifi) -
  all still clean after the lib/menu.sh change.
- New test for diagnostics.sh, exercised mostly for real (no
  destructive-mutation risk here, so minimal stubbing needed): system
  status, all three log views (including the "no such file" paths for
  lightdm log and electron log), full 8-step audio diagnostic with test
  sound declined, and network test - all report gracefully instead of
  crashing, confirmed by re-running after each fix until every bare
  unguarded statement was accounted for.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user, navigating Diagnostics -> System status -> View Logs ->
  System journal -> Audio diagnostics (declined test sound) -> Network
  test -> exit. Confirmed every diagnostic path completes and returns
  to its menu cleanly (exit code 0) even with ping/nslookup missing and
  no audio hardware/network present in this environment.
This commit is contained in:
Claude
2026-08-18 18:48:26 +00:00
parent 2375bf5eab
commit 459da53182
5 changed files with 324 additions and 8 deletions
+14 -5
View File
@@ -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.
+6 -1
View File
@@ -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 <repo>
@@ -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
)
}
+27
View File
@@ -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..."
}
+244
View File
@@ -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
}
+33 -2
View File
@@ -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,