Web UI default port 8888; Lock now in the power menu

New installs put the Web UI on port 8888 instead of 8090 (existing installs
keep their port; change it under Addons -> Web UI, which now also closes the
old port in the firewall). The power menu gets a Lock now button, shown only
when password protection is on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZQveYdjfcrEsyazq32
This commit is contained in:
Claude
2026-11-06 18:37:13 +00:00
parent f09bd6bb95
commit 551df48687
7 changed files with 38 additions and 9 deletions
+2 -2
View File
@@ -91,7 +91,7 @@ Useful options (`iso/build-iso.sh --help` for all):
| Option | Effect | | Option | Effect |
|---|---| |---|---|
| `--unattended` | First-boot install takes every default with no prompts (skips the Core Settings menu) and reboots into the kiosk. Set sites afterwards in the Web UI (`http://<ip>:8090`) or `./install.sh`. | | `--unattended` | First-boot install takes every default with no prompts (skips the Core Settings menu) and reboots into the kiosk. Set sites afterwards in the Web UI (`http://<ip>:8888`) or `./install.sh`. |
| `--username NAME [--password PASS]` | Pre-set the admin account (password is prompted for if omitted; only a hash goes on the ISO). Not `kiosk`. | | `--username NAME [--password PASS]` | Pre-set the admin account (password is prompted for if omitted; only a hash goes on the ISO). Not `kiosk`. |
| `--hostname`, `--ssh-key FILE`, `--locale`, `--keyboard` | Pre-set those. | | `--hostname`, `--ssh-key FILE`, `--locale`, `--keyboard` | Pre-set those. |
| `--auto-network` / `--auto-storage` | Skip the network screen (DHCP on ethernet) / wipe the largest disk without asking. | | `--auto-network` / `--auto-storage` | Skip the network screen (DHCP on ethernet) / wipe the largest disk without asking. |
@@ -1585,7 +1585,7 @@ full migration pass.
- **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`). - **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`).
**Previous (v2.17.0):** **Previous (v2.17.0):**
- **Web UI now installs by default** during first-time provisioning (fixed port 8090, no prompt) instead of being opt-in — the Addons menu entry still works standalone for reconfiguring the port or reinstalling it on a kiosk provisioned before this change. - **Web UI now installs by default** during first-time provisioning (port 8888 for new installs, 8090 on older ones; no prompt) instead of being opt-in — the Addons menu entry still works standalone for reconfiguring the port or reinstalling it on a kiosk provisioned before this change.
- **The web UI can now install/reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk Intercom, and check for updates** — the same four addons plus Update named directly. Every one of these is the exact same interactive `action_*` function the terminal menu already uses (no prompt/mutation refactor of any addon file), driven by piping the right answers on stdin — the same technique this project's own bash tests already use to drive these functions. - **The web UI can now install/reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk Intercom, and check for updates** — the same four addons plus Update named directly. Every one of these is the exact same interactive `action_*` function the terminal menu already uses (no prompt/mutation refactor of any addon file), driven by piping the right answers on stdin — the same technique this project's own bash tests already use to drive these functions.
- **Privilege model:** the web service itself still runs as `$KIOSK_USER` with zero ambient `sudo`. A new narrow, allow-listed root helper 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. Chosen over running the whole service as root after asking directly: since this repo has no login of its own by design, a request that reaches the web UI with no reverse proxy in front is effectively unauthenticated, so the allow-list bounds what that can actually do to five vetted actions, never a root shell. - **Privilege model:** the web service itself still runs as `$KIOSK_USER` with zero ambient `sudo`. A new narrow, allow-listed root helper 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. Chosen over running the whole service as root after asking directly: since this repo has no login of its own by design, a request that reaches the web UI with no reverse proxy in front is effectively unauthenticated, so the allow-list bounds what that can actually do to five vetted actions, never a root shell.
- Long-running installs stream live output to the browser via Server-Sent Events, one action at a time — a second request while one is in flight gets a clear `409`, never silently queued or dropped. - Long-running installs stream live output to the browser via Server-Sent Events, one action at a time — a second request while one is in flight gets a clear `409`, never silently queued or dropped.
+1 -1
View File
@@ -18,7 +18,7 @@
# ~/ubuntu-based-kiosk - interactively by default (same prompts as a # ~/ubuntu-based-kiosk - interactively by default (same prompts as a
# manual install), or with --unattended taking every default and # manual install), or with --unattended taking every default and
# rebooting straight into the kiosk. Configure sites afterwards via # rebooting straight into the kiosk. Configure sites afterwards via
# the Web UI (http://<ip>:8090) or ./install.sh. # the Web UI (http://<ip>:8888) or ./install.sh.
# #
# Nothing in the original ISO is changed except /boot/grub/grub.cfg # Nothing in the original ISO is changed except /boot/grub/grub.cfg
# (menu title, plus the `autoinstall` kernel argument with --no-confirm); # (menu title, plus the `autoinstall` kernel argument with --no-confirm);
+15 -1
View File
@@ -1226,6 +1226,19 @@ function historyHome(){
markActivity(); markActivity();
} }
// Power menu -> Lock now: put up the lock screen right away (the same one
// the inactivity timeout shows). Only when password protection is on and a
// code is set - showLockoutScreen refuses otherwise, so a kiosk with no
// code can't be locked with nothing to unlock it.
function lockNow(){
if(!enablePasswordProtection||!lockoutPassword){
console.log('[LOCKOUT] Lock now ignored - password protection is off');
return;
}
console.log('[LOCKOUT] Lock now requested from the power menu');
showLockoutScreen('unlock');
}
function showPinEntry(){ function showPinEntry(){
if(pinWindow&&!pinWindow.isDestroyed()){ if(pinWindow&&!pinWindow.isDestroyed()){
pinWindow.focus(); pinWindow.focus();
@@ -1545,7 +1558,7 @@ function showPowerMenu(){
// For normal mode, use custom overlay with 30-second timeout // For normal mode, use custom overlay with 30-second timeout
console.log('[POWER] Showing custom power menu overlay'); console.log('[POWER] Showing custom power menu overlay');
const powerInfo={version:VERSION,localIP:localIP,vpnIP:vpnIP.trim()}; const powerInfo={version:VERSION,localIP:localIP,vpnIP:vpnIP.trim(),canLock:!!(enablePasswordProtection&&lockoutPassword)};
// Send to all views (preload.js runs in views, not mainWindow) // Send to all views (preload.js runs in views, not mainWindow)
for(const view of views){ for(const view of views){
if(view&&view.webContents&&!view.webContents.isDestroyed()){ if(view&&view.webContents&&!view.webContents.isDestroyed()){
@@ -1730,6 +1743,7 @@ async function createWindow(){
if(action==='shutdown')exec('systemctl poweroff'); if(action==='shutdown')exec('systemctl poweroff');
else if(action==='restart')exec('systemctl reboot'); else if(action==='restart')exec('systemctl reboot');
else if(action==='reload'){app.relaunch();app.quit();} else if(action==='reload'){app.relaunch();app.quit();}
else if(action==='lock')lockNow();
}); });
ipcMain.on('toggle-hidden',toggleHidden); ipcMain.on('toggle-hidden',toggleHidden);
ipcMain.on('history-nav',(event,direction)=>{ ipcMain.on('history-nav',(event,direction)=>{
+13
View File
@@ -666,6 +666,16 @@ window.addEventListener('DOMContentLoaded',()=>{
ipcRenderer.send('power-action','reload'); ipcRenderer.send('power-action','reload');
}); });
// Only offered when password protection is on (set in showPowerMenu).
const lockBtn=document.createElement('button');
lockBtn.id='power-menu-lock';
lockBtn.textContent='🔒 Lock now';
lockBtn.style.cssText=btnStyle+'background:#8e44ad;color:white;display:none;';
lockBtn.addEventListener('click',()=>{
hidePowerMenu();
ipcRenderer.send('power-action','lock');
});
const cancelBtn=document.createElement('button'); const cancelBtn=document.createElement('button');
cancelBtn.textContent='Cancel'; cancelBtn.textContent='Cancel';
cancelBtn.style.cssText=btnStyle+'background:#7f8c8d;color:white;'; cancelBtn.style.cssText=btnStyle+'background:#7f8c8d;color:white;';
@@ -673,6 +683,7 @@ window.addEventListener('DOMContentLoaded',()=>{
hidePowerMenu(); hidePowerMenu();
}); });
buttonsDiv.appendChild(lockBtn);
buttonsDiv.appendChild(shutdownBtn); buttonsDiv.appendChild(shutdownBtn);
buttonsDiv.appendChild(restartBtn); buttonsDiv.appendChild(restartBtn);
buttonsDiv.appendChild(reloadBtn); buttonsDiv.appendChild(reloadBtn);
@@ -708,6 +719,8 @@ window.addEventListener('DOMContentLoaded',()=>{
} }
infoDiv.innerHTML=infoText; infoDiv.innerHTML=infoText;
} }
const lockBtn=document.getElementById('power-menu-lock');
if(lockBtn)lockBtn.style.display=(info&&info.canLock)?'':'none';
powerMenu.style.display='flex'; powerMenu.style.display='flex';
powerMenuVisible=true; powerMenuVisible=true;
+1 -1
View File
@@ -328,7 +328,7 @@ provision_configure_webui() {
return 0 return 0
fi fi
local port="8090" local port="8888"
webui_write_env_file "$port" webui_write_env_file "$port"
webui_write_unit_file webui_write_unit_file
webui_write_helper_script webui_write_helper_script
+5 -3
View File
@@ -59,9 +59,9 @@ webui_is_active() {
webui_port() { webui_port() {
if sudo test -f "$WEBUI_ENV_DIR/webui.env" 2>/dev/null; then if sudo test -f "$WEBUI_ENV_DIR/webui.env" 2>/dev/null; then
sudo grep -oP '^PORT=\K.*' "$WEBUI_ENV_DIR/webui.env" 2>/dev/null || echo "8090" sudo grep -oP '^PORT=\K.*' "$WEBUI_ENV_DIR/webui.env" 2>/dev/null || echo "8888"
else else
echo "8090" echo "8888"
fi fi
} }
@@ -281,7 +281,7 @@ action_webui_install() {
ask_yes_no "Install the Web UI?" "n" || { echo "Cancelled"; return; } ask_yes_no "Install the Web UI?" "n" || { echo "Cancelled"; return; }
local port local port
port=$(ask_integer "Port to listen on" "8090" 1024 65535) port=$(ask_integer "Port to listen on" "8888" 1024 65535)
echo "Installing app files and npm dependencies..." echo "Installing app files and npm dependencies..."
if ! webui_install_app_files; then if ! webui_install_app_files; then
@@ -321,6 +321,8 @@ action_webui_reconfigure() {
webui_write_env_file "$port" webui_write_env_file "$port"
sudo systemctl restart kiosk-webui sudo systemctl restart kiosk-webui
sudo ufw allow "${port}/tcp" comment 'Kiosk Web UI' 2>/dev/null || true sudo ufw allow "${port}/tcp" comment 'Kiosk Web UI' 2>/dev/null || true
# The old port is no longer used - close it.
sudo ufw delete allow "${current_port}/tcp" 2>/dev/null || true
log_success "Web UI now listening on port ${port}" log_success "Web UI now listening on port ${port}"
} }
+1 -1
View File
@@ -294,7 +294,7 @@ app.put('/api/region', (req, res) => {
runRegionHelper('region-set', input, res); runRegionHelper('region-set', input, res);
}); });
const PORT = process.env.PORT || 8090; const PORT = process.env.PORT || 8888;
const BIND_ADDR = process.env.BIND_ADDR || '0.0.0.0'; const BIND_ADDR = process.env.BIND_ADDR || '0.0.0.0';
if (require.main === module) { if (require.main === module) {