From 074b2ec2e3c34b7fff50fe23731beeabd1ad2584 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:33:12 +0000 Subject: [PATCH] Migrate Timezone and Hidden Site PIN menus; harden menu framework against set -e; bump to v2.1.0 Two more menus migrated onto lib/menu.sh + lib/config.sh, chosen specifically because neither touches config.json - a third and fourth shape for the framework (a system command via timedatectl, and a flat PIN file), on top of Sites' list CRUD and Display's JSON toggles. - menus/timezone.sh: also replaces the legacy script's hand-numbered 18-entry case statement with a plain data list (TIMEZONE_COMMON_ZONES) plus one handler that reads the number run_menu hands it - adding or removing a zone never touches numbering anywhere else. Required a small run_menu addition: handlers now receive the chosen 1-based number as $1, so one handler can serve a whole data-driven list instead of needing a wrapper function per entry. - menus/hidden_pin.sh: set/disable/reset the PIN gating hidden pages. Testing menus/timezone.sh surfaced a real bug before it ever shipped: this whole tool runs under `set -e`, and set_timezone() rejecting an invalid zone via a bare `return 1` as its last statement took down the *entire* install.sh session, not just that one action - a single typo would silently drop the user back to their shell. Fixed at the framework level in lib/menu.sh (run_menu now absorbs a failed handler's exit code) rather than patching set_timezone alone, since any future menu could hit the same trap. Verified against the real install.sh as a genuine non-root user: an invalid timezone now logs an error and redraws the Timezone menu instead of killing the session (confirmed exit code 0 at the end of the run). Note this specific hazard was introduced by this session's own return-1 idiom, not inherited from the legacy script, which never uses a bare return 1 in these functions. Also per the user: left the old configure_sites/configure_touch_controls/ configure_navigation_security/configure_optional_features functions in ubuntu-based-kiosk.sh untouched for now (still carrying the v2.0.0 settings-clobber and reorder bugs) rather than removing them - they'll be retired in one pass once enough of Core Settings/Addons/Advanced is migrated. Bumped SCRIPT_VERSION to 2.1.0 with matching changelog entries in the script header and Readme, and updated the Readme's "Modular Management" section to state plainly what is and isn't migrated yet. Verified: - Full regression: re-ran the Sites and Display scratch-config test suites against the updated run_menu signature - both still clean. - New scratch-config tests for hidden_pin.sh (set/mismatch/reject/ disable/reset, correct file permissions) and timezone.sh (builder entry count, common-zone pick by index, manual entry with legacy US/* alias normalization, region search + cancel, invalid-zone rejection) - all correct, with timedatectl/sudo stubbed only where needed to avoid mutating this sandbox's real system clock/timezone. - End-to-end: ran the real install.sh as a genuine non-root, non-"kiosk" user, navigating Timezone -> manual entry -> invalid zone -> confirmed no crash and a normal return to the menu, then Hidden Site PIN -> set a PIN -> confirmed the file on disk (mode 600, correct content) -> clean exit (code 0). --- Readme.md | 37 ++++++++---- install.sh | 28 +++++---- lib/menu.sh | 13 ++++- menus/hidden_pin.sh | 88 ++++++++++++++++++++++++++++ menus/timezone.sh | 129 ++++++++++++++++++++++++++++++++++++++++++ ubuntu-based-kiosk.sh | 28 ++++++++- 6 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 menus/hidden_pin.sh create mode 100644 menus/timezone.sh diff --git a/Readme.md b/Readme.md index 66a48a3..25adc0a 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Ubuntu Based Kiosk -**Current Version:** 2.0.0 (check script header for latest version) +**Current Version:** 2.1.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/ @@ -1182,8 +1182,14 @@ terminal menu and the web UI, so they can't drift apart). concept for this approach. - `menus/display.sh` — **Display & Interaction**: touch gesture mode, link navigation security, and the on-screen pause/keyboard/navigation - button toggles. A second proof of concept covering a different menu - shape (settings toggles vs. the list CRUD in Sites). + button toggles. A different menu shape from Sites (settings toggles + vs. list CRUD). +- `menus/timezone.sh` — **Timezone**: also replaces the legacy script's + hand-numbered 18-entry `case` statement with a plain data list plus one + handler — the numbering is just `run_menu`'s job now. +- `menus/hidden_pin.sh` — **Hidden Site PIN**: the PIN gating hidden + pages (`duration: -1` in Sites). A fourth shape again — a flat file, + not `config.json`. - `install.sh` — entry point for the modular tool. Run it against an *already-installed* kiosk: ```bash @@ -1192,19 +1198,30 @@ terminal menu and the web UI, so they can't drift apart). ./install.sh ``` -This does **not** yet replace first-time installation — that's still the -single-file script above (`Quick Install`). The rest of Core -Settings/Addons/Advanced will move into `menus/*.sh` the same way, one -menu at a time, and `install.sh` will eventually take over the whole -`show_main_menu` from the legacy script. +**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 WiFi, Power/Display/Quiet Hours, Password Protection & +Lockout, 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 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 +installation itself is the last and largest piece to move, if it moves +at all. --- ## Project Status & Future Plans -**Current Version:** 2.0.0 +**Current Version:** 2.1.0 -**Recent Updates (v2.0.0):** +**Recent Updates (v2.1.0):** +- **Two more menus migrated:** Timezone (`menus/timezone.sh`) and Hidden Site PIN (`menus/hidden_pin.sh`), joining Sites & Page Timing and Display & Interaction in `./install.sh`. Timezone also replaces the old hand-numbered 18-entry list with a data-driven one built on the generic menu framework. +- **Bug fix (framework-level):** `install.sh` runs under `set -e`; a menu action that legitimately fails (e.g. rejecting an invalid timezone) and returns non-zero as its last statement could take down the *entire* session instead of just that action. Caught by testing before this ever shipped broadly; `run_menu()` now absorbs a failed handler's exit code, protecting every menu — present and future. +- The old, unmigrated `configure_sites`/`configure_touch_controls`/`configure_navigation_security`/`configure_optional_features` in `ubuntu-based-kiosk.sh` are staying in place for now (still carrying the v2.0.0 bugs below) until enough of Core Settings/Addons/Advanced is migrated to retire them in one pass — see "Modular Management" below for exactly what's covered so far. + +**Previous (v2.0.0):** - **Modular management path:** new `lib/menu.sh` (reusable numbered-menu framework: auto-numbered entries, `0` always exits/returns) and `lib/config.sh` (single load/save for `config.json`), with menus migrating into `menus/*.sh` one at a time — **Sites & Page Timing** and **Display & Interaction** are migrated so far. Run via `./install.sh` after cloning the repo, against an already-installed kiosk (see "Modular Management" below). Groundwork for the planned web-based GUI, which will share this same `lib/config.sh` layer. - **Bug fix:** the old Sites menu could save `config.json` without first loading swipe/navigation/lockout settings, silently resetting them to script defaults. - **Bug fix:** reordering sites had an off-by-one that left the moved site one slot short of the requested position. diff --git a/install.sh b/install.sh index 0996998..23ab743 100755 --- a/install.sh +++ b/install.sh @@ -3,16 +3,18 @@ # install.sh - Modular management entry point for Ubuntu Based Kiosk. # # This is NOT yet the full system installer - that is still the big -# single-file script (ubuntu-based-kiosk-v1.0.3.sh etc) documented in -# Readme.md, and first-time provisioning of a new kiosk still goes through -# it. This entry point is the start of pulling the *menu system* out of -# that 12k-line file into small, independently editable modules under -# lib/ and menus/, so a change to (say) the Sites menu can't accidentally -# break WiFi setup or the uninstaller three thousand lines away. +# single-file script (ubuntu-based-kiosk.sh) documented in Readme.md, and +# first-time provisioning of a new kiosk still goes through it. That file +# still also contains its own (unmigrated, unmodified) copies of every +# menu below - both copies coexist deliberately until enough of Core +# Settings/Addons/Advanced has moved over to retire the old ones in one +# pass. This entry point is the modular replacement, one menus/*.sh file +# at a time, so a change to (say) the Sites menu can't accidentally break +# WiFi setup or the uninstaller three thousand lines away. # -# Today this wires up Sites & Page Timing (menus/sites.sh) and Display & -# Interaction (menus/display.sh). The rest of Core Settings/Addons/Advanced -# will move over the same way, one menus/*.sh file at a time. +# Migrated so far: Sites & Page Timing (menus/sites.sh), Display & +# Interaction (menus/display.sh), Timezone (menus/timezone.sh), Hidden +# Site PIN (menus/hidden_pin.sh). # # Usage (once the kiosk has already been installed): # git clone @@ -32,6 +34,10 @@ source "$SCRIPT_DIR/lib/config.sh" source "$SCRIPT_DIR/menus/sites.sh" # shellcheck source=menus/display.sh source "$SCRIPT_DIR/menus/display.sh" +# shellcheck source=menus/timezone.sh +source "$SCRIPT_DIR/menus/timezone.sh" +# shellcheck source=menus/hidden_pin.sh +source "$SCRIPT_DIR/menus/hidden_pin.sh" ################################################################################ # Preflight @@ -68,8 +74,8 @@ fi ################################################################################ main_menu_builder() { - MENU_LABELS=("Sites & Page Timing" "Display & Interaction") - MENU_HANDLERS=(sites_menu display_menu) + MENU_LABELS=("Sites & Page Timing" "Display & Interaction" "Timezone" "Hidden Site PIN") + MENU_HANDLERS=(sites_menu display_menu timezone_menu hidden_pin_menu) } main_menu_status() { diff --git a/lib/menu.sh b/lib/menu.sh index 1a25ae0..6fa6d3f 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -193,6 +193,11 @@ print_menu_header() { # # Entries are auto-numbered 1..N. "0" always returns from run_menu - no # menu file needs to hand-roll its own exit case. +# +# The handler is called as `handler "$choice"` (the 1-based number picked), +# so a data-driven list (e.g. a set of timezones) can share one handler +# instead of needing a distinct wrapper function per entry. Handlers that +# don't care can just ignore the argument. run_menu() { local title="$1" local builder="$2" @@ -235,6 +240,12 @@ run_menu() { return 0 fi - "${MENU_HANDLERS[$((choice - 1))]}" + # `|| true`: this whole tool runs under `set -e`. A handler that + # legitimately fails (invalid input, a guard clause, etc) and + # returns non-zero as its last statement must not be allowed to + # take the entire session down - it should just redraw the menu. + # Absorbing that here means no menus/*.sh file has to think about + # set -e at all. + "${MENU_HANDLERS[$((choice - 1))]}" "$choice" || true done } diff --git a/menus/hidden_pin.sh b/menus/hidden_pin.sh new file mode 100644 index 0000000..4ac937d --- /dev/null +++ b/menus/hidden_pin.sh @@ -0,0 +1,88 @@ +#!/bin/bash +################################################################################ +# menus/hidden_pin.sh - "Hidden Site PIN" menu. +# +# Guards access to hidden pages (duration = -1, see menus/sites.sh) via a +# flat PIN file rather than config.json - a fourth shape for the framework +# to prove out (plain file, not JSON at all). +# +# Depends on: lib/menu.sh, lib/config.sh being sourced first. +################################################################################ + +hidden_pin_file() { + echo "$KIOSK_DIR/.jitsi-pin" +} + +hidden_pin_status() { + local pin_file + pin_file=$(hidden_pin_file) + + if sudo -u "$KIOSK_USER" test -f "$pin_file" 2>/dev/null; then + local current_pin + current_pin=$(sudo -u "$KIOSK_USER" cat "$pin_file" 2>/dev/null) + if [[ "$current_pin" == "NOPIN" ]]; then + echo "Current: no PIN (hidden pages open to anyone)" + else + echo "Current: PIN set (${#current_pin} digits)" + fi + else + echo "Current: not configured (default: 1234)" + fi +} + +hidden_pin_menu_builder() { + MENU_LABELS=("Set new PIN (4-8 digits)" "Disable PIN (open access)" "Reset to default (1234)") + MENU_HANDLERS=(action_set_pin action_disable_pin action_reset_pin) +} + +hidden_pin_menu() { + run_menu "HIDDEN SITE PIN" hidden_pin_menu_builder hidden_pin_status +} + +################################################################################ +# Actions +################################################################################ + +write_pin() { + local value="$1" + local pin_file + pin_file=$(hidden_pin_file) + + sudo mkdir -p "$KIOSK_DIR" + echo "$value" | sudo -u "$KIOSK_USER" tee "$pin_file" > /dev/null + sudo -u "$KIOSK_USER" chmod 600 "$pin_file" + log_warning "Restart the kiosk display for this to take effect" +} + +action_set_pin() { + echo + local new_pin confirm_pin + while true; do + read -r -p "Enter new PIN (4-8 digits): " new_pin + + if [[ ! "$new_pin" =~ ^[0-9]{4,8}$ ]]; then + echo "❌ PIN must be 4-8 digits" + continue + fi + + read -r -p "Confirm PIN: " confirm_pin + + if [[ "$new_pin" == "$confirm_pin" ]]; then + write_pin "$new_pin" + log_success "PIN updated" + break + else + echo "❌ PINs don't match, try again" + fi + done +} + +action_disable_pin() { + write_pin "NOPIN" + log_success "PIN disabled - hidden pages accessible without a PIN" +} + +action_reset_pin() { + write_pin "1234" + log_success "PIN reset to default (1234)" +} diff --git a/menus/timezone.sh b/menus/timezone.sh new file mode 100644 index 0000000..6198d85 --- /dev/null +++ b/menus/timezone.sh @@ -0,0 +1,129 @@ +#!/bin/bash +################################################################################ +# menus/timezone.sh - "Timezone" menu. +# +# Third menu migrated off the old single-file installer, and a different +# shape again: not config.json at all - talks to timedatectl/system state +# directly. Also the clearest demonstration of the framework's value: the +# original hand-numbered an 18-entry list ("1) America/New_York ... 18) +# Enter manually") in a single case statement. Here the common-zone list +# is just data, one handler (action_pick_common_timezone) handles all of +# them using the number run_menu hands it, and adding/removing a zone +# from the list never touches numbering anywhere else. +# +# Depends on: lib/menu.sh, lib/config.sh being sourced first. +################################################################################ + +TIMEZONE_COMMON_ZONES=( + "America/New_York" "America/Chicago" "America/Denver" "America/Los_Angeles" + "America/Phoenix" "America/Anchorage" "Pacific/Honolulu" "Europe/London" + "Europe/Paris" "Europe/Berlin" "Europe/Rome" "Asia/Tokyo" "Asia/Shanghai" + "Asia/Dubai" "Australia/Sydney" "Pacific/Auckland" +) +TIMEZONE_COMMON_LABELS=( + "US Eastern" "US Central" "US Mountain" "US Pacific" "US Arizona" "US Alaska" + "US Hawaii" "UK" "Central Europe" "Germany" "Italy" "Japan" "China" "UAE" + "Australia East" "New Zealand" +) + +timezone_status() { + echo "Current timezone: $(timedatectl show -p Timezone --value)" +} + +timezone_menu_builder() { + MENU_LABELS=() + MENU_HANDLERS=() + for i in "${!TIMEZONE_COMMON_ZONES[@]}"; do + MENU_LABELS+=("${TIMEZONE_COMMON_ZONES[$i]} (${TIMEZONE_COMMON_LABELS[$i]})") + MENU_HANDLERS+=(action_pick_common_timezone) + done + MENU_LABELS+=("Search for timezone by region" "Enter timezone manually") + MENU_HANDLERS+=(action_search_timezone action_manual_timezone) +} + +timezone_menu() { + run_menu "TIMEZONE" timezone_menu_builder timezone_status +} + +################################################################################ +# Actions +################################################################################ + +# Called by run_menu as `action_pick_common_timezone "$choice"` - $choice is +# the 1-based menu number, which lines up directly with TIMEZONE_COMMON_ZONES. +action_pick_common_timezone() { + local choice="$1" + set_timezone "${TIMEZONE_COMMON_ZONES[$((choice - 1))]}" +} + +action_search_timezone() { + echo + echo "Available regions:" + local regions + regions=($(timedatectl list-timezones | cut -d'/' -f1 | sort -u)) + for i in "${!regions[@]}"; do + printf " %2d) %s\n" "$((i + 1))" "${regions[$i]}" + done + echo + + local region_num + region_num=$(ask_integer "Select region number (0=cancel)" "0" 0 "${#regions[@]}") + [[ "$region_num" == "0" ]] && { echo "Cancelled"; return; } + local selected_region="${regions[$((region_num - 1))]}" + + echo + echo "Timezones in $selected_region:" + local timezones + timezones=($(timedatectl list-timezones | grep "^${selected_region}/")) + for i in "${!timezones[@]}"; do + printf " %3d) %s\n" "$((i + 1))" "${timezones[$i]}" + done + echo + + local tz_num + tz_num=$(ask_integer "Select timezone number (0=cancel)" "0" 0 "${#timezones[@]}") + [[ "$tz_num" == "0" ]] && { echo "Cancelled"; return; } + set_timezone "${timezones[$((tz_num - 1))]}" +} + +action_manual_timezone() { + echo + local new_tz + read -r -p "Enter timezone (e.g., America/New_York): " new_tz + [[ -z "$new_tz" ]] && { echo "Cancelled"; return; } + set_timezone "$new_tz" +} + +################################################################################ +# Shared apply logic +################################################################################ + +set_timezone() { + local new_tz="$1" + + # A few legacy US/* aliases users might type manually - normalize before + # validating against the canonical IANA list. + case "$new_tz" in + "US/Eastern") new_tz="America/New_York" ;; + "US/Central") new_tz="America/Chicago" ;; + "US/Mountain") new_tz="America/Denver" ;; + "US/Pacific") new_tz="America/Los_Angeles" ;; + "US/Alaska") new_tz="America/Anchorage" ;; + "US/Hawaii") new_tz="Pacific/Honolulu" ;; + "US/Arizona") new_tz="America/Phoenix" ;; + esac + + if ! timedatectl list-timezones | grep -qx "$new_tz"; then + log_error "Invalid timezone: $new_tz" + return 1 + fi + + if sudo timedatectl set-timezone "$new_tz"; then + log_success "Timezone updated to $new_tz" + else + # Fallback: set timezone directly without D-Bus + sudo ln -sf "/usr/share/zoneinfo/$new_tz" /etc/localtime + echo "$new_tz" | sudo tee /etc/timezone > /dev/null + log_success "Timezone updated to $new_tz (direct)" + fi +} diff --git a/ubuntu-based-kiosk.sh b/ubuntu-based-kiosk.sh index c9c408c..d15874f 100644 --- a/ubuntu-based-kiosk.sh +++ b/ubuntu-based-kiosk.sh @@ -1,8 +1,32 @@ #!/bin/bash ################################################################################ -### Ubuntu Based Kiosk v2.0.0 ### +### Ubuntu Based Kiosk v2.1.0 ### ################################################################################ # +# RELEASE v2.1.0 - Two More Menus Migrated, Menu Framework Hardened +# - New in ./install.sh: Timezone (menus/timezone.sh) and Hidden Site PIN +# (menus/hidden_pin.sh) menus, alongside Sites & Page Timing and Display +# & Interaction from v2.0.0. Timezone doubles as a demonstration of the +# framework: the old hand-numbered 18-entry case statement is now just +# a data list plus one handler. +# - Hardened lib/menu.sh: since this whole tool runs under `set -e`, a menu +# action that legitimately fails (e.g. rejecting an invalid timezone) and +# returns non-zero as its last statement could take down the *entire* +# session, not just that one action - one typo would silently drop the +# user back to their shell. Caught by testing menus/timezone.sh (its +# set_timezone() does `return 1` on an invalid zone) before this ever +# shipped; run_menu() now absorbs a failed handler's exit code so it +# only redraws the menu, protecting every menu, present and future. +# - The old (unmigrated) configure_sites/configure_touch_controls/ +# configure_navigation_security/configure_optional_features functions +# still live in this script, unchanged, and still have both v2.0.0 bugs +# above - left in place deliberately until enough of Core Settings/ +# Addons/Advanced is migrated to retire them in one pass. configure_ +# timezone/configure_hidden_site_pin don't share the set -e hazard +# (they never use a bare `return 1`), but are otherwise also still +# here unchanged pending the same cleanup. See Readme.md ("Modular +# Management") for current migration status. +# # RELEASE v2.0.0 - Modular Management & Unversioned Filename # - New git-clone-based management path: lib/menu.sh (reusable numbered-menu # framework) + lib/config.sh (single config.json load/save) + menus/*.sh, @@ -85,7 +109,7 @@ set -euo pipefail ### SECTION 1: CONSTANTS & GLOBALS ################################################################################ -SCRIPT_VERSION="2.0.0" +SCRIPT_VERSION="2.1.0" # Resolve the real path to this script file. # When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,