diff --git a/Readme.md b/Readme.md index a0fc3c4..d9cfe56 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Ubuntu Based Kiosk -**Current Version:** 2.12.0 (check script header for latest version) +**Current Version:** 2.13.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/ @@ -1255,6 +1255,11 @@ terminal menu and the web UI, so they can't drift apart). Settings): the last of the "destructive trio." Composed from every addon's own `*_do_uninstall` helper instead of re-implementing removal a second time — see "Recent Updates (v2.12.0)" below. +- `menus/fleet_profile.sh` — **Fleet Profile** (Advanced): export/apply + the portable parts of `config.json` across several kiosks that should + share the same settings. New, not a legacy port — deliberately never + copies machine-bound credentials (Authelia, WireGuard, Asterisk + Intercom); see "Recent Updates (v2.13.0)" below. - `install.sh` — entry point for the modular tool, now grouped **Core Settings / Addons / Advanced** like the legacy menu. Run it against an *already-installed* kiosk: @@ -1269,11 +1274,14 @@ 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, including the legacy three-option (Client/Server/Full) Easy Asterisk Intercom — the modular version only replaces the Client -option, by design (plus Upgrade, Full Reinstall, Export/Import -Settings, and Fix Squeezelite Audio — none of that has moved yet; -Complete Uninstall *is* now migrated, but Upgrade and Full Reinstall -are staying put — both are coupled to this file's own heredoc self- -extraction of main.js/preload.js/etc, which has no modular equivalent). +option, by design (plus Upgrade, Full Reinstall, and Fix Squeezelite +Audio — none of that has moved yet; Complete Uninstall *is* now +migrated, but Upgrade and Full Reinstall are staying put — both are +coupled to this file's own heredoc self-extraction of main.js/ +preload.js/etc, which has no modular equivalent). The legacy Export/ +Import Settings is also staying as-is; Fleet Profile is a new, +narrower feature alongside it, not a replacement for it — see "Recent +Updates (v2.13.0)" below for why they're not the same thing. 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 @@ -1305,9 +1313,14 @@ full migration pass. ## Project Status & Future Plans -**Current Version:** 2.12.0 +**Current Version:** 2.13.0 -**Recent Updates (v2.12.0):** +**Recent Updates (v2.13.0):** +- **New: Fleet Profile** (Advanced → Fleet Profile) — not a port of the legacy Export/Import Settings, a narrower MVP for the "set up one kiosk, then stamp out a dozen more like it" use case. Exports the portable parts of `config.json` (sites, display/touch/navigation, lockout, password protection) to a JSON file; applies that file to any other already-installed kiosk. +- **Deliberately does not copy machine-bound credentials**, because copying them would be actively wrong: Authelia's encrypted password is keyed off `/etc/machine-id` and decrypts to garbage on another machine; a WireGuard private key is a device identity, and reusing one across machines is a peer conflict, not a saving; most Asterisk PBXes reject two simultaneous registrations to the same extension. Applying a profile prints these as an explicit "needs a human" checklist instead of silently skipping or cloning them. +- Records which addons were present at export time and reports which are/aren't present on the target — doesn't install anything itself. Non-interactive addon installation (so applying a profile needs zero prompts — scriptable over SSH to a whole fleet) is a deliberate follow-up, not bundled into this MVP. + +**Previous (v2.12.0):** - **Complete Uninstall migrated** — the last of the "destructive trio." Rather than re-implementing every addon's teardown a second time (the legacy shape — CUPS/VNC/WireGuard/Tailscale/Netbird/LMS/Squeezelite removal all inlined again, independently of each addon's own uninstall action), `menus/complete_uninstall.sh` composes the `*_do_uninstall` helpers each addon already has. Every addon menu with an uninstall action was split into a confirm-and-call wrapper (unchanged from the user's perspective) plus a silent removal helper that both the wrapper and Complete Uninstall call — no duplicated logic anywhere, and if an addon's removal logic changes later, Complete Uninstall picks it up automatically. - **Important bug found and fixed while composing these:** several `*_do_uninstall` helpers (CUPS's `apt autoremove`/`apt clean`, VNC/WireGuard/Tailscale/Netbird's `apt remove`) had a bare, unguarded `apt` call. Previously this only risked aborting that one menu action if the package was already gone. Composed together as sequential calls inside Complete Uninstall, the same failure would have silently truncated the *entire* uninstall partway through — e.g. the kiosk user might never get removed because an already-uninstalled VPN client's `apt remove` failed first. Guarded all of them with `|| true`. - Non-addon teardown (kiosk user/files, Node.js, LightDM/Openbox, remaining systemd units/scripts, polkit rules, re-enabling virtual consoles, final package cleanup) stays inline in `menus/complete_uninstall.sh`, since no single addon owns those paths — same as the legacy script. diff --git a/install.sh b/install.sh index 26a9ccb..2d40946 100755 --- a/install.sh +++ b/install.sh @@ -30,7 +30,10 @@ # manual update, fix blank screen), Factory Reset # (menus/advanced_factory_reset.sh), Virtual Consoles # (menus/advanced_virtual_consoles.sh), Emergency Hotspot -# (menus/advanced_emergency_hotspot.sh). +# (menus/advanced_emergency_hotspot.sh), Fleet Profile +# (menus/fleet_profile.sh - export/apply portable settings across +# several kiosks; deliberately excludes machine-bound credentials +# like Authelia/WireGuard/Asterisk Intercom - see the file header). # # Usage (once the kiosk has already been installed): # git clone @@ -84,6 +87,9 @@ source "$SCRIPT_DIR/menus/advanced_emergency_hotspot.sh" # Sourced last: composes the *_do_uninstall/*_do_remove_all/*_do_disable # helpers defined in every file above it. source "$SCRIPT_DIR/menus/complete_uninstall.sh" +# shellcheck source=menus/fleet_profile.sh +# Also composes detection helpers (*_is_installed) from every addon above. +source "$SCRIPT_DIR/menus/fleet_profile.sh" ################################################################################ # Preflight @@ -158,8 +164,8 @@ addons_menu() { } advanced_menu_builder() { - MENU_LABELS=("Diagnostics" "Electron Maintenance" "Factory Reset" "Virtual Consoles" "Emergency Hotspot") - MENU_HANDLERS=(diagnostics_menu advanced_electron_menu advanced_factory_reset_menu advanced_virtual_consoles_menu advanced_emergency_hotspot_menu) + MENU_LABELS=("Diagnostics" "Electron Maintenance" "Factory Reset" "Virtual Consoles" "Emergency Hotspot" "Fleet Profile") + MENU_HANDLERS=(diagnostics_menu advanced_electron_menu advanced_factory_reset_menu advanced_virtual_consoles_menu advanced_emergency_hotspot_menu fleet_profile_menu) } advanced_menu() { diff --git a/menus/fleet_profile.sh b/menus/fleet_profile.sh new file mode 100644 index 0000000..776000c --- /dev/null +++ b/menus/fleet_profile.sh @@ -0,0 +1,258 @@ +#!/bin/bash +################################################################################ +# menus/fleet_profile.sh - "Fleet Profile" (Advanced): export the portable +# parts of this kiosk's configuration to a JSON file, and apply that file +# to other already-installed kiosks - for standing up several kiosks that +# should share the same sites/settings. +# +# This is deliberately an MVP, not the legacy Export/Import Settings +# redesigned 1:1. It moves only what's actually safe to copy between +# machines automatically: +# - config.json's portable fields (sites, display/touch, navigation, +# lockout, password protection) - plain data, no machine binding. +# - Which addons were present at export time, as an informational +# checklist on apply - NOT automated installation. That's a +# deliberately separate, bigger follow-up (each addon would need a +# non-interactive install variant, mirroring the *_do_uninstall +# helpers Complete Uninstall already composes). +# +# Explicitly NOT exported, because copying them would be actively wrong, +# not just incomplete: +# - Authelia credentials: encrypted with a key derived from this +# machine's /etc/machine-id (addon_authelia.sh) - decrypts to +# garbage on any other machine. +# - WireGuard/VPN identity: a private key is that device's identity; +# reusing one across machines is a peer conflict, not a saving. +# - Asterisk Intercom's SIP extension: most PBXes reject two +# simultaneous registrations to the same extension. +# Apply prints all three as an explicit "needs a human" checklist rather +# than silently skipping them. +# +# Depends on: lib/menu.sh, lib/config.sh, and every menus/addon_*.sh +# being sourced first (for the *_is_installed detection helpers). +################################################################################ + +fleet_profile_status() { + echo "Exports/applies sites, display, navigation, and lockout settings" + echo "between already-installed kiosks. Addon credentials that are" + echo "bound to one machine (Authelia, WireGuard, Asterisk Intercom)" + echo "are never copied - see the checklist after applying a profile." +} + +fleet_profile_menu_builder() { + MENU_LABELS=("Export fleet profile" "Apply fleet profile") + MENU_HANDLERS=(action_export_fleet_profile action_apply_fleet_profile) +} + +fleet_profile_menu() { + run_menu "FLEET PROFILE" fleet_profile_menu_builder fleet_profile_status +} + +################################################################################ +# Helpers +################################################################################ + +# JSON array of addon identifiers currently present on this machine. +fleet_detect_addons() { + local addons=() + + cups_is_installed 2>/dev/null && addons+=("cups") + lms_is_installed 2>/dev/null && addons+=("lms") + squeezelite_is_installed 2>/dev/null && addons+=("squeezelite") + is_service_active x11vnc 2>/dev/null && addons+=("vnc") + command -v wg &>/dev/null && addons+=("wireguard") + command -v tailscale &>/dev/null && addons+=("tailscale") + command -v netbird &>/dev/null && addons+=("netbird") + baresip_is_installed 2>/dev/null && addons+=("asterisk_intercom") + + if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then + local authelia_url + authelia_url=$(sudo -u "$KIOSK_USER" jq -r '.autheliaURL // ""' "$CONFIG_PATH" 2>/dev/null || true) + [[ -n "$authelia_url" ]] && addons+=("authelia") + fi + + if [[ "${#addons[@]}" -eq 0 ]]; then + echo "[]" + else + printf '%s\n' "${addons[@]}" | jq -R . | jq -s . || echo "[]" + fi +} + +################################################################################ +# Actions +################################################################################ + +action_export_fleet_profile() { + echo + if ! sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then + log_error "config.json not found at $CONFIG_PATH - configure sites/settings first" + pause + return 1 + fi + + local out_path + out_path=$(ask_text "Export profile to" "$HOME/kiosk-fleet-profile.json") + + local raw_config + raw_config=$(sudo -u "$KIOSK_USER" cat "$CONFIG_PATH" 2>/dev/null) + if ! echo "$raw_config" | jq empty 2>/dev/null; then + log_error "config.json is not valid JSON - cannot export" + pause + return 1 + fi + + local settings + settings=$(echo "$raw_config" | jq 'del(.autheliaURL, .autheliaUsername, .autheliaEncryptedPassword)') + + local addons_present + addons_present=$(fleet_detect_addons) + + jq -n \ + --argjson settings "$settings" \ + --argjson addons "$addons_present" \ + --arg script_version "${SCRIPT_VERSION:-unknown}" \ + '{profile_version: 1, script_version: $script_version, settings: $settings, addons_present: $addons}' \ + > "$out_path" + + log_success "Fleet profile exported to $out_path" + echo + echo "Included: sites, display/touch/navigation settings, lockout," + echo "password protection (SHA-256 hash only)." + echo + echo "NOT included (needs fresh setup on each new device):" + echo " - Authelia credentials (encrypted per-machine, won't decrypt elsewhere)" + echo " - WireGuard/VPN keys (each device needs its own identity)" + echo " - Asterisk Intercom extension (most PBXes reject duplicate registrations)" + + pause +} + +action_apply_fleet_profile() { + echo + local in_path + in_path=$(ask_text "Profile file to apply" "") + if [[ -z "$in_path" ]]; then + echo "Cancelled" + pause + return + fi + if [[ ! -f "$in_path" ]]; then + log_error "File not found: $in_path" + pause + return 1 + fi + if ! jq empty "$in_path" 2>/dev/null; then + log_error "Not valid JSON: $in_path" + pause + return 1 + fi + + local settings addons_present + settings=$(jq -c '.settings // {}' "$in_path" || echo '{}') + addons_present=$(jq -r '.addons_present[]? // empty' "$in_path" || true) + + echo "Profile summary:" + echo " Sites: $(echo "$settings" | jq '.tabs | length // 0')" + echo " Addons expected: $(echo "$addons_present" | tr '\n' ' ')" + echo + ask_yes_no "Apply this profile? This overwrites current sites/display/lockout settings." "n" || { echo "Cancelled"; pause; return; } + + sudo mkdir -p "$KIOSK_DIR" + sudo chown -R "$KIOSK_USER:$KIOSK_USER" "$KIOSK_DIR" + + local existing="{}" + if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then + existing=$(sudo -u "$KIOSK_USER" cat "$CONFIG_PATH" 2>/dev/null) + echo "$existing" | jq empty 2>/dev/null || existing="{}" + fi + + # Merge, not replace - same reasoning as save_config: this machine's + # own Authelia fields (never in the exported settings blob) must + # survive an apply untouched. Guarded: $settings came from an + # external file - a corrupted/hand-edited profile whose "settings" + # key isn't a JSON object must not be allowed to crash the session. + local merged + if ! merged=$(echo "$existing" | jq --argjson s "$settings" '. + $s' 2>/dev/null); then + log_error "Profile's settings are not a valid JSON object - nothing was changed" + pause + return 1 + fi + + local tmp + tmp=$(mktemp) + echo "$merged" > "$tmp" + sudo -u "$KIOSK_USER" bash -c "cat > '$CONFIG_PATH'" < "$tmp" + sudo -u "$KIOSK_USER" chmod 644 "$CONFIG_PATH" + rm -f "$tmp" + + log_success "Settings applied" + + echo + echo "Addon checklist (from the exported profile):" + local missing_any=false + if [[ -z "$addons_present" ]]; then + echo " (profile recorded no addons)" + fi + while IFS= read -r addon; do + [[ -z "$addon" ]] && continue + case "$addon" in + cups) + if cups_is_installed; then + echo " [x] CUPS Printing - already installed" + else + echo " [ ] CUPS Printing - not installed, install via Addons" + missing_any=true + fi ;; + lms) + if lms_is_installed; then + echo " [x] LMS Server - already installed" + else + echo " [ ] LMS Server - not installed, install via Addons" + missing_any=true + fi ;; + squeezelite) + if squeezelite_is_installed; then + echo " [x] Squeezelite Player - already installed" + else + echo " [ ] Squeezelite Player - not installed, install via Addons" + missing_any=true + fi ;; + vnc) + if is_service_active x11vnc; then + echo " [x] VNC - already installed" + else + echo " [ ] VNC - not installed, install via Addons" + missing_any=true + fi ;; + wireguard) + echo " [!] WireGuard - needs a NEW keypair/peer on this device, never clone the key" ;; + tailscale) + if command -v tailscale &>/dev/null; then + echo " [x] Tailscale - installed (connect with a reusable auth key if not yet connected)" + else + echo " [ ] Tailscale - not installed, install via Addons" + missing_any=true + fi ;; + netbird) + if command -v netbird &>/dev/null; then + echo " [x] Netbird - installed (connect with a reusable setup key if not yet connected)" + else + echo " [ ] Netbird - not installed, install via Addons" + missing_any=true + fi ;; + asterisk_intercom) + echo " [!] Asterisk Intercom - needs its own SIP extension on this device" ;; + authelia) + echo " [!] Authelia - needs a fresh login on this device" ;; + *) + echo " [?] $addon - unrecognized entry in profile" ;; + esac + done <<< "$addons_present" + + if $missing_any; then + echo + echo "Install anything marked \"not installed\" above via the Addons menu." + fi + + pause +} diff --git a/ubuntu-based-kiosk.sh b/ubuntu-based-kiosk.sh index accce64..59e6c13 100644 --- a/ubuntu-based-kiosk.sh +++ b/ubuntu-based-kiosk.sh @@ -1,8 +1,32 @@ #!/bin/bash ################################################################################ -### Ubuntu Based Kiosk v2.12.0 ### +### Ubuntu Based Kiosk v2.13.0 ### ################################################################################ # +# RELEASE v2.13.0 - Fleet Profile: New MVP for Standing Up Several +# Kiosks with the Same Settings +# - New in ./install.sh's Advanced menu: Fleet Profile +# (menus/fleet_profile.sh). Not a port of the legacy Export/Import +# Settings - a narrower, deliberately-scoped feature for the "set up +# one kiosk, then stamp out a dozen more like it" use case: export the +# portable parts of config.json (sites, display/touch/navigation, +# lockout, password protection) to a JSON file, apply that file to any +# other already-installed kiosk. +# - Explicitly does NOT copy machine-bound credentials, because copying +# them would be actively wrong, not just incomplete: Authelia's +# encrypted password is keyed off /etc/machine-id and decrypts to +# garbage elsewhere; a WireGuard private key is a device identity and +# reusing one across machines is a peer conflict; most Asterisk PBXes +# reject two simultaneous registrations to the same extension. Apply +# prints these as an explicit "needs a human" checklist instead of +# silently skipping them or (worse) cloning them. +# - Does not install missing addons - only records which addons were +# present at export time and reports which of those are/aren't +# present on the machine being applied to. Non-interactive addon +# installation (so applying a profile needs zero prompts, scriptable +# over SSH to a whole fleet) is deliberately left as a follow-up, not +# bundled into this MVP. +# # RELEASE v2.12.0 - Complete Uninstall Migrated (Last of the # "Destructive Trio"); Composed, Not Re-Implemented # - New in ./install.sh's Core Settings menu: Complete Uninstall @@ -450,7 +474,7 @@ set -euo pipefail ### SECTION 1: CONSTANTS & GLOBALS ################################################################################ -SCRIPT_VERSION="2.12.0" +SCRIPT_VERSION="2.13.0" # Resolve the real path to this script file. # When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,