New Addons -> Web UI: a small Node/Express app (webui/) installed as a systemd service running as $KIOSK_USER, giving a browser-based editor for Sites & Page Timing, Display & Interaction, and Password Protection & Lockout - the three Core Settings menus that are pure config.json read/write with no privileged system mutation involved. Runs with no sudo at all, since config.json is already owned by $KIOSK_USER. webui/lib/config.js re-implements lib/config.sh's exact schema and merge-on-save contract in JS (kiosk-app/main.js already reads the same config.json directly in JS, so this isn't a new pattern), so it can never silently clobber fields it doesn't track - the same bug previously fixed in lib/config.sh's own history. No login of its own by design: Authelia runs elsewhere, and the expectation is a reverse proxy (e.g. Caddy) with Authelia forward-auth in front of it, the same way other self-hosted apps get protected - Authelia integration is explicitly out of scope for this repo. Deliberately narrow scope for this first pass: WiFi, Timezone, Power/Display/Quiet Hours, Complete Uninstall, every other addon, and everything in Advanced remain terminal-only, since a network-facing process shouldn't be handed sudo-level system mutation without a lot more thought than this pass gives it. Wired into Complete Uninstall (webui_do_uninstall) and Clone Settings (addon-presence detection) the same way every other addon is. This is the single-kiosk piece of the web-based GUI this repo's "Modular Management" notes have mentioned for a while - a central multi-kiosk fleet dashboard is an intentional follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
260 lines
10 KiB
Bash
260 lines
10 KiB
Bash
#!/bin/bash
|
|
################################################################################
|
|
# menus/clone_settings.sh - "Clone Settings" (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).
|
|
################################################################################
|
|
|
|
clone_settings_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."
|
|
}
|
|
|
|
clone_settings_menu_builder() {
|
|
MENU_LABELS=("Export settings" "Apply settings (clone)")
|
|
MENU_HANDLERS=(action_export_clone_settings action_apply_clone_settings)
|
|
}
|
|
|
|
clone_settings_menu() {
|
|
run_menu "CLONE SETTINGS" clone_settings_menu_builder clone_settings_status
|
|
}
|
|
|
|
################################################################################
|
|
# Helpers
|
|
################################################################################
|
|
|
|
# JSON array of addon identifiers currently present on this machine.
|
|
clone_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")
|
|
webui_is_installed 2>/dev/null && addons+=("webui")
|
|
|
|
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_clone_settings() {
|
|
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-clone-settings.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=$(clone_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 "Settings 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_clone_settings() {
|
|
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
|
|
}
|