diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d7fe82 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# CLAUDE.md + +## No user data in the repo + +Never write anything specific to a person's kiosk, network or accounts into +this repository - not in code, tests, fixtures, docs, comments, commit +messages, branch names or pull request text. + +That includes: + +- WiFi network names (SSIDs), passwords, keys and tokens +- MAC addresses, IP addresses, hostnames and interface names taken from a + real machine +- usernames, email addresses, PINs and passwords +- the URLs of someone's own sites or services +- log output, screenshots or terminal output pasted in by a user + +When an example, test or fixture needs a value like these, use an obvious +placeholder (`ExampleNet`, `192.0.2.10`, `aa:bb:cc:dd:ee:ff`, `wlan0`, +`example.com`) instead of a real one. When debugging from output a user +shares, keep it in the conversation or the scratchpad, not in a file in this +repo, and leave it out of commit messages. diff --git a/Readme.md b/Readme.md index faca1f4..06a1f6e 100644 --- a/Readme.md +++ b/Readme.md @@ -249,6 +249,7 @@ The kiosk machine still needs a working internet connection (ethernet, or WiFi c - **1-finger swipe** (dual mode) - Navigate within page (arrow keys) - **On-screen keyboard** - Auto-shows on text fields or click keyboard icon - **Navigation menu** - Click key icon (top-left) to access site list and gesture cheat sheet +- **Back/forward buttons** (optional, off by default) - two half-transparent ◀ ▶ buttons at the top middle of the screen, for kiosks with no keyboard or mouse side buttons. They appear on a tap, click or mouse movement and fade after a few seconds. Turn on in Core Settings → Display & Interaction, or the Web UI. ### On-Screen Keyboard - **HTML-based keyboard** with full QWERTY layout @@ -775,7 +776,9 @@ smb://WORKGROUP/COMPUTER/PrinterName **Keyboard Shortcuts:** - `Ctrl+Tab` or `Ctrl+]` - Next tab - `Ctrl+Shift+Tab` or `Ctrl+[` - Previous tab -- `Alt+Right/Left` - Next/Previous tab +- `Ctrl+PageDown` / `Ctrl+PageUp` - Next/Previous tab (the standard browser keys) +- `Alt+Right` / `Alt+Left` - Page forward / back within the current site (the standard browser keys; they used to switch tabs) +- Mouse side buttons (back/forward) - the same page forward/back - `F10` or `Ctrl+H` - Toggle hidden tabs - `Escape` - Return to normal tabs (from hidden) - `Ctrl+Alt+Delete` or `Ctrl+Alt+P` - Power menu diff --git a/kiosk-app/main.js b/kiosk-app/main.js index c118b3c..ed2e8ae 100644 --- a/kiosk-app/main.js +++ b/kiosk-app/main.js @@ -60,6 +60,7 @@ let allowNavigation='same-origin'; let enablePauseButton=true; let enableKeyboardButton=true; let enableNavButton=true; +let enableHistoryButtons=false; let enablePasswordProtection=false; let lockoutPassword=""; let lockoutTimeout=0; @@ -102,6 +103,7 @@ function loadConfig(){ enablePauseButton=(config.enablePauseButton!==false); enableKeyboardButton=(config.enableKeyboardButton!==false); enableNavButton=(config.enableNavButton!==false); + enableHistoryButtons=(config.enableHistoryButtons===true); enablePasswordProtection=(config.enablePasswordProtection===true); lockoutPassword=config.lockoutPassword||""; lockoutTimeout=(config.lockoutTimeout||0)*60000; // Convert minutes to ms @@ -122,6 +124,7 @@ function loadConfig(){ console.log('[CONFIG] Navigation:',allowNavigation); console.log('[CONFIG] Pause button:',enablePauseButton); console.log('[CONFIG] Keyboard button:',enableKeyboardButton); + console.log('[CONFIG] Back/forward buttons:',enableHistoryButtons); console.log('[CONFIG] Password protection:',enablePasswordProtection); console.log('[CONFIG] Lockout timeout:',lockoutTimeout/60000,'minutes'); if(lockoutAtTime)console.log('[CONFIG] Lock at time:',lockoutAtTime); @@ -1145,6 +1148,27 @@ function returnToTabs(){ markActivity(); } +// Page back/forward in the site that's on screen (Alt+Left / Alt+Right, the +// mouse's side buttons, and the optional on-screen buttons). Not while the +// screen is locked or a dialog (PIN, WiFi, power...) is open on top - the +// keys would silently navigate the page hidden behind it. +function historyGo(direction){ + if(isLockedOut)return; + for(const w of [pinWindow,wifiWindow,promptWindow,pauseWindow]){ + if(w&&!w.isDestroyed())return; + } + const view=showingHidden?hiddenViews[currentHiddenIndex]:views[currentIndex]; + if(!view||!view.webContents||view.webContents.isDestroyed())return; + const wc=view.webContents; + const nav=wc.navigationHistory; + if(direction==='back'){ + if(nav?nav.canGoBack():wc.canGoBack()){nav?nav.goBack():wc.goBack();} + }else{ + if(nav?nav.canGoForward():wc.canGoForward()){nav?nav.goForward():wc.goForward();} + } + markActivity(); +} + function showPinEntry(){ if(pinWindow&&!pinWindow.isDestroyed()){ pinWindow.focus(); @@ -1526,6 +1550,11 @@ async function createWindow(){ mainWindow.on('focus',()=>markActivity()); mainWindow.webContents.on('before-input-event',()=>markActivity()); + // Mouse side buttons (back/forward). Reported by the window on Linux. + mainWindow.on('app-command',(event,command)=>{ + if(command==='browser-backward')historyGo('back'); + else if(command==='browser-forward')historyGo('forward'); + }); if(!tabs.length){ mainWindow.loadURL('data:text/html,No Sites Configured'); @@ -1611,6 +1640,7 @@ async function createWindow(){ console.log('[MAIN] Page loaded - sending keyboard-button-enabled: '+enableKeyboardButton); view.webContents.send('nav-button-enabled',enableNavButton); + view.webContents.send('history-buttons-enabled',enableHistoryButtons); console.log('[MAIN] Page loaded - sending nav-button-enabled: '+enableNavButton); console.log('[MAIN] Page loaded - resending pause-button-visibility: '+shouldShow+' for '+t.url); @@ -1654,6 +1684,9 @@ async function createWindow(){ else if(action==='reload'){app.relaunch();app.quit();} }); ipcMain.on('toggle-hidden',toggleHidden); + ipcMain.on('history-nav',(event,direction)=>{ + if(direction==='back'||direction==='forward')historyGo(direction); + }); ipcMain.on('return-to-tabs',forceReturnToTabs); ipcMain.on('user-activity',markActivity); ipcMain.on('show-keyboard',()=>{showHTMLKeyboard();}); @@ -1843,9 +1876,15 @@ async function createWindow(){ globalShortcut.register('Control+Shift+Tab',()=>{prevTab();}); globalShortcut.register('Control+]',()=>{nextTab();}); globalShortcut.register('Control+[',()=>{prevTab();}); - globalShortcut.register('Alt+Right',()=>{nextTab();}); - globalShortcut.register('Alt+Left',()=>{prevTab();}); + // The standard browser keys for switching tabs. (Alt+Left/Right used to + // switch sites; they are now page back/forward, below.) + globalShortcut.register('Control+PageDown',()=>{nextTab();}); + globalShortcut.register('Control+PageUp',()=>{prevTab();}); } + + // Standard browser keys for page back/forward. + globalShortcut.register('Alt+Left',()=>{historyGo('back');}); + globalShortcut.register('Alt+Right',()=>{historyGo('forward');}); globalShortcut.register('F10',toggleHidden); globalShortcut.register('Control+H',toggleHidden); diff --git a/kiosk-app/preload.js b/kiosk-app/preload.js index afbd010..7377d9e 100644 --- a/kiosk-app/preload.js +++ b/kiosk-app/preload.js @@ -115,6 +115,19 @@ ipcRenderer.on('keyboard-button-enabled',(event,enabled)=>{ } }); +// On-screen back/forward buttons (top middle, half transparent) - an +// option for screens with no keyboard or mouse side buttons. +let historyButtonsEnabled=false; +let historyBar=null; +let historyBarHideTimer=null; +ipcRenderer.on('history-buttons-enabled',(event,enabled)=>{ + historyButtonsEnabled=enabled===true; + console.log('[HISTORY-BTN] Back/forward buttons enabled: '+historyButtonsEnabled); + if(historyBar&&!historyButtonsEnabled){ + historyBar.style.display='none'; + } +}); + ipcRenderer.on('nav-button-enabled',(event,enabled)=>{ navButtonEnabled=enabled; console.log('[NAV-BTN] Navigation button enabled: '+enabled); @@ -253,6 +266,56 @@ window.addEventListener('DOMContentLoaded',()=>{ document.body.appendChild(navButton); } + // Back/forward buttons, top middle. The corners are often all taken by the + // menu, pause, keyboard and power buttons, so these sit between them. + const HISTORY_BAR_HIDE_DELAY=5000; + function createHistoryBar(){ + if(historyBar)return; + historyBar=document.createElement('div'); + historyBar.id='electron-history-bar'; + historyBar.style.cssText=` + position:fixed;top:12px;left:50%;transform:translateX(-50%); + display:none;gap:14px;z-index:999999;opacity:0.5; + transition:opacity 0.15s;user-select:none; + `; + const makeButton=(label,direction,path)=>{ + const b=document.createElement('div'); + b.title=label; + b.innerHTML=''; + b.style.cssText=` + width:56px;height:56px;background:rgba(44,62,80,0.9); + border:2px solid rgba(255,255,255,0.9);border-radius:50%; + display:flex;align-items:center;justify-content:center;cursor:pointer; + `; + b.addEventListener('click',(e)=>{ + e.preventDefault(); + e.stopPropagation(); + ipcRenderer.send('history-nav',direction); + showHistoryBar(); + }); + return b; + }; + historyBar.append( + makeButton('Back','back','M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'), + makeButton('Forward','forward','M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z') + ); + historyBar.addEventListener('mouseenter',()=>{historyBar.style.opacity='0.9';}); + historyBar.addEventListener('mouseleave',()=>{historyBar.style.opacity='0.5';}); + document.body.appendChild(historyBar); + } + + function showHistoryBar(){ + if(!historyButtonsEnabled)return; + if(!historyBar)createHistoryBar(); + if(!historyBar)return; + historyBar.style.display='flex'; + if(historyBarHideTimer)clearTimeout(historyBarHideTimer); + historyBarHideTimer=setTimeout(()=>{ + if(historyBar)historyBar.style.display='none'; + historyBarHideTimer=null; + },HISTORY_BAR_HIDE_DELAY); + } + // Power button in top-right corner (follows same show/hide logic as nav button) let powerButton=null; let powerButtonHideTimer=null; @@ -918,11 +981,23 @@ window.addEventListener('DOMContentLoaded',()=>{ if(navButtonEnabled){ showNavButton(); } + showHistoryBar(); // Always show power button on user interaction showPowerButton(); } + // Back/forward buttons also appear on plain mouse movement, for screens + // with a mouse but no touch (the other buttons wait for a click/keypress). + let lastHistoryMove=0; + document.addEventListener('mousemove',()=>{ + if(!historyButtonsEnabled)return; + const now=Date.now(); + if(now-lastHistoryMove<300)return; + lastHistoryMove=now; + showHistoryBar(); + },{passive:true,capture:true}); + // Show pause button on any user interaction const pauseButtonTriggers=['mousedown','touchstart','keydown']; pauseButtonTriggers.forEach(eventType=>{ diff --git a/lib/config.sh b/lib/config.sh index 464e13e..ff99912 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -79,6 +79,7 @@ INACTIVITY_TIMEOUT=120 ENABLE_PAUSE_BUTTON="true" ENABLE_KEYBOARD_BUTTON="true" ENABLE_NAV_BUTTON="true" +ENABLE_HISTORY_BUTTONS="false" ENABLE_PASSWORD_PROTECTION="false" LOCKOUT_PASSWORD="" LOCKOUT_TIMEOUT=0 @@ -203,7 +204,7 @@ load_existing_config() { ALLOW_NAVIGATION=$(sudo -u "$KIOSK_USER" jq -r '.allowNavigation // "same-origin"' "$CONFIG_PATH" 2>/dev/null || echo "same-origin") SWIPE_MODE=$(sudo -u "$KIOSK_USER" jq -r '.swipeMode // "dual"' "$CONFIG_PATH" 2>/dev/null || echo "dual") - local pause_btn keyboard_btn nav_btn password_enabled boot_password + local pause_btn keyboard_btn nav_btn history_btn password_enabled boot_password pause_btn=$(sudo -u "$KIOSK_USER" jq -r '.enablePauseButton // true' "$CONFIG_PATH" 2>/dev/null) [[ "$pause_btn" == "true" ]] && ENABLE_PAUSE_BUTTON="true" || ENABLE_PAUSE_BUTTON="false" @@ -213,6 +214,9 @@ load_existing_config() { nav_btn=$(sudo -u "$KIOSK_USER" jq -r '.enableNavButton // true' "$CONFIG_PATH" 2>/dev/null) [[ "$nav_btn" == "true" ]] && ENABLE_NAV_BUTTON="true" || ENABLE_NAV_BUTTON="false" + history_btn=$(sudo -u "$KIOSK_USER" jq -r '.enableHistoryButtons // false' "$CONFIG_PATH" 2>/dev/null) + [[ "$history_btn" == "true" ]] && ENABLE_HISTORY_BUTTONS="true" || ENABLE_HISTORY_BUTTONS="false" + password_enabled=$(sudo -u "$KIOSK_USER" jq -r '.enablePasswordProtection // false' "$CONFIG_PATH" 2>/dev/null) [[ "$password_enabled" == "true" ]] && ENABLE_PASSWORD_PROTECTION="true" || ENABLE_PASSWORD_PROTECTION="false" @@ -263,6 +267,9 @@ save_config() { local nav_btn_json="true" [[ "$ENABLE_NAV_BUTTON" == "false" ]] && nav_btn_json="false" + local history_btn_json="false" + [[ "$ENABLE_HISTORY_BUTTONS" == "true" ]] && history_btn_json="true" + local password_json="false" [[ "$ENABLE_PASSWORD_PROTECTION" == "true" ]] && password_json="true" @@ -301,6 +308,7 @@ save_config() { --argjson enablePauseButton "$pause_btn_json" \ --argjson enableKeyboardButton "$keyboard_btn_json" \ --argjson enableNavButton "$nav_btn_json" \ + --argjson enableHistoryButtons "$history_btn_json" \ --argjson enablePasswordProtection "$password_json" \ --arg lockoutPassword "${LOCKOUT_PASSWORD:-}" \ --argjson lockoutTimeout "${LOCKOUT_TIMEOUT:-0}" \ @@ -314,7 +322,7 @@ save_config() { --arg autheliaURL "${AUTHELIA_URL:-}" \ --arg autheliaUsername "${AUTHELIA_USERNAME:-}" \ --arg autheliaEncryptedPassword "${AUTHELIA_ENCRYPTED_PASSWORD:-}" \ - '. + {autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,lockoutCodeType:$lockoutCodeType,bootPassword:$bootPassword,bootCodeType:$bootCodeType,autheliaURL:$autheliaURL,autheliaUsername:$autheliaUsername,autheliaEncryptedPassword:$autheliaEncryptedPassword,tabs:[]}' > "$tmp" + '. + {autoswitch:$autoswitch,enableTouch:$enableTouch,dualSwipe:$dualSwipe,swipeMode:$swipeMode,allowNavigation:$allowNavigation,homeTabIndex:$homeTabIndex,inactivityTimeout:$inactivityTimeout,enablePauseButton:$enablePauseButton,enableKeyboardButton:$enableKeyboardButton,enableNavButton:$enableNavButton,enableHistoryButtons:$enableHistoryButtons,enablePasswordProtection:$enablePasswordProtection,lockoutPassword:$lockoutPassword,lockoutTimeout:$lockoutTimeout,lockoutAtTime:$lockoutAtTime,lockoutActiveStart:$lockoutActiveStart,lockoutActiveEnd:$lockoutActiveEnd,requirePasswordOnBoot:$requirePasswordOnBoot,lockoutCodeType:$lockoutCodeType,bootPassword:$bootPassword,bootCodeType:$bootCodeType,autheliaURL:$autheliaURL,autheliaUsername:$autheliaUsername,autheliaEncryptedPassword:$autheliaEncryptedPassword,tabs:[]}' > "$tmp" if [[ ${#URLS[@]} -gt 0 ]]; then for idx in "${!URLS[@]}"; do diff --git a/menus/display.sh b/menus/display.sh index 56172ec..2fafb79 100644 --- a/menus/display.sh +++ b/menus/display.sh @@ -20,6 +20,7 @@ display_status() { echo "Pause button: $(onoff "$ENABLE_PAUSE_BUTTON")" echo "Keyboard button: $(onoff "$ENABLE_KEYBOARD_BUTTON")" echo "Navigation button: $(onoff "$ENABLE_NAV_BUTTON")" + echo "Back/forward btns: $(onoff "$ENABLE_HISTORY_BUTTONS")" } display_menu_builder() { @@ -29,6 +30,7 @@ display_menu_builder() { "Toggle pause button (currently: $(onoff "$ENABLE_PAUSE_BUTTON"))" "Toggle on-screen keyboard button (currently: $(onoff "$ENABLE_KEYBOARD_BUTTON"))" "Toggle navigation/help button (currently: $(onoff "$ENABLE_NAV_BUTTON"))" + "Toggle on-screen back/forward buttons (currently: $(onoff "$ENABLE_HISTORY_BUTTONS"))" ) MENU_HANDLERS=( action_set_touch_mode @@ -36,6 +38,7 @@ display_menu_builder() { action_toggle_pause_button action_toggle_keyboard_button action_toggle_nav_button + action_toggle_history_buttons ) } @@ -131,3 +134,18 @@ action_toggle_nav_button() { fi save_config } + +# Page back/forward: Alt+Left / Alt+Right and the mouse's side buttons +# always work; this adds clickable buttons (top middle, half transparent, +# shown on mouse movement or a tap) for screens with no keyboard or mouse +# buttons. Off by default. +action_toggle_history_buttons() { + if [[ "$ENABLE_HISTORY_BUTTONS" == "true" ]]; then + ENABLE_HISTORY_BUTTONS="false" + log_warning "On-screen back/forward buttons disabled" + else + ENABLE_HISTORY_BUTTONS="true" + log_success "On-screen back/forward buttons enabled" + fi + save_config +} diff --git a/webui/lib/config.js b/webui/lib/config.js index ba293c0..0f6046d 100644 --- a/webui/lib/config.js +++ b/webui/lib/config.js @@ -40,6 +40,7 @@ const SCALAR_DEFAULTS = { enablePauseButton: true, enableKeyboardButton: true, enableNavButton: true, + enableHistoryButtons: false, enablePasswordProtection: false, lockoutTimeout: 0, lockoutAtTime: '', diff --git a/webui/public/app.js b/webui/public/app.js index 0a3a947..05180fb 100644 --- a/webui/public/app.js +++ b/webui/public/app.js @@ -173,6 +173,7 @@ displayForm.addEventListener('submit', async (e) => { enablePauseButton: fd.get('enablePauseButton') === 'on', enableKeyboardButton: fd.get('enableKeyboardButton') === 'on', enableNavButton: fd.get('enableNavButton') === 'on', + enableHistoryButtons: fd.get('enableHistoryButtons') === 'on', homeTabIndex: parseInt(fd.get('homeTabIndex'), 10), inactivityTimeoutMinutes: parseInt(fd.get('inactivityTimeoutMinutes'), 10), }; @@ -282,6 +283,7 @@ function populateAll(config) { displayForm.elements.enablePauseButton.checked = !!config.enablePauseButton; displayForm.elements.enableKeyboardButton.checked = !!config.enableKeyboardButton; displayForm.elements.enableNavButton.checked = !!config.enableNavButton; + displayForm.elements.enableHistoryButtons.checked = !!config.enableHistoryButtons; updateHomeTabOptions(config.tabs); homeTabSelect.value = String(config.homeTabIndex); displayForm.elements.inactivityTimeoutMinutes.value = Math.round(config.inactivityTimeout / 60); diff --git a/webui/public/index.html b/webui/public/index.html index 313452a..5e1e846 100644 --- a/webui/public/index.html +++ b/webui/public/index.html @@ -95,6 +95,7 @@ +
Home page diff --git a/webui/server.js b/webui/server.js index 11ff49c..671e748 100644 --- a/webui/server.js +++ b/webui/server.js @@ -70,6 +70,7 @@ app.put('/api/config', (req, res) => { if (body.enablePauseButton !== undefined) patch.enablePauseButton = !!body.enablePauseButton; if (body.enableKeyboardButton !== undefined) patch.enableKeyboardButton = !!body.enableKeyboardButton; if (body.enableNavButton !== undefined) patch.enableNavButton = !!body.enableNavButton; + if (body.enableHistoryButtons !== undefined) patch.enableHistoryButtons = !!body.enableHistoryButtons; let tabCount; if (body.tabs !== undefined) { diff --git a/webui/test/config.test.js b/webui/test/config.test.js index 46bd620..c055641 100644 --- a/webui/test/config.test.js +++ b/webui/test/config.test.js @@ -75,6 +75,14 @@ check('saveConfig merge preserves fields this app never tracks (the previously-f assert.strictEqual(onDisk.enableNavButton, false); }); +check('enableHistoryButtons defaults off and round-trips', () => { + fs.rmSync(process.env.CONFIG_PATH, { force: true }); + assert.strictEqual(loadConfig().enableHistoryButtons, false); + saveConfig({ enableHistoryButtons: true }); + assert.strictEqual(JSON.parse(fs.readFileSync(process.env.CONFIG_PATH, 'utf8')).enableHistoryButtons, true); + assert.strictEqual(loadConfig().enableHistoryButtons, true); +}); + check('saveConfig tabs: new password gets hashed, never stored/returned as plaintext', () => { const result = saveConfig({ tabs: [{ url: 'https://a.example.com', duration: 30, name: 'A', username: 'bob', password: 'hunter2' }],