Added
-
🎨 Status bar light/dark theme (#118): A new "Status Bar Theme" toggle (Light / Dark) is now available in Settings → Display → Status Bar. In Dark mode (default), the status bar renders with white icons on a dark background — suitable for kiosks with dark web content. In Light mode, icons are black on a transparent/light background — suitable for kiosks with white or bright web content. All icons (battery, Wi-Fi, Bluetooth, volume, clock) are now rendered with
MaterialCommunityIconsreplacing the previous emoji characters, for consistent sizing, alignment, and color control across Android versions. -
🎙️ Voice selection for Web Speech API TTS polyfill (#169): The
speechSynthesis.getVoices()polyfill now returns the actual list of installed Android TTS voices instead of an empty array. Web apps that select a specific voice viautterance.voice = voices.find(v => v.name === '...')will have that voice applied natively. REST API:POST /api/ttsnow accepts an optionalvoiceUriparameter to select a voice by URI for a single speak call. The available voice list is pre-cached at startup for performance (cacheTtsVoices()) and refreshed on demand.
Fixed
-
🔐 SSL certificate dialog not shown for initial navigation and same-host redirects (#144): The custom SSL certificate acceptance dialog (Settings → General → Accept Self-Signed Certificates) only appeared when the failing URL exactly matched the currently loaded page URL. This excluded two common cases: (1) initial navigation — when the app launches and loads the first page, there is no currently loaded URL, so the dialog was never shown; (2) HTTP→HTTPS redirects — a redirect from
http://host/tohttps://host/produces the same host but a different URL, which the string equality check rejected. Fixed by replacing the URL equality check with aisMainFrameRequest()helper that matches on same-host (regardless of scheme or path), and treating a null/empty current URL as a main-frame request. Sub-resource SSL errors (images, fonts, iframes from third-party domains) are still silently denied to avoid flooding the user with dialogs. -
📺 MQTT/REST
screenOn/screenOffcommands did not physically lock the screen (#155):POST /api/screen/onandPOST /api/screen/off(and their MQTT equivalents) only activated or deactivated the screensaver overlay — they never calledlockNow()to actually turn off the display. As a result, the MQTTscreenOnstatus field stayedtrueeven after sendingscreenOff. Fixed by delegating both commands toKioskModule.turnScreenOff()/KioskModule.turnScreenOn(), which calllockNow()andwakeUp()respectively.screenOnadditionally callssetIsScreensaverActive(false)+resetTimer()to handle the case where only the overlay was active and no physical lock event fires. -
🏠 Dashboard mode returned to grid when tile page self-refreshes (#159): If the "Return to Start Page on Inactivity" feature was enabled, opening a dashboard tile armed the inactivity return timer. Pages that auto-refresh (e.g. Immich kiosk, Home Assistant dashboards) did not reset the timer because
Reset on Navigationwas off by default — so the timer fired after the configured delay and returned to the dashboard grid without any user interaction. Fixed: in dashboard mode, any page navigation (including self-refresh) always resets the inactivity timer, matching the user expectation that an actively-updating page should not be treated as "inactive." -
🌙 Overnight rules rejected by Scheduled URLs (#157): Recurring scheduled URL events with an end time before the start time (e.g. 22:00–07:00) were rejected with "End time must be after start time". Two fixes: (1) The validation in
RecurringEventEditornow only rejects identical start/end times — crossing midnight is valid. (2)isEventActive()inplanner.tsnow detects overnight ranges (startTime > endTime) and handles the two sub-cases: before midnight (today is a scheduled day andcurrentTime >= startTime) and after midnight (yesterday was a scheduled day andcurrentTime < endTime). This correctly handles the case where an event starts Monday at 22:00 and is still active Tuesday at 06:30. -
📐 Multi-app grid tile widths cut off after device rotation (#160): In External App mode with multiple managed apps, the app grid used
Dimensions.get('window').widthto calculate tile widths.Dimensionsreturns stale values after device rotation until the component re-renders, causing tiles to overflow or be cut off. Fixed by replacingDimensionswith theuseWindowDimensions()hook, which updates reactively on orientation change. -
🎙️ WebRTC microphone audio silent due to missing permission (#147): The
MODIFY_AUDIO_SETTINGSpermission was not declared inAndroidManifest.xml. This permission is required on Android for WebRTC to switch the audio mode toMODE_IN_COMMUNICATION(which activates the microphone path and echo cancellation). Without it, getUserMedia() succeeded but microphone audio was silent in WebRTC calls. Added as a normal protection level permission (auto-granted at install, no runtime prompt needed). -
🌐 REST API returns "Endpoint not found" for valid endpoints when called with POST (#146): Read-only endpoints (
/api/status,/api/health,/api/info,/api/battery, etc.) only accepted GET requests. Automation tools that default to POST (Home Assistant REST integration, curl--request POST, Node-RED HTTP node, etc.) got a misleading404 Endpoint not foundresponse even though the endpoint exists — the wrong HTTP method was the only issue. Fixed by making all read-only status endpoints accept both GET and POST. The two endpoints that have both a read and write variant (/api/brightness,/api/volume) now use the HTTP method to disambiguate: GET reads the current value, POST with a body sets it. POST-only control endpoints that require a JSON body (/api/url,/api/tts, etc.) now return a proper405 Method Not Allowedwith a clear message when called with GET, instead of the generic 404. -
💾 Export backup fails with "Permission denied" on Android 10+ (#166):
exportBackup()wrote directly to/storage/emulated/0/Download/viaRNFS.writeFile(). On Android 10+ (API 29+),WRITE_EXTERNAL_STORAGEis deprecated and silently denied, causing an EACCES crash. Fixed by switching the export flow to the Storage Access Framework: tapping Export now opens the system "Save As" dialog (ACTION_CREATE_DOCUMENT) where the user picks the save location. The file is then written viaContentResolver.openOutputStream()— no storage permission needed, works on all Android versions. The backup data collection logic is unchanged; only the write path changed. A newsaveJsonFile(content, filename)method was added toFilePickerModule(Kotlin + TypeScript) to handle the SAF save dialog. -
🔇 Audio from previous scheduled URL continues playing after planner switches to next URL (#158): When the URL planner transitioned between scheduled events, it called
setUrl()to navigate the WebView to the new URL — but the previous page's JavaScript (including Web Audio, HTML5<audio>, timers) kept running in the background because the same WebView instance was reused. Fixed by incrementingwebViewKeyon each planner URL transition, which forces React to fully unmount and remount the WebViewComponent. The underlying Android WebView is destroyed, terminating all background sessions. The same remount is applied when the planner reverts to the base URL at the end of a scheduled period. -
📸 Screenshot key combination (Power + Volume Down) not disabled in kiosk mode (#172): On Android, pressing Power + Volume Down takes a screenshot even when Device Owner lock task mode is active. Fixed by calling
DevicePolicyManager.setScreenCaptureDisabled(true)when entering kiosk mode (Device Owner only), and re-enabling it on exit. Prevents both the screenshot itself and the screenshot toolbar/preview from appearing over kiosk content. -
🔄 Auto-reload does not trigger on HTTP 5xx errors (e.g. 504 Gateway Timeout) (#173): The "Reload on Error" feature only handled network-level failures (no connectivity, DNS failure) via
onError. HTTP error responses like 504 Gateway Timeout are delivered viaonHttpErrorand were only logged — no reload was triggered. Fixed by extendinghandleHttpErrorto apply the same 5-second auto-reload for any HTTP 5xx status code when "Reload on Error" is enabled. -
💾 Custom User Agent not included in backup/restore (#174): The
@kiosk_custom_user_agentstorage key was missing from the backup key list inBackupService. Exporting and re-importing a configuration would silently drop the Custom User Agent setting. Added to the backup keys list. -
⌨️ Soft keyboard remains visible when screensaver activates in URL/Video/Image mode (#135): The existing fix (v1.2.19) dismissed the keyboard on
ACTION_SCREEN_OFF, which only fires in Dim Only screensaver mode. In URL, Video, and Image screensaver modes the screen stays on — noSCREEN_OFFevent fires — so the keyboard was never dismissed. Fixed by callingKeyboard.dismiss()at both screensaver activation paths inKioskScreen.