100 Commits
Author SHA1 Message Date
Claude d6d6177dd9 Fix on-screen keyboard clearing fields on frameworks like Authelia's React UI
The keyboard-type IPC handler set input.value directly and dispatched a
plain "input" event. React (and similar frameworks) patch the value
setter on input/textarea instances to track the last value they set;
setting el.value directly updates that tracker too, so React never
detects a real change and its controlled state stays empty. The next
re-render (moving focus to another field, toggling a checkbox, etc.)
then redraws the input from that stale empty state, wiping out
whatever was typed. Route the writes through the native value setter
instead so the tracker stays out of sync and the dispatched event
actually reaches the framework's handler.

Also give the "Tab" key its own handler that moves focus to the next
focusable element, instead of falling through to the generic branch
that typed the literal word "Tab" into the field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1D2cv1KyFoYGnwSdszeXm
2026-09-08 00:54:01 +00:00
Claude e607e8cea0 Web UI: install/reconfigure addons, default-on install, visual redesign (v2.17.0)
Web UI now installs by default during first-time provisioning (fixed
port 8090, no prompt) instead of being opt-in, and can install/
reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk
Intercom, and check for updates - the addons and Update action asked
for by name.

Privilege model: the web service itself still runs as $KIOSK_USER with
zero ambient sudo. A new narrow, allow-listed root helper
(menus/addon_webui.sh's webui_write_helper_script) is the only way it
ever gains privilege, reachable only via a single-path passwordless
sudo rule generated and validated with `visudo -c -f` before being
installed, and it re-checks its own fixed action allow-list before
dispatching anything. Each allow-listed action is the exact same
interactive action_* function the terminal menu already uses, driven
by piping the right answers on stdin - the same technique this
project's own bash tests already use, so no prompt/mutation refactor
of any addon file was needed. webui/lib/actions.js's stdin sequences
were cross-validated against the real bash functions (not just read),
which caught two real bugs (Squeezelite and Asterisk Intercom both
silently lost their "decline reconfigure" path).

Long-running installs stream live output via Server-Sent Events
(webui/lib/jobs.js), one action at a time.

Full visual redesign: a sidebar shell (Sites/Display/Lockout/Addons/
Update) replacing the single scrolling page, light+dark themes via
prefers-color-scheme, no external font/CDN dependency. Actually driving
the redesigned UI in a headless browser (not just reading the code)
caught a real bug: refreshing an addon's pill/button after a successful
install used to rebuild the whole card, racing (and usually losing to)
the success status/log that job had just written. Fixed to update
pill/buttons in place.

Uninstall-via-web is deliberately still not offered, for any addon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
2026-08-19 17:51:07 +00:00
Claude b6ed4aad9c Add Web UI addon: browser-based config editor (v2.16.0)
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
2026-08-19 16:01:20 +00:00
Claude d0b76dc6cf Drop Full Reinstall from the migration plan (docs only)
It never worked reliably in the legacy script, and the modular tool
already covers the same outcome more reliably as two already-tested
pieces run back to back: Complete Uninstall (Core Settings), then
./install.sh again to provision fresh. No code changes - nothing was
ever built for it in the modular tool, this just stops carrying it in
docs/comments as a pending gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
2026-08-19 14:38:17 +00:00
Claude d411b05cae Add Upgrade to install.sh (v2.15.0)
The legacy Upgrade re-extracted main.js/preload.js/etc from its own
heredocs on every run - a mechanism the modular tool has no equivalent
of now that kiosk-app/ and provision/files/ are real files in the git
checkout. The new Advanced -> Upgrade is `git pull` (only after a
clean-tree check, and only as a fast-forward - never an automatic
merge) followed by re-running the same packages/kiosk-app/display/
firewall/power-management steps lib/provision.sh already has for a
fresh install, reused rather than reimplemented. Also offers an
on-demand Electron version check via the existing action_update_electron,
since Electron isn't versioned by this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
2026-08-19 13:50:25 +00:00
Claude 037a62e008 Add real first-time provisioning to install.sh (v2.14.0)
Until now ./install.sh only managed an already-installed kiosk;
ubuntu-based-kiosk.sh was still the only path from a bare Ubuntu Server
box to a running one. install.sh now provisions from scratch too:
packages, kiosk user, Node.js/Electron, LightDM+Openbox autologin,
audio/video/HDMI/power-button hardware setup, and the firewall, then
hands off to the already-migrated Core Settings/Advanced menus for
initial configuration instead of reimplementing that logic again.

- lib/provision.sh: the new provisioning flow, built mostly by calling
  existing menus (core_settings_menu, emergency hotspot, virtual
  consoles) - cuts it to ~300 lines against the legacy script's
  ~4,000-line first_time_install().
- lib/electron.sh: electron_install_binary() extracted out of
  menus/advanced_electron.sh so provisioning and the existing "Fix
  blank screen" action share one implementation.
- kiosk-app/ and provision/files/: the Electron app source and every
  system template file, extracted byte-for-byte out of
  ubuntu-based-kiosk.sh's heredocs into real files.
- Found and fixed a bash set -e gotcha along the way: testing a
  multi-statement function as an if-condition (`if ! some_func; then`)
  silently exempts everything inside that function from set -e for the
  duration of the call. Fixed in the new provisioning code and in
  menus/advanced_electron.sh's pre-existing repair action, which had
  the same shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
2026-08-19 05:24:32 +00:00
Claude 1c9447bc5d Rename Fleet Profile to Clone Settings per feedback
menus/fleet_profile.sh -> menus/clone_settings.sh. Same functionality
(export/apply portable config.json settings between kiosks); renamed
the file, every function, the menu label, and the on-disk default
profile filename to match. No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
2026-08-19 04:29:13 +00:00
Claude 5d56c667da 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
2026-08-19 03:45:36 +00:00
Claude b0b558f015 Migrate Complete Uninstall, composed from each addon's own uninstall helper; bump to v2.12.0
New menus/complete_uninstall.sh (Core Settings), the last of the
"destructive trio". Rather than re-implementing every addon's teardown
a second time (the legacy shape), it composes the *_do_uninstall
helpers each addon already has - if an addon's removal logic changes,
Complete Uninstall picks it up automatically.

Every addon menu with an uninstall action (CUPS, VNC, WireGuard,
Tailscale, Netbird, LMS, Squeezelite, Asterisk Intercom) plus
power_schedule's "remove all schedules" and Emergency Hotspot's disable
action were each split into a confirm-and-call wrapper (unchanged from
the user's perspective) and a silent do-the-removal helper that both
the wrapper and Complete Uninstall call.

Bug fix found 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 as their second-to-last
statement. 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 sequence partway through. 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.

Upgrade and Full Reinstall stay in ubuntu-based-kiosk.sh only - both
are coupled to its own heredoc self-extraction of main.js/preload.js/
etc, which has no modular equivalent yet.

Full command-level stubbed test suite exercising the full 12-step
teardown, confirmation-text validation, and reboot prompt. Full
19-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
2026-08-19 03:03:50 +00:00
Claude a3313aa9b8 Migrate 4 more Advanced items (Electron, Factory Reset, Virtual Consoles, Emergency Hotspot); bump to v2.11.0
New in install.sh's Advanced menu, alongside Diagnostics:
- menus/advanced_electron.sh: "Electron Maintenance" - the legacy
  "Manual Electron Update" and "Fix Blank Screen" combined into one
  submenu, sharing the binary-repair logic (electron_install_binary).
- menus/advanced_factory_reset.sh: "Factory Reset" - wipes config.json
  back to defaults only; addons are untouched.
- menus/advanced_virtual_consoles.sh: "Virtual Consoles" - toggles
  Ctrl+Alt+F1-F8 terminal login access.
- menus/advanced_emergency_hotspot.sh: "Emergency Hotspot" - auto-starts
  a WiFi hotspot if no internet is detected 60 seconds after boot. Its
  own runtime script and systemd unit now go through $BIN_DIR/
  $SYSTEMD_DIR like every other addon's own files, instead of the
  legacy's hardcoded /usr/local/bin and /etc/systemd/system.

That covers 8 of the legacy Advanced menu's 12 entries. Not migrated
this round: Export/Import Settings (pending a decision on rebuilding it
around actual paths vs. a hardcoded step list, or whether the future
web UI replaces the need for it) and Fix Squeezelite Audio (small
enough it may fold into the LMS addon instead of staying standalone).

Full command-level stubbed test suite per file, including set -e safety
checks (declined/failed paths never crash the session) and content
verification for every written file. Full 18-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
2026-08-19 02:52:43 +00:00
Claude 3eadcdb584 Migrate Asterisk Intercom, redesigned as SIP-extension-only; bump to v2.10.0
New menus/addon_asterisk_intercom.sh, wired into install.sh's Addons
menu. The legacy addon offered Client Only (Baresip SIP client), Server
Only, and Full (server + client), where Server/Full downloaded and ran
a third-party installer from a separate "Easy Asterisk" repository to
stand up a whole Asterisk PBX. That repository has since gone through a
major rework upstream, so this migration drops the PBX-install path
entirely: the addon now only installs Baresip and registers this kiosk
as a SIP extension against an Asterisk server the user already has
running elsewhere. It never installs or manages Asterisk itself. The
legacy script's own three-option version is untouched, same as every
other migrated menu.

Dropped the legacy client path's dependency on the Easy Asterisk repo's
GitHub API for version tracking - now reads the real installed baresip
package version via dpkg instead. Added an uninstall option, which the
legacy addon never had at all.

Bug fix found while testing: an unguarded `ver=$(baresip_installed_version)`
assignment crashed the whole session under set -e the first time status
was checked before Baresip was installed (dpkg-query legitimately fails
when the package isn't there). Guarded with `|| true`.

Full command-level stubbed test suite covering configure (manual/auto-
answer, TLS port bump, apt-install failure) and uninstall (keep/purge
config) for both fresh and already-configured states. Full 13-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
2026-08-19 02:28:05 +00:00
Claude 2bf4efe2b1 Migrate LMS/Squeezelite addon; fix is_service_enabled dead pre-check; bump to v2.9.0
New menus/addon_lms_squeezelite.sh: install/reconfigure/uninstall for an
LMS (Lyrion/Logitech Media Server) server and a Squeezelite player,
wired into install.sh's Addons menu. Squeezelite's own start script and
systemd unit now go through $BIN_DIR/$SYSTEMD_DIR like every other
addon instead of hardcoded /usr/local/bin and /etc/systemd/system; LMS's
own apt repo/GPG key/ufw rules stay at their real fixed system paths,
same approach as CUPS.

Fixed a real unguarded-pipeline bug from the legacy install_lms():
`sudo systemctl enable "$service_name" 2>&1 | tee ...` made the exit
status depend on tee (always 0) instead of systemctl enable, silently
swallowing real enable/start failures. Now uses enable_and_start_units().

Fixed is_service_enabled() (shared helper, backported into the legacy
script too): its list-unit-files pre-check never matched a bare service
name, so it always fell through to "not enabled" regardless of the real
state. Dropped the dead pre-check.

Full command-level stubbed test suite covering install/reconfigure/
uninstall for both LMS and Squeezelite, including the repo-vs-fallback-
download path, undetectable-service-name path, and enable/start-failure
path. Full 12-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
2026-08-19 02:11:05 +00:00
Claude 0454a259fa Migrate Remote Access addon; fix status-function crash gap in run_menu; bump to v2.8.0
Third and biggest Addon migrated: menus/addon_remote_access.sh - VNC
(x11vnc), WireGuard, Tailscale, and Netbird, each with its own install/
connect/status/uninstall flow. Same risk class as CUPS (real apt
packages, real system state) but broader in scope: Tailscale and
Netbird install via the vendors' own documented `curl -fsSL <url> | sh`
method, preserved exactly as-is rather than redesigned.

- lib/config.sh: new $WIREGUARD_DIR, same pattern as $SYSTEMD_DIR/
  $BIN_DIR/etc - nothing in this file hardcodes /etc/wireguard.
- lib/menu.sh: promoted power_schedule.sh's enable_and_start_timers()
  to a shared enable_and_start_units() (works for services now too, not
  just timers) - Remote Access needed the identical enable+start-with-
  graceful-failure-reporting pattern for x11vnc and wg-quick@, so this
  is fixed once and reused rather than duplicated a second time.
  power_schedule.sh's four call sites renamed to match.

Found and fixed a real framework-level bug while building this file:
run_menu()'s *handler* call has been `|| true`-guarded since v2.1.0,
but the *status function* call (`"$status_func"` on its own line) was
still completely bare. A status function's entire job is read-only
display, but if it contains so much as a pipeline whose grep matches
nothing - which pipefail turns into a pipeline failure even though the
actual last command in it (e.g. sed) succeeds - that bare call would
crash the *entire session*, not just fail to show status text. Found
while writing wireguard_status()'s `sudo wg show | grep ... | sed ...`
and deliberately verifying its exact failure mode rather than assuming
run_menu already covered it. Fixed once in run_menu() itself
(lib/menu.sh), protecting every status function across every menu -
present and future - the same "fix once at the framework level"
pattern as the v2.1.0 handler fix.

Given the framework fix meant this class of bug had been silently
possible since v2.1.0, audited every existing status function across
every already-migrated menu for the same specific shape (a bare
`var=$(...)` assignment from a grep-based pipeline, not embedded in an
echo and not already guarded - embedded substitutions and if-condition
contexts are both already safe on their own). Found and fixed one real
instance in power_schedule_status(). menus/addon_remote_access.sh's own
two equivalent pipelines (wireguard_status, netbird_status) were
written with `|| true` from the start once the pattern was identified.

Verified:
- New scratch/stub test for addon_remote_access.sh, with curl stubbed
  separately from sudo (Tailscale/Netbird's install scripts must never
  reach the real network regardless of what sudo intercepts) and a
  belt-and-suspenders `sh` stub in case anything got past curl: full
  status/menu-builder coverage for all four sub-areas in their real,
  unstubbed "not installed" state (none of the four tools exist in this
  sandbox); VNC install/change-password/uninstall with systemd unit
  content verified (correct $KIOSK_USER/$KIOSK_HOME substitution);
  WireGuard install, paste-config (content written correctly to scratch
  $WIREGUARD_DIR), and uninstall - including documenting a genuine
  cat-until-EOF test-harness limitation (a redirected pipe's EOF is
  permanent for the whole stream, unlike a real terminal's per-read
  Ctrl+D, so only the config's *default* name is testable through
  simple stdin redirection - inherent to the design, matches the legacy
  script's identical `cat`-based approach, not a bug); Tailscale and
  Netbird install/connect-interactive/connect-with-key/uninstall; and
  all four cancel paths confirmed to make zero sudo calls.
- Full regression: re-ran all 11 prior scratch/stub suites after the
  lib/menu.sh and power_schedule.sh changes - all still clean.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user through Addons -> Remote Access -> all four sub-menus in
  turn, each showing accurate real (unstubbed) "not installed" status,
  selecting Install, declining the confirmation, and returning cleanly
  - zero invalid-choice errors, clean exit code 0.
2026-08-19 01:35:15 +00:00
Claude cdd6f5adcf Backport save_config merge fix into legacy ubuntu-based-kiosk.sh; bump to v2.7.0
Per user decision: backport just the config-clobbering fix from v2.6.0
(lib/config.sh) into the legacy single-file installer's own
save_config(), independent of migrating the rest of that menu into
./install.sh.

The bug: save_config() rebuilt config.json from a fixed list of known
fields via `jq -n`, silently deleting anything it didn't know about -
specifically autheliaURL/autheliaUsername/autheliaEncryptedPassword,
written by configure_authelia()'s own careful `. + {...}` merge.
Configuring Authelia and then visiting Sites, Touch Controls,
Navigation, or Password Protection (all of which call save_config)
silently deleted the Authelia credentials. Real, currently-shipping
credential-loss bug, unrelated to whether the rest of that menu is ever
migrated - didn't need to wait for a full pass.

Fixed the same way as lib/config.sh: merge the known fields onto
whatever's already in config.json (`. + {...}`) instead of rebuilding
from nothing, with a `jq empty` validity check falling back to `{}` if
the existing file is missing or corrupt. This is a standalone fix to
one function only - nothing else about Sites/Touch/Navigation/Authelia
changed, and none of that is migrated by this commit.

Verified before touching the shipping copy: extracted the exact
save_config() function (now lines 3682-3805) into an isolated test
harness with stubbed dependencies (kiosk_user_exists, is_service_active,
log_success/warning), seeded a stub config.json with Authelia-style
fields via the same `. + {...}` merge configure_authelia() uses, called
save_config() a second time simulating a visit to an unrelated menu,
and confirmed the Authelia fields survive while an actual settings
change (duration 60 -> 90) still correctly takes effect. Also verified
the corrupt-JSON and missing-file edge cases don't crash the function.
Full syntax check on the whole 12,000+ line script, and the entire
modular test suite (11 scratch/stub suites), both still clean.
2026-08-19 01:21:43 +00:00
Claude 6c68897935 Migrate Authelia addon; fix real config-clobbering bug in save_config; bump to v2.6.0
Second Addon migrated: menus/addon_authelia.sh (encrypted SSO
credentials, AES-256-CBC with a key derived from /etc/machine-id via
scrypt - same algorithm main.js decrypts with - plus the full
Dockerized server-side setup instructions, now viewable again later
without reconfiguring).

Investigating how to wire its three config.json fields (autheliaURL/
autheliaUsername/autheliaEncryptedPassword) into lib/config.sh surfaced
a real, currently-shipping bug that has nothing to do with Authelia
specifically: save_config() did a full `jq -n` rebuild of config.json
from a fixed list of known fields - identical to what the legacy
script's own save_config still does. The legacy configure_authelia()
writes its three fields via a careful `. + {...}` merge that preserves
everything else already in the file, but neither save_config knew those
fields existed - so the next time a user visited Sites, Touch Controls,
Navigation, or Password Protection (all of which call save_config),
their Authelia credentials were silently deleted. This bug already
existed in the shipped single-file installer; it was ported faithfully
into lib/config.sh's first version because no test happened to set an
untracked field before calling save_config.

Fixed in lib/config.sh: save_config now merges its known fields onto
whatever's already in config.json (jq `. + {...}`) instead of rebuilding
the file from nothing, with a `jq empty` validity check falling back to
`{}` if the existing file is missing or corrupt. Any field this tool
doesn't track - Authelia's three today, anything else a future addon
adds tomorrow - now survives automatically. autheliaURL/
autheliaUsername/autheliaEncryptedPassword are also tracked fields in
their own right now (load_existing_config/save_config), consistent with
every other config.json field this tool manages, giving Authelia both a
direct fix and the general safety net.

The equivalent bug still exists, unfixed, in ubuntu-based-kiosk.sh's own
save_config - noted in both that script's changelog and the Readme's
"Modular Management" section as an open question: whether to backport
just that one fix into the legacy script now, independent of the wider
migration, given it's a real credential-loss bug affecting the
currently-shipping installer today.

Verified:
- New dedicated test (test_save_merge.sh) proving the save_config fix
  itself: seeded config.json with a simulated untracked field via the
  same `. + {...}` merge Authelia's own code uses, called save_config
  from an unrelated context (Sites deleting a tab), and confirmed the
  untracked field survived while the tab deletion still correctly took
  effect (not undone by the merge) - plus corrupt-JSON and
  missing-file edge cases both handled without crashing.
- New scratch-config test for addon_authelia.sh using REAL encryption
  (this sandbox has both Node and /etc/machine-id): configured with a
  real password, then decrypted the stored ciphertext using main.js's
  exact algorithm (independently reproduced in the test) and confirmed
  it recovers the original password exactly - true interoperability,
  not just "some ciphertext was produced." Also covered cancel paths,
  clearing the configuration, the encryption-unavailable failure path,
  and confirmed Authelia's config survives an unrelated Sites save.
- Full regression: re-ran all 9 prior scratch/stub test suites after
  both the lib/config.sh changes - all still clean.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user with a seeded minimal config.json, through Addons ->
  Authelia -> Configure with a real URL/username/password -> confirmed
  the resulting config.json on disk, and independently decrypted the
  stored password for real using main.js's algorithm to confirm it
  matches exactly. Clean exit code 0 throughout.
2026-08-18 21:56:21 +00:00
Claude 1b16bcf3ee Migrate CUPS Printing addon; restructure install.sh into Core Settings/Addons/Advanced; bump to v2.5.0
First Addon migrated: menus/addon_cups.sh (install, reconfigure for
network access, complete uninstall/purge). Different risk profile from
everything migrated so far - it genuinely mutates real system state
(apt install/remove --purge, /etc/cups, ufw) at fixed paths CUPS itself
doesn't let us relocate, unlike the systemd/cron/bin paths this project
already controls via $SYSTEMD_DIR etc. Only the polkit rule's directory
is parameterized ($POLKIT_DIR, lib/config.sh, since that one is ours to
place); everything else gets full command-level `sudo` stubbing in
every test - there is no scratch equivalent for a real apt-managed
subsystem's own file layout. Also added $BUILD_USER (the admin account
actually running the tool, as opposed to $KIOSK_USER) since CUPS needs
to grant it lpadmin group membership.

Restructured install.sh's top-level menu into Core Settings / Addons /
Advanced (matching the legacy tool) instead of one flat list, now that
Addons exists as its own category - cheap to do with one item in it,
much more annoying to retrofit once the flat list has fifteen.

Two bugs caught and fixed before they shipped:

- A "wait for CUPS to start" retry loop used a bare `cmd1 && cmd2 &&
  break` as its body while "simplifying" the legacy script's `if cmd1
  && cmd2; then break; fi`. Being inside a loop doesn't protect a bare
  &&/|| list from set -e - only if/while/until conditions and the
  protected side of &&/|| do that - so the first command failing on an
  early iteration (near-certain right after a fresh install, before
  CUPS has actually started) would have crashed the entire session.
  Restored the `if` form; noted the lesson in the file's own header
  comment since it's a general trap, not CUPS-specific.

- Resolved real uncertainty, rather than assuming: how far does
  run_menu's `handler || true` guard (v2.1.0) actually protect? Wrote a
  minimal isolated test (a bare `false` three function calls deep,
  called via `outer || true` at the top) and confirmed bash's errexit
  exemption for the left side of `||` covers the *entire* evaluation,
  arbitrarily deep through function calls - not just the immediately
  invoked function. So the session-crash risk this project has been
  chasing since v2.1.0 is already covered end-to-end by that one fix.
  Per-statement guards (`|| true`, explicit `if`) still earn their keep
  for a different reason: without them a deep failure bubbles silently
  past the menu actually responsible for it to wherever the nearest
  `|| true` happens to sit, which can be several menu levels above
  where the user actually was - not a crash, but a confusing jump.

Verified:
- Full regression: re-ran every existing scratch-config/stub test suite
  after the lib/config.sh change (new $BUILD_USER/$POLKIT_DIR) and
  after the install.sh restructuring - all still clean.
- New scratch/stub test for addon_cups.sh: full state-machine coverage
  (not installed -> decline -> install -> running -> reconfigure ->
  stopped -> start -> uninstall decline -> uninstall confirm -> not
  installed again) with every `sudo` call intercepted and only `rm`
  targeting the scratch $POLKIT_DIR ever actually executed; confirmed
  the polkit rule's content and that declining install makes zero sudo
  calls. Added both apt-failure paths (update fails, install fails)
  and confirmed the tool reports clearly and returns to the menu
  instead of dying, exercising the exact bug class just fixed.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user through the full new three-level structure - Core
  Settings -> Sites -> back -> back, Addons -> CUPS -> declined install
  (using this container's real, unstubbed dpkg check, correctly
  reporting "not installed" and making no apt/systemctl calls) -> back
  -> back, Advanced -> Diagnostics -> System status -> back -> back ->
  Exit. Zero invalid-choice errors, clean exit code 0 throughout.
2026-08-18 19:10:43 +00:00
Claude 459da53182 Migrate Diagnostics menu; bump to v2.4.0
Deliberately skipped Upgrade/Full Reinstall/Complete Uninstall for now:
all three are large (130-250 lines), genuinely destructive (wipe/
reinstall the kiosk), and Upgrade specifically is coupled to the legacy
script's own self-extraction mechanism (it greps its own running source
for embedded heredocs to pull out main.js/preload.js) - there's no
modular equivalent to migrate it to yet, since those files don't exist
as separate assets outside the monolith. Migrated Diagnostics instead:
4 of the legacy Advanced menu's 12 items (System Status, View Logs,
Audio Diagnostics, Network Test), all read-only except one optional
"play a test sound?" prompt - a deliberate change of pace with no
destructive-action risk to design around, after Sites/WiFi/Power.

- lib/menu.sh: ported get_vpn_ips alongside get_ip_address.
- menus/diagnostics.sh: straight port, using $KIOSK_USER/$KIOSK_HOME
  throughout instead of the legacy code's mix of the variable and a
  hardcoded "kiosk" literal.

Bug fixed, same set -e-safety class as v2.1.0's run_menu fix and
v2.3.0's netplan/systemctl fixes, but a bigger batch this time: nearly
every diagnostic command here was a bare unguarded statement whose
*expected, common* failure - no lightdm running, no audio hardware, no
network, missing log files, ping/nslookup not even installed - would
have crashed the entire session instead of reporting "not found" and
continuing. A diagnostics tool has to be the most crash-proof code in
the project, since it exists to run when something is already broken.
Fixed at every call site: systemctl status | head, tail on lightdm's
log, journalctl, ping, nslookup, and three pactl-backed variable
assignments.

Also noted for future menus in this migration: writing `local var;` and
`var=$(cmd)` as separate statements (good practice, and how several
earlier real bugs were caught) removes an accidental safety net -
`local x=$(cmd)` on one line masks the substitution's exit code with
`local`'s own always-success status. Splitting them is correct, but
each split assignment needs an explicit `|| true` (or real fallback)
where failure is expected and non-fatal, rather than relying on that
masking by accident. Caught three instances of exactly this while
writing this file fresh, not just porting old bugs.

Verified:
- Full regression: re-ran every existing scratch-config/stub test suite
  (sites, display, timezone/pin, lockout, power schedule + RTC, wifi) -
  all still clean after the lib/menu.sh change.
- New test for diagnostics.sh, exercised mostly for real (no
  destructive-mutation risk here, so minimal stubbing needed): system
  status, all three log views (including the "no such file" paths for
  lightdm log and electron log), full 8-step audio diagnostic with test
  sound declined, and network test - all report gracefully instead of
  crashing, confirmed by re-running after each fix until every bare
  unguarded statement was accounted for.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user, navigating Diagnostics -> System status -> View Logs ->
  System journal -> Audio diagnostics (declined test sound) -> Network
  test -> exit. Confirmed every diagnostic path completes and returns
  to its menu cleanly (exit code 0) even with ping/nslookup missing and
  no audio hardware/network present in this environment.
2026-08-18 18:48:26 +00:00
Claude 2375bf5eab Migrate WiFi and Power/Display/Quiet Hours menus; bump to v2.3.0
By far the riskiest menus migrated so far. Both can affect real system
state outside config.json in ways that are hard to reverse: WiFi
rewrites live netplan config and, over SSH, can disconnect the very
session configuring it; power scheduling can shut the physical machine
down and wake it via RTC.

- lib/config.sh: new $SYSTEMD_DIR/$CRON_D_DIR/$BIN_DIR/$NETPLAN_DIR,
  same `: "${VAR:=default}"` pattern as $KIOSK_DIR. Nothing under
  menus/ hardcodes /etc/systemd/system, /etc/cron.d, /usr/local/bin, or
  /etc/netplan directly, so every test in this change points them at
  scratch space instead of ever touching this sandbox's real systemd
  units, cron, or network config.
- lib/menu.sh: ported get_ip_address (also fixing its "No IP" fallback,
  which never actually fired before - `hostname -I | awk` always exits
  0 even on empty output).
- menus/wifi.sh: apply_wifi_config split out from wifi_menu specifically
  so tests can drive the netplan-writing logic without needing real
  scan hardware. Preserves the legacy netplan backup, 60s SSH watchdog,
  and restore-on-failure behavior exactly.
- menus/power_schedule.sh: power schedule (+ RTC wake), display
  schedule, quiet hours, and an Electron reload timer (with its own
  nested run_menu, mirroring the legacy configured/not-configured
  dispatch), plus remove-all. Deliberately excludes the legacy
  dispatcher's "Test schedules & system" - a shared diagnostics submenu
  (audio/network/keyboard tests) that isn't specific to scheduling and
  belongs with a future Advanced/Diagnostics migration instead.

Bugs found and fixed along the way, none papered over:
- The legacy dispatcher refused to open "Configure power schedule" at
  all without RTC hardware, even though shutdown-only mode never needed
  RTC. Now always available.
- None of the six HH:MM prompts across these menus (shutdown, wake,
  display off/on, quiet start/end, custom Electron reload time) were
  validated before - plain `read`, no format check. All now go through
  ask_time.
- set -e safety (same class as the v2.1.0 run_menu fix), three more
  instances: `ls *.yaml` when no netplan file exists still fails under
  pipefail even with stderr silenced (masked in practice by cloud-init
  usually leaving a file behind); the restore-and-reapply `netplan
  apply` after an initial failure was a bare unguarded statement; and
  `systemctl enable`/`start` after writing each of the four timer pairs
  was unguarded too - caught only by testing in an environment without
  a live systemd, but a real enable/start failure on actual hardware
  (bad unit, daemon-reload skipped, ...) would hit the exact same crash.
  Added a shared enable_and_start_timers() helper used at all four call
  sites; all now report a clear warning and return to the menu instead
  of taking the session down.

Testing discipline for this round, given the risk:
- No automated test calls the real netplan/nmcli/iw/wpa_cli/systemctl -
  confirmed no WiFi tools or `wl*` interface exist in this sandbox, so
  wifi_menu's own tools-check safely short-circuits before touching
  anything; apply_wifi_config's actual YAML/backup/failure-recovery
  logic is tested with sudo/netplan/get_ip_address stubbed instead.
- One stubbing pitfall caught and fixed in the test itself: `nohup sudo
  bash "$watchdog" ... &` execs nohup as a real external binary, which
  then execs the real sudo - a bash function stub named `sudo` does NOT
  intercept that, only stubbing `nohup` itself does. Verified via pgrep
  that no real watchdog process or `sleep 60` was ever spawned.
- power_schedule.sh tested with SYSTEMD_DIR/CRON_D_DIR/BIN_DIR pointed
  at scratch dirs and only `sudo systemctl` stubbed (tee/rm/chmod/cp
  left real, since they only ever touch scratch paths): full lifecycle
  for all four schedule types plus remove-all, the RTC-available branch
  (including the wake-time-before-shutdown-time hour/day wraparound
  arithmetic) via a stubbed rtc_wake_available, and the new
  enable_and_start_timers failure path via a stub that fails `enable`
  specifically.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user for both menus. WiFi correctly short-circuits on missing
  tools without crashing. Power/Display/Quiet Hours (SYSTEMD_DIR/
  CRON_D_DIR/BIN_DIR redirected to scratch space) configured all four
  schedule types in sequence including the nested Electron Reload menu,
  survived four consecutive real "systemctl enable/start failed"
  warnings (this container has no live systemd) without the session
  dying, then removed everything - confirmed the scratch dirs ended up
  empty and config.json was never touched (correctly out of scope for
  this menu).
2026-08-18 18:28:49 +00:00
Claude 8672c15469 Migrate Password Protection & Lockout menu; add missing ask_time helper; bump to v2.2.0
Fifth menu migrated onto lib/menu.sh + lib/config.sh: menus/lockout.sh
covers enable/disable, changing the password, inactivity timeout, daily
lock time, and boot password. The password is SHA-256 hashed before
it's ever assigned to LOCKOUT_PASSWORD (matching main.js's comparison
logic) - verified by test that the stored value is the correct hash and
never plaintext.

Rewrote the legacy configure_password_protection's linear "ask
everything, confirm save at the end" wizard as the same immediate-save
pattern used by every other migrated menu: each action (change
password, change timeout, toggle boot password, ...) is a complete,
standalone change, consistent with Sites/Display/Timezone/Hidden PIN.
LOCKOUT_ACTIVE_START/END are deliberately left untouched - per the
Readme they're inert leftover fields the app ignores, so lib/config.sh
just carries whatever is already in config.json through unchanged.

Testing this menu surfaced a real gap before it ever shipped: lib/menu.sh
never had ask_time/validate_time at all (only validate_integer/ask_integer,
ask_url, etc were ported when the framework was first built) - "set a
daily lock time" would have failed for every single user with
"ask_time: command not found". Ported both from the legacy script.

Also promoted the ON/OFF toggle-label helper (previously private to
menus/display.sh as display_onoff) to a shared onoff() in lib/menu.sh,
since menus/lockout.sh needed the same thing and menu files should only
ever depend on lib/, never on each other.

Bumped SCRIPT_VERSION to 2.2.0 with matching changelog entries in the
script header and Readme, and updated "Modular Management" to list the
new menu and drop Password Protection & Lockout from the "not yet
migrated" list.

Verified:
- Full regression: re-ran the Sites, Display, Timezone/PIN scratch-config
  suites after every change in this round (the onoff refactor, and again
  after adding ask_time) - all still clean.
- New scratch-config test for lockout.sh: enable (password+timeout+daily
  lock+boot toggle), independently recomputed the expected SHA-256 hash
  and confirmed it matches config.json exactly, change password, change
  timeout, clear daily lock, toggle boot password, disable (confirmed
  every field clears), and that the menu builder's options correctly
  differ between the enabled and disabled states.
- End-to-end: ran the real install.sh as a genuine non-root, non-"kiosk"
  user - Lockout menu -> enable protection with a real password entered
  via the masked prompt -> set 20m timeout, 23:00 daily lock, boot
  password on -> confirmed the menu redraws with the new state -> clean
  exit (code 0). Checked the resulting config.json and file permissions
  on disk.
2026-08-18 16:48:30 +00:00
Claude 074b2ec2e3 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).
2026-08-18 16:33:12 +00:00
Claude c1370edc3e Migrate Display & Interaction menu; drop version number from installer filename; bump to v2.0.0
- menus/display.sh: second menu migrated onto lib/menu.sh + lib/config.sh,
  covering touch gesture mode, link navigation security, and the
  pause/keyboard/navigation button toggles (previously three separate
  Core Settings entries). Deliberately a different shape from Sites
  (toggle list vs. list CRUD) to exercise the framework more broadly.
  Wired into install.sh's top-level menu alongside Sites.

- Renamed ubuntu-based-kiosk-v1.0.3.sh -> ubuntu-based-kiosk.sh so the
  installer can be updated in place instead of growing a new
  version-numbered filename every release; released versions are now
  tracked via git history and the in-script changelog. Updated all
  Readme download/re-run commands accordingly. Older versioned files
  (ubuntu-based-kiosk-v*.sh, install_kiosk_*.sh) are left in place as
  archived releases.

- Bumped SCRIPT_VERSION to 2.0.0 (new script-level changelog entry) and
  the Readme version/changelog to match, given the new modular
  management path, the rename, and the two real bugs fixed along the
  way (settings clobbered on save, off-by-one in reorder).

Verified before moving on to the web admin work:
- Regression: re-ran the full Sites scratch-config test suite (add,
  edit, delete, reorder, home) - still clean, no invalid-input paths hit.
- New: scratch-config test for every display.sh action (touch mode,
  navigation security, all three toggles), confirming values persist
  through save/reload and that a previously-added site survives
  untouched across Display-menu saves.
- End-to-end: ran the real install.sh (not just sourced functions) as a
  genuine non-root, non-"kiosk" user with real sudo, driving actual menu
  input through Sites -> add a page -> Display -> toggle a setting ->
  exit. Confirmed final config.json on disk matches every action taken,
  and both guard clauses (run as root; no installed kiosk found) fire
  correctly.
2026-08-18 15:48:21 +00:00
Claude 21a0768c8c Add modular menu framework, migrate Sites & Page Timing onto it
Start of pulling the menu system out of the 12k-line single-file
installer so individual menus can change without risking the rest of
the script (network, VNC, addons, etc). This is groundwork for the
planned web-based management UI, which will share the same lib/config.sh
read/write layer instead of duplicating it.

- lib/menu.sh: generic numbered-menu framework (auto-numbered entries,
  "0" always exits/returns) plus the validated input helpers menus need.
- lib/config.sh: single load/save for config.json. Fixes a latent bug
  where the old Sites menu wrote config.json without first loading
  swipe/navigation/lockout settings, silently resetting them to
  defaults on save.
- menus/sites.sh: Sites & Page Timing fully migrated - add/edit/delete/
  reorder pages, set duration (auto-rotate/manual/hidden) and home page.
  Also fixes an off-by-one in the ported reorder logic (moving an item
  landed one slot short of the requested position) caught by testing.
- install.sh: new entry point for managing an already-installed kiosk
  via `git clone` + `./install.sh`, wired to the Sites menu. Does not
  yet replace first-time provisioning, which still uses the existing
  single-file installer.

All new site CRUD/reorder/home-page paths were exercised against a
scratch config.json (add with/without basic auth, edit duration, set
home + timeout, 2- and 3-item reorders in both directions, delete) to
confirm the resulting config.json matches expectations.
2026-08-18 15:26:16 +00:00
Claude 10c4c22f44 Route audio to HDMI automatically when an external display is connected
Previously nothing switched PipeWire's default sink, so audio always stayed on the laptop/built-in speakers regardless of HDMI mirroring. Adds /usr/local/bin/kiosk-audio-route.sh, which finds a sink whose name contains 'hdmi' and makes it the default (moving any already-playing streams onto it) whenever an external display is connected, falling back to the non-HDMI sink when it isn't. Called from autostart once PipeWire is confirmed ready, and from kiosk-hotplug.sh alongside the existing display mirroring on every plug/unplug event. Documents the behavior and manual fallback in the README.
2026-08-02 22:54:23 +00:00
Claude 6f6d34512e Mirror external display at the kiosk's exact resolution, not its own native mode
Previously mirroring used 'xrandr --auto', which picks the external output's own native resolution (e.g. a TV's 1920x1080) rather than matching the kiosk/laptop panel's resolution. Extracts the shared mirroring logic (previously duplicated between autostart and the hotplug handler) into /usr/local/bin/kiosk-mirror-display.sh, which now checks whether the external output natively lists the primary's resolution and uses it directly, or generates a matching mode with cvt and forces it via --newmode/--addmode when it doesn't. Documents the behavior and a manual fallback for displays that reject non-native CVT timings in the README.
2026-08-02 22:51:08 +00:00
Claude f01781de41 Add live HDMI/display hotplug handling via udev + systemd
Adds a udev rule (DRM 'change' events) that triggers a new kiosk-hotplug.service, which re-runs the same xrandr mirroring logic as the Openbox autostart script. External displays plugged in after boot now get mirrored without requiring a login or lightdm restart. Named kiosk-hotplug (not kiosk-display-*) to avoid collision with the existing kiosk-display-* wildcard cleanup in remove_all_schedules(). Also removes the udev rule during Complete Uninstall and documents the behavior in the README.
2026-08-02 22:37:39 +00:00
Claude 68a7345f5b Mirror external HDMI/monitor output onto primary display automatically
Openbox autostart now detects any connected output beyond the primary and mirrors the kiosk content onto it via xrandr, instead of leaving detected-but-unused external displays dark. Also documents the behavior, its limitation (applies at session start, not live hotplug), and adds an HDMI troubleshooting section to the README.
2026-08-02 22:34:19 +00:00
Claude b5d37fa646 Install net-tools and ncdu; curl/git already covered
apt install already includes curl and git; adds net-tools and ncdu to the same idempotent apt install so they're present on fresh installs without duplicating already-installed packages.
2026-08-02 22:15:15 +00:00
Claude ca00674e4c Add offline/air-gapped download instructions to README
Clarifies that the installer script can be downloaded on another machine and transferred via USB, while noting the kiosk machine still needs internet access during install for apt/npm packages.
2026-08-02 22:06:48 +00:00
Claude 29b5783052 Fix LightDM autologin on fresh server installs (nopasswdlogin group + missing config)
Two bugs caused the login screen to appear on new Ubuntu 24.04 server hardware:

1. Ubuntu 24.04's PAM config checks 'user ingroup nopasswdlogin', not 'autologin'.
   Add kiosk user to nopasswdlogin group (and autologin for older versions).

2. The upgrade path never wrote /etc/lightdm/lightdm.conf.d/10-kiosk.conf,
   so on new hardware running through upgrade the file simply didn't exist.

Refactor: extract configure_lightdm_autologin() shared helper called from
both fresh install (step 19/27) and upgrade, so both paths are consistent.
Also use [Seat:*] instead of [SeatDefaults] for forward compatibility.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 18:15:05 +00:00
Claude 91d905f45a Fix LightDM autologin on Ubuntu 22.04+ (new hardware)
[SeatDefaults] is silently ignored by LightDM on newer Ubuntu versions.
Replace with [Seat:*] which is the correct section name for Ubuntu 22.04+.

Also add the kiosk user to the autologin group, which newer Ubuntu
requires for passwordless autologin to work.

Without these fixes, LightDM shows the login screen instead of
auto-logging in and launching the kiosk app.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 18:04:36 +00:00
Claude 879e2ecd14 Fix all file existence checks to run as kiosk user
/home/kiosk is mode 700 so root cannot traverse it. Every [[ -f ]] or
[[ ! -f ]] check on paths inside /home/kiosk was silently returning
'not found' even after the kiosk user had successfully written the file.

Replace all three [[ ! -f "$electron_bin" ]] checks and the
[[ -f "$sandbox" ]] check in install_electron_binary with
sudo -u "$KIOSK_USER" test -f so they run in the kiosk user's
security context and can actually see the files.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 17:32:32 +00:00
Claude 56e3d17459 Restore chmod 644 on tmp zip so kiosk user can read it for unzip
Accidentally dropped this line when rewriting the extraction block.
mktemp creates the file as root:root 600, so sudo -u kiosk unzip
gets 'Permission denied' trying to open the zipfile.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 17:25:36 +00:00
Claude 43266a4cf0 Fix Electron binary extraction: chown dist/ before extracting as kiosk user
The previous fix incorrectly ran unzip as root, which fails because
/home/kiosk is not accessible to root. The kiosk user is the right
actor for the extraction, but two things blocked it:

1. node_modules/electron/dist/ can be owned by root when npm's electron
   postinstall runs with --unsafe-perm, so the kiosk user gets
   'Permission denied' trying to write there. Fix: sudo chown -R the
   electron directory to the kiosk user before extracting.

2. With set -euo pipefail active (upgrade call had no || guard), a
   failed unzip or chmod would abort the script silently before the
   diagnostic error messages could print. Fix: add || true to both
   commands so the function always reaches the explicit -f check which
   prints the real error and returns 1. The upgrade call already has
   || { log_error ...; return 1; } from the previous commit.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 17:17:37 +00:00
Claude 670b32f53c Fix Electron binary extraction failing silently during upgrade
Two bugs combined to cause the 'Electron binary download failed' error
even though the zip downloaded and unzip reported inflating all files:

1. The upgrade path called install_electron_binary bare (no ||), so
   set -euo pipefail was active inside the function. Any failing command
   (e.g. chmod on a file that wasn't written) killed the script before
   the error messages printed. Fresh install used || exit 1, which
   disables set -e inside the function body. Upgrade now uses
   || { log_error ...; return 1; } to match.

2. The unzip ran as the kiosk user, but node_modules/electron/dist/ can
   be owned by root when npm's electron postinstall script runs with
   --unsafe-perm. The kiosk user can't write there, so unzip's write
   errors go to stderr (not visible in the log) while inflating: lines
   still appear on stdout. The binary is never actually written.
   Fix: run mkdir/unzip/chmod as root, then chown -R to kiosk.

https://claude.ai/code/session_01VQ13Fwq4MXxwThLfCXBeGr
2026-06-16 17:11:56 +00:00
Claude 174bbe46ae Fix permission denied on Electron zip: chmod 644 before kiosk-user unzip
mktemp creates the tmp zip owned by root with mode 600.
sudo -u kiosk unzip then fails with "Permission denied".
Add chmod 644 immediately after download so the kiosk user can read it.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 16:54:51 +00:00
Claude b7e35c84c9 Detect pipe execution; show clear upgrade error instead of cryptic path failure
When the script is run via curl|bash or wget|bash, BASH_SOURCE[0] is a
pipe descriptor, not a real file. The upgrade function grep-extracts
heredocs from the script file, so it fails with a confusing path error.

Fixes:
- Set SCRIPT_FILE global at startup (empty string when piped)
- upgrade_kiosk() checks SCRIPT_FILE before asking "Continue?" and shows
  a clear message explaining how to download the script to a file first
- Removes the silent failure path (no more cryptic "Cannot find script at
  /proc/.../pipe:[...]" error)

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 16:45:58 +00:00
Claude 440b074245 Add unzip to apt packages; guard install_electron_binary against missing unzip
Fresh Ubuntu 24.04 minimal installs don't include unzip. The wget fallback
in install_electron_binary() downloaded the 120MB Electron zip successfully
but then failed on the unzip call. Two fixes:
1. Add unzip to the main apt install step so it's always present.
2. Auto-install unzip inside install_electron_binary() as a safety net for
   upgrades on existing systems that may not have it.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 16:39:36 +00:00
Claude 27aaf5a15c feat: match any touch screen for libinput, not just Wacom "Finger" devices
Drop the MatchProduct "Finger" restriction from the xorg libinput rule,
leaving only MatchIsTouchscreen "on". MatchIsTouchscreen is set by udev
from hardware capabilities, so it matches finger touch screens of any
brand (ELAN, Goodix, eGalax, Wacom, etc.) while never matching keyboards,
mice, or pen/stylus digitizers (which are tagged as tablets, not
touchscreens). This makes the script work on any touch hardware without
hardcoding device names. Behavior on existing Wacom machines is unchanged
since their finger device matched either way.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 15:05:28 +00:00
Claude 66a633b22c fix: resolve touch input with keyring flag + libinput driver
Two genuinely separate root causes were behind the dead touchscreen:

1. GNOME keyring grab — under LightDM autologin the keyring stays locked.
   When Chromium accessed it, the gcr-prompter unlock dialog grabbed all
   keyboard and touch input at the X level. The app rendered (timers ran)
   but ignored every tap and keypress. Fix: --password-store=basic stops
   Electron from using the keyring, so the dialog never appears.

2. Wacom driver single-touch emulation — the wacom X driver only does
   single-touch pointer emulation and never passes real multitouch to
   Chromium, so 1-finger and 2-finger swipe gestures could not fire.
   Fix: force the finger touch device to the libinput driver via
   /etc/X11/xorg.conf.d/99-finger-libinput.conf. libinput delivers proper
   XI2 multitouch which Chromium turns into real JS touch events. The
   pen/stylus stays on the wacom driver.

Removed the earlier dead-end attempts (xsetwacom MapToOutput / CTM reset,
Wacom Enable Touch Gesture, 99-wacom-touch.conf) which were all chasing the
wrong cause while the keyring grab masked any real testing. The upgrade path
removes the stale 99-wacom-touch.conf so it can't override libinput.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 14:52:31 +00:00
Claude 4e7fbe497c fix: use xsetwacom MapToOutput instead of xinput set-prop for CTM
The Wacom driver owns the Coordinate Transformation Matrix and silently
overrides any xinput set-prop changes. xsetwacom MapToOutput tells the
driver to recalculate the CTM for the primary connected output, which is
the correct API and persists across driver resets.

Dynamically detects the primary output (eDP1, HDMI1, DP1, etc.) so the
fix works on any machine without hardcoding a display name.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 13:55:09 +00:00
Claude 3315ccc44a fix: reset Coordinate Transformation Matrix to identity in start.sh
The Wacom driver can initialise the CTM to all-zeros, which maps every
touch event to screen coordinate (0,0). The touchscreen appears completely
dead even though the hardware and kernel are working correctly.

Reset the CTM to the identity matrix for every touch/finger device at
startup, before launching Electron, so coordinates are always correct.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 13:44:50 +00:00
Claude 14172782c9 fix: correct xorg MatchProduct glob — Wacom*Finger* not Wacom.*Finger
xorg uses fnmatch (shell glob) for MatchProduct, where . is a literal
dot. Wacom.*Finger never matched "Wacom HID 48E3 Finger touch" because
there is no literal dot in that string. Wacom*Finger* matches correctly.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 13:13:41 +00:00
Claude 5cbade03d7 fix: add xorg.conf.d Wacom touch config so driver starts in touch event mode
Without /etc/X11/xorg.conf.d/99-wacom-touch.conf the Wacom driver initialises
the finger touch device in pointer emulation mode (generating RawButtonPress/
RawButtonRelease/RawMotion). Electron never sees TouchBegin/TouchEnd events so
touchstart/pointerdown(touch) never fire in the renderer.

Setting Option "Gesture" "on" and Option "Touch" "on" at the driver level means
the device initialises in XI2 touch mode on every X server start, regardless of
any post-init xinput set-prop calls.

Added to both fresh install (step 18) and upgrade function.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 13:08:38 +00:00
Claude 95027fe6fe fix: export XAUTHORITY in start.sh so xinput touch init never hits auth error
Without XAUTHORITY set, xinput can fail with "Authorization required"
if the display manager doesn't propagate it through the session environment.
Hardcode the kiosk user's .Xauthority path to guarantee xinput works.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 12:57:21 +00:00
Claude 55039c510b chore: restore original ubuntu-based-kiosk-v1.0.2.sh alongside v1.0.3
Keep v1.0.2 in the repo as a stable fallback while v1.0.3 touch screen
support is being validated on different hardware.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 02:27:13 +00:00
Claude 00dd806e9c chore: bump version to v1.0.3 and rename script file
Update all version strings (header, SCRIPT_VERSION, JS VERSION constant)
and rename ubuntu-based-kiosk-v1.0.2.sh → ubuntu-based-kiosk-v1.0.3.sh.
Update README with v1.0.3 change log and archive v1.0.2 as previous.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 02:23:43 +00:00
Claude 6f18a2f897 fix: detect touch screens dynamically instead of hardcoding Wacom device name
The xinput loop now scans all input devices at startup, matches anything
with "touch" or "finger" in the name (excluding touchpads/trackpads), and
attempts to enable Wacom touch gestures on each match. Non-Wacom devices
silently ignore the set-prop call, so the loop is safe on any hardware.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 02:21:09 +00:00
Claude c18615a78e fix: enable Wacom touch gestures on startup and update start.sh on upgrade
Add xinput call to start.sh so Wacom HID 48E3 touch gesture support is
initialized every time the kiosk starts, not just after lightdm restarts.
Also add start.sh to the upgrade extraction list so it is updated in place
instead of keeping the stale version from the original install.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 02:19:03 +00:00
Claude 37663be91b fix: add PointerEvent fallback for swipe detection
touchstart/touchend never fire on this device (confirmed by zero [TOUCH]
log entries). The activity tracker already uses pointerdown/pointerup and
works fine, proving PointerEvents reach the preload. Added pointer event
handlers that mirror the touch handlers for all gestures (2-finger swipe,
3-finger toggle, 1-finger arrow keys). A 500ms debounce on the IPC send
prevents double-firing on devices where both event types fire.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 01:59:25 +00:00
Claude 8f886854f2 revert: restore v1.0.0 touch handler approach for two-finger swipe
capture:true and --touch-events=enabled were added to handle Authelia's
login page blocking touch events. Authelia now auto-logs in on startup
so the login page never shows. Reverting to the v1.0.0 approach (passive:true
only, no --touch-events flag) which had working two-finger swipe.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 01:52:46 +00:00
Claude f40d8e337c fix: add --touch-events=enabled to Electron launch flags
With --ozone-platform=x11, Chromium defaults touch event detection to
'auto' and may not identify the hardware as a touchscreen, so touchstart/
touchend never fire in the renderer. --touch-events=enabled forces W3C
touch events on unconditionally, restoring two-finger swipe navigation.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 01:16:58 +00:00
Claude d595464400 fix: preserve node_modules during upgrade to avoid re-downloading Electron
The upgrade was wiping node_modules then relying on npm to re-download the
~120MB Electron binary. npm returns exit 0 even when the download times out,
leaving the kiosk with no Electron binary and a blank screen on next boot.

node_modules only needs to be deleted on a fresh install or when explicitly
changing Electron version. For a JS-file-only upgrade, npm install without
a wipe is either a no-op (no changes) or applies dependency updates cleanly.
The install_electron_binary fallback remains as a safety net.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 00:52:22 +00:00
Claude 543a442775 fix: use sudo to remove root-owned config backup after upgrade
Backup is created with sudo cp (owned by root), so the cleanup rm -f
fails with "Operation not permitted" when run without sudo.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 00:39:53 +00:00
Claude 3d5a5839b3 fix: use capture:true on touch event listeners to prevent page JS interference
Touch handlers used bubble phase (no capture:true), so any page script that
called stopPropagation() on touchstart/touchend — e.g. Authelia's login form
or scroll containers — silently blocked the preload's swipe detection.

Using capture:true fires the preload's listeners in the capture phase (before
any element-level handlers), so swipe works even on pages with their own
touch handling. Applied to both preloads (standard and auto-show keyboard).

Also adds missing [TOUCH] 2-finger HORIZONTAL console.log to the standard
preload so swipe events are visible in electron.log for debugging.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 00:31:14 +00:00
Claude 31eb750514 docs: clarify Authelia access_control merge with before/after example
The duplicate-block pitfall (YAML silently ignores duplicate keys, causing
a white screen) is now called out explicitly in both the script's printed
output and the README. Added a before/after example showing the correct
merged result with the kiosk one_factor rule above the two_factor wildcard.
Also explains why one_factor is required (TOTP/WebAuthn need interactive
second step, impossible via API).

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 00:25:11 +00:00
Claude effb726979 fix: add 10-second timeout to Authelia fetch to prevent white screen
Without a timeout, session.defaultSession.fetch() hangs for 1-2 minutes
on TCP timeout when Authelia is unreachable (wrong URL, server down,
firewall). Since createWindow() awaits autheliaAuthenticate(), the main
window is visible but no BrowserView is attached during that wait —
causing a persistent white screen with ibeam cursor.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-16 00:15:18 +00:00
Claude 0eef318e78 docs: update Authelia setup instructions to use MERGE workflow
- Step 3 now says MERGE (not replace/append) with clear warning to keep existing config
- access_control: kiosk one_factor rule must go ABOVE any existing two_factor rule,
  with explanation that Authelia applies rules top-down (first match wins)
- session block: keep existing values; only add the block if none exists yet
- Kiosk can only do one_factor — TOTP/WebAuthn via API is not possible
- Updated in both configure_authelia() printed output and README Authentication section

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 23:55:53 +00:00
Claude 0b52ec27fb fix: shared install_electron_binary() covers both fresh install and upgrade
The upgrade_kiosk() path deleted node_modules then ran npm install but
never checked the binary or set chrome-sandbox permissions — so every
upgrade produced a blank screen.

Changes:
- Extract electron binary verification, fallback downloads, and
  chrome-sandbox chmod 4755 into a shared install_electron_binary()
  function called by both step 17/27 (fresh install) and step 5/6
  (upgrade_kiosk) so neither path can silently skip the permission fix
- Add repair_electron() function: stops display, re-runs
  install_electron_binary, restarts lightdm — no SSH needed
- Wire repair_electron as Advanced menu option 12 "Fix Blank Screen"

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 23:38:37 +00:00
Claude bd4d4cadc8 fix: robust Electron binary download with timeouts, retry, and wget fallback
The ~120MB Electron binary download was silently failing because npm's
default 60s fetch timeout is too short on slower connections.

Changes to step 17/27:
- Set npm fetch-timeout to 600s and retries to 5 before running npm install
- If binary still missing after npm install, retry via install.js with
  ELECTRON_FORCE_DOWNLOAD=true
- If still missing, fall back to direct wget download of the exact
  versioned zip from GitHub releases (300s timeout, 3 tries, shows progress)
- Exit 1 with clear message if all three attempts fail
- Log chrome-sandbox permission step for visibility

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 23:34:00 +00:00
Claude 2556863129 fix: use sudo for all config.json access in configure_authelia
/home/kiosk/ has 750 permissions so the install user can't read inside
it. The -f check and both jq reads were running as the current user and
failing silently, causing the false "config.json not found" error.

Changed:
  [[ ! -f "$config_file" ]]  →  sudo test -f "$config_file"
  jq -r ... "$config_file"   →  sudo -u kiosk jq -r ... "$config_file"
  jq ... > "$tmp"            →  sudo -u kiosk jq ... > "$tmp" && sudo mv

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 23:20:14 +00:00
Claude 4c3f5576b9 fix: detect and retry Electron binary download when npm install silently fails
npm install returns 0 even when Electron's postinstall binary download
fails, leaving node_modules/electron/dist/electron missing and causing
a blank screen with no useful error.

After npm install, explicitly check for the binary. If absent, retry
via ELECTRON_FORCE_DOWNLOAD=true node install.js. If still missing,
print a clear error and exit 1 instead of silently continuing to a
broken install.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 22:52:49 +00:00
Claude f1febbd914 release: v1.0.2 - Authelia auto-login, pipewire fix, dynamic README
New file ubuntu-based-kiosk-v1.0.2.sh containing all changes made
since v1.0.1:
- Authelia auto-login addon (Addons → 5): AES-256 encrypted credentials,
  startup API auth, full Dockerized server-side setup printed on save
- Fix: PipeWire .config dirs created as root caused Permission denied
  at step [5.5/27] on fresh Ubuntu 24.04 minimal installs
- README install commands now pull latest script dynamically via
  GitHub contents API (no more hardcoded version numbers)

SCRIPT_VERSION and VERSION constants updated to 1.0.2.
README changelog and current version updated to 1.0.2.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 14:57:51 +00:00
Claude 05538611cf docs: add Authelia server-side setup guide to script output and README
After configure_authelia() saves credentials, it now prints the full
Dockerized Authelia server-side checklist: argon2 hash generation
command, users.yml kiosk user template, configuration.yml session
duration and access_control rules, and a docker compose restart step.

README gains a new Authentication section under Optional Add-ons
covering the same steps in Markdown with a table comparing Authelia
SSO vs HTTP Basic Auth (both can coexist).

Also clarifies that the Authelia password is encrypted at rest and
not stored in plain text.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 14:49:32 +00:00
Claude 2a5fb6188b feat: Authelia auto-login with machine-ID-bound AES-256 encryption
Adds an Authelia Auto-Login addon (Addons menu → 5) that:
- Prompts for Authelia URL, username, and password
- Encrypts the password with AES-256-CBC keyed from /etc/machine-id
  via scrypt (the encrypted blob is machine-specific and useless elsewhere)
- Stores autheliaURL, autheliaUsername, autheliaEncryptedPassword in config.json

On every kiosk startup, main.js decrypts the password and calls
Authelia's /api/firstfactor with keepMeLoggedIn:true before any
BrowserViews are created. Electron's session.defaultSession handles
the Set-Cookie response automatically, so all sites load already
authenticated.

To set up credentials via SSH:
  ssh user@kiosk
  ./ubuntu-based-kiosk-v*.sh  →  Addons → 5. Authelia Auto-Login

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 14:35:41 +00:00
Claude 42aafff219 fix: create pipewire config dirs as kiosk user to avoid permission denied
sudo mkdir -p created the .config/pipewire/pipewire.conf.d directories
owned by root, causing the subsequent sudo -u kiosk tee to fail with
"Permission denied" at step [5.5/27] on a fresh install.

Switching to sudo -u kiosk mkdir -p ensures the directories are owned
by the kiosk user before the tee writes into them.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 14:11:56 +00:00
Claude c2d9b34d53 docs: remove hardcoded version numbers from install instructions
Quick Install section now queries the GitHub contents API to find and
download the latest ubuntu-based-kiosk-v*.sh script dynamically, so the
README never needs a manual version bump when a new release is pushed.

Post-install "run again" references use `ls ubuntu-based-kiosk-v*.sh | sort -V | tail -1`
for the same reason.

Also bumps version references from 1.0.0 → 1.0.1, Electron 41 → 42,
Node.js 20 → 22, and adds the v1.0.1 changelog entry.

https://claude.ai/code/session_01EyjEQLWbTXcZgbMDarf7NU
2026-06-15 13:45:26 +00:00
Claude dc8f2b3a9c Add v1.0.1: update Node.js 20→22 LTS and Electron 39→42
Node.js 20 reached EOL April 2026; bumps nodesource setup to 22.x.
Electron pin updated from ^39.2.4 to ^42.0.0 (current stable).

https://claude.ai/code/session_0143uHgfTvF3Pv1oDinkhJRX
2026-06-03 01:16:43 +00:00
Claude 11173bbcda Add --allow-server-ssh --disable-ssh-auth to netbird up commands
NetBird v0.60 changed SSH to JWT/IdP-based auth by default.
Using --disable-ssh-auth keeps access controlled purely by NetBird
ACL policies (machine-level, like pre-v0.60 behaviour) without
requiring an identity provider or OIDC flow.

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 02:55:48 +00:00
Claude a7894ede51 v1.0.1-beta: replace nodeIntegration/contextIsolation:false with contextBridge preload
All 6 popup BrowserWindows (lockout, prompt, pause, pin, keyboard ×2)
now use contextIsolation:true + popup-preload.js instead of the
deprecated nodeIntegration:true pattern. A new popup-preload.js file
exposes crypto.hashPassword, fs.readPin, ipcRenderer.send/on/once to
the renderer via contextBridge. All affected HTML files updated to use
window.electronAPI.* instead of direct require('electron') calls.
The popup-preload.js heredoc is also added to the upgrade extract_file
list so upgrades re-extract it correctly.

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 01:29:27 +00:00
Claude 589a65bc55 feat: add v1.0.1-beta with BrowserView → WebContentsView migration
BrowserView has been deprecated since Electron 29 and will be removed
in a future major release. This beta migrates all usage to the
WebContentsView API introduced in Electron 28.

Changes in main.js:
- Import WebContentsView instead of BrowserView
- Add bringViewToTop() helper (remove+re-add as last child = on top)
- createWindow: new WebContentsView / contentView.addChildView
- attachView: contentView.removeChildView + bringViewToTop
- showLockoutScreen: contentView.removeChildView for all views
- unlockScreen: bringViewToTop to restore hidden view
- returnToTabs: bringViewToTop instead of setTopBrowserView
- showHiddenTab: bringViewToTop instead of setTopBrowserView

v1.0.0 is kept unchanged. The legacy update_mainjs_keyboard() patch
function is guarded by a grep check that prevents it running against
the new WebContentsView-based main.js.

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 01:12:22 +00:00
Claude 5085a19805 chore: upgrade Electron from v39.2.4 to v41.2.1
No breaking changes affecting the kiosk app between these versions:
- BrowserView still present (deprecated but not removed until future release)
- WebContentsView destroyed-event change does not apply (app uses BrowserView)
- Session.clearStorageData quotas removal not used
- PDF OOPIF change not relevant

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 01:04:54 +00:00
Claude ae65853f5a docs: update README for v1.0.0
- Bump version references from 0.9.9.1/0.9.8 to 1.0.0
- Update script filename to ubuntu-based-kiosk-v1.0.0.sh throughout
- Add v1.0.0 changelog entries (upgrade fix, sudo/timezone fixes)
- Preserve prior version history (v0.9.9.1, v0.9.8) as changelog
- Update Claude model reference to Sonnet 4.6
- Update last-updated date

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 01:02:08 +00:00
Claude 145320998e chore: bump version to 1.0.0 and rename script file
https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 00:59:09 +00:00
Claude 4b7ab7c0be fix: prevent sudo cache expiry during install and add timezone fallback
Two issues caused the timezone step to fail on first run:

1. Sudo credential cache (default 15 min) can expire during the long
   apt install step before configure_timezone runs. Added `sudo -v`
   immediately after the install confirmation prompt to prime the cache
   as late as possible, just before the first long-running step.

2. `sudo timedatectl set-timezone` can fail with "Access denied" if
   polkit/D-Bus is not yet fully ready in the install environment.
   Added a direct fallback (ln -sf localtime + tee /etc/timezone)
   that bypasses D-Bus entirely.

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-21 00:16:55 +00:00
Claude 07823423e5 fix: use sudo test -s to verify extracted files in upgrade_kiosk
On Ubuntu 22.04+, useradd creates home directories with 750 permissions,
so the non-root user running the script cannot traverse /home/kiosk to
check file existence with [[ -s ]]. sudo tee (running as root) writes the
files successfully, but the bash test always returned false, falsely
reporting all extractions as failed.

Switch to `sudo test -s` to match the pattern already used elsewhere in
the script (line ~10320) when checking files under /home/kiosk.

https://claude.ai/code/session_01M3tiofbGfmTddeMcXr8nXr
2026-04-20 23:10:47 +00:00
Claude 96747fa411 Rename UBK to Ubuntu Based Kiosk and rename main install script
- Rename install_kiosk_v0.9.9.1.sh to ubuntu-based-kiosk.sh
- Update all repository URLs from ubk to ubuntu-based-kiosk
- Remove UBK abbreviation from documentation and script headers
- Update installation instructions in Readme.md
2025-12-30 05:28:48 +00:00
Claude eb2f31f818 Fix power menu IPC and export/import permissions
- Power menu: Send IPC to views[] instead of mainWindow.webContents
  (preload.js runs in BrowserViews, not mainWindow)
- Export: Add chmod 777 to temp dir, use sudo for all file operations,
  use sudo tar and fix archive ownership
- Import: Use sudo tar, add proper permissions to temp directory
2025-12-28 21:35:37 +00:00
Claude 79d112794b Add 30-second auto-dismiss timeout to secondary screens
- PIN entry window: auto-closes after 30 seconds of inactivity
- Pause dialog: auto-closes after 30 seconds of inactivity
- Power menu: converted from native dialog to custom overlay with
  30-second timeout (lockout mode still uses native dialog)
- Nav menu already had 30-second timeout

All modal windows and overlays now automatically dismiss after
30 seconds to prevent screens being left open indefinitely.
2025-12-28 18:05:21 +00:00
Claude 4ac9676ef5 Power button follows same show/hide logic as nav button
- Power button now starts hidden and appears on user interaction
- Auto-hides after 5 seconds of inactivity (matching nav button)
- Increased size to 60px with 3px border for consistency
- Removed hover transitions for simpler, consistent behavior
2025-12-28 17:29:27 +00:00
Claude c4f93571a9 Add power button, fix EPIPE errors, fix display schedule
- Add red power icon button in top-right corner of UI
- Click power button triggers showPowerMenu via IPC
- Add EPIPE error suppression for stdout/stderr (no more error dialogs)
- Fix display on/off scripts: add XAUTHORITY, hardcode kiosk user
- Add logging to display scripts for debugging
2025-12-28 16:41:36 +00:00
Claude 27de108579 Add error handling to SIGUSR1 power button handler 2025-12-28 15:58:13 +00:00
Claude 7ed5faa2dc Simplify power button: direct SIGUSR1 from root ACPI handler
- Simplified power button script runs as root from acpid
- No longer needs DISPLAY/XAUTHORITY (just sends signal)
- Finds all Electron processes and sends SIGUSR1 to each
- Updated ACPI event handlers to call /usr/local/bin/kiosk-power-button.sh
- Updated test-power-button to actually trigger and test
- Upgrade function now installs simplified handler
2025-12-27 14:23:40 +00:00
Claude e04fc42a85 Fix on-screen keyboard: ignore Ctrl/Alt modifier keys
When pressing Ctrl or Alt on the virtual keyboard, the literal text
"Control" or "Alt" was being inserted into text fields. Now these
modifier keys are properly ignored since they don't function as
standalone keys in input fields.
2025-12-27 03:24:43 +00:00
Claude 966709547f Fix upgrade: ensure dir exists, use sudo tee directly
- Create kiosk directory if it doesn't exist before extraction
- Use 'sudo tee' instead of 'sudo -u kiosk tee' for reliable writes
- Add line numbers to output for debugging
- chown at end fixes permissions
2025-12-24 18:48:38 +00:00
Claude bb82108809 Fix upgrade extraction using grep+sed line numbers
The awk-based extraction had variable scoping issues when run
as a nested function. Switch to grep for finding line numbers
and sed for extraction - more reliable approach.
2025-12-24 18:42:53 +00:00
Claude b17adbde2b Fix upgrade: use awk extraction, restart lightdm not kiosk
- Replace sed with awk for more reliable heredoc extraction
- Add debug output showing script path
- Fix restart to use lightdm instead of non-existent kiosk.service
- Kill electron process before upgrade, check electron after
- Add script path validation before extraction
2025-12-24 18:36:20 +00:00
Claude ec0d3ee633 Fix sed patterns in upgrade - remove strict anchors
The sed patterns were too strict with ^ and $ anchors, causing
extraction to fail on lines with leading whitespace.
2025-12-24 18:27:17 +00:00
Claude 703339c5e9 v0.9.9.1: Silent upgrade, import number selection, power button fix
- Silent upgrade: extracts app files from script without user input
- Import now shows numbered list, user can select by number
- Improved power button handler with better Electron process detection
- SIGUSR1 now primary method for power menu (more reliable)
- Upgrade automatically regenerates power button handler
- Added XAUTHORITY export for X11 authentication
2025-12-24 18:17:11 +00:00
Claude 2ba980ce94 Add upgrade feature that preserves all settings
- New upgrade_kiosk() function in Core Settings menu (option 10)
- Auto-exports config, timers, and addon configs before reinstall
- Auto-imports everything after reinstall completes
- User just needs to press Enter through installer prompts
- Also fixed SUDO_USER unbound variable issues
2025-12-24 13:36:52 +00:00
Claude da99774c44 Fix SUDO_USER unbound variable in export/import
- Use ${SUDO_USER:-} to avoid unbound variable error with set -u
- Fall back to USER, then /tmp if neither is set
- Validate home_dir exists and is writable
- Use whoami instead of $USER in scp hint
2025-12-24 02:34:52 +00:00
Claude 113adeb119 Fix export/import exit on counter increment with set -e
The script uses 'set -euo pipefail' which causes ((var++)) to exit
when var is 0, since the expression evaluates to 0 (falsey).
Changed to var=$((var + 1)) which always succeeds.
2025-12-24 02:31:13 +00:00
Claude f2f645b7b8 Add VPN configs to settings export/import
Export now includes:
- WireGuard: /etc/wireguard/*.conf files
- Netbird: config.json, state directory (machine keys), user config
- OpenVPN: /etc/openvpn/ directory
- Tailscale: notes installation status (requires re-auth)

Import restores all VPN configs and auto-enables services.
Warns if VPN software not installed after restore.
2025-12-24 02:22:02 +00:00
Claude 2acc8aaa1d Add settings export/import feature for easy backup/restore
- Added export_settings: backs up all config to timestamped .tar.gz
  - Core config (sites, touch controls, passwords)
  - Schedule timers (power, display, quiet hours)
  - Addon configs (Squeezelite, VNC, Easy Asterisk)
- Added import_settings: restores from backup file
  - Lists available backups in home directory
  - Enables timers and sets permissions automatically
- Added to Advanced menu (options 8 and 9)
- Bump version to 0.9.9
2025-12-24 02:16:30 +00:00
Claude 5ec37d8457 Fix kiosk power button, pause dialog timeout, and mic static
- Add SIGUSR1 signal handler for power button trigger script fallback
- Add 30-second auto-close timeout to pause dialog popup
- Configure PipeWire noise cancellation/echo suppression for microphone
- Bump version to 0.9.8.1
2025-12-24 01:43:11 +00:00
Claude 20cb5d0a5e Add Easy Asterisk client-only installation option
- Add sub-menu to Easy Asterisk addon with Server/Client/Both options
- Implement Baresip SIP client installation for client-only mode
- Add configuration prompts for server connection details (IP, port, extension, password)
- Create systemd user service for automatic Baresip startup
- Support TLS encryption and auto-answer mode options
- Update status display to show both server and client installation status
2025-12-17 06:19:58 +00:00
Claude 2048ac7bc8 Fix version regex to support 4-part version numbers (e.g., 0.9.8.7) 2025-12-11 00:53:11 +00:00
Claude 234dd900f6 Integrate Easy Asterisk Intercom into v0.9.8 installer
Added to install_kiosk_v0.9.8.sh:
- SECTION 14.5: Easy Asterisk Intercom addon functions
- get_latest_easy_asterisk_version() - Fetches latest version from GitHub API
- get_installed_easy_asterisk_version() - Checks installed version
- backup_easy_asterisk_configs() - Backs up configs before updates
- restore_easy_asterisk_configs() - Restores configs after install
- download_and_install_easy_asterisk() - Downloads and runs installer
- addon_easy_asterisk_intercom() - Main menu function with update logic

- Added option 4 "Easy Asterisk Intercom" to Addons menu
- Updated show_addon_status() to display Intercom installation status
- Integration uses stable v0.9.8 as base (v0.9.9 was broken)

README updates:
- Fixed all references from v0.9.9 to v0.9.8
- Updated menu paths (2) Addons → (4) Easy Asterisk Intercom
- Corrected version number throughout
- Updated Quick Install section

Features:
- Downloads latest easy-asterisk-v*.sh from GitHub repo
- Automatic version detection and update checking
- Configuration preservation during updates/reruns
- Safe to run multiple times
- User prompts for install/update/rerun decisions
2025-12-10 22:32:22 +00:00
Claude 38e732b976 Remove broken Copilot code that would install wrong Asterisk version
Removed:
- install_easy_asterisk() function that installed Ubuntu Asterisk packages
- configure_easy_asterisk() function for non-existent addon
- Menu option 1 "Install Easy Asterisk" (broken implementation)
- Menu option 5 "Configure Easy Asterisk" (broken implementation)

The removed code would have:
- Installed default Ubuntu Asterisk (apt-get install asterisk)
- Created fake local config files
- Conflicted with actual Easy Asterisk from GitHub repo

Updated:
- Menu renumbered: Intercom is now option 1 (was option 2)
- Configure Intercom is now option 4 (was option 6)
- Updated README to reflect new menu numbers
- Updated error messages with correct option references

Result: Clean implementation with only the working GitHub-based
Easy Asterisk Intercom installer that properly manages versions
and preserves configurations.
2025-12-10 12:52:26 +00:00
Claude 62e4a906b9 Add Easy Asterisk Intercom addon with automatic update management
Features:
- Download and install latest Easy Asterisk from GitHub repository
- Automatic version detection using GitHub API
- Smart update checking with user confirmation
- Configuration preservation during updates and reruns
- Backup and restore functionality for configs
- Safe re-run capability without breaking existing setup

Menu changes:
- Added "Install/Update Intercom (Easy Asterisk)" option
- Updated configure_intercom to work with Easy Asterisk installation
- Enhanced configuration interface with file editing support

README updates:
- Added Communication section with Easy Asterisk Intercom
- Documented installation, update, and configuration workflows
- Updated version to 0.9.9
- Added installation locations and management commands

The intercom addon integrates with outis1one/easy-asterisk repository
and follows the pattern of easy-asterisk-v*.sh version files.
2025-12-10 12:21:18 +00:00
Claude 160c39a47d Fix power menu VPN display and navigation rendering issues
- Fixed power menu showing VPN address twice by excluding VPN interfaces
  (tailscale, wg, netbird, tun, wt) when detecting local IP
- Fixed site bleeding through during navigation by removing all other
  BrowserViews before attaching new one
- Fixed dim navigation popup by forcing reflow and adding fade-in effect
- Added webContents.invalidate() to ensure proper view rendering after switch

These changes improve the visual clarity and reliability of the navigation
system and power menu display.
2025-12-04 18:06:10 +00:00