Offline addons, LAN-aware emergency hotspot, LAN time server

Offline bundle now carries every addon that works on a network without
internet: LMS (Lyrion's own .deb, from the addon's LMS_DEB_URL) +
Squeezelite, Asterisk Intercom (baresip), VNC, WireGuard and the
Emergency Hotspot packages - each list in a variable next to the code
that installs it (SQUEEZELITE_APT_PACKAGES, ASTERISK_INTERCOM_APT_PACKAGES,
VNC_APT_PACKAGES, WIREGUARD_APT_PACKAGES, EMERGENCY_HOTSPOT_APT_PACKAGES),
read by build-bundle.sh. Each install goes through
run_with_offline_fallback like CUPS, so they install offline from the
normal menus / Web UI. LMS installs as a bundle package when offline
(lms_offline_install_wanted); its online repo/direct-download paths are
unchanged. Tailscale/Netbird deliberately not bundled (vendor scripts,
vendor servers). Bundle: 524 MB (+87 MB).

Emergency hotspot: the boot check pinged only 8.8.8.8, so on a LAN with
no internet the hotspot started on every boot. Now chosen at setup:
"network" (default - router or internet must answer; a router that
ignores ping still counts if it answered ARP), "internet" (old check),
or "host" (a specific address). Three tries 10s apart. Existing scripts
keep the old check until Reconfigure.

New Core Settings -> Time Server (NTP) (menus/time_sync.sh): point
systemd-timesyncd at LAN time server(s) via a timesyncd.conf.d drop-in,
back to Ubuntu's defaults, or set the clock by hand (and the RTC) for a
network with no time server. Complete Uninstall resets it.

Also: lms_service_name used `systemctl list-unit-files | grep -q`, the
same pipefail/SIGPIPE pattern as the earlier cups_is_installed bug; now
`systemctl cat`.

