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).
This commit is contained in:
Claude
2026-08-18 16:33:12 +00:00
parent c1370edc3e
commit 074b2ec2e3
6 changed files with 299 additions and 24 deletions
+27 -10
View File
@@ -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.
+17 -11
View File
@@ -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 <repo>
@@ -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() {
+12 -1
View File
@@ -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
}
+88
View File
@@ -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)"
}
+129
View File
@@ -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
}
+26 -2
View File
@@ -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,