Skip to content

Kitchen Surface Tablet

Last Updated: 2026-07-21 Status: βœ… Active, working. On-screen keyboard, Mealie login, in-page launcher menu, fullscreen recovery, and Desktop Access off-ramp all built and verified (incl. a real reboot). Mealie now stays logged in ~1 year. Remaining work is cosmetic: improving the HA "Kitchen Controls" dashboard layout, deferred to a future session. Purpose: Wall-mounted Surface Go 2 running a Home Assistant dashboard (dashboard-surface/0) with views for HA controls, Immich photo slideshow, and Mealie. Screen is powered on/off automatically by presence via Home Assistant.


Hardware

  • Model: Microsoft Surface Go 2 β€” fanless, low-power (Pentium Gold/Core m3 class)
  • IP: 192.168.1.178
  • OS: Fedora 43 Workstation

Access

Method Detail
SSH ssh kitchen-tablet from Brabham (alias in ~/.ssh/config β†’ dan@192.168.1.178:22, key: ~/.ssh/id_ed25519, comment brabham-claude β€” added to dan's authorized_keys 2026-07-20)
Login user dan, member of wheel (sudo) and video groups
Sudo Password required (%wheel ALL=(ALL) ALL, no NOPASSWD rule exists in /etc/sudoers.d/, which is empty). Sudo timestamp caching on this system is not tied to the tty/session β€” once authenticated once, a fresh SSH connection within the cache window (~15 min default) can run sudo again without re-prompting. This is load-bearing for the screen on/off script (see below) β€” it relies on being invoked often enough to keep re-priming its own sudo timestamp. If the tablet goes untouched long enough for the timestamp to expire, the CPU-governor step of the script will silently fail (no password to supply non-interactively) while the screen power-save toggle (D-Bus, no sudo needed) still works β€” so a symptom of "governor switching stopped working but screen blank/wake still works" points at this.

Display / Kiosk Session

Prior to 2026-07-20: Autologin (GDM) into gnome-kiosk-script β€” GNOME Kiosk, a stripped-down Mutter-based compositor session (gnome-kiosk package) with no shell chrome, launching Firefox in --kiosk mode via /home/dan/.local/bin/gnome-kiosk-script:

firefox --kiosk http://192.168.1.12:8123/dashboard-surface/0?kiosk

Problem found 2026-07-20: No on-screen keyboard ever appears when tapping a text field (Mealie search, HA text inputs, etc.) β€” there's no physical keyboard attached. Root cause: gnome-kiosk is a minimal session that doesn't include GNOME Shell's built-in on-screen keyboard component at all. Confirmed no standalone OSK (squeekboard/onboard) was installed. Tested installing Stevia (stevia package, the modern squeekboard successor, in Fedora repos as of F43) as a standalone companion app β€” it segfaults immediately under gnome-kiosk (exit code 139, SIGSEGV on startup), confirming the compositor doesn't support the overlay protocol standalone OSK apps need. Standalone OSK is not viable under gnome-kiosk.

Fix applied and verified working 2026-07-20: Migrated the autologin session from gnome-kiosk-script to a full GNOME (Wayland) session, configured to behave like a kiosk: - /var/lib/AccountsService/users/dan: Session=gnome-wayland (was gnome-kiosk-script-wayland; backup at dan.bak-20260720) - Firefox autostarted via ~/.config/autostart/firefox-kiosk.desktop rather than the old systemd org.gnome.Kiosk.Script.service β€” note this means Firefox no longer auto-restarts if it crashes (the old service had Restart=always; GNOME session autostart is fire-once-at-login only). Not yet re-added β€” worth a small systemd user service wrapping Firefox with Restart=always if crashes turn out to be a real problem in practice. (Launch command and boot URL changed again 2026-07-21 β€” now firefox --new-window <Photos URL>, no --kiosk; see the Launcher section below for why.) - ~/.config/gnome-initial-setup-done already existed, so the first-run wizard didn't fire β€” nothing to do there, but worth knowing if this is ever done on a truly fresh account. - GNOME lockdown gsettings applied (persist regardless of session type, dconf-backed): org.gnome.desktop.session idle-delay 0, org.gnome.desktop.screensaver lock-enabled false, org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' + sleep-inactive-ac-timeout 0 + idle-dim false, org.gnome.desktop.notifications show-banners false, org.gnome.desktop.interface enable-hot-corners false, org.gnome.desktop.a11y.applications screen-keyboard-enabled true. - Verified after reboot: gnome-shell running (not gnome-kiosk), Firefox came up automatically at the dashboard URL, on-screen keyboard now pops up correctly for text fields (confirmed physically by Dan).

This trades a small amount of RAM overhead (~100–200MB, full GNOME Shell vs. the stripped kiosk session) for a working native on-screen keyboard β€” the same mechanism used by any touchscreen Fedora laptop. CPU impact while running is negligible since Mutter skips compositing entirely for a fullscreen client in both session types, as expected.

Regression introduced by this migration β€” FIXED 2026-07-21 by the launcher extension. Full GNOME Shell allows a touch gesture (e.g. edge-swipe) to exit Firefox's fullscreen state β€” something gnome-kiosk never allowed at all. This is now handled by the launcher extension's fullscreen watchdog (see the Launcher section below), which re-asserts fullscreen automatically within a couple of seconds. As part of that fix, Firefox no longer launches with --kiosk at all (see below for why).


Screen On/Off Automation (presence-triggered power control)

This was arrived at through significant trial and error β€” do not casually "simplify" it without testing each removed piece. Earlier attempts tried, in rough order: busctl on org.gnome.SettingsDaemon.Power, raw /sys/class/backlight/intel_backlight/brightness writes (including chmod +s on brightnessctl to make it setuid-root so it could write without sudo β€” this setuid bit is still set and is a deliberate, permanent part of the setup, not leftover cruft: -rwsr-sr-x root root /usr/bin/brightnessctl), xset dpms force off (X11-only, doesn't work under Wayland), gsettings set org.gnome.settings-daemon.plugins.power brightness, and dbus-send calls to org.gnome.ScreenSaver. The final working script is ~/screen_off.sh on the tablet itself, called remotely over SSH by a Home Assistant automation (there is a dedicated SSH key in dan's authorized_keys commented root@a0d7b954-ssh, matching an HA add-on container's generated key β€” it authenticates as dan, it does not grant root login; /root/.ssh/authorized_keys doesn't exist).

Current script (~/screen_off.sh on the tablet):

#!/bin/bash
# Finalized Power Control for Surface Go 2 Kiosk

if [ "$1" == "off" ]; then
  # 1. Blank the screen
  busctl --user set-property org.gnome.Mutter.DisplayConfig /org/gnome/Mutter/DisplayConfig org.gnome.Mutter.DisplayConfig PowerSaveMode i 1

  # 2. Set CPU to Powersave
  echo "powersave" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null
else
  # 1. Wake the screen
  busctl --user set-property org.gnome.Mutter.DisplayConfig /org/gnome/Mutter/DisplayConfig org.gnome.Mutter.DisplayConfig PowerSaveMode i 0

  # 2. Set CPU to Performance (makes the dashboard snappy when you walk up)
  echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null

  # 3. Simulate activity to bypass any GNOME lockscreens
  dbus-send --type=method_call --dest=org.gnome.ScreenSaver /org/gnome/ScreenSaver org.gnome.ScreenSaver.SimulateUserActivity
fi
Called as ~/screen_off.sh off / ~/screen_off.sh on.

Why this specific approach works, piece by piece: - org.gnome.Mutter.DisplayConfig PowerSaveMode is a D-Bus interface exposed by Mutter itself (the compositor), not by GNOME Shell's JS UI layer β€” confirmed present under both gnome-kiosk and full GNOME sessions (busctl --user list | grep -i mutter shows it under the old kiosk session too). This is why the 2026-07-20 session migration doesn't require changing this script β€” it's decoupled from which session type is running on top of Mutter. Setting it to 1 does a true hardware-level DPMS-style panel power-off (not just software brightness=0), setting it to 0 wakes it. - CPU governor switching (scaling_governor β†’ powersave/performance) is a secondary power-saving measure on top of the screen blank, and also makes the dashboard feel snappy on wake (performance governor kicks in the moment you walk up, before the screen even finishes powering on). This is the piece that needs a live sudo timestamp β€” see the Sudo note in Access above. - SimulateUserActivity on wake exists specifically to stop GNOME's own lock screen from kicking in and intercepting the dashboard when the screen wakes β€” without it, waking the screen could land on a lock screen instead of the Firefox kiosk view. - brightnessctl being setuid-root was an earlier, now-superseded approach (direct backlight brightness manipulation) that's no longer called by the current script, but the setuid bit was left in place and does no harm β€” leave it unless doing a clean rebuild.

On session migration: since the whole mechanism is Mutter/D-Bus + kernel-level (not GNOME Shell UI), it should carry over to the full GNOME session unchanged. Re-test both directions (off then on) after the migration and update this doc with the result β€” if PowerSaveMode or the lockscreen behavior differs under full GNOME Shell (e.g. GNOME Shell's own lock screen is more aggressive than gnome-kiosk's, which has none), the SimulateUserActivity step or the idle-delay settings may need adjusting.


Mealie Login Broken In-Dashboard (found 2026-07-20, fixed 2026-07-21)

Symptom: logging into Mealie from its view on the tablet's dashboard appears to submit, then immediately reverts to logged-out. Works fine everywhere else β€” Brabham via https://mealie.djchome.uk, and even the exact plain http://192.168.1.80:9000 address the tablet uses, when opened directly (not through the dashboard).

Diagnosis process (for reference if something similar happens with another embedded app): 1. Checked Mealie's BASE_URL env var (https://mealie.djchome.uk) and initially suspected a Secure-cookie-over-plain-HTTP mismatch β€” ruled out: grepping Mealie's own Python package (docker exec mealie grep -rln set_cookie /opt/mealie/lib/python3.12/site-packages/mealie) found no server-side cookie-setting code at all. 2. Confirmed mealie.djchome.uk resolves to a Tailscale IP (100.106.140.33), unreachable from the tablet (not Tailscale-connected) β€” ruled out simply pointing the iframe at the HTTPS domain as-is. 3. Live-tailed Mealie's container logs (docker logs -f mealie) during a real login attempt: POST /api/auth/token β†’ 200 (login succeeds server-side!), immediately followed by GET /api/users/self β†’ 401. 4. Confirmed via direct navigation test (Dan opened http://192.168.1.80:9000 standalone on Brabham) that this exact URL works fine outside the iframe β€” isolated the problem to the iframe embedding specifically, not the URL/protocol. 5. Tried disabling Firefox's storage partitioning (network.cookie.cookieBehavior=0 in the kiosk profile's user.js) β€” did not fix it. Confirmed via inspecting the profile's on-disk storage (~/.mozilla/firefox/<profile>/storage/default/) that Firefox was still creating a separate partitioned storage area for Mealie-under-HA (http+++192.168.1.80+9000^partitionKey=%28http%2C192.168.1.12%29) even with that pref set. 6. Inspected that partitioned storage directly: other Mealie preference keys (activity-preferences, cookbook-preferences, etc.) were present and working, but no mealie.access_token β€” meaning localStorage itself wasn't the mechanism at all. 7. Checked cookies.sqlite instead and found mealie.access_token is set as a cookie (Mealie's frontend JS sets it directly via document.cookie, not via the Python backend β€” hence missing it in step 1's grep), correctly scoped to the matching partition key. 8. Captured raw traffic with a temporary nicolaka/netshoot container sharing Mealie's network namespace (docker run -d --rm --net=container:mealie -v /tmp:/tmp nicolaka/netshoot tcpdump -i any -A -s 0 'tcp port 9000' -w /tmp/cap.pcap) during a real login attempt β€” the GET /api/users/self request has no Authorization header and no Cookie header at all. The cookie exists in the browser but isn't being attached to the request. 9. Found the actual root cause on Mealie's GitHub: PR #7741 ("refactor cookie settings for Home Assistant i-frame login", merged 2026-06-15, first in v3.20.0) sets the login cookie as SameSite=None; Secure; Partitioned only when served over HTTPS; over plain HTTP it falls back to SameSite=Lax, which browsers correctly refuse to attach to any request originating from a page framed under a different top-level site (the HA dashboard) β€” regardless of the iframe's own same-origin relationship to Mealie. This is a known, tracked upstream issue: #6368. Dan's mealie container image is the nightly tag, last pulled 2026-06-29 β€” after this fix landed, so it has the fix, but the fix only activates over HTTPS. 10. Confirmed Firefox itself hasn't auto-updated since January 2026 (rpm -qa --last | grep firefox) β€” ruling out a browser-side regression as an alternative explanation.

Conclusion: not a local misconfiguration, not a Firefox regression β€” it's Mealie's own upstream fix for exactly this HA-iframe scenario requiring HTTPS, and the tablet's dashboard currently points at the plain-HTTP address.

Phase 1 fix β€” applied and confirmed working 2026-07-21: 1. Added a DNS rewrite (mealie.djchome.uk β†’ 192.168.1.80) to both AdGuard instances so the domain resolves on the LAN instead of returning the Tailscale IP: - Primary (Pacific LXC 101): edited /opt/AdGuardHome/AdGuardHome.yaml directly via pct exec 101 (no web UI credentials on hand), backup at AdGuardHome.yaml.bak-20260721, systemctl restart AdGuardHome. - Replica (Lotus Docker): edited /mnt/cache/appdata/adguard/config/AdGuardHome.yaml, backup at AdGuardHome.yaml.bak-20260721, docker restart AdGuard-Home. Note: Unraid's host OS has no python3 β€” a first attempt to script the YAML edit failed silently; redid it with awk instead (GNU sed's multi-line a\ append also produced mangled output on Unraid's sed β€” worth remembering for any future scripted YAML edit on Lotus's host, awk was the reliable option). - Didn't wait for the hourly adguardhome-sync β€” edited both instances directly so the fix was immediate. 2. Confirmed SWAG serves a real, valid Let's Encrypt wildcard cert (CN=*.djchome.uk, issued 2026-07-21) on https://mealie.djchome.uk reachable from the tablet's LAN position β€” no cert warnings, plain curl (no -k) succeeds. 3. Updated the dashboard: /config/.storage/lovelace.dashboard_surface Mealie card url changed from http://192.168.1.80:9000 β†’ https://mealie.djchome.uk (backup at lovelace.dashboard_surface.bak-20260721). Edited directly over SSH (sudo sed -i ... β€” the HA SSH add-on user danclark needs sudo for anything under /config/.storage/, which works passwordlessly in this add-on's shell, unlike the tablet's own sudo). 4. Restarting Home Assistant did not, by itself, make the tablet pick up the change β€” the SSH add-on has no Supervisor API token available to it (ha core restart fails with unauthorized: missing or invalid API token), so the restart had to be triggered by Dan via Settings β†’ System β†’ Restart. Even after that, the tablet's already-open Firefox tab kept using its previously-loaded page state (HA reconnects over websocket without necessarily re-fetching lovelace config) β€” had to pkill -9 -f firefox and relaunch it fresh on the tablet before the new HTTPS iframe URL actually took effect. Any future direct edit to lovelace storage files needs both an HA restart and a fresh Firefox reload on the tablet, not just one or the other. 5. Login confirmed working by Dan.

One-year login (2026-07-21): to stop the every-48-hours re-login on the touch keyboard, TOKEN_TIME=8760 (hours = 1 year) was added to the Mealie container (Unraid template my-mealie.xml, backup .bak-20260721, applied via the Docker UI's Edit→Apply). Mealie's default token life is only 48h regardless of "Remember Me". With this, a single login on the tablet (with Remember Me ticked) persists ~1 year, and Firefox keeps the cookie across restarts — no credentials are stored on the device. Deliberately not auto-filling the password via the extension: the Mealie password is reused for sudo/NUT/PBS across the homelab, so embedding it in a wall-tablet extension would widen its exposure badly (this reuse is itself worth fixing — see proxmox-nuc NUT note).


Launcher (in-page Firefox extension) β€” built 2026-07-21

Replaces the old HA-native input_select.tablet_view menu + conditional iframe cards. Each destination (Kitchen Controls, Mealie, Photos, Gousto) is now a real top-level Firefox navigation, not an iframe β€” permanently sidestepping the whole iframe/cookie bug class (Mealie was just the first casualty; Gousto was iframed too and equally at risk). The photo frame (Immich-Kiosk) is the idle/resting state and the boot default.

How the menu is provided: a small custom Firefox extension (kiosk-launcher@djchome.local) whose content script injects a semi-transparent ☰ button (bottom-right, mirrors the look of the old HA chip) into every page via matches: ["<all_urls>"]. Because it renders inside whatever page is loaded, it survives real navigation and needs no "always on top" OS support (which Wayland/GNOME doesn't offer for ordinary apps anyway β€” the same protocol gap that killed the OSK app in Phase 1). Tapping ☰ opens a destination list; each item does window.location.href = <url>. A tucked-away Desktop Access item at the bottom drops out of fullscreen to the normal GNOME desktop; picking any destination afterward re-arms kiosk mode. Design intent: photo-frame is the rest state, no idle auto-return (Dan wants a recipe to stay up while cooking), manual navigation only. Immich-Kiosk's own left/right tap zones and swipe-between-photos still work β€” the ☰ button only captures taps on itself in the bottom-right corner.

Fullscreen watchdog + why --kiosk was dropped: the extension's background script polls window state every 4s and re-asserts fullscreen (browser.windows.update(id, {state:"fullscreen"})) whenever it drifts β€” this is what fixes the Phase-1 edge-swipe-out-of-fullscreen regression. Crucially, Firefox is no longer launched with --kiosk: --kiosk locks fullscreen at a level the windows API cannot override, so the Desktop Access off-ramp silently did nothing under it. Dropping --kiosk and letting the extension manage fullscreen makes both the watchdog and Desktop Access work through the same mechanism. Chrome stays hidden anyway because API/F11 fullscreen hides Firefox's toolbars, and on a touch device there's no mouse-hover to reveal them. Boot flow: GDM autologin β†’ full GNOME β†’ ~/.config/autostart/firefox-kiosk.desktop runs firefox --new-window <Photos URL> β†’ watchdog fullscreens it within ~4s. Verified end-to-end across a real reboot 2026-07-21.

Signing β€” the hard part (important for any future change). This Fedora Firefox build is compiled with MOZ_REQUIRE_SIGNING: true (confirmed by grepping omni.ja), which means xpinstall.signatures.required=false does nothing β€” unsigned extensions cannot load by any method (profile extensions/ drop, system-scope dir, and enterprise force_installed policy were all tried and silently failed). The extension must be signed by Mozilla. It was signed as an unlisted / self-distributed add-on via web-ext sign against a free Mozilla Add-on Developer account (Dan's, at addons.mozilla.org). The signed .xpi is installed permanently via an enterprise policy at /etc/firefox/policies/policies.json (ExtensionSettings β†’ force_installed, install_url: file:///etc/firefox/kiosk-launcher.xpi). To change the extension in future you must: bump the version in manifest.json, re-run npx web-ext sign --channel=unlisted --api-key=... --api-secret=... with the AMO API credentials (from the Add-on Developer hub β†’ Manage API Keys), copy the new signed xpi to /etc/firefox/kiosk-launcher.xpi, and restart Firefox. Unsigned won't load, full stop.

Extension source lives in this repo's session artifacts / Brabham scratchpad (manifest.json, menu.js, menu.css, background.js); destination URLs and the ☰/menu styling are all in those files. The four destinations: Kitchen Controls (http://192.168.1.12:8123/dashboard-surface/0), Photos (Immich-Kiosk album URL, 192.168.1.80:3014), Mealie (https://mealie.djchome.uk), Gousto (https://www.gousto.co.uk/my-gousto). Adding a future "Media Control" tile = add one entry to the DESTINATIONS array in menu.js + re-sign.

HA dashboard stripped (2026-07-21): lovelace.dashboard_surface (backup .pre-strip-20260721) had its old floating ☰ menu card and all input_select.tablet_view conditional iframe cards removed β€” the panel view now shows only the Kitchen Controls content (lights/climate/media/power). The input_select.tablet_view helper still exists but is no longer referenced by anything (harmless; the leftover "Music" option in it was always an unwired placeholder).

Debug tooling left in place: Firefox remote debugger server prefs in the profile's user.js (devtools.debugger.remote-enabled, etc.) β€” nothing connects to it, harmless, fine to strip later.

Still open / next session: improve the Home Assistant "Kitchen Controls" layout itself (the dashboard content, not the tablet plumbing) β€” deferred to a future day at Dan's request.


Backups (before starting the Phase 2 launcher redesign, 2026-07-21)

Taken with everything in a known-working state, before any launcher/redesign changes. On-device (tablet/HA), pulled off-device to Brabham at C:\Users\dan_c.BRABHAM\kitchen-tablet-backups\, and copied from there to Lotus's backups share at /mnt/user/backups/kitchen-surface/ (matches the existing per-host folder convention alongside brabham, cooper, lotus, etc.):

Backup Tablet path Brabham copy
Full Firefox profile (kvq4iv2v.default-release) + autostart .desktop + screen_off.sh ~/backups/kitchen-tablet-working-20260721.tar.gz βœ…
GNOME session account config (Session=gnome-wayland) ~/backups/AccountsService-dan-working-20260721 βœ…
Full dconf database (kiosk lockdown settings β€” idle-delay, hot-corners, notifications, etc.) ~/backups/dconf-dump-working-20260721.ini βœ…
HA dashboard config (lovelace.dashboard_surface, working Mealie HTTPS URL) /config/.storage/lovelace.dashboard_surface.working-20260721 on HA (danclark@192.168.1.12) βœ… (lovelace.dashboard_surface.working-20260721.json)

To restore the tablet from this backup:

# On the tablet, as dan:
tar xzf ~/backups/kitchen-tablet-working-20260721.tar.gz -C ~
sudo cp ~/backups/AccountsService-dan-working-20260721 /var/lib/AccountsService/users/dan
dconf reset -f /
dconf load / < ~/backups/dconf-dump-working-20260721.ini
pkill -9 -f firefox
sudo reboot

To restore the HA dashboard:

# On HA, as danclark:
sudo cp /config/.storage/lovelace.dashboard_surface.working-20260721 /config/.storage/lovelace.dashboard_surface
# then restart HA (Settings β†’ System β†’ Restart)


  • Home Assistant β€” dashboard source (dashboard-surface/0 view), presence automation calling this script