Tested in a network-less chroot of the 26.04.1 server filesystem: kiosk
base, then action_wireguard_install and action_install_lms (real
functions) and the Squeezelite/Intercom/VNC/hotspot package sets all
installed from the bundle, apt left on normal sources after each.
Hotspot script generated for all three modes (plus an invalid address,
which falls back to "network") and its check run against simulated
networks: LAN-only with router answering ping or only ARP -> no hotspot
in "network" mode; nothing reachable -> hotspot. Time Server actions
driven with timedatectl stubbed: drop-in contents, input validation,
manual set-time sequence, reset removes the drop-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
Claude
2026-10-03 16:08:46 +00:00
parent fb4ee6fa62
commit fcbb890dbf
13 changed files with 388 additions and 75 deletions
+3 -2
View File
@@ -70,8 +70,9 @@
# --repo-url URL git remote recorded in the copied repo, used by
# Advanced -> Upgrade (default: this checkout's
# origin, else the upstream GitHub repo).
# --offline Also bundle everything install.sh downloads (~420 MB:
# every apt package incl. the CUPS printing addon,
# --offline Also bundle everything install.sh downloads (~520 MB:
# every apt package incl. the CUPS, LMS/Squeezelite,
# Asterisk Intercom, VNC, WireGuard and hotspot addons,
# Node.js, prebuilt Electron/Web UI node_modules), so
# first boot can install the kiosk with NO internet.
# With internet it still installs online as usual;
+35 -7
View File
@@ -8,17 +8,20 @@
#
# OUT_DIR ends up as /opt/kiosk-offline on the installed system:
# apt/ Local apt repository (flat, [trusted=yes]) with every
# package install.sh's provisioning and the CUPS addon
# install ask for, NodeSource's nodejs, and all their
# package install.sh's provisioning and the addons ask
# for (CUPS, LMS + Squeezelite, Asterisk Intercom, VNC,
# WireGuard, Emergency Hotspot - not Tailscale/Netbird,
# which need their vendors' servers anyway), Lyrion's
# own .deb, NodeSource's nodejs, and all their
# dependencies that a fresh Ubuntu Server install from
# SOURCE_ISO doesn't already have.
# npm/kiosk-app-node_modules.tar.gz Prebuilt node_modules (Electron
# npm/webui-node_modules.tar.gz included) in place of `npm install`.
# bundle-info.txt What was bundled, from where, when.
#
# Package lists are read from REPO_DIR's own lib/provision.sh and
# menus/addon_cups.sh (PROVISION_APT_PACKAGES, PROVISION_INTEL_APT_PACKAGES,
# CUPS_APT_PACKAGES), so the bundle always matches what install.sh asks for.
# Package lists are read from REPO_DIR's own lib/provision.sh and menus/
# (PROVISION_APT_PACKAGES, CUPS_APT_PACKAGES, SQUEEZELITE_APT_PACKAGES,
# LMS_DEB_URL, ...), so the bundle always matches what install.sh asks for.
#
# Dependencies are resolved against SOURCE_ISO's own package manifest - a
# fake dpkg status built from it (fake-dpkg-status.py) - so apt only
@@ -127,21 +130,45 @@ python3 "$HERE/fake-dpkg-status.py" "$manifest" "$R/var/lib/apt/lists" > "$R/var
################################################################################
# shellcheck disable=SC1091,SC2034
# Each list lives next to the code that installs it; lib/config.sh first
# for the path defaults those files use at the top level.
mapfile -t packages < <(
SCRIPT_DIR="$REPO_DIR"
source "$REPO_DIR/lib/config.sh"
source "$REPO_DIR/lib/provision.sh"
source "$REPO_DIR/menus/addon_cups.sh"
source "$REPO_DIR/menus/addon_lms_squeezelite.sh"
source "$REPO_DIR/menus/addon_asterisk_intercom.sh"
source "$REPO_DIR/menus/addon_remote_access.sh"
source "$REPO_DIR/menus/advanced_emergency_hotspot.sh"
printf '%s\n' "${PROVISION_APT_PACKAGES[@]}" "${PROVISION_INTEL_APT_PACKAGES[@]}" \
"${CUPS_APT_PACKAGES[@]}" "${EXTRA_APT_PACKAGES[@]}"
"${CUPS_APT_PACKAGES[@]}" "${SQUEEZELITE_APT_PACKAGES[@]}" \
"${ASTERISK_INTERCOM_APT_PACKAGES[@]}" "${VNC_APT_PACKAGES[@]}" \
"${WIREGUARD_APT_PACKAGES[@]}" "${EMERGENCY_HOTSPOT_APT_PACKAGES[@]}" \
"${EXTRA_APT_PACKAGES[@]}"
)
(( ${#packages[@]} > 20 )) || die "couldn't read the package lists from $REPO_DIR"
# Lyrion Music Server isn't in Ubuntu's archive: bundle the exact .deb the
# LMS addon downloads online, and let apt resolve its dependencies from it
# (a local .deb path in the install list).
# shellcheck disable=SC1091,SC2034
lms_url="$(SCRIPT_DIR="$REPO_DIR"; source "$REPO_DIR/lib/config.sh"; source "$REPO_DIR/menus/addon_lms_squeezelite.sh"; echo "$LMS_DEB_URL")"
lms_deb="$CACHE_DIR/$(basename "$lms_url")"
if [[ ! -s "$lms_deb" ]]; then
info "Downloading $(basename "$lms_url")..."
curl -fL --retry 3 -o "$lms_deb.part" "$lms_url" || die "couldn't download $lms_url"
mv "$lms_deb.part" "$lms_deb"
fi
dpkg-deb -I "$lms_deb" >/dev/null 2>&1 || { rm -f "$lms_deb"; die "$lms_deb isn't a valid .deb - deleted, re-run to retry"; }
packages+=("$lms_deb")
info "Resolving ${#packages[@]} packages (with recommends, as install.sh installs them)..."
# --print-uris against an empty archive dir: it leaves out anything already
# in the cache, which on a rebuild would be everything.
rm -rf "$R/empty-archives"; mkdir -p "$R/empty-archives/partial"
mapfile -t debs < <("${APT[@]}" -qq -o "Dir::Cache::archives=$R/empty-archives" \
install --print-uris -y "${packages[@]}" | awk '{print $2}')
install --print-uris -y "${packages[@]}" | awk '$1 !~ /^.file:/ {print $2}') # the local LMS .deb is copied below
(( ${#debs[@]} )) || die "apt resolved nothing to download - check the output above"
info "Downloading ${#debs[@]} .debs (cached between builds)..."
@@ -150,6 +177,7 @@ info "Downloading ${#debs[@]} .debs (cached between builds)..."
for f in "${debs[@]}"; do
cp "$R/var/cache/apt/archives/$f" "$OUT_DIR/apt/"
done
cp "$lms_deb" "$OUT_DIR/apt/"
(
cd "$OUT_DIR/apt"
apt-ftparchive packages . > Packages 2>/dev/null