Add Fleet Profile MVP for replicating settings across kiosks; bump to v2.13.0

New menus/fleet_profile.sh (Advanced), for the "set up one kiosk, then
stand up a dozen more like it" use case. Not a port of the legacy
Export/Import Settings - a narrower, deliberately-scoped feature:

- Export: writes config.json's portable fields (sites, display/touch/
  navigation, lockout, password protection) plus a list of addons
  present at export time to a JSON profile file.
- Apply: merges those fields onto a target kiosk's config.json (same
  merge-not-replace pattern as save_config, so the target's own fields
  survive untouched) and prints a checklist of which listed addons are/
  aren't installed on the target.

Deliberately excludes machine-bound credentials rather than silently
mishandling them: Authelia's encrypted password is keyed off
/etc/machine-id and decrypts to garbage elsewhere; a WireGuard private
key is a device identity, reusing one across machines is a peer
conflict; most Asterisk PBXes reject duplicate registrations to the
same extension. Apply prints all three as an explicit "needs a human"
checklist. Non-interactive addon installation (for a fully scriptable
fleet rollout) is a deliberate follow-up, not part of this MVP.

Full command-level stubbed test suite covering export (site/setting
content, Authelia stripped, addon detection) and apply (merge
correctness, target's own Authelia preserved, bad path/invalid JSON
handled cleanly). Full 20-suite regression + real end-to-end menu
navigation via install.sh all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
This commit is contained in:
Claude
2026-08-19 03:45:36 +00:00
parent b0b558f015
commit 5d56c667da
4 changed files with 314 additions and 13 deletions
+21 -8
View File
@@ -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.
+9 -3
View File
@@ -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 <repo>
@@ -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() {
+258
View File
@@ -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
}
+26 -2
View File
@@ -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,